Compare commits

...

299 Commits

Author SHA1 Message Date
Kovid Goyal
a4af0599cc version 0.10.1 2018-05-24 19:26:12 +05:30
Kovid Goyal
de4384a420 tui: Use correct key names for enter and backspace keys 2018-05-24 15:23:34 +05:30
Kovid Goyal
a33ebce3c9 diff kitten: Add keybindings to scroll by a page 2018-05-24 15:17:03 +05:30
Kovid Goyal
219d3161d9 diff kitten: Fix incorrect syntax highlighting with a few file formats such as yaml 2018-05-24 15:00:26 +05:30
Kovid Goyal
2e07159461 macOS: Fix regression that caused the macos_option_al_alt setting to always be set to no for all os windows ina kitty instance after the first. Fixes #571 2018-05-24 09:14:13 +05:30
Kovid Goyal
23d0f5ce32 Preserve line numbers when adding in shifts to shader 2018-05-23 19:41:54 +05:30
Kovid Goyal
f4792a1e3d Add link to diff kitten in main README 2018-05-23 13:38:44 +05:30
Kovid Goyal
2a5ef628fa DRYer 2018-05-23 13:25:46 +05:30
Kovid Goyal
4b259dd719 diff kitten: Make the keyboard shortcuts configurable
Fixes #563
2018-05-23 13:10:32 +05:30
Kovid Goyal
c127476c4e ... 2018-05-23 09:56:53 +05:30
Kovid Goyal
beded75547 Make ssh kitten work with the execrable dash shell 2018-05-23 09:54:33 +05:30
Kovid Goyal
74a6cdb3bb ... 2018-05-23 09:35:57 +05:30
Kovid Goyal
ba554fb01d ... 2018-05-23 09:35:29 +05:30
Kovid Goyal
39c98e83da Cleanup key action with arguments parsing 2018-05-23 09:32:52 +05:30
Kovid Goyal
73660f7119 ... 2018-05-23 09:15:27 +05:30
Kovid Goyal
46b4f33850 A nice error message when using shortcuts to change background_opacity without setting dynamic_background_opacity
Also change the default shortcuts
2018-05-23 09:14:10 +05:30
Kovid Goyal
ad8e16aac0 ... 2018-05-23 08:50:19 +05:30
Kovid Goyal
ae6e082419 Keyboard shortcuts to control background opacity 2018-05-23 08:48:59 +05:30
Kovid Goyal
f46d3057ba Add an example of setting environment variables in a session file 2018-05-23 08:28:18 +05:30
Kovid Goyal
8e0e4b8fd1 cmds: Check for extra arguments 2018-05-23 08:24:08 +05:30
Kovid Goyal
76da315f81 ... 2018-05-23 08:19:30 +05:30
Kovid Goyal
95de39feb9 Nice error message when dynamic_background_opacity is not set 2018-05-23 08:16:22 +05:30
Kovid Goyal
c0f790c928 Add a remote command to control window opacity
Fixes #569
2018-05-23 08:13:37 +05:30
Kovid Goyal
6fbb953aa4 Add a note about needing a temp file for tic 2018-05-22 23:18:11 +05:30
Kovid Goyal
30b38e9fa0 Drop the requirement for having python on the server when using the ssh kitten 2018-05-22 23:15:24 +05:30
Kovid Goyal
d20e801793 Add a kitten to easily ssh into servers that automatically copies the terminfo files over 2018-05-22 22:32:51 +05:30
Kovid Goyal
fe65b346c1 Fix Ctrl+Alt+Space not working in normal and application keyboard modes.
Fixes #562
2018-05-22 14:52:00 +05:30
Kovid Goyal
c5da1c0f1b Update link to Debian package 2018-05-22 10:44:36 +05:30
Kovid Goyal
3fd2f03f11 Print failure message to standard error 2018-05-21 23:26:57 +05:30
Kovid Goyal
4ad4e28a9c ... 2018-05-21 23:25:59 +05:30
Kovid Goyal
d3edd2e73f Add a kitten to get/set the clipboard via OSC 52 2018-05-21 23:24:27 +05:30
Kovid Goyal
3d37348c2b ... 2018-05-21 23:17:44 +05:30
Kovid Goyal
8a3e81b7ba Send an empty response to clipboard queries if reading from clipboard is not allowed or the clipboard is empty
Prevents utilities from hanging when querying the clipboard. Also fix 52
prefix missing from the OSC response.
2018-05-21 23:17:39 +05:30
Kovid Goyal
a29c317217 Add code to disable terminal echo to non_blocking_read 2018-05-21 22:09:34 +05:30
Kovid Goyal
48847cdaf8 hints kitten: Fix detection of URLs in ReStructuredText 2018-05-21 16:14:49 +05:30
Kovid Goyal
a59cda8b60 ... 2018-05-21 16:02:24 +05:30
Kovid Goyal
e70b5114fb diff kitten: resolve symlinks before passing paths to the diff tool 2018-05-21 15:50:28 +05:30
Kovid Goyal
d2ab7fbb36 Add a ref 2018-05-21 14:17:21 +05:30
Kovid Goyal
70352ca722 kittens: Don't fail on systems that return invalid values for SC_IOV_MAX. Fixes #558 2018-05-21 12:22:46 +05:30
Kovid Goyal
be28e7bf46 Update README 2018-05-21 11:30:19 +05:30
Kovid Goyal
6b2b8e0208 ... 2018-05-21 11:00:24 +05:30
Kovid Goyal
c997dc8ea7 Add a link to the default diff.conf 2018-05-21 10:59:10 +05:30
Kovid Goyal
f52afbb396 Add a reference to kitty-diff as a program that uses the kitty graphics protocol 2018-05-21 10:53:31 +05:30
Kovid Goyal
a20e7e2a6b Add a note about why kitty-diff is kitty only 2018-05-21 10:51:50 +05:30
Kovid Goyal
9903b8e8db version 0.10.0 2018-05-21 09:50:36 +05:30
Kovid Goyal
1d18f63eb5 Update README 2018-05-21 09:06:19 +05:30
Kovid Goyal
a63682b160 ... 2018-05-20 18:35:46 +05:30
Kovid Goyal
812f3e60ca ... 2018-05-20 18:35:15 +05:30
Kovid Goyal
f8f5838ab5 Update CHANGELOG 2018-05-20 18:34:07 +05:30
Kovid Goyal
07782e071a Update CHANGELOG 2018-05-20 18:09:08 +05:30
Kovid Goyal
624dfdf913 Update README 2018-05-20 17:33:09 +05:30
Kovid Goyal
a01dad8c3c Render the shade unicode box characters as per the spec 2018-05-20 16:34:17 +05:30
Kovid Goyal
b6baa99fce diff kitten: Make the representation of tabs configurable 2018-05-20 15:37:52 +05:30
Kovid Goyal
912b395316 diff kitten: Render tabs as four spaces instead of <9> 2018-05-20 15:29:12 +05:30
Kovid Goyal
b1b90d2bf7 Replace tabs in README 2018-05-20 15:17:19 +05:30
Kovid Goyal
37bd964fee Add some notes about integrating with git 2018-05-20 15:12:21 +05:30
Kovid Goyal
fa3e173c18 diff kitten: Report errors converting images 2018-05-20 15:01:25 +05:30
Kovid Goyal
2a9dc23c61 ... 2018-05-20 12:44:16 +05:30
Kovid Goyal
2885f54360 Add README for diff kitten 2018-05-20 12:41:40 +05:30
Kovid Goyal
ac2f4c8ab6 Nicer window title for the diff kitten 2018-05-20 12:26:14 +05:30
Kovid Goyal
7282d8edc1 Add an option to control how much dimming SGR dim does 2018-05-20 11:00:13 +05:30
Kovid Goyal
6f2d63eb87 Implement SGR dim
kitty now supports the SGR DIM escape code, which makes text fade into
the background. It works by alpha blending the text color into the
background color. Fixes #446
2018-05-20 10:43:26 +05:30
Kovid Goyal
9cab8a2de5 diff kitten: Do not specify the default foreground color in formatting codes.
Fixes an issue with foreground color being incorrect after a highlighted
segment if the syntax highlight does not specify a foreground color at
the boundary.
2018-05-20 08:51:41 +05:30
Kovid Goyal
709d19119d Update bundled glfw 2018-05-20 08:03:58 +05:30
Kovid Goyal
029b82807c Ignore \ prefixed lines when parsing patches 2018-05-19 23:02:49 +05:30
Kovid Goyal
67c0942e01 Fix error reporting in the diff kitten 2018-05-19 22:51:02 +05:30
Kovid Goyal
fb57653665 Make debugging kittens a little nicer
Now debug() writes to the stderr of the parent kitty process instead of
a temp file.
2018-05-19 16:32:08 +05:30
Kovid Goyal
646a349d82 ... 2018-05-19 15:11:08 +05:30
Kovid Goyal
ad1109b6fe Hints kitten: Overhaul making it more intelligent
It now uses information about the geometry of the screen.
So URLs that stretch over multiple lines are detected even if there is a
hard line break in between (some programs like mutt generate these).
2018-05-19 14:31:00 +05:30
Kovid Goyal
c2bd6a66d9 ... 2018-05-19 08:49:03 +05:30
Kovid Goyal
ba9af3df8c Fix moving cursor outside a defined page area incorrectly causing the cursor to be placed inside the page area. Caused incorrect rendering in neovim, which relies on this behavior.
Fixes #542
2018-05-19 08:04:37 +05:30
Kovid Goyal
5d4e968f50 Allow commenting lines in command logs 2018-05-19 07:33:29 +05:30
Kovid Goyal
0e327f3d13 Allow kittens to ask for wrap markers 2018-05-18 23:09:27 +05:30
Kovid Goyal
bb06bfa627 Cleanup the as_text_generic macro
Also allow adding a \r at the end of each visual line
2018-05-18 23:06:06 +05:30
Kovid Goyal
2a713cab60 kittens should specify their own required input type
It is DRYer. Also replace the run_kitten function with just kitten that
takes no type of input argument. Backward compat for run_kitten is
maintained.
2018-05-18 22:55:42 +05:30
Kovid Goyal
26df57a1c7 More elegant handling for pressing enter with no valid current input in the hints kitten 2018-05-18 21:28:43 +05:30
Kovid Goyal
18893f86ce Render a couple more powerline symbols directly, bypassing the font
There are apparently a set of "extra" powerline glyphs that need to be
boundary aligned. https://github.com/ryanoasis/powerline-extra-symbols
Fixes #550
2018-05-18 19:20:10 +05:30
Kovid Goyal
8fead5ebe8 Add a test function to test drawing of single box characters easily 2018-05-18 15:43:40 +05:30
Kovid Goyal
6020aadbf2 diff kitten: Show overall diff stats 2018-05-18 14:19:25 +05:30
Kovid Goyal
f8b6b532e0 diff kitten: Show scroll % 2018-05-18 13:36:12 +05:30
Kovid Goyal
05ec174868 @resize-window now has a useful return code 2018-05-18 00:01:31 +05:30
Kovid Goyal
3875ee021a re-org screenshots 2018-05-17 23:53:36 +05:30
Kovid Goyal
2931afa0ae Fix vertical biasing of last window in grid not working when number of windows is odd 2018-05-17 23:42:37 +05:30
Kovid Goyal
2803e45380 Fix swapped row/col when biasing grid layout 2018-05-17 23:39:29 +05:30
Kovid Goyal
965dde599e Cleanup layout.py 2018-05-17 23:34:41 +05:30
Kovid Goyal
229ea45d81 ... 2018-05-17 23:27:35 +05:30
Kovid Goyal
8ea84c97d5 Use an overlay window for window resizing
Also implement a remote command to resize windows
2018-05-17 23:26:41 +05:30
Kovid Goyal
7c47bd547f Fix ctrl+alt+<special> not working in normal and application keyboard modes.
Fixes #548
2018-05-17 20:43:18 +05:30
Kovid Goyal
6ae58e6882 Add a couple more functions to --replay-commands 2018-05-17 20:15:59 +05:30
Kovid Goyal
3bc0f90a82 Fix incorrect parsing of --replay-commands 2018-05-17 20:06:59 +05:30
Kovid Goyal
2a99ef254f Dont segfault when printing a freetype font object with a NULL postscript name 2018-05-17 19:49:31 +05:30
Kovid Goyal
5bc811f26e Improve the help text for --replay-commands 2018-05-17 19:42:46 +05:30
Kovid Goyal
83893d762a Implement resizing for the grid layout 2018-05-17 18:47:41 +05:30
Kovid Goyal
24bed6273b Simplify grid layout code some more 2018-05-17 17:47:42 +05:30
Kovid Goyal
2a5b314f67 Simplify grid layout code 2018-05-17 17:20:09 +05:30
Kovid Goyal
e3c9fc3fba DRYer 2018-05-17 17:17:03 +05:30
Kovid Goyal
b81700a8fd DRYer 2018-05-17 17:12:24 +05:30
Kovid Goyal
b319fd4b93 Implement resizing for vertical and horizontal layouts 2018-05-17 17:09:03 +05:30
Kovid Goyal
23d6995558 ... 2018-05-17 16:50:16 +05:30
Kovid Goyal
190612d507 Make the resize step size configurable 2018-05-17 16:48:35 +05:30
Kovid Goyal
cddfe425e1 Dont keep increasing bias in Tall layout when it wont change the actual layout
Also refactor the bias code so that it is shared with the fat layout.
2018-05-17 16:02:42 +05:30
Kovid Goyal
bc6a03a41b Implement resizing for the Fat layout 2018-05-17 15:19:05 +05:30
Kovid Goyal
e053d1f566 Implement resizing of individual windows in a layout
See the Layouts section in the README for details. Fixes #362

Note that it is only implemented for the Tall layout currently. Other
layouts to follow. The implementation might also be refined in the
future.
2018-05-17 15:09:41 +05:30
Kovid Goyal
e429b8484c Update kitty invocation when preprocessing README 2018-05-17 14:56:52 +05:30
Kovid Goyal
22159c5eb4 Fix mockup for testing 2018-05-16 16:43:39 +05:30
Kovid Goyal
903df9972a ... 2018-05-16 16:25:41 +05:30
Kovid Goyal
4dcbb1f077 Avoid needless re-allocation of Layout objects on layout change
Also allows layout instance data to be preserved when cycling layouts
2018-05-16 16:24:31 +05:30
Kovid Goyal
5158d49781 Generalize bias handling 2018-05-16 15:57:04 +05:30
Kovid Goyal
3fdf47c535 Allow specifying the width of the tall window in the Tall layout as a percentage of available space
Also remove the --window-layout option as it was redundant. Same effect
can be achieved using -o enabled_layouts=some_layout,*
2018-05-16 14:02:58 +05:30
Kovid Goyal
b59d1bda8b Add a comment for RTL shaping 2018-05-16 07:00:36 +05:30
Kovid Goyal
1760fb9db2 Partial fix for rendering RTL languages like arabic 2018-05-16 06:56:16 +05:30
Kovid Goyal
082c771fc0 Fix typo in conf file 2018-05-16 00:08:52 +05:30
Kovid Goyal
7ec989d485 Fix Ctrl+backspace acting as plain backspace in normal and application keyboard modes. Fixes #538 2018-05-16 00:07:54 +05:30
Kovid Goyal
0f9613eb9e ... 2018-05-11 06:15:26 +05:30
Kovid Goyal
85a354351a ... 2018-05-11 06:06:18 +05:30
Kovid Goyal
24db8cef4f Merge branch 'add-fat-layout' of https://github.com/theJian/kitty 2018-05-11 05:23:14 +05:30
theJian
9908eee40c Update readme 2018-05-11 01:00:29 +08:00
theJian
b6517758d9 Add fat layout 2018-05-11 00:59:46 +08:00
Kovid Goyal
029500f3ed Store fd for screen_size at creation time 2018-05-10 18:27:18 +05:30
Kovid Goyal
686727a7c3 Nicer hooks for kitty-bar
Allows kitty-bar to be refactored into a pure CLI app
2018-05-10 17:47:51 +05:30
Kovid Goyal
91a1d61cc6 diff: Fix margins not being reset on exit 2018-05-10 16:21:26 +05:30
Kovid Goyal
b8e9921f3d Forgot this file in previous commit 2018-05-10 16:20:58 +05:30
Kovid Goyal
e0ce5d5629 tui: Cleanup unhandled exception processing
Also handler finalization should happen before term manager
finalization.
2018-05-10 16:20:33 +05:30
Kovid Goyal
ed511c296c Avoid needing to resend images on every draw_screen() 2018-05-10 15:58:52 +05:30
Kovid Goyal
eab1fbbbb0 diff: Mark displayed images as change starts 2018-05-10 15:29:53 +05:30
Kovid Goyal
ee3b052a50 tui: Preserve cursor position when placing images 2018-05-10 15:27:35 +05:30
Kovid Goyal
5b98f15d9b typo in comment 2018-05-10 15:15:02 +05:30
Kovid Goyal
51d76e5a2b ... 2018-05-10 14:37:43 +05:30
Kovid Goyal
a6bce0b221 diff: More work on showing images 2018-05-10 14:33:09 +05:30
Kovid Goyal
fb5dc8a2ba A spot of refactoring 2018-05-10 11:52:51 +05:30
Kovid Goyal
d571ee0d01 diff: More work on displaying images 2018-05-10 11:50:46 +05:30
Kovid Goyal
5d7ed67316 diff: More work on displaying images 2018-05-10 10:36:50 +05:30
Kovid Goyal
9f6c754339 Add a note about KITTY_WINDOW_ID 2018-05-10 10:27:34 +05:30
Kovid Goyal
2819a4ebf3 ... 2018-05-10 09:53:17 +05:30
Kovid Goyal
dec1203ea8 Make the kitty window id available as the environment variable KITTY_WINDOW_ID
Fixes #532
2018-05-10 08:52:28 +05:30
Kovid Goyal
87ab0d7b18 Make it clear that focus-window focuses the window it is run inside by default. 2018-05-10 08:46:38 +05:30
Kovid Goyal
043530409c DRYer 2018-05-10 07:47:29 +05:30
Kovid Goyal
9710282d22 Have the paste_from_selection action paste from the clipboard on platforms that do not have a primary selection such as Wayland and macOS. Fixes #529 2018-05-10 07:42:03 +05:30
Kovid Goyal
5e8e141fb5 Fix cursor_stop_blinking_after=0 not working. Fixes #530 2018-05-10 07:31:53 +05:30
Kovid Goyal
2e313fbdd2 Better exception for image conversion failure 2018-05-09 20:50:56 +05:30
Kovid Goyal
5c3e4db05f DRYer 2018-05-09 20:36:53 +05:30
Kovid Goyal
2be2f6aa0c Make icat a kitten
The kittens framework did not exist when icat was first written.
2018-05-09 20:26:30 +05:30
Kovid Goyal
6d038f5cdf Pass the image manager to the render code 2018-05-09 20:12:39 +05:30
Kovid Goyal
27feccba39 diff: More work on image support 2018-05-09 19:41:37 +05:30
Kovid Goyal
7d39d5c088 Ensure only a single screen size function is used 2018-05-09 14:14:09 +05:30
Kovid Goyal
2e69b904da diff: More work on images 2018-05-09 14:06:15 +05:30
Kovid Goyal
79dcc8c771 Don't search in names.h 2018-05-09 11:32:22 +05:30
Kovid Goyal
683e5a4754 DRYer 2018-05-09 11:32:09 +05:30
Kovid Goyal
d51a424b98 diff: Start work on image display 2018-05-09 10:25:53 +05:30
Kovid Goyal
e07d216a11 Expand environment variables in config file include directives 2018-05-09 09:35:50 +05:30
Kovid Goyal
b9fa8dbe42 DRYer 2018-05-09 09:28:38 +05:30
Kovid Goyal
3820c46801 Ensure there is a blank line before each new item 2018-05-09 08:33:24 +05:30
Kovid Goyal
b663160bc2 Fix rendering of binary files 2018-05-09 08:17:52 +05:30
Kovid Goyal
a65c807a4a diff: Add keyboard shortcuts to got to next/previous change 2018-05-09 08:12:40 +05:30
Kovid Goyal
3a1f85cb69 diff: Make the diff implementation configurable 2018-05-08 23:32:21 +05:30
Kovid Goyal
196bd9c22b Allow setting the number of context lines in diff.conf 2018-05-08 23:17:40 +05:30
Kovid Goyal
9bf2087b55 Better error message when pygments style is not found 2018-05-08 23:13:50 +05:30
Kovid Goyal
51e589c7e6 diff: Add a option for pygments style 2018-05-08 23:08:20 +05:30
Kovid Goyal
7365ed9b05 diff: Better title rendering 2018-05-08 23:02:00 +05:30
Kovid Goyal
9cb7a6b2c7 ... 2018-05-08 22:50:00 +05:30
Kovid Goyal
9ff7eae1af diff: Add an option to control syntax aliases 2018-05-08 22:46:25 +05:30
Kovid Goyal
61e1b74434 Highlight .pyj files as python 2018-05-08 22:32:34 +05:30
Kovid Goyal
324c223d54 diff kitten: keyboard shortcuts to change the number of lines of context 2018-05-08 22:19:50 +05:30
Kovid Goyal
96793a296c diff kitten has working syntax highlighting 2018-05-08 14:48:01 +05:30
Kovid Goyal
7b1626a38f Ensure highlighting is run only once 2018-05-08 09:20:36 +05:30
Kovid Goyal
09a3a3dafb More work on the diff kitten 2018-05-08 08:43:41 +05:30
Kovid Goyal
0e21708f25 ... 2018-05-08 08:26:47 +05:30
Kovid Goyal
89fa2343a6 Sign the released macOS app 2018-05-07 23:04:20 +05:30
Kovid Goyal
03256d8ac9 Code to integrate with pygments 2018-05-07 13:30:59 +05:30
Kovid Goyal
889ca77912 Allow programs running in kitty to read/write from the clipboard
By default only writing is allowed. There is a config option to enable
reading, if needed.
2018-05-07 09:57:39 +05:30
Kovid Goyal
23ee6b3109 More work on the diff kitten 2018-05-06 16:25:04 +05:30
Kovid Goyal
d133ffac25 Make using tui.operations more convenient 2018-05-06 15:34:11 +05:30
Kovid Goyal
ffc6e65392 Implement scrolling for the diff kitten 2018-05-06 14:41:10 +05:30
Kovid Goyal
d61086907d Add a global debug function to the kittens 2018-05-06 13:48:40 +05:30
Kovid Goyal
5b58c2892d Rename module 2018-05-06 10:35:01 +05:30
Kovid Goyal
67379c67cc More work on the diff kitten 2018-05-05 13:38:28 +05:30
Kovid Goyal
4691257233 ... 2018-05-05 11:19:56 +05:30
Kovid Goyal
d46a8c893a Fix --debug-config failing when no options are changed 2018-05-05 11:18:51 +05:30
Kovid Goyal
577ba0b39d Cleanup 2018-05-05 11:14:20 +05:30
Kovid Goyal
f761245280 pep8 2018-05-05 11:13:32 +05:30
Kovid Goyal
f1b4e585aa Merge branch 'master' of https://github.com/FrancescoMagliocco/kitty 2018-05-05 11:08:18 +05:30
FrancescoMagliocco
255db2cb66 Fixed where output of '--debug-config' would not be aligned if an option key was longer than 20 characters. 2018-05-05 00:39:25 -04:00
Kovid Goyal
8b6240a067 version 0.9.1 2018-05-05 09:46:28 +05:30
Kovid Goyal
f5c713b118 More work on the diff kitten 2018-05-05 09:45:35 +05:30
Kovid Goyal
ac7596b7c5 ... 2018-05-05 07:30:59 +05:30
Kovid Goyal
f69ed984a4 Output GL version when running with --debug-gl
See #521
2018-05-05 07:26:30 +05:30
Kovid Goyal
83850b28ae Update GL wrapper generated by new version of glad 2018-05-04 18:12:31 +05:30
Kovid Goyal
5efa26a98d Micro-optimization 2018-05-04 15:48:58 +05:30
Kovid Goyal
51d2c01bc1 Highlight changed in individual lines in chunks that have the same number of adds/removes 2018-05-04 15:35:11 +05:30
Kovid Goyal
be9d876997 find common prefix and suffix of two strings 2018-05-03 20:15:28 +05:30
Kovid Goyal
866b53f384 More work on the diff kitten 2018-05-03 18:05:55 +05:30
Kovid Goyal
de1a2f7682 Delay load some particularly heavy stdlib modules that are not often needed 2018-05-03 17:53:22 +05:30
Kovid Goyal
86686bfac7 Avoid rendering tab bar more than once per tick 2018-05-03 17:00:18 +05:30
Kovid Goyal
3f316c39d1 Dynamically allocate the scrollback history buffer
Reduces startup memory consumption when using very large scrollback
buffer sizes.
2018-05-03 15:17:02 +05:30
Kovid Goyal
14459a7d18 Fix #506 2018-05-03 11:19:11 +05:30
Kovid Goyal
c062b84bc6 Forgot to remove args.cmd 2018-05-03 11:14:10 +05:30
Kovid Goyal
890a68fa53 Use -c as an alias for --config instead 2018-05-03 11:12:16 +05:30
Kovid Goyal
59017a6e6f Remove the -c command line flag as it is better to use +runpy instead 2018-05-03 11:11:25 +05:30
Kovid Goyal
06cb2cafe6 Add ssh script with terminfo copy to README 2018-05-03 09:40:18 +05:30
Kovid Goyal
e4ed170748 ... 2018-05-03 09:15:05 +05:30
Kovid Goyal
f011a6c86c Disallow use of both macos_titlebar_color and background_opacity
Fixes #519
2018-05-03 09:11:41 +05:30
Kovid Goyal
45fc1ace0c Ignore -e if it is specified on the command line.
This is for compatibility with broken software that assumes terminals
should run with an -e option to execute commands instead of just passing
the commands as arguments. Fixes #502
2018-05-03 08:41:39 +05:30
Kovid Goyal
8d51edec17 Option to disable bell symbol on tab 2018-05-02 22:05:38 +05:30
Kovid Goyal
f2afba2ef4 Change the window border color if a bell occurs in an unfocused window. Can be disabled by setting the bell_border_color to be the same as the inactive_border_color. 2018-05-02 22:02:45 +05:30
Kovid Goyal
efcd3a5df7 Show a bell on the tab if a bell occurs in one of the windows in the tab and the window is not the currently focused window. Fixes #514 2018-05-02 21:44:14 +05:30
Kovid Goyal
2a52acdef4 Fix tab title not changing to reflect the window title when switching between different windows in a tab 2018-05-02 21:07:45 +05:30
Kovid Goyal
eadfeeec9b Fix window focus gained/lost events not being reported to child programs when switching windows/tabs using the various keyboard shortcuts. 2018-05-02 20:50:42 +05:30
Kovid Goyal
f8a581129f Fix #513 2018-05-02 15:52:56 +05:30
Kovid Goyal
93ff651d9e ... 2018-05-02 15:52:10 +05:30
Kovid Goyal
bec0f02b11 More work on the diff kitten 2018-05-02 15:26:47 +05:30
Kovid Goyal
0721951c79 Add hunk title lines 2018-05-02 09:47:24 +05:30
Kovid Goyal
b37d07d31a Update CHANGELOG 2018-05-01 23:41:19 +05:30
Kovid Goyal
9cacfc0c26 macOS: When the macos_titlebar_color is set to background change the titlebar background to match the current background color of the active kitty window 2018-05-01 23:37:48 +05:30
Kovid Goyal
0b93b85cf2 Dont use case range in names.h as it prevents compilation with Visual Studio 2018-05-01 11:27:10 +05:30
Kovid Goyal
f7001ea068 Fix character names for control characters not being read from unicode database
Also allow unicode_names.c to be compiled with python 2 so I can re-use
it in calibre.
2018-05-01 10:13:58 +05:30
Kovid Goyal
aa93c3fb66 Fix passing @text to other programs such as when viewing the scrollback buffer not working correctly if kitty is itself scrolled up. Fixes #509 2018-05-01 07:38:05 +05:30
Kovid Goyal
166c7bf0e6 Update CHANGELOG 2018-04-29 20:16:11 +05:30
Kovid Goyal
bb1374f64e Only define VCS_REV where it is needed 2018-04-29 18:47:14 +05:30
Kovid Goyal
053835afbe Add an option to not request window attention on bell. Fixes #507 2018-04-29 18:16:41 +05:30
Kovid Goyal
4760580d1c More work on the diff kitten 2018-04-29 08:07:03 +05:30
Kovid Goyal
8fa492e0ee More work on the diff kitten 2018-04-28 14:07:24 +05:30
Kovid Goyal
040ae75c77 Remote control: Allow matching windows by number (visible position).
Fixes #501
2018-04-28 11:00:42 +05:30
Kovid Goyal
759aaf294a Remove extra level of indirection in creating iterator 2018-04-28 10:39:59 +05:30
Kovid Goyal
9ea0de868c Parse send_text payload only once at startup 2018-04-27 18:13:37 +05:30
Kovid Goyal
78002cf3ba Update glfw from upstream
Fixes #488
2018-04-27 15:32:54 +05:30
Kovid Goyal
f68f8bf9c9 macOS: Fix changing tab title and kitty shell not working
I forgot that Apple has its own special snowflake readline variant.
Fixes #494
2018-04-27 15:16:26 +05:30
Kovid Goyal
54438cae3f DRYer 2018-04-27 11:19:46 +05:30
Kovid Goyal
2e693c31df Refactor shortcut config parsing
Resolution of kitty_mod and creation of the maps now happens in one
central place after all config is parsed. Fixes #496

Also have --debug-config output multi-key shortcuts nicely as well.
2018-04-27 11:16:58 +05:30
Kovid Goyal
0f06bf1bdb Merge branch 'macos_hide_from_tasks' of https://github.com/cfal/kitty 2018-04-27 08:19:32 +05:30
Alex Lau
b7f4f2e898 Move call to cocoa_window.m 2018-04-26 12:42:56 -07:00
Kovid Goyal
7148ee8a0f When triple-clicking select all wrapped lines belonging to a single logical line. Fixes #492 2018-04-26 10:16:00 +05:30
Kovid Goyal
238508fc0a Update glfw from upstream
Proper cross-platform fix for issue of mouse cursor not being restored
over un-focused windows.
2018-04-26 08:31:52 +05:30
Kovid Goyal
84ab0f5062 More work on the diff kitten 2018-04-26 08:01:12 +05:30
Kovid Goyal
cf04bdb36a Merge branch 'master' of https://github.com/FabioLolix/kitty 2018-04-26 06:58:16 +05:30
FabioLolix
d09a30d16c Updated Readme - Arch package
Kitty has been moved to the Arch community repository
2018-04-25 21:13:17 +02:00
Kovid Goyal
d54b58d2f3 ... 2018-04-25 19:50:40 +05:30
Kovid Goyal
0be0963dc7 Utility method to truncate formatted lines to specified width 2018-04-25 19:34:04 +05:30
Kovid Goyal
61a2360df5 hints kitten: Detect bracketed URLs and dont include the closing bracket in the URL. 2018-04-25 14:33:37 +05:30
Kovid Goyal
b3ed4e3bc2 ... 2018-04-25 09:16:10 +05:30
Kovid Goyal
316e7cb9f5 Also use cwd of child when opening programs via the hints kitten 2018-04-25 09:07:35 +05:30
Kovid Goyal
23851e31bf When calling pass_selection_to_program use the current directory of the child process as the cwd of the program. Fixes #491 2018-04-25 08:30:22 +05:30
Alex Lau
b7b64ec513 Default to off 2018-04-24 17:13:37 -07:00
Kovid Goyal
2b372373ae Update glfw from upstream 2018-04-24 18:15:28 +05:30
Kovid Goyal
7a543fac86 Wire up color handling in the diff kitten 2018-04-24 10:58:15 +05:30
Kovid Goyal
5c4b14468c Apply default colors in diff kitten 2018-04-24 10:33:18 +05:30
Alex Lau
7d4d978d97 Add macos_hide_from_tasks option 2018-04-23 22:02:54 -07:00
Kovid Goyal
fe5b8f3aec Fix using modifiers with subsequent keys in a muti-key shortcut not working 2018-04-24 09:42:37 +05:30
Kovid Goyal
f5abe7cbf7 Add a setting to clear all shortcuts defined up to that point 2018-04-24 09:08:22 +05:30
Kovid Goyal
bd0382830a Add a setting (kitty_mod) to change the modifier used by all the default kitty shortcuts, globally
See #487
2018-04-24 08:53:07 +05:30
Kovid Goyal
0b99bb534f Unicode input: When searching by name search for prefix matches as well as whole word matches
So now hori matches both "hori" and "horizontal". Switched to a
prefix-trie internally.
2018-04-24 07:45:20 +05:30
Kovid Goyal
9d67198ff9 Update path in gen-wcwidth.py 2018-04-23 21:25:33 +05:30
Kovid Goyal
fda57a81e1 Fix Shift+function key not working
Also support pressing F13-f25 function keys. Fixes #485
2018-04-23 11:28:02 +05:30
Kovid Goyal
9962ac2703 ... 2018-04-23 10:54:04 +05:30
Kovid Goyal
edc4b91d15 Expand ~ and env vars in program names. Fixes #483 2018-04-23 10:53:20 +05:30
Kovid Goyal
92ca8ab9cc Fix #482 2018-04-23 07:19:40 +05:30
Kovid Goyal
07e2d9175c Merge branch 'fixconfig' of https://github.com/Abominas/kitty 2018-04-23 06:59:44 +05:30
Abominas
c90dbb25b9 Fix config format crash 2018-04-22 17:35:58 -07:00
Kovid Goyal
b5da109e7f Wire up config infrastructure for diff kitten 2018-04-22 21:44:47 +05:30
Kovid Goyal
aa18429a8c DRYer 2018-04-22 21:22:08 +05:30
Kovid Goyal
e578d41adb Make merge_dicts() re-useable 2018-04-22 21:00:17 +05:30
Kovid Goyal
f3579e78af Make load_config() re-useable 2018-04-22 20:58:43 +05:30
Kovid Goyal
9c43707f40 Make the hinting defaults zero when missing 2018-04-22 20:34:00 +05:30
Kovid Goyal
99ff92a5a7 Don't fail to start if the user deletes the hintstyle key from their fontconfig configuration. Fixes #480 2018-04-22 20:29:46 +05:30
Kovid Goyal
d866c0d202 Fix NULL return without error set 2018-04-22 20:19:21 +05:30
Kovid Goyal
20c42ab519 tui: Allow handlers to finalize() as well as initialize() 2018-04-22 13:14:25 +05:30
Kovid Goyal
8bbc2b82d4 More work on the diff kitten 2018-04-22 13:05:58 +05:30
Kovid Goyal
6929358976 More work on the diff kitten 2018-04-21 15:45:16 +05:30
Kovid Goyal
82acd6b511 Add a note on how to create a script to launch kitty
Useful when running from source
2018-04-21 15:33:21 +05:30
Kovid Goyal
bb939ed4f8 Remove unused code 2018-04-20 22:19:31 +05:30
Kovid Goyal
cd25abc49f ... 2018-04-20 22:06:42 +05:30
Kovid Goyal
2e90e443ed Fix #476 2018-04-20 21:33:24 +05:30
Kovid Goyal
92bb6e293a Add threaded job infrastructure to tui 2018-04-20 21:12:10 +05:30
Kovid Goyal
ea9b431386 Allow changing the source used by test_compile 2018-04-20 21:02:05 +05:30
Kovid Goyal
23077fd6a6 DRYer 2018-04-20 20:56:29 +05:30
Kovid Goyal
5eee7ce338 Fix #475 2018-04-20 20:32:16 +05:30
Kovid Goyal
784894cac3 xkbcommon.org's HTTPS certificate has expired 2018-04-20 18:39:27 +05:30
Kovid Goyal
757983b729 Another ParseTuple mismatch 2018-04-20 18:29:55 +05:30
Kovid Goyal
b6660353cd Merge branch 'bigendian-fixes' of https://github.com/jamessan/kitty 2018-04-20 18:26:26 +05:30
James McCoy
215a7689d9 Correct types for various PyArg_ParseTuple/Py_BuildValue calls
Debian's [mips] build failed one of the font tests:

    FAIL: test_sprite_map (kitty_tests.fonts.Rendering)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/<<PKGBUILDDIR>>/kitty_tests/fonts.py", line 48, in test_sprite_map
        self.ae(test_sprite_position_for(0, 1), (0, 1, 1))
        self = <kitty_tests.fonts.Rendering testMethod=test_sprite_map>
    AssertionError: Tuples differ: (0, 0, 0) != (0, 1, 1)

    First differing element 1:
    0
    1

    - (0, 0, 0)
    ?     ^  ^

    + (0, 1, 1)
    ?     ^  ^

This was due to test_sprite_position_for() using the "I" code to parse
into an unsigned short.  Since mips is big endian, the "wrong" byte was
stored into extra_glyphs.data.

This commit changes all similar data type mismatches that were found
from auditing calls to PyArg_ParseTuple/Py_BuildValue.

[mips]: https://buildd.debian.org/status/fetch.php?pkg=kitty&arch=mips&ver=0.9.0-1&stamp=1524126606&raw=0
2018-04-20 08:28:26 -04:00
Kovid Goyal
dd18a1dfd8 Simplify is_ascii_control_char
No need to use macros for unsigned char
2018-04-20 14:30:07 +05:30
Kovid Goyal
32f16ee5f5 macOS: Add support for dead keys
I have modified GLFW to support dead keys on macOS. That was painful.
Fixes #465
2018-04-20 14:25:53 +05:30
Kovid Goyal
d4f426d2ce Merge branch 'unsigned-char-build' of https://github.com/jamessan/kitty 2018-04-20 10:15:13 +05:30
Kovid Goyal
a6d96c9a14 Fix #471 2018-04-20 09:32:01 +05:30
Kovid Goyal
bf3bfeaf69 Merge branch '32bit-long-fix' of https://github.com/jamessan/kitty 2018-04-20 09:24:18 +05:30
Kovid Goyal
6ce82177ea Merge branch 'remove-sysctl-include' of https://github.com/jamessan/kitty 2018-04-20 09:21:47 +05:30
James McCoy
dc6efb5b37 Use uint64_t to detect out of range graphics command id
parse_graphics_code() uses an unsigned long to detect if the id is
"> UINT32_MAX", however sizeof(long) == sizeof(void *) on Linux systems.
So this condition will be impossible on any 32-bit system.
2018-04-19 20:32:48 -04:00
James McCoy
b26d1f306d travis: Force unsigned char on one build 2018-04-19 20:23:11 -04:00
James McCoy
cf678d8179 Remove unnecessary <sys/sysctl.h> include
The code that required this include was removed in
66803e6873, and the include is causing
build failures for some of Debian's architectures.
2018-04-19 19:14:40 -04:00
Kovid Goyal
8cecb22481 Add a link to the debian package
See #263
2018-04-19 19:42:46 +05:30
Kovid Goyal
2504266806 macOS: Fix option as alt not working for keys that act as dead keys in the current keyboard layout 2018-04-19 17:44:31 +05:30
Kovid Goyal
e3795eb07e Dont add CPPFLAGS from the python build
They contain include paths specific to the python build such as -I. that
are not appropriate for building kitty. Also move cppflags in front of
cflags when invoking the compiler.
2018-04-19 10:33:10 +05:30
Kovid Goyal
796ead5d75 Merge branch 'support-cppflags' of https://github.com/jamessan/kitty 2018-04-19 10:27:22 +05:30
Kovid Goyal
d5907a1bf9 More work on the diff kitten 2018-04-19 10:26:14 +05:30
James McCoy
ddd4774169 setup.py: Support specifying CPPFLAGS distinctly from CFLAGS 2018-04-18 23:08:45 -04:00
Kovid Goyal
1dc83a47cf When rendering a private use unicode codepoint and a space as a two cell
ligature, set the foreground colors of the space cell to match the
colors of the first cell. Works around broken applications like
powerline that use different colors for the two cells. Fixes #467
2018-04-19 07:30:36 +05:30
Kovid Goyal
0a8dc4be0a Start work on the diff kitten 2018-04-17 17:43:56 +05:30
Kovid Goyal
3d99361790 Add a note about dotted and dashed underline styles 2018-04-17 08:21:28 +05:30
Kovid Goyal
1e278938cc Fix #463 2018-04-16 21:51:55 +05:30
Kovid Goyal
ba177c61bc ... 2018-04-16 16:46:42 +05:30
127 changed files with 44018 additions and 36603 deletions

1
.ignore Normal file
View File

@@ -0,0 +1 @@
kittens/unicode_input/names.h

View File

@@ -6,7 +6,7 @@ matrix:
group: beta
sudo: false
env:
- CC=gcc SANITIZE_ARG=--sanitize
- CC=gcc CFLAGS=-funsigned-char SANITIZE_ARG=--sanitize
language: python
python: "3.5"
addons:
@@ -96,7 +96,7 @@ install: |
fi
else
pushd /tmp
wget http://xkbcommon.org/download/libxkbcommon-0.6.1.tar.xz
wget --no-check-certificate http://xkbcommon.org/download/libxkbcommon-0.6.1.tar.xz
tar xf libxkbcommon-0.6.1.tar.xz
cd libxkbcommon-0.6.1
./configure --prefix=$HOME/harfbuzz --disable-dependency-tracking --disable-static

View File

@@ -3,6 +3,155 @@ Changelog
kitty is a feature full, cross-platform, *fast*, GPU based terminal emulator.
version 0.10.1 [2018-05-24]
------------------------------
- Add a kitten to easily ssh into servers that automatically copies the
terminfo files over. ``kitty +kitten ssh myserver``.
- diff kitten: Make the keyboard shortcuts configurable
(`#563 <https://github.com/kovidgoyal/kitty/issues/563>`_)
- Allow controlling *background_opacity* via either keyboard shortcuts or
remote control. Note that you must set *dynamic_background_opacity yes* in
kitty.conf first. (`#569 <https://github.com/kovidgoyal/kitty/issues/569>`_)
- diff kitten: Add keybindings to scroll by page
- diff kitten: Fix incorrect syntax highlighting for a few file formats such as
yaml
- macOS: Fix regression that caused the *macos_option_as_alt* setting to always
be disabled for all OS windows in a kitty instance after the first window
(`#571 <https://github.com/kovidgoyal/kitty/issues/571>`_)
- Fix Ctrl+Alt+Space not working in normal and application keyboard modes
(`#562 <https://github.com/kovidgoyal/kitty/issues/562>`_)
version 0.10.0 [2018-05-21]
------------------------------
- A diff kitten to show side-by-side diffs with syntax highlighting and support
for images. See `diff README
<https://github.com/kovidgoyal/kitty/blob/master/kittens/diff/README.asciidoc>`_.
- Make windows in the various kitty layouts manually resizable. See the
`Layouts section of the README
<https://github.com/kovidgoyal/kitty#layouts>`_ for details.
- Implement support for the SGR *faint* escape code to make text blend
into the background (`#446 <https://github.com/kovidgoyal/kitty/issues/446>`_).
- Make the hints kitten a little `smarter
<https://github.com/kovidgoyal/kitty/commit/ad1109b6fe0a6802ca4f77182a7a0b36086b3e9f>`_,
so that URLs that stretch over multiple lines are detected. Also improve
detection of surrounding brackets/quotes.
- Make the kitty window id available as the environment variable
`KITTY_WINDOW_ID` (`#532 <https://github.com/kovidgoyal/kitty/issues/532>`_).
- Add a "fat" layout that is similar to the "tall" layout but vertically
oriented.
- Expand environment variables in config file include directives
- Allow programs running in kitty to `read/write from the clipboard
<https://github.com/kovidgoyal/kitty/commit/889ca7791244253cb08fbc3eca8883a87fb943a7>`_.
By default only writing is allowed. This feature is supported in many
terminals, search for `OSC 52 clipboard` to find out more about using it.
- Fix moving cursor outside a defined page area incorrectly causing the cursor
to be placed inside the page area. Caused incorrect rendering in neovim, in
some situations (`#542 <https://github.com/kovidgoyal/kitty/issues/542>`_).
- Render a couple more powerline symbols directly, bypassing the font
(`#550 <https://github.com/kovidgoyal/kitty/issues/550>`_).
- Fix ctrl+alt+<special> not working in normal and application keyboard (`#548
<https://github.com/kovidgoyal/kitty/issues/548>`_).
- Partial fix for rendering Right-to-left languages like Arabic. Rendering of
Arabic is never going to be perfect, but now it is at least readable.
- Fix Ctrl+backspace acting as plain backspace in normal and application
keyboard modes (`#538 <https://github.com/kovidgoyal/kitty/issues/538>`_).
- Have the paste_from_selection action paste from the clipboard on platforms
that do not have a primary selection such as Wayland and macOS
(`#529 <https://github.com/kovidgoyal/kitty/issues/529>`_)
- Fix cursor_stop_blinking_after=0 not working (`#530 <https://github.com/kovidgoyal/kitty/issues/530>`_)
version 0.9.1 [2018-05-05]
------------------------------
- Show a bell symbol on the tab if a bell occurs in one of the windows in the tab and
the window is not the currently focused window
- Change the window border color if a bell occurs in an unfocused window. Can
be disabled by setting the bell_border_color to be the same as the
inactive_border_color.
- macOS: Add support for dead keys
- Unicode input: When searching by name search for prefix matches as well as
whole word matches
- Dynamically allocate the memory used for the scrollback history buffer.
Reduces startup memory consumption when using very large scrollback
buffer sizes.
- Add an option to not request window attention on bell.
- Remote control: Allow matching windows by number (visible position).
- macOS: Fix changing tab title and kitty shell not working
- When triple-clicking select all wrapped lines belonging to a single logical line.
- hints kitten: Detect bracketed URLs and dont include the closing bracket in the URL.
- When calling pass_selection_to_program use the current directory of the child
process as the cwd of the program.
- Add macos_hide_from_tasks option to hide kitty from the macOS task switcher
- macOS: When the macos_titlebar_color is set to background change the titlebar
colors to match the current background color of the active kitty window
- Add a setting to clear all shortcuts defined up to that point in the config
file(s)
- Add a setting (kitty_mod) to change the modifier used by all the default
kitty shortcuts, globally
- Fix Shift+function key not working
- Support the F13 to F25 function keys
- Don't fail to start if the user deletes the hintstyle key from their
fontconfig configuration.
- When rendering a private use unicode codepoint and a space as a two cell
ligature, set the foreground colors of the space cell to match the colors of
the first cell. Works around applications like powerline that use different
colors for the two cells.
- Fix passing @text to other programs such as when viewing the scrollback
buffer not working correctly if kitty is itself scrolled up.
- Fix window focus gained/lost events not being reported to child programs when
switching windows/tabs using the various keyboard shortcuts.
- Fix tab title not changing to reflect the window title when switching between different windows in a tab
- Ignore -e if it is specified on the command line. This is for compatibility
with broken software that assumes terminals should run with an -e option to
execute commands instead of just passing the commands as arguments.
version 0.9.0 [2018-04-15]
------------------------------

View File

@@ -13,6 +13,11 @@
:sc_fourth_window: pass:quotes[`ctrl+shift+4`]
:sc_increase_font_size: pass:quotes[`ctrl+shift+equal`]
:sc_input_unicode_character: pass:quotes[`ctrl+shift+u`]
:sc_kitten_hints: pass:quotes[`ctrl+shift+e`]
:sc_kitten_hints_type_line_program: pass:quotes[`ctrl+shift+p → l`]
:sc_kitten_hints_type_path: pass:quotes[`ctrl+shift+p → shift+f`]
:sc_kitten_hints_type_path_program: pass:quotes[`ctrl+shift+p → f`]
:sc_kitten_hints_type_word_program: pass:quotes[`ctrl+shift+p → w`]
:sc_kitty_shell_window: pass:quotes[`ctrl+shift+escape`]
:sc_move_tab_backward: pass:quotes[`ctrl+shift+,`]
:sc_move_tab_forward: pass:quotes[`ctrl+shift+.`]
@@ -32,11 +37,6 @@
:sc_previous_tab: pass:quotes[`ctrl+shift+left`]
:sc_previous_window: pass:quotes[`ctrl+shift+[`]
:sc_restore_font_size: pass:quotes[`ctrl+shift+backspace`]
:sc_run_kitten_text_hints: pass:quotes[`ctrl+shift+e`]
:sc_run_kitten_text_hints_type_line_program: pass:quotes[`ctrl+shift+p → l`]
:sc_run_kitten_text_hints_type_path: pass:quotes[`ctrl+shift+p → shift+f`]
:sc_run_kitten_text_hints_type_path_program: pass:quotes[`ctrl+shift+p → f`]
:sc_run_kitten_text_hints_type_word_program: pass:quotes[`ctrl+shift+p → w`]
:sc_scroll_end: pass:quotes[`ctrl+shift+end`]
:sc_scroll_home: pass:quotes[`ctrl+shift+home`]
:sc_scroll_line_down: pass:quotes[`ctrl+shift+down` or `ctrl+shift+j`]
@@ -44,10 +44,15 @@
:sc_scroll_page_down: pass:quotes[`ctrl+shift+page_down`]
:sc_scroll_page_up: pass:quotes[`ctrl+shift+page_up`]
:sc_second_window: pass:quotes[`ctrl+shift+2`]
:sc_set_background_opacity_0_1: pass:quotes[`ctrl+shift+a → l`]
:sc_set_background_opacity_1: pass:quotes[`ctrl+shift+a → 1`]
:sc_set_background_opacity_default: pass:quotes[`ctrl+shift+a → d`]
:sc_set_background_opacity_plus0_1: pass:quotes[`ctrl+shift+a → m`]
:sc_set_tab_title: pass:quotes[`ctrl+shift+alt+t`]
:sc_seventh_window: pass:quotes[`ctrl+shift+7`]
:sc_show_scrollback: pass:quotes[`ctrl+shift+h`]
:sc_sixth_window: pass:quotes[`ctrl+shift+6`]
:sc_start_resizing_window: pass:quotes[`ctrl+shift+r`]
:sc_tenth_window: pass:quotes[`ctrl+shift+0`]
:sc_third_window: pass:quotes[`ctrl+shift+3`]
:sc_toggle_fullscreen: pass:quotes[`ctrl+shift+f11`]
@@ -73,7 +78,7 @@ link:#layouts[layouts] without needing to use an extra program like tmux
* Can be link:remote-control.asciidoc[controlled from scripts or the shell prompt], even over SSH.
* Has a framework for _kittens_, small terminal programs that can be used to extend kitty's functionality.
For example, they are used for link:#unicode-input[Unicode input] and link:#url-hints[Hints].
For example, they are used for link:#unicode-input[Unicode input], link:#hints[Hints] and link:https://github.com/kovidgoyal/kitty/blob/master/kittens/diff/README.asciidoc[Side-by-side diff].
* Supports link:#startup-sessions[startup sessions] which allow you to specify the window/tab layout,
working directories and programs to run on startup.
@@ -87,7 +92,7 @@ window using arbitrary programs of your choice. This is useful for browsing
the history comfortably in a pager or editor.
image::screenshot.png?raw=true[Screenshot, showing three programs in the "Tall" layout]
image::screenshots/screenshot.png?raw=true[Screenshot, showing three programs in the "Tall" layout]
toc::[]
@@ -106,9 +111,10 @@ the following dependencies are installed first.
* freetype (not needed on macOS)
* fontconfig (not needed on macOS)
* ImageMagick (optional, needed to use the `kitty icat` tool to display images in the terminal)
* pygments (optional, need for syntax highlighting in `kitty +kitten diff`)
* gcc or clang (required only for building)
* pkg-config (required only for building)
* For building on Linux in addition to the above dependencies you might also need to install the `-dev` packages for `xcursor`, `xrandr`, `xinerama`, `libgl1-mesa` and `xkbcommon-x11`, if they are not already installed by your distro.
* For building on Linux in addition to the above dependencies you might also need to install the `-dev` packages for `xcursor`, `xrandr`, `libxi`, `xinerama`, `libgl1-mesa` and `xkbcommon-x11`, if they are not already installed by your distro.
=== Install and run from source
@@ -116,7 +122,7 @@ the following dependencies are installed first.
git clone https://github.com/kovidgoyal/kitty && cd kitty
....
Now build the C parts of kitty with the following command:
Now build the native code parts of kitty with the following command:
....
make
@@ -128,11 +134,23 @@ You can run kitty, as:
python3 .
....
If that works, you can create a script to launch kitty:
....
#!/usr/bin/env python3
import runpy
runpy.run_path('/path/to/kitty/dir', run_name='__main__')
....
And place it in `~/bin` or `/usr/bin` so that you can run kitty using
just `kitty`.
=== Linux packages
* Arch Linux: AUR git package https://aur.archlinux.org/packages/kitty-git/
* Arch Linux: https://www.archlinux.org/packages/community/x86_64/kitty/
* Arch Linux: AUR stable package https://aur.archlinux.org/packages/kitty/
* Arch Linux: AUR git package https://aur.archlinux.org/packages/kitty-git/
* NixOS / nixpkgs: https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/misc/kitty/default.nix
@@ -140,6 +158,8 @@ python3 .
* openSUSE: https://build.opensuse.org/package/show/X11:terminals/kitty
* Debian: https://packages.debian.org/buster/kitty
=== macOS packages
kitty is available as a macOS `dmg` file for easy installation from the
@@ -246,6 +266,10 @@ windows are:
|Pass current selection to program | {sc_pass_selection_to_program}
|Edit kitty config file | {sc_edit_config_file}
|Open a kitty shell | {sc_kitty_shell}
|Increase background opacity | {sc_set_background_opacity_plus0_1}
|Decrease background opacity | {sc_set_background_opacity_0_1}
|Full background opacity | {sc_set_background_opacity_1}
|Reset background opacity | {sc_set_background_opacity_default}
|===
@@ -278,7 +302,7 @@ the scrollback pager comfortably within the pager.
You can input unicode characters by name, hex code, recently used and even an editable favorites list.
Press {sc_input_unicode_character} to start the unicode input widget, shown below.
image::unicode.png?raw=true[Unicode input widget, showing selection of unicode characters by name]
image::screenshots/unicode.png?raw=true[Unicode input widget, showing selection of unicode characters by name]
In Code mode, you enter a unicode character by typing in the hex code for the
character and pressing enter, for example, type in 2716 and press enter to get
@@ -297,7 +321,7 @@ visible on the screen. For example, you can press {sc_run_kitten_text_hints}
to choose any URL visible on the screen and then open it using your system
browser.
image::hints_mode.png?raw=true[URL hints mode]
image::screenshots/hints_mode.png?raw=true[URL hints mode]
Similarly, you can press {sc_run_kitten_text_hints_type_path_program} to
select anything that looks like a path or filename and then insert it into the
@@ -320,6 +344,7 @@ Currently, there are five 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
* Grid -- All windows are shown in a grid
* Horizontal -- All windows are shown side-by-side
* Vertical -- All windows are shown one below the other
@@ -328,7 +353,25 @@ You can switch between layouts using the {sc_next_layout} key combination. You c
also create shortcuts to select particular layouts, and choose which layouts
you want to enable/disable, see link:kitty/kitty.conf[kitty.conf] for examples.
Writing a new layout only requires about fifty lines of code, so if there is
You can resize windows inside layouts. Press {sc_start_resizing_window} 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.
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 kitty.conf 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 link:kitty/layout.py[layout.py] and submit
a pull request!
@@ -361,7 +404,8 @@ layout tall
cd ~
# Create a window and run the specified command in it
launch zsh
launch vim
# Create a window with some environment variables set and run vim in it
launch env FOO=BAR vim
# Set the title for the next window
title Chat with x
launch irssi --profile x
@@ -525,8 +569,16 @@ for why kitty does not support background color erase.
=== I get errors about the terminal being unknown or opening the terminal failing when SSHing into a different computer?
This happens because the kitty terminfo files are not available on the server.
Either install kitty on the server, or simply copy over the terminfo files,
using:
You can ssh in using the following command which will automatically copy the
terminfo files to the server:
....
kitty +kitten ssh myserver
....
If for some reason that does not work (typically because the server is using a
very limited shell), you can use the following one-liner instead (it is slower
as it needs to ssh into the server twice, but will work with most servers).
....
infocmp xterm-kitty | ssh myserver tic -x -o \~/.terminfo /dev/stdin
@@ -559,6 +611,20 @@ You can, of course, also run kitty from a terminal with command line options, us
And within kitty itself, you can always run kitty using just `kitty` as it
cleverly adds itself to the PATH.
=== kitty is not able to use my favorite font?
kitty achieves its stellar performance by caching alpha masks of each rendered
character on the GPU, so that every character needs to be rendered only once.
This means it is a strictly character cell based display. As such it can use
only monospace fonts, since every cell in the grid has to be the same size. If
your font is not listed in `kitty list-fonts` it means that it is not
monospace. On Linux you can list all monospace fonts with,
```
fc-list : family spacing | grep spacing=100
```
== Resources on terminal behavior
http://invisible-island.net/xterm/ctlseqs/ctlseqs.html

View File

@@ -6,7 +6,7 @@ import sys
def icat(args):
from kitty.icat import main
from kittens.icat.main import main
main(args)

View File

@@ -44,7 +44,8 @@ def parse_ucd():
def add_word(w, c):
if c <= 32 or c == 127 or 128 <= c <= 159:
return
word_search_map[w.lower()].add(c)
if len(w) > 1:
word_search_map[w.lower()].add(c)
first = None
for word, c in html5.items():
@@ -54,7 +55,9 @@ def parse_ucd():
for line in get_data('ucd/UnicodeData.txt'):
parts = [x.strip() for x in line.split(';')]
codepoint = int(parts[0], 16)
name = parts[1]
name = parts[1] or parts[10]
if name == '<control>':
name = parts[10]
if name:
name_map[codepoint] = name
for word in name.lower().split():
@@ -207,7 +210,8 @@ def codepoint_to_mark_map(p, mark_map):
for spec in get_ranges(mark_map):
if isinstance(spec, tuple):
s = rmap[spec[0]]
p(f'\t\tcase {spec[0]} ... {spec[1]}: return {s} + c - {spec[0]};')
cases = ' '.join(f'case {i}:' for i in range(spec[0], spec[1]+1))
p(f'\t\t{cases} return {s} + c - {spec[0]};')
else:
p(f'\t\tcase {spec}: return {rmap[spec]};')
p('default: return 0;')
@@ -215,10 +219,12 @@ def codepoint_to_mark_map(p, mark_map):
return rmap
def classes_to_regex(classes):
def classes_to_regex(classes, exclude=''):
chars = set()
for c in classes:
chars |= class_maps[c]
for c in map(ord, exclude):
chars.discard(c)
def as_string(codepoint):
if codepoint < 256:
@@ -256,53 +262,111 @@ def gen_ucd():
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]
))
with open('kittens/url_hints/url_regex.py', 'w') as f:
f.write("url_delimiters = '{}' # noqa".format(''.join(classes_to_regex(cz))))
with open('kittens/hints/url_regex.py', 'w') as f:
f.write("url_delimiters = '{}' # noqa".format(''.join(classes_to_regex(cz, exclude='\n'))))
def gen_names():
words = tuple(sorted(word_search_map))
with create_header('kittens/unicode_input/names.h') as p:
cp_map = list(sorted(name_map))
p(f'static const char* name_map[{len(cp_map)}] = {{' ' // {{{')
for cp in cp_map:
mark_to_cp = list(sorted(name_map))
cp_to_mark = {cp: m for m, cp in enumerate(mark_to_cp)}
# Mapping of mark to codepoint name
p(f'static const char* name_map[{len(mark_to_cp)}] = {{' ' // {{{')
for cp in mark_to_cp:
w = name_map[cp].replace('"', '\\"')
p(f'\t"{w}",')
p("}; // }}}\n")
p(f'static const char* idx_to_word[{len(words)}] = ' '{ // {{{')
for s in words:
s = s.replace('"', '\\"')
p(f'\t"{s}",')
p("}; // }}}\n")
first_letters = {ord(w[0]) for w in words if ord(w[0]) < 256}
wmap = {w: i for i, w in enumerate(words)}
p(f'static const unsigned short* words_for_first_letter[256] = ' '{ // {{{')
for fl in range(0, 256):
if fl in first_letters:
winds = [str(wmap[w]) for w in words if w.startswith(chr(fl))]
p(f'\t(const unsigned short[{len(winds) + 1}]){{{len(winds)}, ', ', '.join(winds), '},')
else:
p('NULL,')
p("}; // }}}\n")
p(f'static const char_type* codepoints_for_word_idx[{len(words)}] = ' '{ // {{{')
for s in words:
cps = word_search_map[s]
a = ', '.join(map(str, cps))
p(f'\t(const char_type[{len(cps) + 1}]){{{len(cps)}, ', a, '},')
p("}; // }}}\n")
# Mapping of mark to codepoint
p(f'static const char_type mark_to_cp[{len(mark_to_cp)}] = {{' ' // {{{')
p(', '.join(map(str, mark_to_cp)))
p('}; // }}}\n')
# Function to get mark number for codepoint
p('static char_type mark_for_codepoint(char_type c) {')
codepoint_to_mark_map(p, cp_map)
codepoint_to_mark_map(p, mark_to_cp)
p('}\n')
p('static inline const char* name_for_codepoint(char_type cp) {')
p('\tchar_type m = mark_for_codepoint(cp); if (m == 0) return NULL;')
p('\treturn name_map[m];')
p('}\n')
# Array of all words
word_map = tuple(sorted(word_search_map))
word_rmap = {w: i for i, w in enumerate(word_map)}
p(f'static const char* all_words_map[{len(word_map)}] = {{' ' // {{{')
cwords = (w.replace('"', '\\"') for w in word_map)
p(', '.join(f'"{w}"' for w in cwords))
p('}; // }}}\n')
# Array of sets of marks for each word
word_to_marks = {word_rmap[w]: frozenset(map(cp_to_mark.__getitem__, cps)) for w, cps in word_search_map.items()}
all_mark_groups = frozenset(word_to_marks.values())
array = [0]
mg_to_offset = {}
for mg in all_mark_groups:
mg_to_offset[mg] = len(array)
array.append(len(mg))
array.extend(sorted(mg))
p(f'static const char_type mark_groups[{len(array)}] = {{' ' // {{{')
p(', '.join(map(str, array)))
p('}; // }}}\n')
offsets_array = []
for wi, w in enumerate(word_map):
mg = word_to_marks[wi]
offsets_array.append(mg_to_offset[mg])
p(f'static const char_type mark_to_offset[{len(offsets_array)}] = {{' ' // {{{')
p(', '.join(map(str, offsets_array)))
p('}; // }}}\n')
# The trie
p(f'typedef struct {{ uint32_t children_offset; uint32_t match_offset; }} word_trie;\n')
all_trie_nodes = []
class TrieNode:
def __init__(self):
self.match_offset = 0
self.children_offset = 0
self.children = {}
def add_letter(self, letter):
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):
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 = word_map[word_idx]
parent = root
for letter in map(ord, word):
idx = parent.add_letter(letter)
parent = all_trie_nodes[idx]
parent.match_offset = offsets_array[word_idx]
for i in range(len(word_map)):
add_word(i)
children_array = [0]
for node in all_trie_nodes:
if node.children:
node.children_offset = len(children_array)
children_array.append(len(node.children))
for letter, child_offset in node.children.items():
children_array.append((child_offset << 8) | (letter & 0xff))
p(f'static const word_trie all_trie_nodes[{len(all_trie_nodes)}] = {{' ' // {{{')
p(',\n'.join(map(str, all_trie_nodes)))
p('\n}; // }}}\n')
p(f'static const uint32_t children_array[{len(children_array)}] = {{' ' // {{{')
p(', '.join(map(str, children_array)))
p('}; // }}}\n')
def gen_wcwidth():
seen = set()

View File

@@ -27,6 +27,10 @@ def regenerate():
)
def strip_trailing_whitespace(c):
return re.sub(r'\s+$', '', c, flags=re.MULTILINE) + '\n'
def export():
c = open('out/src/glad.c', 'rb').read().decode('utf-8')
functions = []
@@ -48,12 +52,13 @@ init_glad(GLADloadproc load, int debug) {
}
return ret;
}'''.replace('SUB', ';\n '.join(switch), 1)
open('../kitty/gl-wrapper.c', 'w').write(c)
open('../kitty/gl-wrapper.c', 'w').write(strip_trailing_whitespace(c))
raw = open('out/include/glad/glad.h').read()
raw = raw.replace('<KHR/khrplatform.h>', '"khrplatform.h"')
raw += '\nint init_glad(GLADloadproc, int);\n'
open('../kitty/gl-wrapper.h', 'w').write(raw)
shutil.copy2('out/include/KHR/khrplatform.h', '../kitty')
open('../kitty/gl-wrapper.h', 'w').write(strip_trailing_whitespace(raw))
raw = open('out/include/KHR/khrplatform.h', 'rb').read().decode('utf-8')
open('../kitty/khrplatform.h', 'w').write(strip_trailing_whitespace(raw))
if __name__ == '__main__':

View File

@@ -208,6 +208,9 @@ static GLFWbool updateUnicodeDataNS(void)
_glfw.ns.unicodeData = nil;
}
for (_GLFWwindow *window = _glfw.windowListHead; window; window = window->next)
window->ns.deadKeyState = 0;
_glfw.ns.inputSource = TISCopyCurrentKeyboardLayoutInputSource();
if (!_glfw.ns.inputSource)
{

View File

@@ -37,6 +37,7 @@ typedef void* id;
#endif
typedef VkFlags VkMacOSSurfaceCreateFlagsMVK;
typedef int (* GLFWcocoatextinputfilterfun)(int,int,int);
typedef struct VkMacOSSurfaceCreateInfoMVK
{
@@ -98,6 +99,10 @@ typedef struct _GLFWwindowNS
// This is kept to counteract Cocoa doing the same internally
double cursorWarpDeltaX, cursorWarpDeltaY;
// The text input filter callback
GLFWcocoatextinputfilterfun textInputFilterCallback;
// Dead key state
UInt32 deadKeyState;
} _GLFWwindowNS;
// Cocoa-specific global data

View File

@@ -128,6 +128,32 @@ static void updateCursorImage(_GLFWwindow* window)
hideCursor(window);
}
// Apply chosen cursor mode to a focused window
//
static void updateCursorMode(_GLFWwindow* window)
{
if (window->cursorMode == GLFW_CURSOR_DISABLED)
{
_glfw.ns.disabledCursorWindow = window;
_glfwPlatformGetCursorPos(window,
&_glfw.ns.restoreCursorPosX,
&_glfw.ns.restoreCursorPosY);
centerCursor(window);
CGAssociateMouseAndMouseCursorPosition(false);
}
else if (_glfw.ns.disabledCursorWindow == window)
{
_glfw.ns.disabledCursorWindow = NULL;
CGAssociateMouseAndMouseCursorPosition(true);
_glfwPlatformSetCursorPos(window,
_glfw.ns.restoreCursorPosX,
_glfw.ns.restoreCursorPosY);
}
if (cursorInClientArea(window))
updateCursorImage(window);
}
// Transforms the specified y-coordinate between the CG display and NS screen
// coordinate systems
//
@@ -423,7 +449,7 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
centerCursor(window);
_glfwInputWindowFocus(window, GLFW_TRUE);
_glfwPlatformSetCursorMode(window, window->cursorMode);
updateCursorMode(window);
}
- (void)windowDidResignKey:(NSNotification *)notification
@@ -719,18 +745,98 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
[super updateTrackingAreas];
}
static inline UInt32
convert_cocoa_to_carbon_modifiers(NSUInteger flags) {
UInt32 mods = 0;
if (flags & NSEventModifierFlagShift)
mods |= shiftKey;
if (flags & NSEventModifierFlagControl)
mods |= controlKey;
if (flags & NSEventModifierFlagOption)
mods |= optionKey;
if (flags & NSEventModifierFlagCommand)
mods |= cmdKey;
if (flags & NSEventModifierFlagCapsLock)
mods |= alphaLock;
return (mods >> 8) & 0xFF;
}
static inline void
convert_utf16_to_utf8(UniChar *src, UniCharCount src_length, char *dest, size_t dest_sz) {
CFStringRef string = CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault,
src,
src_length,
kCFAllocatorNull);
CFStringGetCString(string,
dest,
dest_sz,
kCFStringEncodingUTF8);
CFRelease(string);
}
static inline GLFWbool
is_ascii_control_char(char x) {
return x == 0 || (1 <= x && x <= 31) || x == 127;
}
- (void)keyDown:(NSEvent *)event
{
const unsigned int scancode = [event keyCode];
const NSUInteger flags = [event modifierFlags];
const int mods = translateFlags(flags);
const int key = translateKey(scancode, GLFW_TRUE);
const int mods = translateFlags([event modifierFlags]);
const GLFWbool process_text = !window->ns.textInputFilterCallback || window->ns.textInputFilterCallback(key, mods, scancode) != 1;
_glfw.ns.text[0] = 0;
// this will call insertText with the text for this event, if any
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
if ((1 <= _glfw.ns.text[0] && _glfw.ns.text[0] <= 31) || (unsigned)_glfw.ns.text[0] == 127) _glfw.ns.text[0] = 0; // dont send text for ascii control codes
debug_key(@"scancode: 0x%x (%s) mods: %stext: %s glfw_key: %s\n",
if (!_glfw.ns.unicodeData) {
// Using the cocoa API for key handling is disabled, as there is no
// reliable way to handle dead keys using it. Only use it if the
// keyboard unicode data is not available.
if (process_text) {
// this will call insertText with the text for this event, if any
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
}
} else {
static UniChar text[256];
UniCharCount char_count = 0;
if (UCKeyTranslate(
[(NSData*) _glfw.ns.unicodeData bytes],
scancode,
kUCKeyActionDown,
convert_cocoa_to_carbon_modifiers(flags),
LMGetKbdType(),
(process_text ? 0 : kUCKeyTranslateNoDeadKeysMask),
&(window->ns.deadKeyState),
sizeof(text)/sizeof(text[0]),
&char_count,
text
) != noErr) {
debug_key(@"UCKeyTranslate failed for scancode: 0x%x (%s) %s\n", scancode, safe_name_for_scancode(scancode), format_mods(mods));
window->ns.deadKeyState = 0;
return;
}
if (process_text) {
// We check if cocoa wants to insert text, as UCKeyTranslate
// inserts text even when the cmd key is pressed. For instance,
// cmd+a will result in the text a.
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
debug_key(@"char_count: %lu cocoa text: %s\n", char_count, format_text(_glfw.ns.text));
GLFWbool cocoa_wants_to_insert_text = !is_ascii_control_char(_glfw.ns.text[0]);
_glfw.ns.text[0] = 0;
if (char_count && cocoa_wants_to_insert_text) convert_utf16_to_utf8(text, char_count, _glfw.ns.text, sizeof(_glfw.ns.text));
} else {
window->ns.deadKeyState = 0;
}
if (window->ns.deadKeyState && char_count == 0) {
debug_key(@"Ignoring dead key. deadKeyState: 0x%x scancode: 0x%x (%s) %s\n",
window->ns.deadKeyState, scancode, safe_name_for_scancode(scancode), format_mods(mods));
return;
}
}
debug_key(@"scancode: 0x%x (%s) %stext: %s glfw_key: %s\n",
scancode, safe_name_for_scancode(scancode), format_mods(mods),
format_text(_glfw.ns.text), _glfwGetKeyName(key));
if (is_ascii_control_char(_glfw.ns.text[0])) _glfw.ns.text[0] = 0; // dont send text for ascii control codes
_glfwInputKeyboard(window, key, scancode, GLFW_PRESS, mods, _glfw.ns.text, 0);
}
@@ -1233,6 +1339,7 @@ int _glfwPlatformCreateWindow(_GLFWwindow* window,
const _GLFWctxconfig* ctxconfig,
const _GLFWfbconfig* fbconfig)
{
window->ns.deadKeyState = 0;
if (!initializeAppKit())
return GLFW_FALSE;
@@ -1732,26 +1839,8 @@ void _glfwPlatformSetCursorPos(_GLFWwindow* window, double x, double y)
void _glfwPlatformSetCursorMode(_GLFWwindow* window, int mode)
{
if (mode == GLFW_CURSOR_DISABLED)
{
_glfw.ns.disabledCursorWindow = window;
_glfwPlatformGetCursorPos(window,
&_glfw.ns.restoreCursorPosX,
&_glfw.ns.restoreCursorPosY);
centerCursor(window);
CGAssociateMouseAndMouseCursorPosition(false);
}
else if (_glfw.ns.disabledCursorWindow == window)
{
_glfw.ns.disabledCursorWindow = NULL;
CGAssociateMouseAndMouseCursorPosition(true);
_glfwPlatformSetCursorPos(window,
_glfw.ns.restoreCursorPosX,
_glfw.ns.restoreCursorPosY);
}
if (cursorInClientArea(window))
updateCursorImage(window);
if (_glfwPlatformWindowFocused(window))
updateCursorMode(window);
}
const char* _glfwPlatformGetScancodeName(int scancode)
@@ -1777,16 +1866,7 @@ const char* _glfwPlatformGetScancodeName(int scancode)
if (!characterCount)
return NULL;
CFStringRef string = CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault,
characters,
characterCount,
kCFAllocatorNull);
CFStringGetCString(string,
_glfw.ns.keyName,
sizeof(_glfw.ns.keyName),
kCFStringEncodingUTF8);
CFRelease(string);
convert_utf16_to_utf8(characters, characterCount, _glfw.ns.keyName, sizeof(_glfw.ns.keyName));
return _glfw.ns.keyName;
}
@@ -2000,3 +2080,11 @@ GLFWAPI id glfwGetCocoaWindow(GLFWwindow* handle)
_GLFW_REQUIRE_INIT_OR_RETURN(nil);
return window->ns.object;
}
GLFWAPI GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow *handle, GLFWcocoatextinputfilterfun callback) {
_GLFWwindow* window = (_GLFWwindow*) handle;
_GLFW_REQUIRE_INIT_OR_RETURN(nil);
GLFWcocoatextinputfilterfun previous = window->ns.textInputFilterCallback;
window->ns.textInputFilterCallback = callback;
return previous;
}

View File

@@ -50,7 +50,7 @@ def init_env(env, pkg_config, at_least_version, module='x11'):
at_least_version('xkbcommon', 0, 5)
if module == 'x11':
for dep in 'x11 xrandr xinerama xcursor xkbcommon xkbcommon-x11'.split():
for dep in 'x11 xrandr xinerama xcursor xkbcommon xkbcommon-x11 x11-xcb'.split():
ans.cflags.extend(pkg_config(dep, '--cflags-only-I'))
ans.ldpaths.extend(pkg_config(dep, '--libs'))
@@ -101,7 +101,7 @@ def collect_source_information():
).group(1).strip().split()
)
wayland_protocols = re.search('WaylandProtocols\s+(\S+)\s+', mraw).group(1)
wayland_protocols = re.search(r'WaylandProtocols\s+(\S+)\s+', mraw).group(1)
wayland_protocols = list(map(int, wayland_protocols.split('.')))
ans = {
'common': dict(extract_sources('common')),
@@ -196,6 +196,7 @@ def generate_wrappers(glfw_header, glfw_native_header):
for line in '''\
void* glfwGetCocoaWindow(GLFWwindow* window)
uint32_t glfwGetCocoaMonitor(GLFWmonitor* monitor)
GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow* window, GLFWcocoatextinputfilterfun callback)
void* glfwGetX11Display(void)
int32_t glfwGetX11Window(GLFWwindow* window)
void glfwSetX11SelectionString(const char* string)
@@ -212,6 +213,7 @@ def generate_wrappers(glfw_header, glfw_native_header):
#pragma once
#include <stddef.h>
#include <stdint.h>
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int);
{}
{}

4
glfw/glfw3.h vendored
View File

@@ -4582,7 +4582,7 @@ GLFWAPI const char* glfwGetJoystickGUID(int jid);
* This function may be called from the joystick callback, even for a joystick
* that is being disconnected.
*
* @param[in] joystick The joystick whose pointer to set.
* @param[in] jid The joystick whose pointer to set.
* @param[in] pointer The new value.
*
* @errors Possible errors include @ref GLFW_NOT_INITIALIZED.
@@ -4607,7 +4607,7 @@ GLFWAPI void glfwSetJoystickUserPointer(int jid, void* pointer);
* This function may be called from the joystick callback, even for a joystick
* that is being disconnected.
*
* @param[in] joystick The joystick whose pointer to return.
* @param[in] jid The joystick whose pointer to return.
*
* @errors Possible errors include @ref GLFW_NOT_INITIALIZED.
*

56
glfw/init.c vendored
View File

@@ -57,37 +57,6 @@ static _GLFWinitconfig _glfwInitHints =
}
};
// Returns a generic string representation of the specified error
//
static const char* getErrorString(int code)
{
switch (code)
{
case GLFW_NOT_INITIALIZED:
return "The GLFW library is not initialized";
case GLFW_NO_CURRENT_CONTEXT:
return "There is no current context";
case GLFW_INVALID_ENUM:
return "Invalid argument for enum parameter";
case GLFW_INVALID_VALUE:
return "Invalid value for parameter";
case GLFW_OUT_OF_MEMORY:
return "Out of memory";
case GLFW_API_UNAVAILABLE:
return "The requested API is unavailable";
case GLFW_VERSION_UNAVAILABLE:
return "The requested API version is unavailable";
case GLFW_PLATFORM_ERROR:
return "An undocumented platform-specific error occurred";
case GLFW_FORMAT_UNAVAILABLE:
return "The requested format is unavailable";
case GLFW_NO_WINDOW_CONTEXT:
return "The specified window has no context";
default:
return "ERROR: UNKNOWN GLFW ERROR";
}
}
// Terminate the library
//
static void terminate(void)
@@ -173,7 +142,30 @@ void _glfwInputError(int code, const char* format, ...)
description[sizeof(description) - 1] = '\0';
}
else
strcpy(description, getErrorString(code));
{
if (code == GLFW_NOT_INITIALIZED)
strcpy(description, "The GLFW library is not initialized");
else if (code == GLFW_NO_CURRENT_CONTEXT)
strcpy(description, "There is no current context");
else if (code == GLFW_INVALID_ENUM)
strcpy(description, "Invalid argument for enum parameter");
else if (code == GLFW_INVALID_VALUE)
strcpy(description, "Invalid value for parameter");
else if (code == GLFW_OUT_OF_MEMORY)
strcpy(description, "Out of memory");
else if (code == GLFW_API_UNAVAILABLE)
strcpy(description, "The requested API is unavailable");
else if (code == GLFW_VERSION_UNAVAILABLE)
strcpy(description, "The requested API version is unavailable");
else if (code == GLFW_PLATFORM_ERROR)
strcpy(description, "A platform-specific error occurred");
else if (code == GLFW_FORMAT_UNAVAILABLE)
strcpy(description, "The requested format is unavailable");
else if (code == GLFW_NO_WINDOW_CONTEXT)
strcpy(description, "The specified window has no context");
else
strcpy(description, "ERROR: UNKNOWN GLFW ERROR");
}
if (_glfw.initialized)
{

11
glfw/input.c vendored
View File

@@ -32,7 +32,6 @@
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
// Internal key state used for sticky keys
#define _GLFW_STICK 3
@@ -619,11 +618,7 @@ GLFWAPI void glfwSetInputMode(GLFWwindow* handle, int mode, int value)
_glfwPlatformGetCursorPos(window,
&window->virtualCursorPosX,
&window->virtualCursorPosY);
#ifdef __APPLE__
if (_glfwPlatformWindowFocused(window))
#endif
_glfwPlatformSetCursorMode(window, value);
_glfwPlatformSetCursorMode(window, value);
}
else if (mode == GLFW_STICKY_KEYS)
{
@@ -1180,7 +1175,9 @@ GLFWAPI int glfwUpdateGamepadMappings(const char* string)
while (*c)
{
if (isxdigit(*c))
if ((*c >= '0' && *c <= '9') ||
(*c >= 'a' && *c <= 'f') ||
(*c >= 'A' && *c <= 'F'))
{
char line[1024];

View File

@@ -120,6 +120,8 @@ typedef struct
HRGN hRgnBlur;
BOOL fTransitionOnMaximized;
} DWM_BLURBEHIND;
#else
#include <dwmapi.h>
#endif /*Windows Vista*/
#ifndef DPI_ENUMS_DECLARED
@@ -240,7 +242,9 @@ typedef VkBool32 (APIENTRY *PFN_vkGetPhysicalDeviceWin32PresentationSupportKHR)(
#include "egl_context.h"
#include "osmesa_context.h"
#define _GLFW_WNDCLASSNAME L"GLFW30"
#if !defined(_GLFW_WNDCLASSNAME)
#define _GLFW_WNDCLASSNAME L"GLFW30"
#endif
#define _glfw_dlopen(name) LoadLibraryA(name)
#define _glfw_dlclose(handle) FreeLibrary((HMODULE) handle)

130
glfw/win32_window.c vendored
View File

@@ -235,26 +235,6 @@ static void centerCursor(_GLFWwindow* window)
_glfwPlatformSetCursorPos(window, width / 2.0, height / 2.0);
}
// Returns whether the cursor is in the client area of the specified window
//
static GLFWbool cursorInClientArea(_GLFWwindow* window)
{
RECT area;
POINT pos;
if (!GetCursorPos(&pos))
return GLFW_FALSE;
if (WindowFromPoint(pos) != window->win32.handle)
return GLFW_FALSE;
GetClientRect(window->win32.handle, &area);
ClientToScreen(window->win32.handle, (POINT*) &area.left);
ClientToScreen(window->win32.handle, (POINT*) &area.right);
return PtInRect(&area, pos);
}
// Updates the cursor image according to its cursor mode
//
static void updateCursorImage(_GLFWwindow* window)
@@ -286,6 +266,67 @@ static void updateClipRect(_GLFWwindow* window)
ClipCursor(NULL);
}
// Apply disabled cursor mode to a focused window
//
static void disableCursor(_GLFWwindow* window)
{
const RAWINPUTDEVICE rid = { 0x01, 0x02, 0, window->win32.handle };
_glfw.win32.disabledCursorWindow = window;
_glfwPlatformGetCursorPos(window,
&_glfw.win32.restoreCursorPosX,
&_glfw.win32.restoreCursorPosY);
updateCursorImage(window);
centerCursor(window);
updateClipRect(window);
if (!RegisterRawInputDevices(&rid, 1, sizeof(rid)))
{
_glfwInputErrorWin32(GLFW_PLATFORM_ERROR,
"Win32: Failed to register raw input device");
}
}
// Exit disabled cursor mode for the specified window
//
static void enableCursor(_GLFWwindow* window)
{
const RAWINPUTDEVICE rid = { 0x01, 0x02, RIDEV_REMOVE, NULL };
_glfw.win32.disabledCursorWindow = NULL;
updateClipRect(NULL);
_glfwPlatformSetCursorPos(window,
_glfw.win32.restoreCursorPosX,
_glfw.win32.restoreCursorPosY);
updateCursorImage(window);
if (!RegisterRawInputDevices(&rid, 1, sizeof(rid)))
{
_glfwInputErrorWin32(GLFW_PLATFORM_ERROR,
"Win32: Failed to remove raw input device");
}
}
// Returns whether the cursor is in the client area of the specified window
//
static GLFWbool cursorInClientArea(_GLFWwindow* window)
{
RECT area;
POINT pos;
if (!GetCursorPos(&pos))
return GLFW_FALSE;
if (WindowFromPoint(pos) != window->win32.handle)
return GLFW_FALSE;
GetClientRect(window->win32.handle, &area);
ClientToScreen(window->win32.handle, (POINT*) &area.left);
ClientToScreen(window->win32.handle, (POINT*) &area.right);
return PtInRect(&area, pos);
}
// Update native window styles to match attributes
//
static void updateWindowStyles(const _GLFWwindow* window)
@@ -575,7 +616,7 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg,
if (lParam == 0 && window->win32.frameAction)
{
if (window->cursorMode == GLFW_CURSOR_DISABLED)
_glfwPlatformSetCursorMode(window, GLFW_CURSOR_DISABLED);
disableCursor(window);
window->win32.frameAction = GLFW_FALSE;
}
@@ -593,7 +634,7 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg,
break;
if (window->cursorMode == GLFW_CURSOR_DISABLED)
_glfwPlatformSetCursorMode(window, GLFW_CURSOR_DISABLED);
disableCursor(window);
return 0;
}
@@ -601,7 +642,7 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg,
case WM_KILLFOCUS:
{
if (window->cursorMode == GLFW_CURSOR_DISABLED)
_glfwPlatformSetCursorMode(window, GLFW_CURSOR_NORMAL);
enableCursor(window);
if (window->monitor && window->autoIconify)
_glfwPlatformIconifyWindow(window);
@@ -857,10 +898,10 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg,
case WM_ENTERSIZEMOVE:
case WM_ENTERMENULOOP:
{
// HACK: Postpone cursor disabling while the user is moving or
// resizing the window or using the menu
// HACK: Enable the cursor while the user is moving or
// resizing the window or using the window menu
if (window->cursorMode == GLFW_CURSOR_DISABLED)
_glfwPlatformSetCursorMode(window, GLFW_CURSOR_NORMAL);
enableCursor(window);
break;
}
@@ -871,7 +912,7 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg,
// HACK: Disable the cursor once the user is done moving or
// resizing the window or using the menu
if (window->cursorMode == GLFW_CURSOR_DISABLED)
_glfwPlatformSetCursorMode(window, GLFW_CURSOR_DISABLED);
disableCursor(window);
break;
}
@@ -1777,39 +1818,12 @@ void _glfwPlatformSetCursorMode(_GLFWwindow* window, int mode)
{
if (mode == GLFW_CURSOR_DISABLED)
{
const RAWINPUTDEVICE rid = { 0x01, 0x02, 0, window->win32.handle };
_glfw.win32.disabledCursorWindow = window;
_glfwPlatformGetCursorPos(window,
&_glfw.win32.restoreCursorPosX,
&_glfw.win32.restoreCursorPosY);
centerCursor(window);
updateClipRect(window);
if (!RegisterRawInputDevices(&rid, 1, sizeof(rid)))
{
_glfwInputErrorWin32(GLFW_PLATFORM_ERROR,
"Win32: Failed to register raw input device");
}
if (_glfwPlatformWindowFocused(window))
disableCursor(window);
}
else if (_glfw.win32.disabledCursorWindow == window)
{
const RAWINPUTDEVICE rid = { 0x01, 0x02, RIDEV_REMOVE, NULL };
_glfw.win32.disabledCursorWindow = NULL;
updateClipRect(NULL);
_glfwPlatformSetCursorPos(window,
_glfw.win32.restoreCursorPosX,
_glfw.win32.restoreCursorPosY);
if (!RegisterRawInputDevices(&rid, 1, sizeof(rid)))
{
_glfwInputErrorWin32(GLFW_PLATFORM_ERROR,
"Win32: Failed to remove raw input device");
}
}
if (cursorInClientArea(window))
enableCursor(window);
else if (cursorInClientArea(window))
updateCursorImage(window);
}

53
glfw/x11_init.c vendored
View File

@@ -166,7 +166,11 @@ static GLFWbool initExtensions(void)
&_glfw.x11.vidmode.errorBase);
}
#if defined(__CYGWIN__)
_glfw.x11.xi.handle = _glfw_dlopen("libXi-6.so");
#else
_glfw.x11.xi.handle = _glfw_dlopen("libXi.so.6");
#endif
if (_glfw.x11.xi.handle)
{
_glfw.x11.xi.QueryVersion = (PFN_XIQueryVersion)
@@ -192,7 +196,11 @@ static GLFWbool initExtensions(void)
}
}
#if defined(__CYGWIN__)
_glfw.x11.randr.handle = _glfw_dlopen("libXrandr-2.so");
#else
_glfw.x11.randr.handle = _glfw_dlopen("libXrandr.so.2");
#endif
if (_glfw.x11.randr.handle)
{
_glfw.x11.randr.AllocGamma = (PFN_XRRAllocGamma)
@@ -280,7 +288,11 @@ static GLFWbool initExtensions(void)
RROutputChangeNotifyMask);
}
#if defined(__CYGWIN__)
_glfw.x11.xcursor.handle = _glfw_dlopen("libXcursor-1.so");
#else
_glfw.x11.xcursor.handle = _glfw_dlopen("libXcursor.so.1");
#endif
if (_glfw.x11.xcursor.handle)
{
_glfw.x11.xcursor.ImageCreate = (PFN_XcursorImageCreate)
@@ -291,7 +303,11 @@ static GLFWbool initExtensions(void)
_glfw_dlsym(_glfw.x11.xcursor.handle, "XcursorImageLoadCursor");
}
#if defined(__CYGWIN__)
_glfw.x11.xinerama.handle = _glfw_dlopen("libXinerama-1.so");
#else
_glfw.x11.xinerama.handle = _glfw_dlopen("libXinerama.so.1");
#endif
if (_glfw.x11.xinerama.handle)
{
_glfw.x11.xinerama.IsActive = (PFN_XineramaIsActive)
@@ -310,16 +326,11 @@ static GLFWbool initExtensions(void)
}
}
_glfw.x11.x11xcb.handle = _glfw_dlopen("libX11-xcb.so.1");
if (!_glfw.x11.x11xcb.handle)
{
_glfwInputError(GLFW_PLATFORM_ERROR, "X11: Failed to load libX11-xcb");
return GLFW_FALSE;
}
_glfw.x11.x11xcb.GetXCBConnection = (PFN_XGetXCBConnection)
_glfw_dlsym(_glfw.x11.x11xcb.handle, "XGetXCBConnection");
#if defined(__CYGWIN__)
_glfw.x11.xrender.handle = _glfw_dlopen("libXrender-1.so");
#else
_glfw.x11.xrender.handle = _glfw_dlopen("libXrender.so.1");
#endif
if (_glfw.x11.xrender.handle)
{
_glfw.x11.xrender.QueryExtension = (PFN_XRenderQueryExtension)
@@ -659,12 +670,6 @@ void _glfwPlatformTerminate(void)
_glfw.x11.display = NULL;
}
if (_glfw.x11.x11xcb.handle)
{
_glfw_dlclose(_glfw.x11.x11xcb.handle);
_glfw.x11.x11xcb.handle = NULL;
}
if (_glfw.x11.xcursor.handle)
{
_glfw_dlclose(_glfw.x11.xcursor.handle);
@@ -683,6 +688,24 @@ void _glfwPlatformTerminate(void)
_glfw.x11.xinerama.handle = NULL;
}
if (_glfw.x11.xrender.handle)
{
_glfw_dlclose(_glfw.x11.xrender.handle);
_glfw.x11.xrender.handle = NULL;
}
if (_glfw.x11.vidmode.handle)
{
_glfw_dlclose(_glfw.x11.vidmode.handle);
_glfw.x11.vidmode.handle = NULL;
}
if (_glfw.x11.xi.handle)
{
_glfw_dlclose(_glfw.x11.xi.handle);
_glfw.x11.xi.handle = NULL;
}
// NOTE: These need to be unloaded after XCloseDisplay, as they register
// cleanup callbacks that get called by that function
_glfwTerminateEGL();

11
glfw/x11_platform.h vendored
View File

@@ -35,6 +35,9 @@
#include <X11/Xatom.h>
#include <X11/Xcursor/Xcursor.h>
// The xcb library is needed to work with libxkb
#include <X11/Xlib-xcb.h>
// The XRandR extension provides mode setting and gamma control
#include <X11/extensions/Xrandr.h>
@@ -47,6 +50,7 @@
// The XInput extension provides raw mouse motion input
#include <X11/extensions/XInput2.h>
// The libxkb library is used for improved keyboard support
#include "xkb_glfw.h"
typedef XRRCrtcGamma* (* PFN_XRRAllocGamma)(int);
@@ -99,8 +103,6 @@ typedef XineramaScreenInfo* (* PFN_XineramaQueryScreens)(Display*,int*);
#define XineramaQueryScreens _glfw.x11.xinerama.QueryScreens
typedef struct xcb_connection_t xcb_connection_t;
typedef xcb_connection_t* (* PFN_XGetXCBConnection)(Display*);
#define XGetXCBConnection _glfw.x11.x11xcb.GetXCBConnection
typedef Bool (* PFN_XF86VidModeQueryExtension)(Display*,int*,int*);
typedef Bool (* PFN_XF86VidModeGetGammaRamp)(Display*,int,int,unsigned short*,unsigned short*,unsigned short*);
@@ -341,11 +343,6 @@ typedef struct _GLFWlibraryX11
PFN_XineramaQueryScreens QueryScreens;
} xinerama;
struct {
void* handle;
PFN_XGetXCBConnection GetXCBConnection;
} x11xcb;
struct {
GLFWbool available;
void* handle;

123
glfw/x11_window.c vendored
View File

@@ -535,6 +535,61 @@ static void updateCursorImage(_GLFWwindow* window)
}
}
// Apply disabled cursor mode to a focused window
//
static void disableCursor(_GLFWwindow* window)
{
if (_glfw.x11.xi.available)
{
XIEventMask em;
unsigned char mask[XIMaskLen(XI_RawMotion)] = { 0 };
em.deviceid = XIAllMasterDevices;
em.mask_len = sizeof(mask);
em.mask = mask;
XISetMask(mask, XI_RawMotion);
XISelectEvents(_glfw.x11.display, _glfw.x11.root, &em, 1);
}
_glfw.x11.disabledCursorWindow = window;
_glfwPlatformGetCursorPos(window,
&_glfw.x11.restoreCursorPosX,
&_glfw.x11.restoreCursorPosY);
updateCursorImage(window);
centerCursor(window);
XGrabPointer(_glfw.x11.display, window->x11.handle, True,
ButtonPressMask | ButtonReleaseMask | PointerMotionMask,
GrabModeAsync, GrabModeAsync,
window->x11.handle,
_glfw.x11.hiddenCursorHandle,
CurrentTime);
}
// Exit disabled cursor mode for the specified window
//
static void enableCursor(_GLFWwindow* window)
{
if (_glfw.x11.xi.available)
{
XIEventMask em;
unsigned char mask[] = { 0 };
em.deviceid = XIAllMasterDevices;
em.mask_len = sizeof(mask);
em.mask = mask;
XISelectEvents(_glfw.x11.display, _glfw.x11.root, &em, 1);
}
_glfw.x11.disabledCursorWindow = NULL;
XUngrabPointer(_glfw.x11.display, CurrentTime);
_glfwPlatformSetCursorPos(window,
_glfw.x11.restoreCursorPosX,
_glfw.x11.restoreCursorPosY);
updateCursorImage(window);
}
// Create the X11 window (and its colormap)
//
static GLFWbool createNativeWindow(_GLFWwindow* window,
@@ -1306,7 +1361,7 @@ static void processEvent(XEvent *event)
// HACK: This is a workaround for WMs (KWM, Fluxbox) that otherwise
// ignore the defined cursor for hidden cursor mode
if (window->cursorMode == GLFW_CURSOR_HIDDEN)
_glfwPlatformSetCursorMode(window, GLFW_CURSOR_HIDDEN);
updateCursorImage(window);
_glfwInputCursorEnter(window, GLFW_TRUE);
return;
@@ -1599,7 +1654,7 @@ static void processEvent(XEvent *event)
case FocusIn:
{
if (window->cursorMode == GLFW_CURSOR_DISABLED)
_glfwPlatformSetCursorMode(window, GLFW_CURSOR_DISABLED);
disableCursor(window);
if (event->xfocus.mode == NotifyGrab ||
event->xfocus.mode == NotifyUngrab)
@@ -1616,7 +1671,7 @@ static void processEvent(XEvent *event)
case FocusOut:
{
if (window->cursorMode == GLFW_CURSOR_DISABLED)
_glfwPlatformSetCursorMode(window, GLFW_CURSOR_NORMAL);
enableCursor(window);
if (event->xfocus.mode == NotifyGrab ||
event->xfocus.mode == NotifyUngrab)
@@ -2484,8 +2539,9 @@ void _glfwPlatformPollEvents(void)
#if defined(__linux__)
_glfwDetectJoystickConnectionLinux();
#endif
int count = XPending(_glfw.x11.display);
while (count--)
XPending(_glfw.x11.display);
while (XQLength(_glfw.x11.display))
{
XEvent event;
XNextEvent(_glfw.x11.display, &event);
@@ -2575,53 +2631,14 @@ void _glfwPlatformSetCursorMode(_GLFWwindow* window, int mode)
{
if (mode == GLFW_CURSOR_DISABLED)
{
if (_glfw.x11.xi.available)
{
XIEventMask em;
unsigned char mask[XIMaskLen(XI_RawMotion)] = { 0 };
em.deviceid = XIAllMasterDevices;
em.mask_len = sizeof(mask);
em.mask = mask;
XISetMask(mask, XI_RawMotion);
XISelectEvents(_glfw.x11.display, _glfw.x11.root, &em, 1);
}
_glfw.x11.disabledCursorWindow = window;
_glfwPlatformGetCursorPos(window,
&_glfw.x11.restoreCursorPosX,
&_glfw.x11.restoreCursorPosY);
centerCursor(window);
XGrabPointer(_glfw.x11.display, window->x11.handle, True,
ButtonPressMask | ButtonReleaseMask | PointerMotionMask,
GrabModeAsync, GrabModeAsync,
window->x11.handle,
_glfw.x11.hiddenCursorHandle,
CurrentTime);
if (_glfwPlatformWindowFocused(window))
disableCursor(window);
}
else if (_glfw.x11.disabledCursorWindow == window)
{
if (_glfw.x11.xi.available)
{
XIEventMask em;
unsigned char mask[] = { 0 };
enableCursor(window);
else
updateCursorImage(window);
em.deviceid = XIAllMasterDevices;
em.mask_len = sizeof(mask);
em.mask = mask;
XISelectEvents(_glfw.x11.display, _glfw.x11.root, &em, 1);
}
_glfw.x11.disabledCursorWindow = NULL;
XUngrabPointer(_glfw.x11.display, CurrentTime);
_glfwPlatformSetCursorPos(window,
_glfw.x11.restoreCursorPosX,
_glfw.x11.restoreCursorPosY);
}
updateCursorImage(window);
XFlush(_glfw.x11.display);
}
@@ -2704,7 +2721,7 @@ void _glfwPlatformGetRequiredInstanceExtensions(char** extensions)
if (!_glfw.vk.KHR_surface)
return;
if (!_glfw.vk.KHR_xcb_surface || !_glfw.x11.x11xcb.handle)
if (!_glfw.vk.KHR_xcb_surface)
{
if (!_glfw.vk.KHR_xlib_surface)
return;
@@ -2714,7 +2731,7 @@ void _glfwPlatformGetRequiredInstanceExtensions(char** extensions)
// NOTE: VK_KHR_xcb_surface is preferred due to some early ICDs exposing but
// not correctly implementing VK_KHR_xlib_surface
if (_glfw.vk.KHR_xcb_surface && _glfw.x11.x11xcb.handle)
if (_glfw.vk.KHR_xcb_surface)
extensions[1] = "VK_KHR_xcb_surface";
else
extensions[1] = "VK_KHR_xlib_surface";
@@ -2727,7 +2744,7 @@ int _glfwPlatformGetPhysicalDevicePresentationSupport(VkInstance instance,
VisualID visualID = XVisualIDFromVisual(DefaultVisual(_glfw.x11.display,
_glfw.x11.screen));
if (_glfw.vk.KHR_xcb_surface && _glfw.x11.x11xcb.handle)
if (_glfw.vk.KHR_xcb_surface)
{
PFN_vkGetPhysicalDeviceXcbPresentationSupportKHR
vkGetPhysicalDeviceXcbPresentationSupportKHR =
@@ -2778,7 +2795,7 @@ VkResult _glfwPlatformCreateWindowSurface(VkInstance instance,
const VkAllocationCallbacks* allocator,
VkSurfaceKHR* surface)
{
if (_glfw.vk.KHR_xcb_surface && _glfw.x11.x11xcb.handle)
if (_glfw.vk.KHR_xcb_surface)
{
VkResult err;
VkXcbSurfaceCreateInfoKHR sci;

7
glfw/xkb_glfw.c vendored
View File

@@ -263,12 +263,14 @@ glfw_xkb_compile_keymap(_GLFWXKBData *xkb, const char *map_str) {
S(super, XKB_MOD_NAME_LOGO);
S(capsLock, XKB_MOD_NAME_CAPS);
S(numLock, XKB_MOD_NAME_NUM);
#undef S
size_t capacity = sizeof(xkb->unknownModifiers)/sizeof(xkb->unknownModifiers[0]), j = 0;
for (xkb_mod_index_t i = 0; i < capacity; i++) xkb->unknownModifiers[i] = XKB_MOD_INVALID;
for (xkb_mod_index_t i = 0; i < xkb_keymap_num_mods(xkb->keymap) && j < capacity - 1; i++) {
if (i != xkb->controlIdx && i != xkb->altIdx && i != xkb->shiftIdx && i != xkb->superIdx && i != xkb->capsLockIdx && i != xkb->numLockIdx) xkb->unknownModifiers[j++] = i;
}
#undef S
xkb->modifiers = 0;
xkb->activeUnknownModifiers = 0;
}
return ok;
}
@@ -290,6 +292,9 @@ glfw_xkb_update_modifiers(_GLFWXKBData *xkb, xkb_mod_mask_t depressed, xkb_mod_m
if (!xkb->keymap) return;
xkb->modifiers = 0;
xkb_state_update_mask(xkb->state, depressed, latched, locked, base_group, latched_group, locked_group);
// We have to update the groups in clean_state, as they change for
// different keyboard layouts, see https://github.com/kovidgoyal/kitty/issues/488
xkb_state_update_mask(xkb->clean_state, 0, 0, 0, base_group, latched_group, locked_group);
#define S(attr, name) if (xkb_state_mod_index_is_active(xkb->state, xkb->attr##Idx, XKB_STATE_MODS_EFFECTIVE)) xkb->modifiers |= GLFW_MOD_##name
S(control, CONTROL); S(alt, ALT); S(shift, SHIFT); S(super, SUPER); S(capsLock, CAPS_LOCK); S(numLock, NUM_LOCK);
#undef S

View File

@@ -24,10 +24,11 @@ kitty icat path/to/some/image.png
You can also see a screenshot with more sophisticated features such as alpha-blending and text over graphics
link:https://github.com/kovidgoyal/kitty/issues/33#issuecomment-334436100[here].
Some third-party programs that use the kitty graphics protocol:
Some programs that use the kitty graphics protocol:
* link:https://github.com/dsanson/termpdf[termdpf] - a terminal PDF/DJVU/CBR viewer
* link:https://github.com/ranger/ranger[ranger] - a terminal file manager, with image previews, see this link:https://github.com/ranger/ranger/pull/1077[PR]
* link:https://github.com/kovidgoyal/kitty/tree/master/kittens/diff[kitty-diff] - a side-by-side terminal diff program with support for images
toc::[]

View File

@@ -53,7 +53,6 @@ class HistoryCompleter:
if os.path.exists(self.history_path):
readline.read_history_file(self.history_path)
readline.set_completer(self.complete)
readline.parse_and_bind('tab: complete')
return self
def __exit__(self, *a):
@@ -87,6 +86,7 @@ def main(args):
global readline
import readline as rl
readline = rl
from kitty.shell import init_readline
msg = 'Ask the user for input'
try:
args, items = parse_args(args[1:], option_text, '', msg, 'kitty ask')
@@ -96,7 +96,7 @@ def main(args):
input('Press enter to quit...')
raise SystemExit(e.code)
readline.read_init_file()
init_readline(readline)
ans = {'items': items}
with alternate_screen(), HistoryCompleter(args.name):

View File

76
kittens/clipboard/main.py Normal file
View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import sys
from kitty.cli import parse_args
from ..tui.handler import Handler
from ..tui.loop import Loop
class Clipboard(Handler):
def __init__(self, data_to_send, args):
self.args = args
self.print_on_fail = None
self.clipboard_contents = None
self.data_to_send = data_to_send
def initialize(self):
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:
self.quit_loop(0)
return
self.cmd.request_from_clipboard(self.args.use_primary)
def on_clipboard_response(self, text, from_primary=False):
self.clipboard_contents = text
self.quit_loop(0)
OPTIONS = r'''
--get-clipboard
default=False
type=bool-set
Output the current contents of the clipboard to stdout. Note that this
will not work if you have not enabled the option to allow reading the clipboard
in kitty.conf
--use-primary
default=False
type=bool-set
Use the primary selection rather than the clipboard on systems that support it,
such as X11.
'''.format
def main(args):
msg = '''\
Read or write to the system clipboard.
To set the clipboard text, pipe in the new text on stdin. Use the --get-clipboard option \
to output the current clipboard contents to stdout. Note that you must enable reading of clipboard in kitty.conf first. '''
args, items = parse_args(args[1:], OPTIONS, '', msg, 'clipboard')
if items:
raise SystemExit('Unrecognized extra command line arguments')
data = None
if not sys.stdin.isatty():
data = sys.stdin.buffer.read()
sys.stdin = open('/dev/tty', 'r')
loop = Loop()
handler = Clipboard(data, args)
loop.loop(handler)
if loop.return_code == 0 and handler.clipboard_contents:
sys.stdout.write(handler.clipboard_contents)
sys.stdout.flush()
if handler.print_on_fail:
print(handler.print_on_fail, file=sys.stderr)
input('Press Enter to quit')
raise SystemExit(loop.return_code)
if __name__ == '__main__':
main(sys.argv)

View File

@@ -0,0 +1,127 @@
= kitty-diff - A side-by-side diff tool with syntax highlighting and images
:toc:
:toc-placement!:
== Major Features
* Displays diffs side-by-side in the kitty terminal.
* Does syntax highlighting of displayed diffs
* Displays images as well as text diffs, even over SSH
* Does recursive directory diffing
image::../../screenshots/diff.png?raw=true[Screenshot, showing a sample diff]
toc::[]
== Installation
Simply install link:https://github.com/kovidgoyal/kitty[kitty]. You also need
to have either the link:https://git-scm.com/[git] program or the `diff` program
installed. Additionally, for syntax highlighting to work,
link:http://pygments.org/[pygments] must be installed (note that pygments is
included in the macOS kitty app).
== Usage
In the kitty terminal, run:
....
kitty +kitten diff file1 file2
....
to see the diff between file1 and file2.
Create an alias in your shell's startup file to shorten the command, for example:
```sh
alias d="kitty +kitten diff"
```
Now all you need to do to diff two files is:
```
d file1 file2
```
You can also pass directories instead of files to see the recursive diff of the
directory contents.
== Keyboard controls
|===
|Action |Shortcut
|Quit | `q, Ctrl+c`
|Scroll line up | `k, up`
|Scroll line down | `j, down`
|Scroll page up | `PgUp`
|Scroll page down | `PgDn`
|Scroll to top | `Home`
|Scroll to bottom | `End`
|Scroll to next change | `n`
|Scroll to previous change | `p`
|Increase lines of context | `+`
|Decrease lines of context | `-`
|All lines of context | `a`
|Restore context to default| `=`
|===
[options="header"]
== Configuring kitty-diff
You can configure the colors used, the diff implementation, the default lines of context, etc.
by creating a diff.conf in your kitty config folder. See the link:diff.conf[default diff.conf]
for details.
== Integrating with git
Add the following to `~/.gitconfig`:
```gitconfig
[diff]
tool = kitty
guitool = kitty.gui
[difftool]
prompt = false
trustExitCode = true
[difftool "kitty"]
cmd = kitty +kitten diff $LOCAL $REMOTE
[difftool "kitty.gui"]
cmd = kitty kitty +kitten diff $LOCAL $REMOTE
```
Now to use kitty-diff to view git diffs, you can simply do:
```
git difftool --no-symlinks --dir-diff
```
Once again, creating an alias for this command is useful.
== Why does this work only in kitty?
The diff kitten makes use of various features that are
link:https://github.com/kovidgoyal/kitty/blob/master/protocol-extensions.asciidoc[kitty
only], such as the
link:https://github.com/kovidgoyal/kitty/blob/master/graphics-protocol.asciidoc[kitty
graphics protocol], the extended keyboard protocol, etc. It also leverages
terminal program infrastructure I created for all of kitty's other kittens to
reduce the amount of code needed (the entire implementation is under 2000 lines
of code).
And fundamentally, it's kitty only because I wrote it for myself, and I am
highly unlikely to use any other terminals :)

0
kittens/diff/__init__.py Normal file
View File

189
kittens/diff/collect.py Normal file
View File

@@ -0,0 +1,189 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import os
import re
from functools import lru_cache
from hashlib import md5
from mimetypes import guess_type
path_name_map = {}
class Segment:
__slots__ = ('start', 'end', 'start_code', 'end_code')
def __init__(self, start, start_code):
self.start = start
self.start_code = start_code
def __repr__(self):
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)
)
class Collection:
def __init__(self):
self.changes = {}
self.renames = {}
self.adds = set()
self.removes = set()
self.all_paths = []
self.type_map = {}
self.added_count = self.removed_count = 0
def add_change(self, left_path, right_path):
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):
self.renames[left_path] = right_path
self.all_paths.append(left_path)
self.type_map[left_path] = 'rename'
def add_add(self, right_path):
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):
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):
self.all_paths.sort(key=path_name_map.get)
def __iter__(self):
for path in self.all_paths:
typ = self.type_map[path]
if typ == 'diff':
data = self.changes[path]
elif typ == 'rename':
data = self.renames[path]
else:
data = None
yield path, typ, data
def __len__(self):
return len(self.all_paths)
def collect_files(collection, left, right):
left_names, right_names = set(), set()
left_path_map, right_path_map = {}, {}
def walk(base, names, pmap):
for dirpath, dirnames, filenames in os.walk(base):
for filename in filenames:
path = os.path.abspath(os.path.join(dirpath, filename))
path_name_map[path] = name = os.path.relpath(path, base)
names.add(name)
pmap[name] = path
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:
collection.add_change(left_path_map[n], right_path_map[n])
removed = left_names - common_names
added = right_names - common_names
ahash = {a: hash_for_path(right_path_map[a]) for a in added}
rhash = {r: hash_for_path(left_path_map[r]) for r in removed}
for name, rh in rhash.items():
for n, ah in ahash.items():
if ah == rh and data_for_path(left_path_map[name]) == data_for_path(right_path_map[n]):
collection.add_rename(left_path_map[name], right_path_map[n])
added.discard(n)
break
else:
collection.add_removal(left_path_map[name])
for name in added:
collection.add_add(right_path_map[name])
sanitize_pat = re.compile('[\x00-\x09\x0b-\x1f\x7f\x80-\x9f]')
def sanitize_sub(m):
return '<{:x}>'.format(ord(m.group()[0]))
def sanitize(text):
return sanitize_pat.sub(sanitize_sub, text)
@lru_cache(maxsize=1024)
def mime_type_for_path(path):
return guess_type(path)[0] or 'application/octet-stream'
@lru_cache(maxsize=1024)
def raw_data_for_path(path):
with open(path, 'rb') as f:
return f.read()
def is_image(path):
return mime_type_for_path(path).startswith('image/')
@lru_cache(maxsize=1024)
def data_for_path(path):
ans = raw_data_for_path(path)
if not is_image(path) and not os.path.samefile(path, os.devnull):
try:
ans = ans.decode('utf-8')
except UnicodeDecodeError:
pass
return ans
@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):
return md5(raw_data_for_path(path)).digest()
def create_collection(left, right):
collection = Collection()
if os.path.isdir(left):
collect_files(collection, left, right)
else:
pl, pr = os.path.abspath(left), os.path.abspath(right)
path_name_map[pl] = left
path_name_map[pr] = right
collection.add_change(pl, pr)
collection.finalize()
return collection
highlight_data = {}
def set_highlight_data(data):
global highlight_data
highlight_data = data
def highlights_for_path(path):
return highlight_data.get(path, [])

144
kittens/diff/config.py Normal file
View File

@@ -0,0 +1,144 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import os
from kitty.config_utils import (
init_config, key_func, load_config as _load_config, merge_dicts,
parse_config_base, parse_kittens_key, python_string, resolve_config,
to_color
)
from kitty.constants import config_dir
from kitty.rgb import color_as_sgr
defaults = None
default_config_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)), 'diff.conf'
)
formats = {
'title': '',
'margin': '',
'text': '',
}
def set_formats(opts):
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)
formats['added_margin'] = '38' + color_as_sgr(opts.margin_fg) + ';48' + color_as_sgr(opts.added_margin_bg)
formats['removed_margin'] = '38' + color_as_sgr(opts.margin_fg) + ';48' + color_as_sgr(opts.removed_margin_bg)
formats['added'] = '48' + color_as_sgr(opts.added_bg)
formats['removed'] = '48' + color_as_sgr(opts.removed_bg)
formats['filler'] = '48' + color_as_sgr(opts.filler_bg)
formats['hunk_margin'] = '38' + color_as_sgr(opts.margin_fg) + ';48' + color_as_sgr(opts.hunk_margin_bg)
formats['hunk'] = '38' + color_as_sgr(opts.margin_fg) + ';48' + color_as_sgr(opts.hunk_bg)
formats['removed_highlight'] = '48' + color_as_sgr(opts.highlight_removed_bg)
formats['added_highlight'] = '48' + color_as_sgr(opts.highlight_added_bg)
def syntax_aliases(raw):
ans = {}
for x in raw.split():
a, b = x.partition(':')[::2]
if a and b:
ans[a.lower()] = b
return ans
type_map = {
'syntax_aliases': syntax_aliases,
'num_context_lines': int,
'replace_tab_by': python_string,
}
for name in (
'foreground background title_fg title_bg margin_bg margin_fg removed_bg removed_margin_bg added_bg added_margin_bg filler_bg hunk_bg hunk_margin_bg'
' highlight_removed_bg highlight_added_bg'
).split():
type_map[name] = to_color
func_with_args, args_funcs = key_func()
@func_with_args('scroll_by')
def parse_scroll_by(func, rest):
try:
return func, int(rest)
except Exception:
return func, 1
@func_with_args('scroll_to')
def parse_scroll_to(func, rest):
rest = rest.lower()
if rest not in {'start', 'end', 'next-change', 'prev-change', 'next-page', 'prev-page'}:
rest = 'start'
return func, rest
@func_with_args('change_context')
def parse_change_context(func, rest):
rest = rest.lower()
if rest in {'all', 'default'}:
return func, rest
try:
amount = int(rest)
except Exception:
amount = 5
return func, amount
def special_handling(key, val, ans):
if key == 'map':
action, *key_def = parse_kittens_key(val, args_funcs)
ans['key_definitions'][tuple(key_def)] = action
return True
def parse_config(lines, check_keys=True):
ans = {'key_definitions': {}}
parse_config_base(
lines,
defaults,
type_map,
special_handling,
ans,
check_keys=check_keys
)
return ans
def merge_configs(defaults, vals):
ans = {}
for k, v in defaults.items():
if isinstance(v, dict):
newvals = vals.get(k, {})
ans[k] = merge_dicts(v, newvals)
else:
ans[k] = vals.get(k, v)
return ans
def parse_defaults(lines, check_keys=False):
return parse_config(lines, check_keys)
Options, defaults = init_config(default_config_path, parse_defaults)
def load_config(*paths, overrides=None):
return _load_config(Options, defaults, parse_config, merge_configs, *paths, overrides=overrides)
SYSTEM_CONF = '/etc/xdg/kitty/diff.conf'
defconf = os.path.join(config_dir, 'diff.conf')
def init_config(args):
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)
set_formats(opts)
return opts

58
kittens/diff/diff.conf Normal file
View File

@@ -0,0 +1,58 @@
# vim:fileencoding=utf-8:ft=conf
# File extension aliases for syntax highlight
# For example, to syntax highlight file.xyz as
# file.abc use a setting of xyz:abc
syntax_aliases pyj:py recipe:py
# The pygments color scheme to use for syntax highlighting.
# See https://help.farbox.com/pygments.html for a list of schemes.
pygments_style default
# The number of lines of context to show around each change.
num_context_lines 3
# The diff command to use. Must contain the placeholder _CONTEXT_
# which will be replaced by the number of lines of context. The default
# is to search the system for either git or diff and use that, if found.
diff_cmd auto
# The string to replace tabs with. Default is to use four spaces.
replace_tab_by \x20\x20\x20\x20
# Colors
foreground black
background white
title_fg black
title_bg white
margin_bg #fafbfc
margin_fg #aaaaaa
removed_bg #ffeef0
highlight_removed_bg #fdb8c0
removed_margin_bg #ffdce0
added_bg #e6ffed
highlight_added_bg #acf2bd
added_margin_bg #cdffd8
filler_bg #fafbfc
hunk_margin_bg #dbedff
hunk_bg #f1f8ff
# Keyboard shortcuts
map q quit
map esc quit
map j scroll_by 1
map k scroll_by -1
map down scroll_by 1
map up scroll_by -1
map home scroll_to start
map end scroll_to end
map page_down scroll_to next-page
map page_up scroll_to prev-page
map space scroll_to next-page
map n scroll_to next-change
map p scroll_to prev-change
map a change_context all
map = change_context default
map + change_context 5
map - change_context -5

162
kittens/diff/highlight.py Normal file
View File

@@ -0,0 +1,162 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import concurrent
import os
import re
from pygments import highlight
from pygments.formatter import Formatter
from pygments.lexers import get_lexer_for_filename
from pygments.util import ClassNotFound
from kitty.rgb import color_as_sgr, parse_sharp
from .collect import Segment, data_for_path, lines_for_path
class StyleNotFound(Exception):
pass
class DiffFormatter(Formatter):
def __init__(self, style='default'):
try:
Formatter.__init__(self, style=style)
initialized = True
except ClassNotFound:
initialized = False
if not initialized:
raise StyleNotFound('pygments style "{}" not found'.format(style))
self.styles = {}
for token, style in self.style:
start = []
end = []
# 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']:
start.append('1')
end.append('22')
if style['italic']:
start.append('3')
end.append('23')
if 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 ''
def format(self, tokensource, outfile):
for ttype, value in tokensource:
not_found = True
if value.rstrip('\n'):
while ttype and not_found:
tok = self.styles.get(ttype)
if tok is None:
ttype = ttype[:-1]
else:
on, off = tok
lines = value.split('\n')
for line in lines:
if line:
outfile.write(on + line + off)
if line is not lines[-1]:
outfile.write('\n')
not_found = False
if not_found:
outfile.write(value)
formatter = None
def initialize_highlighter(style='default'):
global formatter
formatter = DiffFormatter(style)
def highlight_data(code, filename, aliases=None):
if aliases:
base, ext = os.path.splitext(filename)
alias = aliases.get(ext[1:])
if alias is not None:
filename = base + '.' + alias
try:
lexer = get_lexer_for_filename(filename, stripnl=False)
except ClassNotFound:
pass
else:
return highlight(code, lexer, formatter)
split_pat = re.compile(r'(\033\[.*?m)')
def highlight_line(line):
ans = []
current = None
pos = 0
for x in split_pat.split(line):
if x.startswith('\033'):
if current is None:
current = Segment(pos, x)
else:
current.end = pos
current.end_code = x
ans.append(current)
current = None
else:
pos += len(x)
return ans
def highlight_for_diff(path, aliases):
ans = []
lines = lines_for_path(path)
hd = highlight_data('\n'.join(lines), path, aliases)
if hd is not None:
for line in hd.splitlines():
ans.append(highlight_line(line))
return ans
def highlight_collection(collection, aliases=None):
jobs = {}
ans = {}
with concurrent.futures.ProcessPoolExecutor(max_workers=os.cpu_count()) as executor:
for path, item_type, other_path in collection:
if item_type != 'rename':
for p in (path, other_path):
if p:
is_binary = isinstance(data_for_path(p), bytes)
if not is_binary:
jobs[executor.submit(highlight_for_diff, p, aliases)] = p
for future in concurrent.futures.as_completed(jobs):
path = jobs[future]
try:
highlights = future.result()
except Exception as e:
return 'Running syntax highlighting for {} generated an exception: {}'.format(path, e)
ans[path] = highlights
return ans
def main():
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)

416
kittens/diff/main.py Normal file
View File

@@ -0,0 +1,416 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import os
import sys
import warnings
from collections import defaultdict
from functools import partial
from gettext import gettext as _
from kitty.cli import CONFIG_HELP, appname, parse_args
from kitty.fast_data_types import wcswidth
from kitty.key_encoding import RELEASE
from ..tui.handler import Handler
from ..tui.images import ImageManager
from ..tui.loop import Loop
from ..tui.operations import styled
from .collect import (
create_collection, data_for_path, lines_for_path, set_highlight_data
)
from .config import init_config
from .patch import Differ, set_diff_command
from .render import ImageSupportWarning, LineRef, render_diff
try:
from .highlight import initialize_highlighter, highlight_collection
except ImportError:
initialize_highlighter = None
INITIALIZING, COLLECTED, DIFFED = range(3)
def generate_diff(collection, context):
d = Differ()
for path, item_type, changed_path in collection:
if item_type == 'diff':
is_binary = isinstance(data_for_path(path), bytes)
if not is_binary:
d.add_diff(path, changed_path)
return d(context)
class DiffHandler(Handler):
image_manager_class = ImageManager
def __init__(self, args, opts, left, right):
self.state = INITIALIZING
self.opts = opts
self.left, self.right = left, right
self.report_traceback_on_exit = 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
for key_def, action in self.opts.key_definitions.items():
self.add_shortcut(action, *key_def)
def perform_action(self, action):
func, args = action
if func == 'quit':
self.quit_loop(0)
return
if self.state <= DIFFED:
if func == 'scroll_by':
return self.scroll_lines(*args)
if func == 'scroll_to':
where = args[0]
if 'change' in where:
return self.scroll_to_next_change(backwards='prev' in where)
if 'page' in where:
amt = self.num_lines * (1 if 'next' in where else -1)
else:
amt = len(self.diff_lines) * (1 if 'end' in where else -1)
return self.scroll_lines(amt)
if func == 'change_context':
new_ctx = self.current_context_count
to = args[0]
if to == 'all':
new_ctx = 100000
elif to == 'default':
new_ctx = self.original_context_count
else:
new_ctx += to
return self.change_context_count(new_ctx)
def create_collection(self):
self.start_job('collect', create_collection, self.left, self.right)
def generate_diff(self):
self.start_job('diff', generate_diff, self.collection, self.current_context_count)
def calculate_statistics(self):
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))
self.ref_path_map = 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
@property
def current_position(self):
return self.diff_lines[min(len(self.diff_lines) - 1, self.scroll_pos)].ref
@current_position.setter
def current_position(self, ref):
num = None
if isinstance(ref.extra, LineRef):
sln = ref.extra.src_line_number
for i, q in self.ref_path_map[ref.path]:
if isinstance(q.extra, LineRef):
if q.extra.src_line_number >= sln:
if q.extra.src_line_number == sln:
num = i
break
num = i
if num is None:
for i, q in self.ref_path_map[ref.path]:
num = i
break
if num is not None:
self.scroll_pos = min(num, self.max_scroll_pos)
@property
def num_lines(self):
return self.screen_size.rows - 1
def scroll_to_next_change(self, backwards=False):
if backwards:
r = range(self.scroll_pos - 1, -1, -1)
else:
r = range(self.scroll_pos + 1, len(self.diff_lines))
for i in r:
line = self.diff_lines[i]
if line.is_change_start:
self.scroll_lines(i - self.scroll_pos)
return
self.cmd.bell()
def set_scrolling_region(self):
self.cmd.set_scrolling_region(self.screen_size, 0, self.num_lines - 2)
def scroll_lines(self, amt=1):
new_pos = max(0, min(self.scroll_pos + amt, self.max_scroll_pos))
if new_pos == self.scroll_pos:
self.cmd.bell()
return
if abs(new_pos - self.scroll_pos) >= self.num_lines - 1:
self.scroll_pos = new_pos
self.draw_screen()
return
self.enforce_cursor_state()
self.cmd.scroll_screen(amt)
self.scroll_pos = new_pos
if amt < 0:
self.cmd.set_cursor_position(0, 0)
self.draw_lines(-amt)
else:
self.cmd.set_cursor_position(0, self.num_lines - amt)
self.draw_lines(amt, self.num_lines - amt)
self.draw_status_line()
def init_terminal_state(self):
self.cmd.set_line_wrapping(False)
self.cmd.set_window_title(main.title)
self.cmd.set_default_colors(self.opts.foreground, self.opts.background)
def initialize(self):
self.init_terminal_state()
self.set_scrolling_region()
self.draw_screen()
self.create_collection()
def enforce_cursor_state(self):
self.cmd.set_cursor_visible(self.state > DIFFED)
def finalize(self):
self.cmd.set_cursor_visible(True)
self.cmd.set_default_colors()
self.cmd.set_scrolling_region()
def draw_lines(self, num, offset=0):
offset += self.scroll_pos
image_involved = False
limit = len(self.diff_lines)
for i in range(num):
lpos = offset + i
if lpos >= limit:
text = ''
else:
line = self.diff_lines[lpos]
text = line.text
if line.image_data is not None:
image_involved = True
self.write('\r\x1b[K' + text + '\x1b[0m')
if i < num - 1:
self.write('\n')
if image_involved:
self.place_images()
def place_images(self):
self.cmd.clear_images_on_screen()
offset = self.scroll_pos
limit = len(self.diff_lines)
in_image = 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:
self.place_image(row, left_placement, True)
in_image = True
if right_placement is not None:
self.place_image(row, right_placement, False)
in_image = True
def place_image(self, row, placement, is_left):
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:
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):
self.enforce_cursor_state()
if self.state < DIFFED:
self.cmd.clear_screen()
self.write(_('Calculating diff, please wait...'))
return
self.cmd.clear_images_on_screen()
self.cmd.set_cursor_position(0, 0)
self.draw_lines(self.num_lines)
self.draw_status_line()
def draw_status_line(self):
if self.state < DIFFED:
return
self.cmd.set_cursor_position(0, self.num_lines)
self.cmd.clear_to_eol()
scroll_frac = styled('{:.0%}'.format(self.scroll_pos / (self.max_scroll_pos or 1)), fg=self.opts.margin_fg)
counts = '{}{}{}'.format(
styled(str(self.added_count), fg=self.opts.highlight_added_bg),
styled(',', fg=self.opts.margin_fg),
styled(str(self.removed_count), fg=self.opts.highlight_removed_bg)
)
suffix = counts + ' ' + scroll_frac
prefix = styled(':', fg=self.opts.margin_fg)
filler = self.screen_size.cols - wcswidth(prefix) - wcswidth(suffix)
text = '{}{}{}'.format(prefix, ' ' * filler, suffix)
self.write(text)
def change_context_count(self, new_ctx):
new_ctx = max(0, new_ctx)
if new_ctx != self.current_context_count:
self.current_context_count = new_ctx
self.state = COLLECTED
self.generate_diff()
self.restore_position = self.current_position
self.draw_screen()
def on_text(self, text, in_bracketed_paste=False):
action = self.shortcut_action(text)
if action is not None:
return self.perform_action(action)
def on_key(self, key_event):
if key_event.type is RELEASE:
return
action = self.shortcut_action(key_event)
if action is not None:
return self.perform_action(action)
def on_resize(self, screen_size):
self.screen_size = screen_size
self.set_scrolling_region()
if self.state > COLLECTED:
self.image_manager.delete_all_sent_images()
self.render_diff()
self.draw_screen()
def on_job_done(self, job_id, job_result):
if 'tb' in job_result:
self.report_traceback_on_exit = job_result['tb']
self.quit_loop(1)
return
if job_id == 'collect':
self.collection = job_result['result']
self.state = COLLECTED
self.generate_diff()
elif job_id == 'diff':
diff_map = job_result['result']
if isinstance(diff_map, str):
self.report_traceback_on_exit = diff_map
self.quit_loop(1)
return
self.state = DIFFED
self.diff_map = diff_map
self.calculate_statistics()
self.render_diff()
self.scroll_pos = 0
if self.restore_position is not None:
self.current_position = self.restore_position
self.restore_position = None
self.draw_screen()
if initialize_highlighter is not None and not self.highlighting_done:
from .highlight import StyleNotFound
self.highlighting_done = True
try:
initialize_highlighter(self.opts.pygments_style)
except StyleNotFound as e:
self.report_traceback_on_exit = str(e)
self.quit_loop(1)
return
self.start_job('highlight', highlight_collection, self.collection, self.opts.syntax_aliases)
elif job_id == 'highlight':
hdata = job_result['result']
if isinstance(hdata, str):
self.report_traceback_on_exit = diff_map
self.quit_loop(1)
return
set_highlight_data(hdata)
self.render_diff()
self.draw_screen()
def on_interrupt(self):
self.quit_loop(1)
def on_eot(self):
self.quit_loop(1)
OPTIONS = partial('''\
--context
type=int
default=-1
Number of lines of context to show between changes. Negative values
use the number set in diff.conf
--config
type=list
{config_help}
--override -o
type=list
Override individual configuration options, can be specified multiple times.
Syntax: |_ name=value|. For example: |_ -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)
showwarning.warnings = []
def main(args):
warnings.showwarning = showwarning
msg = 'Show a side-by-side diff of the specified files/directories'
args, items = parse_args(args[1:], OPTIONS, 'file_or_directory file_or_directory', msg, 'kitty +kitten diff')
if len(items) != 2:
raise SystemExit('You must specify exactly two files/directories to compare')
left, right = items
main.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)
set_diff_command(opts.diff_cmd)
lines_for_path.replace_tab_by = opts.replace_tab_by
loop = Loop()
handler = DiffHandler(args, opts, left, right)
loop.loop(handler)
for message in showwarning.warnings:
from kitty.utils import safe_print
safe_print(message, file=sys.stderr)
if loop.return_code != 0:
if handler.report_traceback_on_exit:
print(handler.report_traceback_on_exit, file=sys.stderr)
input('Press Enter to quit.')
raise SystemExit(loop.return_code)
if __name__ == '__main__':
main(sys.argv)

241
kittens/diff/patch.py Normal file
View File

@@ -0,0 +1,241 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import concurrent.futures
import os
import shlex
import shutil
import subprocess
from .collect import lines_for_path
from .diff_speedup import changed_center
left_lines = right_lines = None
GIT_DIFF = 'git diff --no-color --no-ext-diff --exit-code -U_CONTEXT_ --no-index --'
DIFF_DIFF = 'diff -p -U _CONTEXT_ --'
def find_differ():
if shutil.which('git'):
return GIT_DIFF
if shutil.which('diff'):
return DIFF_DIFF
def set_diff_command(opt):
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
def run_diff(file1, file2, context=3):
# returns: ok, is_different, patch
cmd = shlex.split(set_diff_command.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))
p = subprocess.Popen(
cmd + [path1, path2],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL)
stdout, stderr = p.communicate()
returncode = p.wait()
if returncode in (0, 1):
return True, returncode == 1, stdout.decode('utf-8')
return False, returncode, stderr.decode('utf-8')
class Chunk:
__slots__ = ('is_context', 'left_start', 'right_start', 'left_count', 'right_count', 'centers')
def __init__(self, left_start, right_start, is_context=False):
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
def add_line(self):
self.right_count += 1
def remove_line(self):
self.left_count += 1
def context_line(self):
self.left_count += 1
self.right_count += 1
def finalize(self):
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):
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):
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.largest_line_number = max(self.left_start + self.left_count, self.right_start + self.right_count)
def new_chunk(self, is_context=False):
if self.chunks:
c = self.chunks[-1]
left_start = c.left_start + c.left_count
right_start = c.right_start + c.right_count
else:
left_start = self.left_start
right_start = self.right_start
return Chunk(left_start, right_start, is_context)
def ensure_diff_chunk(self):
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):
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):
self.ensure_diff_chunk()
self.current_chunk.add_line()
self.added_count += 1
def remove_line(self):
self.ensure_diff_chunk()
self.current_chunk.remove_line()
self.removed_count += 1
def context_line(self):
self.ensure_context_chunk()
self.current_chunk.context_line()
def finalize(self):
self.chunks.append(self.current_chunk)
del self.current_chunk
# Sanity check
c = self.chunks[-1]
if c.left_start + c.left_count != self.left_start + self.left_count:
raise ValueError('Left side line mismatch {} != {}'.format(c.left_start + c.left_count, self.left_start + self.left_count))
if c.right_start + c.right_count != self.right_start + self.right_count:
raise ValueError('Left side line mismatch {} != {}'.format(c.right_start + c.right_count, self.right_start + self.right_count))
for c in self.chunks:
c.finalize()
def parse_range(x):
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)))
linespec = parts[0].strip()
title = ''
if len(parts) == 2:
title = parts[1].strip()
left, right = map(parse_range, linespec.split())
return Hunk(title, left, right)
class Patch:
def __init__(self, all_hunks):
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):
return iter(self.all_hunks)
def __len__(self):
return len(self.all_hunks)
def parse_patch(raw):
all_hunks = []
current_hunk = None
for line in raw.splitlines():
if line.startswith('@@ '):
current_hunk = parse_hunk_header(line)
all_hunks.append(current_hunk)
else:
if current_hunk is None:
continue
q = line[0]
if q == '+':
current_hunk.add_line()
elif q == '-':
current_hunk.remove_line()
elif q == '\\':
continue
else:
current_hunk.context_line()
for h in all_hunks:
h.finalize()
return Patch(all_hunks)
class Differ:
def __init__(self):
self.jmap = {}
self.jobs = []
def add_diff(self, file1, file2):
self.jmap[file1] = file2
self.jobs.append(file1)
def __call__(self, context=3):
global left_lines, right_lines
ans = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=os.cpu_count()) as executor:
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]
left_path, right_path = key, self.jmap[key]
try:
ok, returncode, output = future.result()
except FileNotFoundError as err:
return 'Could not find the {} executable. Is it in your PATH?'.format(err.filename)
except Exception as e:
return 'Running git diff for {} vs. {} generated an exception: {}'.format(left_path, right_path, e)
if not ok:
return output + '\nRunning git diff for {} vs. {} failed'.format(left_path, right_path)
left_lines = lines_for_path(left_path)
right_lines = lines_for_path(right_path)
try:
patch = parse_patch(output)
except Exception:
import traceback
return traceback.format_exc() + '\nParsing diff for {} vs. {} failed'.format(left_path, right_path)
else:
ans[key] = patch
return ans

489
kittens/diff/render.py Normal file
View File

@@ -0,0 +1,489 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import warnings
from gettext import gettext as _
from itertools import repeat, zip_longest
from math import ceil
from kitty.fast_data_types import truncate_point_for_length, wcswidth
from ..tui.images import can_display_images, screen_size
from .collect import (
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
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)
return ans
class Ref:
def __setattr__(self, name, value):
raise AttributeError("can't set attribute")
def __repr__(self):
return '{}({})'.format(self.__class__.__name__, ', '.join(
'{}={}'.format(n, getattr(self, n)) for n in self.__slots__ if n != '_hash'))
class LineRef(Ref):
__slots__ = ('src_line_number', 'wrapped_line_idx')
def __init__(self, sln, wli=0):
object.__setattr__(self, 'src_line_number', sln)
object.__setattr__(self, 'wrapped_line_idx', wli)
class Reference(Ref):
__slots__ = ('path', 'extra')
def __init__(self, path, extra=None):
object.__setattr__(self, 'path', path)
object.__setattr__(self, 'extra', extra)
class Line:
__slots__ = ('text', 'ref', 'is_change_start', 'image_data')
def __init__(self, text, ref, change_start=False, image_data=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):
for text in iterator:
yield Line(text, reference, is_change_start)
is_change_start = False
def human_readable(size, sep=' '):
""" 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
def fit_in(text, count):
p = truncate_point_for_length(text, count)
if p >= len(text):
return text
if count > 1:
p = truncate_point_for_length(text, count - 1)
return text[:p] + ''
def fill_in(text, sz):
w = wcswidth(text)
if w < sz:
text += ' ' * (sz - w)
return text
def place_in(text, sz):
return fill_in(fit_in(text, sz), sz)
def format_func(which):
def formatted(text):
fmt = formats[which]
return '\x1b[' + fmt + 'm' + text + '\x1b[0m'
formatted.__name__ = which + '_format'
return formatted
text_format = format_func('text')
title_format = format_func('title')
margin_format = format_func('margin')
added_format = format_func('added')
removed_format = format_func('removed')
removed_margin_format = format_func('removed_margin')
added_margin_format = format_func('added_margin')
filler_format = format_func('filler')
hunk_margin_format = format_func('hunk_margin')
hunk_format = format_func('hunk')
highlight_map = {'remove': ('removed_highlight', 'removed'), 'add': ('added_highlight', 'added')}
def highlight_boundaries(ltype):
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):
m = ' ' * margin_size
left_name, right_name = map(path_name_map.get, (left_path, right_path))
if right_name and right_name != left_name:
n1 = fit_in(m + sanitize(left_name), 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)
yield title_format(place_in(name, columns))
yield title_format('' * columns)
def binary_lines(path, other_path, columns, margin_size):
template = _('Binary file: {}')
available_cols = columns // 2 - margin_size
def fl(path, fmt):
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)
yield filler + fl(other_path, added_format)
elif other_path is None:
filler = render_diff_line('', '', 'filler', margin_size, available_cols)
yield fl(path, removed_format) + filler
else:
yield fl(path, removed_format) + fl(other_path, added_format)
def split_to_size(line, width):
if not line:
yield line
while line:
p = truncate_point_for_length(line, width)
yield line[:p]
line = line[p:]
def truncate_points(line, width):
pos = 0
sz = len(line)
while True:
pos = truncate_point_for_length(line, width, pos)
if pos < sz:
yield pos
else:
break
def split_with_highlights(line, width, highlights, bg_highlight=None):
truncate_pts = list(truncate_points(line, width))
return _split_with_highlights(line, truncate_pts, highlights, bg_highlight)
margin_bg_map = {'filler': filler_format, 'remove': removed_margin_format, 'add': added_margin_format, 'context': margin_format}
text_bg_map = {'filler': filler_format, 'remove': removed_format, 'add': added_format, 'context': text_format}
class DiffData:
def __init__(self, left_path, right_path, available_cols, margin_size):
self.left_path, self.right_path = left_path, right_path
self.available_cols = available_cols
self.margin_size = margin_size
self.left_lines, self.right_lines = map(lines_for_path, (left_path, right_path))
self.filler_line = render_diff_line('', '', 'filler', margin_size, available_cols)
self.left_filler_line = render_diff_line('', '', 'remove', margin_size, available_cols)
self.right_filler_line = render_diff_line('', '', 'add', margin_size, available_cols)
self.left_hdata = highlights_for_path(left_path)
self.right_hdata = highlights_for_path(right_path)
def left_highlights_for_line(self, line_num):
if line_num < len(self.left_hdata):
return self.left_hdata[line_num]
return []
def right_highlights_for_line(self, line_num):
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))
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):
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 (
render_diff_line(left_line_number if is_first else None, left, ltype, margin_size, available_cols) +
render_diff_line(right_line_number if is_first else None, right, rtype, margin_size, available_cols)
)
def hunk_title(hunk_num, hunk, margin_size, available_cols):
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
if changed_center is not None and changed_center[0]:
prefix_count, suffix_count = changed_center
line_sz = len(line)
if prefix_count + suffix_count < line_sz:
start, stop = highlight_boundaries(ltype)
seg = Segment(prefix_count, start)
seg.end = line_sz - suffix_count
seg.end_code = stop
bg_highlight = seg
if highlights or bg_highlight:
lines = split_with_highlights(line, available_cols, highlights, bg_highlight)
else:
lines = split_to_size(line, available_cols)
line_number = str(line_number + 1)
for line in lines:
yield render_diff_line(line_number, line, ltype, margin_size, available_cols)
line_number = ''
def lines_for_chunk(data, hunk_num, chunk, chunk_num):
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)
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)
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:
r = line
else:
r = render_diff_line(right_line_number, 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 = ''
else:
common = min(chunk.left_count, chunk.right_count)
for i in range(max(chunk.left_count, chunk.right_count)):
ll, rl = [], []
if i < chunk.left_count:
rln = ref_ln = chunk.left_start + i
ll.extend(render_half_line(
rln, data.left_lines[rln], data.left_highlights_for_line(rln),
'remove', data.margin_size, data.available_cols,
None if chunk.centers is None else chunk.centers[i]))
ref_path = data.left_path
if i < chunk.right_count:
rln = ref_ln = chunk.right_start + i
rl.extend(render_half_line(
rln, data.right_lines[rln], data.right_highlights_for_line(rln),
'add', data.margin_size, data.available_cols,
None if chunk.centers is None else chunk.centers[i]))
ref_path = data.right_path
if i < common:
extra = len(ll) - len(rl)
if extra != 0:
if extra < 0:
x, fl = ll, data.left_filler_line
extra = -extra
else:
x, fl = rl, data.right_filler_line
x.extend(repeat(fl, extra))
else:
if ll:
x, count = rl, len(ll)
else:
x, count = ll, len(rl)
x.extend(repeat(data.filler_line, count))
for wli, (left_line, right_line) in enumerate(zip(ll, rl)):
ref = Reference(ref_path, LineRef(ref_ln, wli))
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):
available_cols = columns // 2 - margin_size
data = DiffData(left_path, right_path, available_cols, margin_size)
for hunk_num, hunk in enumerate(hunks):
yield Line(hunk_title(hunk_num, hunk, margin_size, columns - margin_size), Reference(left_path, LineRef(hunk.left_start)))
for cnum, chunk in enumerate(hunk.chunks):
yield from lines_for_chunk(data, hunk_num, chunk, cnum)
def all_lines(path, args, columns, margin_size, is_add=True):
available_cols = columns // 2 - margin_size
ltype = 'add' if is_add else 'remove'
lines = lines_for_path(path)
filler = render_diff_line('', '', 'filler', margin_size, available_cols)
msg_written = False
hdata = highlights_for_path(path)
def highlights(num):
return hdata[num] if num < len(hdata) else []
for line_number, line in enumerate(lines):
h = render_half_line(line_number, line, highlights(line_number), ltype, margin_size, available_cols)
for i, hl in enumerate(h):
ref = Reference(path, LineRef(line_number, i))
empty = filler
if not msg_written:
msg_written = True
empty = render_diff_line(
'', _('This file was added') if is_add else _('This file was removed'),
'filler', margin_size, available_cols)
text = (empty + hl) if is_add else (hl + empty)
yield Line(text, ref, line_number == 0 and i == 0)
def rename_lines(path, other_path, args, columns, margin_size):
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):
yield m + line
class Image:
def __init__(self, image_id, width, height, margin_size):
self.image_id = image_id
self.width, self.height = width, height
ss = screen_size()
self.rows = int(ceil(self.height / ss.cell_height))
self.columns = int(ceil(self.width / ss.cell_width))
self.margin_size = margin_size
class ImagePlacement:
def __init__(self, image, row):
self.image = image
self.row = row
def render_image(path, is_left, available_cols, margin_size, image_manager):
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):
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
lnum += 1
try:
image_id, width, height = image_manager.send_image(path, available_cols - margin_size, screen_size().rows - 2)
except Exception as e:
yield from yield_split(_('Failed to render image, with error:'))
yield from yield_split(' '.join(str(e).splitlines()))
return
meta = _('Dimensions: {0}x{1} pixels Size: {2}').format(
width, height, human_readable(len(data_for_path(path))))
yield from yield_split(meta)
bg_line = m + fmt(' ' * available_cols)
img = Image(image_id, width, height, margin_size)
for r in range(img.rows):
yield bg_line, Reference(path, LineRef(lnum)), ImagePlacement(img, r)
lnum += 1
def image_lines(left_path, right_path, columns, margin_size, image_manager):
available_cols = columns // 2 - margin_size
left_lines, right_lines = iter(()), 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:
right_lines = render_image(right_path, False, available_cols, margin_size, image_manager)
filler = ' ' * (available_cols + margin_size)
is_change_start = True
for left, right in zip_longest(left_lines, right_lines):
left_placement = right_placement = None
if left is None:
left = filler
right, ref, right_placement = right
elif right is None:
right = filler
left, ref, left_placement = left
else:
right, ref, right_placement = right
left, ref, left_placement = left
image_data = (left_placement, right_placement) if left_placement or right_placement else None
yield Line(left + right, ref, is_change_start, image_data)
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)
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)
is_img = is_binary and is_image(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 = 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 = 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 = 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)

179
kittens/diff/speedup.c Normal file
View File

@@ -0,0 +1,179 @@
/*
* speedup.c
* Copyright (C) 2018 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "data-types.h"
static PyObject*
changed_center(PyObject *self UNUSED, PyObject *args) {
unsigned int prefix_count = 0, suffix_count = 0;
PyObject *lp, *rp;
if (!PyArg_ParseTuple(args, "UU", &lp, &rp)) return NULL;
const size_t left_len = PyUnicode_GET_LENGTH(lp), right_len = PyUnicode_GET_LENGTH(rp);
#define R(which, index) PyUnicode_READ(PyUnicode_KIND(which), PyUnicode_DATA(which), index)
while(prefix_count < MIN(left_len, right_len)) {
if (R(lp, prefix_count) != R(rp, prefix_count)) break;
prefix_count++;
}
if (left_len && right_len && prefix_count < MIN(left_len, right_len)) {
while(suffix_count < MIN(left_len - prefix_count, right_len - prefix_count)) {
if(R(lp, left_len - 1 - suffix_count) != R(rp, right_len - 1 - suffix_count)) break;
suffix_count++;
}
}
#undef R
return Py_BuildValue("II", prefix_count, suffix_count);
}
typedef struct {
unsigned int start_pos, end_pos, current_pos;
PyObject *start_code, *end_code;
} Segment;
typedef struct {
Segment sg;
unsigned int num, pos;
} SegmentPointer;
static const Segment EMPTY_SEGMENT = { .current_pos = UINT_MAX };
static inline bool
convert_segment(PyObject *highlight, Segment *dest) {
PyObject *val = NULL;
#define I
#define A(x, d, c) { \
val = PyObject_GetAttrString(highlight, #x); \
if (val == NULL) return false; \
dest->d = c(val); Py_DECREF(val); \
}
A(start, start_pos, PyLong_AsUnsignedLong);
A(end, end_pos, PyLong_AsUnsignedLong);
dest->current_pos = dest->start_pos;
A(start_code, start_code, I);
A(end_code, end_code, I);
if (!PyUnicode_Check(dest->start_code)) { PyErr_SetString(PyExc_TypeError, "start_code is not a string"); return false; }
if (!PyUnicode_Check(dest->end_code)) { PyErr_SetString(PyExc_TypeError, "end_code is not a string"); return false; }
#undef A
#undef I
return true;
}
static inline bool
next_segment(SegmentPointer *s, PyObject *highlights) {
if (s->pos < s->num) {
if (!convert_segment(PyList_GET_ITEM(highlights, s->pos), &s->sg)) return false;
s->pos++;
} else s->sg.current_pos = UINT_MAX;
return true;
}
static inline bool
insert_code(PyObject *code, Py_UCS4 *buf, size_t bufsz, unsigned int *buf_pos) {
unsigned int csz = PyUnicode_GET_LENGTH(code);
if (*buf_pos + csz >= bufsz) return false;
for (unsigned int s = 0; s < csz; s++) buf[(*buf_pos)++] = PyUnicode_READ(PyUnicode_KIND(code), PyUnicode_DATA(code), s);
return true;
}
static inline bool
add_line(Segment *bg_segment, Segment *fg_segment, Py_UCS4 *buf, size_t bufsz, unsigned int *buf_pos, PyObject *ans) {
bool bg_is_active = bg_segment->current_pos == bg_segment->end_pos, fg_is_active = fg_segment->current_pos == fg_segment->end_pos;
if (bg_is_active) { if(!insert_code(bg_segment->end_code, buf, bufsz, buf_pos)) return false; }
if (fg_is_active) { if(!insert_code(fg_segment->end_code, buf, bufsz, buf_pos)) return false; }
PyObject *wl = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, buf, *buf_pos);
if (!wl) return false;
int ret = PyList_Append(ans, wl); Py_DECREF(wl); if (ret != 0) return false;
*buf_pos = 0;
if (bg_is_active) { if(!insert_code(bg_segment->start_code, buf, bufsz, buf_pos)) return false; }
if (fg_is_active) { if(!insert_code(fg_segment->start_code, buf, bufsz, buf_pos)) return false; }
return true;
}
static PyObject*
split_with_highlights(PyObject *self UNUSED, PyObject *args) {
PyObject *line, *truncate_points_py, *fg_highlights, *bg_highlight;
if (!PyArg_ParseTuple(args, "UO!O!O", &line, &PyList_Type, &truncate_points_py, &PyList_Type, &fg_highlights, &bg_highlight)) return NULL;
PyObject *ans = PyList_New(0);
if (!ans) return NULL;
static unsigned int truncate_points[256];
unsigned int num_truncate_pts = PyList_GET_SIZE(truncate_points_py), truncate_pos = 0, truncate_point;
for (unsigned int i = 0; i < MIN(num_truncate_pts, arraysz(truncate_points)); i++) {
truncate_points[i] = PyLong_AsUnsignedLong(PyList_GET_ITEM(truncate_points_py, i));
}
SegmentPointer fg_segment = { .sg = EMPTY_SEGMENT, .num = PyList_GET_SIZE(fg_highlights)}, bg_segment = { .sg = EMPTY_SEGMENT };
if (bg_highlight != Py_None) { if (!convert_segment(bg_highlight, &bg_segment.sg)) { Py_CLEAR(ans); return NULL; }; bg_segment.num = 1; }
#define CHECK_CALL(func, ...) if (!func(__VA_ARGS__)) { Py_CLEAR(ans); if (!PyErr_Occurred()) PyErr_SetString(PyExc_ValueError, "line too long"); return NULL; }
CHECK_CALL(next_segment, &fg_segment, fg_highlights);
#define NEXT_TRUNCATE_POINT truncate_point = (truncate_pos < num_truncate_pts) ? truncate_points[truncate_pos++] : UINT_MAX
NEXT_TRUNCATE_POINT;
#define INSERT_CODE(x) { CHECK_CALL(insert_code, x, buf, arraysz(buf), &buf_pos); }
#define ADD_LINE CHECK_CALL(add_line, &bg_segment.sg, &fg_segment.sg, buf, arraysz(buf), &buf_pos, ans);
#define ADD_CHAR(x) { \
buf[buf_pos++] = x; \
if (buf_pos >= arraysz(buf)) { Py_CLEAR(ans); PyErr_SetString(PyExc_ValueError, "line too long"); return NULL; } \
}
#define CHECK_SEGMENT(sgp, is_fg) { \
if (i == sgp.sg.current_pos) { \
INSERT_CODE(sgp.sg.current_pos == sgp.sg.start_pos ? sgp.sg.start_code : sgp.sg.end_code); \
if (sgp.sg.current_pos == sgp.sg.start_pos) sgp.sg.current_pos = sgp.sg.end_pos; \
else { \
if (is_fg) { \
CHECK_CALL(next_segment, &fg_segment, fg_highlights); \
if (sgp.sg.current_pos == i) { \
INSERT_CODE(sgp.sg.start_code); \
sgp.sg.current_pos = sgp.sg.end_pos; \
} \
} else sgp.sg.current_pos = UINT_MAX; \
} \
}\
}
const unsigned int line_sz = PyUnicode_GET_LENGTH(line);
static Py_UCS4 buf[4096];
unsigned int i = 0, buf_pos = 0;
for (; i < line_sz; i++) {
if (i == truncate_point) { ADD_LINE; NEXT_TRUNCATE_POINT; }
CHECK_SEGMENT(bg_segment, false);
CHECK_SEGMENT(fg_segment, true)
ADD_CHAR(PyUnicode_READ(PyUnicode_KIND(line), PyUnicode_DATA(line), i));
}
if (buf_pos) ADD_LINE;
return ans;
#undef INSERT_CODE
#undef CHECK_SEGMENT
#undef CHECK_CALL
#undef ADD_CHAR
#undef ADD_LINE
#undef NEXT_TRUNCATE_POINT
}
static PyMethodDef module_methods[] = {
{"changed_center", (PyCFunction)changed_center, METH_VARARGS, ""},
{"split_with_highlights", (PyCFunction)split_with_highlights, METH_VARARGS, ""},
{NULL, NULL, 0, NULL} /* Sentinel */
};
static struct PyModuleDef module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "diff_speedup", /* name of module */
.m_doc = NULL,
.m_size = -1,
.m_methods = module_methods
};
EXPORTED PyMODINIT_FUNC
PyInit_diff_speedup(void) {
PyObject *m;
m = PyModule_Create(&module);
if (m == NULL) return NULL;
return m;
}

View File

@@ -12,15 +12,15 @@ from gettext import gettext as _
from kitty.cli import parse_args
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 ..tui.handler import Handler
from ..tui.loop import Loop
from ..tui.operations import (
clear_screen, faint, set_cursor_visible, set_window_title, styled
)
from ..tui.operations import faint, styled
URL_PREFIXES = 'http https file ftp'.split()
HINT_ALPHABET = string.digits + string.ascii_lowercase
screen_size = screen_size_function()
class Mark(object):
@@ -46,50 +46,37 @@ def decode_hint(x):
return int(x, 36)
def render(lines, current_input):
ans = []
def highlight_mark(m, text, current_input):
hint = encode_hint(m.index)
if current_input and not hint.startswith(current_input):
return faint(text)
hint = hint[len(current_input):] or ' '
text = text[len(hint):]
return styled(
hint,
fg='black',
bg='green',
bold=True
) + styled(
text, fg='gray', fg_intense=True, bold=True
)
def mark(m):
hint = encode_hint(m.index)
text = m.text
if current_input and not hint.startswith(current_input):
return faint(text)
hint = hint[len(current_input):] or ' '
text = text[len(hint):]
return styled(
hint,
fg='black',
bg='green',
bold=True
) + styled(
text, fg='gray', fg_intense=True, bold=True
)
for line, marks in lines:
if not marks:
ans.append(faint(line))
continue
buf = []
def render(text, current_input, all_marks):
for mark in reversed(all_marks):
mtext = highlight_mark(mark, text[mark.start:mark.end], current_input)
text = text[:mark.start] + mtext + text[mark.end:]
for i, m in enumerate(marks):
if i == 0 and m.start:
buf.append(faint(line[:m.start]))
buf.append(mark(m))
if m is not marks[-1]:
buf.append(faint(line[m.end:marks[i + 1].start]))
text = text.replace('\0', '')
rest = line[marks[-1].end:]
if rest:
buf.append(faint(rest))
ans.append(''.join(buf))
return '\r\n'.join(ans)
return text.replace('\n', '\r\n').rstrip()
class Hints(Handler):
def __init__(self, lines, index_map, args):
self.lines, self.index_map = tuple(lines), index_map
def __init__(self, text, all_marks, index_map, args):
self.text, self.index_map = text, index_map
self.all_marks = all_marks
self.current_input = ''
self.current_text = None
self.args = args
@@ -97,11 +84,11 @@ class Hints(Handler):
self.chosen = None
def init_terminal_state(self):
self.write(set_cursor_visible(False))
self.write(set_window_title(self.window_title))
self.cmd.set_cursor_visible(False)
self.cmd.set_window_title(self.window_title)
self.cmd.set_line_wrapping(False)
def initialize(self, *args):
Handler.initialize(self, *args)
def initialize(self):
self.init_terminal_state()
self.draw_screen()
@@ -129,9 +116,15 @@ class Hints(Handler):
self.current_text = None
self.draw_screen()
elif key_event is enter_key and self.current_input:
idx = decode_hint(self.current_input)
self.chosen = self.index_map[idx].text
self.quit_loop(0)
try:
idx = decode_hint(self.current_input)
self.chosen = self.index_map[idx].text
except Exception:
self.current_input = ''
self.current_text = None
self.draw_screen()
else:
self.quit_loop(0)
elif key_event.key is ESCAPE:
self.quit_loop(1)
@@ -147,44 +140,86 @@ class Hints(Handler):
def draw_screen(self):
if self.current_text is None:
self.current_text = render(self.lines, self.current_input)
self.write(clear_screen())
self.current_text = render(self.text, self.current_input, self.all_marks)
self.cmd.clear_screen()
self.write(self.current_text)
def regex_finditer(pat, minimum_match_length, line):
for m in pat.finditer(line):
def regex_finditer(pat, minimum_match_length, text):
for m in pat.finditer(text):
s, e = m.span(pat.groups)
while e > s + 1 and text[e-1] == '\0':
e -= 1
if e - s >= minimum_match_length:
yield s, e
def find_urls(pat, line):
for m in pat.finditer(line):
s, e = m.span()
if s > 4 and line[s - 5:s] == 'link:': # asciidoc URLs
url = line[s:e]
idx = url.rfind('[')
if idx > -1:
e -= len(url) - idx
while line[e - 1] in '.,?!' and e > 1: # remove trailing punctuation
closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>'}
opening_brackets = ''.join(closing_bracket_map)
postprocessor_map = {}
def postprocessor(func):
postprocessor_map[func.__name__] = func
return func
@postprocessor
def url(text, s, e):
if s > 4 and text[s - 5:s] == 'link:': # asciidoc URLs
url = text[s:e]
idx = url.rfind('[')
if idx > -1:
e -= len(url) - idx
while text[e - 1] in '.,?!' and e > 1: # remove trailing punctuation
e -= 1
# Restructured Text URLs
if e > 3 and text[e-2:e] == '`_':
e -= 2
# Remove trailing bracket if matched by leading bracket
if s > 0 and e < len(text) and text[s-1] in opening_brackets and text[e-1] == closing_bracket_map[text[s-1]]:
e -= 1
# Remove trailing quote if matched by leading quote
if s > 0 and e < len(text) and text[s-1] in '\'"' and text[e-1] == text[s-1]:
e -= 1
return s, e
@postprocessor
def brackets(text, s, e):
# Remove matching brackets
if e > s and e <= len(text):
before = text[s]
if before in '({[<' and text[e-1] == closing_bracket_map[before]:
s += 1
e -= 1
yield s, e
return s, e
def mark(finditer, line, all_marks):
marks = []
for s, e in finditer(line):
idx = len(all_marks)
text = line[s:e]
marks.append(Mark(idx, s, e, text))
all_marks.append(marks[-1])
return line, marks
@postprocessor
def quotes(text, s, e):
# Remove matching quotes
if e > s and e <= len(text):
before = text[s]
if before in '\'"' and text[e-1] == before:
s += 1
e -= 1
return s, e
def run_loop(args, lines, index_map):
def mark(pattern, post_processors, text, args):
pat = re.compile(pattern)
for idx, (s, e) in enumerate(regex_finditer(pat, args.minimum_match_length, text)):
for func in post_processors:
s, e = func(text, s, e)
mark_text = text[s:e].replace('\n', '').replace('\0', '')
yield Mark(idx, s, e, mark_text)
def run_loop(args, text, all_marks, index_map):
loop = Loop()
handler = Hints(lines, index_map, args)
handler = Hints(text, all_marks, index_map, args)
loop.loop(handler)
if handler.chosen and loop.return_code == 0:
return {'match': handler.chosen, 'program': args.program}
@@ -195,45 +230,74 @@ def escape(chars):
return chars.replace('\\', '\\\\').replace('-', r'\-').replace(']', r'\]')
def run(args, text):
def functions_for(args):
post_processors = []
if args.type == 'url':
from .url_regex import url_delimiters
url_pat = '(?:{})://[^{}]{{3,}}'.format(
pattern = '(?:{})://[^{}]{{3,}}'.format(
'|'.join(args.url_prefixes.split(',')), url_delimiters
)
finditer = partial(find_urls, re.compile(url_pat))
post_processors.append(url)
elif args.type == 'path':
finditer = partial(regex_finditer, re.compile(r'(?:\S*/\S+)|(?:\S+[.][a-zA-Z0-9]{2,5})'), args.minimum_match_length)
pattern = r'(?:\S*/\S+)|(?:\S+[.][a-zA-Z0-9]{2,5})'
post_processors.extend((brackets, quotes))
elif args.type == 'line':
finditer = partial(regex_finditer, re.compile(r'(?m)^\s*(.+)\s*$'), args.minimum_match_length)
pattern = '(?m)^\\s*(.+)[\\s\0]*$'
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']
pat = re.compile('(?u)[{}\w]{{{},}}'.format(escape(chars), args.minimum_match_length))
finditer = partial(regex_finditer, pat, args.minimum_match_length)
pattern = r'(?u)[{}\w]{{{},}}'.format(escape(chars), args.minimum_match_length)
post_processors.extend((brackets, quotes))
else:
finditer = partial(regex_finditer, re.compile(args.regex), args.minimum_match_length)
pattern = args.regex
return pattern, post_processors
def convert_text(text, cols):
lines = []
all_marks = []
for line in text.splitlines():
marked = mark(finditer, line, all_marks)
lines.append(marked)
if not all_marks:
input(_('No {} found, press Enter to abort.').format(
'URLs' if args.type == 'url' else 'matches'
))
return
largest_index = all_marks[-1].index
for m in all_marks:
m.index = largest_index - m.index
index_map = {m.index: m for m in all_marks}
return run_loop(args, lines, index_map)
for full_line in text.split('\n'):
if full_line:
for line in full_line.split('\r'):
if line:
lines.append(line.ljust(cols, '\0'))
return '\n'.join(lines)
def parse_input(text):
try:
cols = int(os.environ['OVERLAID_WINDOW_COLS'])
except KeyError:
cols = screen_size().cols
return convert_text(text, cols)
def run(args, text):
try:
pattern, post_processors = functions_for(args)
text = parse_input(text)
all_marks = tuple(mark(pattern, post_processors, text, args))
if not all_marks:
input(_('No {} found, press Enter to quit.').format(
'URLs' if args.type == 'url' else 'matches'
))
return
largest_index = all_marks[-1].index
for m in all_marks:
m.index = largest_index - m.index
index_map = {m.index: m for m in all_marks}
except Exception:
import traceback
traceback.print_exc()
input('Press Enter to quit.')
raise SystemExit(1)
return run_loop(args, text, all_marks, index_map)
# CLI {{{
OPTIONS = partial(r'''
--program
default=default
@@ -261,7 +325,7 @@ Comma separated list of recognized URL prefixes.
--word-characters
Characters to consider as part of a word. In addition, all chacraters marked as
Characters to consider as part of a word. In addition, all characters marked as
alpha-numeric in the unicode database will be considered as word characters.
Defaults to the select_by_word_characters setting from kitty.conf.
@@ -273,8 +337,12 @@ The minimum number of characters to consider a match.
'''.format, ','.join(sorted(URL_PREFIXES)))
def main(args):
def parse_hints_args(args):
msg = 'Select text from the screen using the keyboard. Defaults to searching for URLs.'
return parse_args(args, OPTIONS, '', msg, 'hints')
def main(args):
text = ''
if sys.stdin.isatty():
if '--help' not in args and '-h' not in args:
@@ -285,7 +353,7 @@ def main(args):
text = sys.stdin.buffer.read().decode('utf-8')
sys.stdin = open('/dev/tty')
try:
args, items = parse_args(args[1:], OPTIONS, '', msg, 'hints')
args, items = parse_hints_args(args[1:])
except SystemExit as e:
if e.code != 0:
print(e.args[0], file=sys.stderr)
@@ -307,7 +375,14 @@ def handle_result(args, data, target_window_id, boss):
elif program == '@':
set_clipboard_string(data['match'])
else:
boss.open_url(data['match'], None if program == 'default' else program)
cwd = None
w = boss.window_id_map.get(target_window_id)
if w is not None:
cwd = w.cwd_of_child
boss.open_url(data['match'], None if program == 'default' else program, cwd=cwd)
handle_result.type_of_input = 'screen'
if __name__ == '__main__':
@@ -315,3 +390,4 @@ if __name__ == '__main__':
ans = main(sys.argv)
if ans:
print(ans)
# }}}

View File

@@ -1 +1 @@
url_delimiters = '\x00-\x20\x7f-\xa0\xad\u0600-\u0605\u061c\u06dd\u070f\u08e2\u1680\u180e\u2000-\u200f\u2028-\u202f\u205f-\u2064\u2066-\u206f\u3000\ud800-\uf8ff\ufeff\ufff9-\ufffb\U000110bd\U0001bca0-\U0001bca3\U0001d173-\U0001d17a\U000e0001\U000e0020-\U000e007f\U000f0000-\U000ffffd\U00100000-\U0010fffd' # noqa
url_delimiters = '\x00-\x09\x0b-\x20\x7f-\xa0\xad\u0600-\u0605\u061c\u06dd\u070f\u08e2\u1680\u180e\u2000-\u200f\u2028-\u202f\u205f-\u2064\u2066-\u206f\u3000\ud800-\uf8ff\ufeff\ufff9-\ufffb\U000110bd\U0001bca0-\U0001bca3\U0001d173-\U0001d17a\U000e0001\U000e0020-\U000e007f\U000f0000-\U000ffffd\U00100000-\U0010fffd' # noqa

0
kittens/icat/__init__.py Normal file
View File

View File

@@ -2,41 +2,26 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import array
import codecs
import fcntl
import mimetypes
import os
import re
import signal
import subprocess
import sys
import termios
import zlib
from base64 import standard_b64encode
from collections import namedtuple
from math import ceil, floor
from math import ceil
from tempfile import NamedTemporaryFile
from kitty.cli import parse_args
from kitty.constants import appname
from kitty.utils import read_with_timeout
try:
fsenc = sys.getfilesystemencoding() or 'utf-8'
codecs.lookup(fsenc)
except Exception:
fsenc = 'utf-8'
class OpenFailed(ValueError):
def __init__(self, path, message):
ValueError.__init__(
self, 'Failed to open: {} with error: {}'.format(path, message)
)
self.path = path
from kitty.utils import fit_image, read_with_timeout
from ..tui.images import (
ConvertFailed, NoImageMagick, OpenFailed, convert, fsenc, identify,
screen_size
)
from ..tui.operations import clear_images_on_screen, serialize_gr_command
OPTIONS = '''\
--align
@@ -100,46 +85,11 @@ def options_spec():
return options_spec.ans
Size = namedtuple('Size', 'rows cols width height')
def screen_size():
if screen_size.changed:
buf = array.array('H', [0, 0, 0, 0])
fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, buf)
screen_size.ans = Size(*buf)
screen_size.changed = False
return screen_size.ans
screen_size.changed = True
def write_gr_cmd(cmd, payload=None):
cmd = ','.join('{}={}'.format(k, v) for k, v in cmd.items())
w = sys.stdout.buffer.write
w(b'\033_G'), w(cmd.encode('ascii'))
if payload:
w(b';')
w(payload)
w(b'\033\\')
sys.stdout.buffer.write(serialize_gr_command(cmd, payload))
sys.stdout.flush()
def fit_image(width, height, pwidth, pheight):
if height > pheight:
corrf = pheight / float(height)
width, height = floor(corrf * width), pheight
if width > pwidth:
corrf = pwidth / float(width)
width, height = pwidth, floor(corrf * height)
if height > pheight:
corrf = pheight / float(height)
width, height = floor(corrf * width), pheight
return int(width), int(height)
def calculate_in_cell_x_offset(width, cell_width, align):
if align == 'left':
return 0
@@ -217,52 +167,6 @@ def show(outfile, width, height, fmt, transmit_mode='t', align='center', place=N
write_chunked(cmd, data)
ImageData = namedtuple('ImageData', 'fmt width height mode')
def run_imagemagick(path, cmd, keep_stdout=True):
try:
p = subprocess.run(cmd, stdout=subprocess.PIPE if keep_stdout else subprocess.DEVNULL, stderr=subprocess.PIPE)
except FileNotFoundError:
raise SystemExit('ImageMagick is required to cat images')
if p.returncode != 0:
raise OpenFailed(path, p.stderr.decode('utf-8'))
return p
def identify(path):
p = run_imagemagick(path, ['identify', '-format', '%m %w %h %A', path])
parts = 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):
width, height = m.width, m.height
cmd = ['convert', '-background', 'none', path]
if scale_up:
if width < available_width:
r = available_width / width
width, height = available_width, int(height * r)
if width > available_width or height > available_height:
width, height = fit_image(width, height, available_width, available_height)
cmd += ['-resize', '{}x{}'.format(width, height)]
with NamedTemporaryFile(prefix='icat-', suffix='.' + m.mode, delete=False) as outfile:
run_imagemagick(path, cmd + [outfile.name])
# ImageMagick sometimes generated rgba images smaller than the specified
# size. See https://github.com/kovidgoyal/kitty/issues/276 for examples
sz = os.path.getsize(outfile.name)
bytes_per_pixel = 3 if m.mode == 'rgb' else 4
expected_size = bytes_per_pixel * width * height
if sz < expected_size:
missing = expected_size - sz
if missing % (bytes_per_pixel * width) != 0:
raise SystemExit('ImageMagick failed to convert {} correctly, it generated {} < {} of data'.format(path, sz, expected_size))
height -= missing // (bytes_per_pixel * width)
return outfile.name, width, height
def process(path, args):
m = identify(path)
ss = screen_size()
@@ -369,7 +273,7 @@ def main(args=sys.argv):
detect_support.has_files = args.transfer_mode == 'file'
errors = []
if args.clear:
write_gr_cmd({'a': 'd'})
sys.stdout.buffer.write(clear_images_on_screen(delete_data=True))
if not items:
return
if not items:
@@ -385,6 +289,10 @@ def main(args=sys.argv):
process(item, args)
else:
process(item, args)
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:

View File

View File

@@ -0,0 +1,131 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import sys
from kitty.cli import parse_args
from kitty.cmds import cmap, parse_subcommand_cli
from kitty.constants import version
from kitty.key_encoding import CTRL, ESCAPE, RELEASE, N, S, T, W
from kitty.remote_control import encode_send, parse_rc_args
from ..tui.handler import Handler
from ..tui.loop import Loop
from ..tui.operations import styled
global_opts = None
class Resize(Handler):
print_on_fail = None
def __init__(self, opts):
self.opts = opts
def initialize(self):
global global_opts
global_opts = parse_rc_args(['kitty', '@resize-window'])[0]
self.original_size = self.screen_size
self.cmd.set_cursor_visible(False)
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']
increment = self.opts.horizontal_increment if is_horizontal else self.opts.vertical_increment
increment *= multiplier
if is_decrease:
increment *= -1
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)
send = {'cmd': resize_window.name, 'version': version, 'payload': payload}
self.write(encode_send(send))
def on_kitty_cmd_response(self, response):
if not response.get('ok'):
err = response['error']
if response.get('tb'):
err += '\n' + response['tb']
self.print_on_fail = err
self.quit_loop(1)
return
res = response.get('data')
if res:
self.cmd.bell()
def on_text(self, text, in_bracketed_paste=False):
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):
if key_event.type is RELEASE:
return
if key_event.key is ESCAPE:
self.quit_loop(0)
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):
Handler.on_resize(self, new_size)
self.draw_screen()
def draw_screen(self):
self.cmd.clear_screen()
print = self.print
print(styled('Resize this window', bold=True, fg='gray', fg_intense=True))
print()
print('Press one of the following keys:')
print(' {}ider'.format(styled('W', fg='green')))
print(' {}arrower'.format(styled('N', fg='green')))
print(' {}aller'.format(styled('T', fg='green')))
print(' {}horter'.format(styled('S', fg='green')))
print(' {}eset'.format(styled('R', fg='red')))
print()
print('Press {} to quit resize mode'.format(styled('Esc', italic=True)))
print('Hold down {} to double step size'.format(styled('Ctrl', italic=True)))
print()
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')))
OPTIONS = r'''
--horizontal-increment
default=2
type=int
The base horizontal increment.
--vertical-increment
default=2
type=int
The base vertical increment.
'''.format
def main(args):
msg = 'Resize the current window'
try:
args, items = parse_args(args[1:], OPTIONS, '', msg, 'resize_window')
except SystemExit as e:
if e.code != 0:
print(e.args[0], file=sys.stderr)
input('Press Enter to quit')
return
loop = Loop()
handler = Resize(args)
loop.loop(handler)
if handler.print_on_fail:
print(handler.print_on_fail, file=sys.stderr)
input('Press Enter to quit')
raise SystemExit(loop.return_code)

View File

@@ -28,18 +28,27 @@ def import_kitten_main_module(config_dir, kitten):
code = compile(src, path, 'exec')
g = {'__name__': 'kitten'}
exec(code, g)
return {'start': g['main'], 'end': g['handle_result']}
hr = g.get('handle_result', lambda *a, **kw: None)
return {'start': g['main'], 'end': hr}
else:
kitten = resolved_kitten(kitten)
m = importlib.import_module('kittens.{}.main'.format(kitten))
return {'start': m.main, 'end': m.handle_result}
return {'start': m.main, 'end': getattr(m, 'handle_result', lambda *a, **k: None)}
def create_kitten_handler(kitten, orig_args):
from kitty.constants import config_dir
kitten = resolved_kitten(kitten)
m = import_kitten_main_module(config_dir, kitten)
return partial(m['end'], [kitten] + orig_args)
ans = partial(m['end'], [kitten] + orig_args)
ans.type_of_input = getattr(m['end'], 'type_of_input', None)
return ans
def set_debug(kitten):
from kittens.tui.loop import debug
import builtins
builtins.debug = debug
def launch(args):
@@ -49,19 +58,32 @@ def launch(args):
args = [kitten] + args
os.environ['KITTY_CONFIG_DIRECTORY'] = config_dir
from kittens.tui.operations import clear_screen, reset_mode
set_debug(kitten)
m = import_kitten_main_module(config_dir, kitten)
result = m['start'](args)
try:
result = m['start'](args)
finally:
sys.stdin = sys.__stdin__
print(reset_mode('ALTERNATE_SCREEN') + clear_screen(), end='')
if result is not None:
import json
print('OK:', json.dumps(result))
data = json.dumps(result)
print('OK:', len(data), data)
sys.stderr.flush()
sys.stdout.flush()
def deserialize(output):
import json
if output.startswith('OK: '):
prefix, sz, rest = output.split(' ', 2)
return json.loads(rest[:int(sz)])
def run_kitten(kitten):
import runpy
kitten = resolved_kitten(kitten)
set_debug(kitten)
runpy.run_module('kittens.{}.main'.format(kitten), run_name='__main__')

View File

View File

@@ -0,0 +1,42 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import sys
from kitty.cli import parse_args
from ..tui.operations import styled
OPTIONS = '''\
--title
default=ERROR
The title for the error message.
'''.format
def real_main(args):
error_message = sys.stdin.buffer.read().decode('utf-8')
sys.stdin = open('/dev/tty')
msg = 'Show an error message'
args, items = parse_args(args, OPTIONS, '', msg, 'hints')
print(styled(args.title, fg_intense=True, fg='red', bold=True))
print()
print(error_message)
print()
input('Press Enter to close.')
def main(args):
try:
real_main(args)
except KeyboardInterrupt:
pass
except Exception:
import traceback
traceback.print_exc()
input('Press Enter to close.')
if __name__ == '__main__':
main(sys.argv)

0
kittens/ssh/__init__.py Normal file
View File

51
kittens/ssh/main.py Normal file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import subprocess
import sys
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)
cat >$tmp << 'TERMEOF'
TERMINFO
TERMEOF
tic_out=$(tic -x -o ~/.terminfo $tmp 2>&1)
rc=$?
rm $tmp
if [ "$rc" != "0" ]; then echo "$tic_out"; exit 1; fi
if [ -z "$USER" ]; then export USER=$(whoami); fi
shell_name=$(basename $0)
# We need to pass the first argument to the executed program with a leading -
# to make sure the shell executes as a login shell. Note that not all shells
# support exec -a so we use the below to try to detect such shells
case "dash" in
*$shell_name*)
python=$(command -v python3)
if [ -z "$python" ]; then python=$(command -v python2); fi
if [ -z "$python" ]; then python=python; fi
exec $python -c "import os; os.execlp('$0', '-' '$shell_name')"
;;
esac
exec -a "-$shell_name" "$0"
'''
def main(args):
server = args[1]
terminfo = subprocess.check_output(['infocmp']).decode('utf-8')
sh_script = SHELL_SCRIPT.replace('TERMINFO', terminfo, 1)
cmd = ['ssh', '-t', server, sh_script]
p = subprocess.Popen(cmd)
raise SystemExit(p.wait())
if __name__ == '__main__':
main(sys.argv)

View File

@@ -3,11 +3,52 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
from .operations import commander
class Handler:
def initialize(self, screen_size, quit_loop, wakeup):
image_manager_class = None
def _initialize(self, screen_size, quit_loop, wakeup, start_job, debug, image_manager=None):
self.screen_size, self.quit_loop = screen_size, quit_loop
self.wakeup = wakeup
self.debug = debug
self.start_job = start_job
self.cmd = commander(self)
self.image_manager = image_manager
def add_shortcut(self, action, key, mods=None, is_text=False):
if not hasattr(self, '_text_shortcuts'):
self._text_shortcuts, self._key_shortcuts = {}, {}
if is_text:
self._text_shortcuts[key] = action
else:
self._key_shortcuts[(key, mods or 0)] = action
def shortcut_action(self, key_event_or_text):
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__()
self.debug.fobj = self
self.initialize()
def __exit__(self, *a):
del self.write_buf[:]
del self.debug.fobj
self.finalize()
if self.image_manager is not None:
self.image_manager.__exit__(*a)
def initialize(self):
pass
def finalize(self):
pass
def on_resize(self, screen_size):
self.screen_size = screen_size
@@ -30,6 +71,18 @@ class Handler:
def on_eot(self):
pass
def on_wakeup(self):
pass
def on_job_done(self, job_id, job_result):
pass
def on_kitty_cmd_response(self, response):
pass
def on_clipboard_response(self, text, from_primary=False):
pass
def write(self, data):
if isinstance(data, str):
data = data.encode('utf-8')

256
kittens/tui/images.py Normal file
View File

@@ -0,0 +1,256 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import codecs
import os
import sys
from base64 import standard_b64encode
from collections import defaultdict, deque
from itertools import count
from kitty.utils import fit_image, screen_size_function
from .operations import cursor
try:
fsenc = sys.getfilesystemencoding() or 'utf-8'
codecs.lookup(fsenc)
except Exception:
fsenc = 'utf-8'
screen_size = screen_size_function()
class ImageData:
def __init__(self, fmt, width, height, mode):
self.width, self.height, self.fmt, self.mode = width, height, fmt, mode
self.transmit_fmt = str(24 if self.mode == 'rgb' else 32)
class OpenFailed(ValueError):
def __init__(self, path, message):
ValueError.__init__(
self, 'Failed to open image: {} with error: {}'.format(path, message)
)
self.path = path
class ConvertFailed(ValueError):
def __init__(self, path, message):
ValueError.__init__(
self, 'Failed to convert image: {} with error: {}'.format(path, message)
)
self.path = path
class NoImageMagick(Exception):
pass
def run_imagemagick(path, cmd, keep_stdout=True):
import subprocess
try:
p = subprocess.run(cmd, stdout=subprocess.PIPE if keep_stdout else subprocess.DEVNULL, stderr=subprocess.PIPE)
except FileNotFoundError:
raise NoImageMagick('ImageMagick is required to process images')
if p.returncode != 0:
raise OpenFailed(path, p.stderr.decode('utf-8'))
return p
def identify(path):
p = run_imagemagick(path, ['identify', '-format', '%m %w %h %A', path])
parts = 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):
from tempfile import NamedTemporaryFile
width, height = m.width, m.height
cmd = ['convert', '-background', 'none', path]
if scale_up:
if width < available_width:
r = available_width / width
width, height = available_width, int(height * r)
if width > available_width or height > available_height:
width, height = fit_image(width, height, available_width, available_height)
cmd += ['-resize', '{}x{}'.format(width, height)]
with NamedTemporaryFile(prefix='icat-', suffix='.' + m.mode, delete=False, dir=tdir) as outfile:
run_imagemagick(path, cmd + [outfile.name])
# ImageMagick sometimes generated rgba images smaller than the specified
# size. See https://github.com/kovidgoyal/kitty/issues/276 for examples
sz = os.path.getsize(outfile.name)
bytes_per_pixel = 3 if m.mode == 'rgb' else 4
expected_size = bytes_per_pixel * width * height
if sz < expected_size:
missing = expected_size - sz
if missing % (bytes_per_pixel * width) != 0:
raise ConvertFailed(path, 'ImageMagick failed to convert {} correctly, it generated {} < {} of data'.format(path, sz, expected_size))
height -= missing // (bytes_per_pixel * width)
return outfile.name, width, height
def can_display_images():
import shutil
ans = getattr(can_display_images, 'ans', None)
if ans is None:
ans = shutil.which('convert') is not None
can_display_images.ans = ans
return ans
class ImageManager:
def __init__(self, handler):
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)
@property
def next_image_id(self):
return next(self.image_id_counter) + 2
def __enter__(self):
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)))
def __exit__(self, *a):
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):
for img_id in self.transmission_status:
self.handler.cmd.gr_command({'a': 'd', 'i': img_id})
self.transmission_status.clear()
def handle_response(self, apc):
cdata, payload = apc[1:].partition(';')[::2]
control = {}
for x in cdata.split(','):
k, v = x.partition('=')[::2]
control[k] = v
try:
image_id = int(control.get('i', '0'))
except Exception:
image_id = 0
if image_id == 1:
self.filesystem_ok = payload == 'OK'
return
if not image_id:
return
if not self.transmission_status.get(image_id):
self.transmission_status[image_id] = payload
else:
in_flight = self.placements_in_flight[image_id]
if in_flight:
pl = in_flight.popleft()
if payload.startswith('ENOENT:'):
try:
self.resend_image(image_id, pl)
except Exception:
pass
if not in_flight:
self.placements_in_flight.pop(image_id, None)
def resend_image(self, image_id, pl):
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)
with cursor(self.handler.write):
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):
path = os.path.abspath(path)
if path in self.failed_images:
raise self.failed_images[path]
if path not in self.image_data:
try:
self.image_data[path] = identify(path)
except Exception as e:
self.failed_images[path] = e
raise
m = self.image_data[path]
ss = screen_size()
if max_cols is None:
max_cols = ss.cols
if max_rows is None:
max_rows = ss.rows
available_width = max_cols * ss.cell_width
available_height = max_rows * ss.cell_height
key = path, available_width, available_height
skey = self.converted_images.get(key)
if skey is None:
try:
self.converted_images[key] = skey = self.convert_image(path, available_width, available_height, m, scale_up)
except Exception as e:
self.failed_images[path] = e
raise
final_width, final_height = skey[1:]
if final_width == 0:
return 0, 0, 0
image_id = self.sent_images.get(skey)
if image_id is None:
image_id = self.next_image_id
self.transmit_image(m, image_id, *skey)
self.sent_images[skey] = image_id
self.image_id_to_converted_data[image_id] = skey
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 show_image(self, image_id, x, y, src_rect=None):
cmd = {'a': 'p', '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})
with cursor(self.handler.write):
self.handler.cmd.set_cursor_position(x, y)
self.handler.cmd.gr_command(cmd)
def convert_image(self, path, available_width, available_height, image_data, scale_up=False):
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):
self.transmission_status[image_id] = 0
cmd = {'a': 't', 'f': image_data.transmit_fmt, 's': width, 'v': height, 'i': image_id}
if self.filesystem_ok:
cmd['t'] = 'f'
self.handler.cmd.gr_command(
cmd, standard_b64encode(rgba_path.encode(fsenc)))
else:
import zlib
data = open(rgba_path, 'rb').read()
cmd['S'] = len(data)
data = zlib.compress(data)
cmd['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()
return image_id

View File

@@ -15,25 +15,32 @@ import tty
from collections import namedtuple
from contextlib import closing, contextmanager
from functools import partial
from queue import Empty, Queue
from kitty.fast_data_types import parse_input_from_terminal, safe_pipe
from kitty.icat import screen_size
from kitty.key_encoding import (
ALT, CTRL, PRESS, RELEASE, REPEAT, SHIFT, C, D, backspace_key,
decode_key_event, enter_key
)
from kitty.utils import screen_size_function
from .handler import Handler
from .operations import init_state, reset_state, clear_screen
from .operations import init_state, reset_state
screen_size = screen_size_function()
def log(*a, **kw):
fd = getattr(log, 'fd', None)
if fd is None:
fd = log.fd = open('/tmp/kitten-debug', 'w')
kw['file'] = fd
def debug(*a, **kw):
from base64 import standard_b64encode
buf = io.StringIO()
kw['file'] = buf
print(*a, **kw)
fd.flush()
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'):
fobj.flush()
def write_all(fd, data):
@@ -52,6 +59,7 @@ class TermManager:
self.input_fd = input_fd
self.output_fd = output_fd
self.original_fl = fcntl.fcntl(self.input_fd, fcntl.F_GETFL)
self.extra_finalize = None
self.isatty = os.isatty(self.input_fd)
if self.isatty:
self.original_termios = termios.tcgetattr(self.input_fd)
@@ -66,6 +74,8 @@ class TermManager:
if self.isatty:
termios.tcsetattr(self.input_fd, termios.TCSADRAIN, self.original_termios)
fcntl.fcntl(self.input_fd, fcntl.F_SETFL, self.original_fl)
if self.extra_finalize:
write_all(self.output_fd, self.extra_finalize)
write_all(self.output_fd, reset_state())
@contextmanager
@@ -120,9 +130,11 @@ class UnhandledException(Handler):
def __init__(self, tb):
self.tb = tb
def initialize(self, screen_size, quit_loop, wakeup):
Handler.initialize(self, screen_size, quit_loop, wakeup)
self.write(clear_screen())
def initialize(self):
self.cmd.clear_screen()
self.cmd.set_scrolling_region()
self.cmd.set_cursor_visible(True)
self.cmd.set_default_colors()
self.write(self.tb.replace('\n', '\r\n'))
self.write('\r\n')
self.write('Press the Enter key to quit')
@@ -158,7 +170,7 @@ class Loop:
self.read_buf = ''
self.decoder = codecs.getincrementaldecoder('utf-8')('ignore')
try:
self.iov_limit = os.sysconf('SC_IOV_MAX') - 1
self.iov_limit = max(os.sysconf('SC_IOV_MAX') - 1, 255)
except Exception:
self.iov_limit = 255
self.parse_input_from_terminal = partial(parse_input_from_terminal, self._on_text, self._on_dcs, self._on_csi, self._on_osc, self._on_pm, self._on_apc)
@@ -167,6 +179,24 @@ class Loop:
self.sanitize_bracketed_paste = bool(sanitize_bracketed_paste)
if self.sanitize_bracketed_paste:
self.sanitize_ibp_pat = re.compile(sanitize_bracketed_paste)
self.jobs_queue = Queue()
def start_job(self, job_id, func, *args, **kw):
from threading import Thread
t = Thread(target=partial(self._run_job, job_id, func), args=args, kwargs=kw, name='LoopJob')
t.daemon = True
t.start()
def _run_job(self, job_id, func, *args, **kw):
try:
result = func(*args, **kw)
except Exception as err:
import traceback
entry = {'id': job_id, 'exception': err, 'tb': traceback.format_exc()}
else:
entry = {'id': job_id, 'result': result}
self.jobs_queue.put(entry)
self._wakeup_write(b'j')
def _read_ready(self, handler):
if not self.read_allowed:
@@ -210,7 +240,9 @@ class Loop:
self.handler.on_text(chunk, self.in_bracketed_paste)
def _on_dcs(self, dcs):
pass
if dcs.startswith('@kitty-cmd'):
import json
self.handler.on_kitty_cmd_response(json.loads(dcs[len('@kitty-cmd'):]))
def _on_csi(self, csi):
q = csi[-1]
@@ -233,7 +265,15 @@ class Loop:
pass
def _on_osc(self, osc):
pass
m = re.match(r'(\d+);', osc)
if m is not None:
code = int(m.group(1))
rest = osc[m.end():]
if code == 52:
where, rest = rest.partition(';')[::2]
from_primary = 'p' in where
from base64 import standard_b64decode
self.handler.on_clipboard_response(standard_b64decode(rest).decode('utf-8'), from_primary)
def _on_apc(self, apc):
if apc.startswith('K'):
@@ -250,6 +290,9 @@ class Loop:
self.handler.on_eot()
return
self.handler.on_key(k)
elif apc.startswith('G'):
if self.handler.image_manager is not None:
self.handler.image_manager.handle_response(apc)
def _write_ready(self, handler):
if len(handler.write_buf) > self.iov_limit:
@@ -283,6 +326,17 @@ class Loop:
handler.on_term()
if b'i' in data:
handler.on_interrupt()
if b'j' in data:
while True:
try:
entry = self.jobs_queue.get_nowait()
except Empty:
break
else:
job_id = entry.pop('id')
handler.on_job_done(job_id, entry)
if b'1' in data:
handler.on_wakeup()
def _wakeup_write(self, val):
while not os.write(self.wakeup_write_fd, val):
@@ -327,31 +381,31 @@ class Loop:
signal.signal(signal.SIGINT, self._on_sigint)
handler.write_buf = []
handler._term_manager = term_manager
image_manager = None
if handler.image_manager_class is not None:
image_manager = handler.image_manager_class(handler)
keep_going = True
try:
handler.initialize(screen_size(), self.quit, self.wakeup)
handler._initialize(screen_size(), self.quit, self.wakeup, self.start_job, debug, image_manager)
with handler:
while keep_going:
has_data_to_write = bool(handler.write_buf)
if not has_data_to_write and not self.read_allowed:
break
if has_data_to_write != waiting_for_write:
waiting_for_write = has_data_to_write
self._modify_output_selector(waiting_for_write)
events = select()
for key, mask in events:
key.data(handler)
except Exception:
import traceback
tb = traceback.format_exc()
self.return_code = 1
keep_going = False
while keep_going:
has_data_to_write = bool(handler.write_buf)
if not has_data_to_write and not self.read_allowed:
break
if has_data_to_write != waiting_for_write:
waiting_for_write = has_data_to_write
self._modify_output_selector(waiting_for_write)
events = select()
for key, mask in events:
try:
key.data(handler)
except Exception:
import traceback
tb = traceback.format_exc()
self.return_code = 1
keep_going = False
break
term_manager.extra_finalize = b''.join(handler.write_buf).decode('utf-8')
if tb is not None:
self._report_error_loop(tb, term_manager)
@@ -361,14 +415,15 @@ class Loop:
handler = UnhandledException(tb)
handler.write_buf = []
handler._term_manager = term_manager
handler.initialize(screen_size(), self.quit, self.wakeup)
while True:
has_data_to_write = bool(handler.write_buf)
if not has_data_to_write and not self.read_allowed:
break
if has_data_to_write != waiting_for_write:
waiting_for_write = has_data_to_write
self._modify_output_selector(waiting_for_write)
events = select()
for key, mask in events:
key.data(handler)
handler._initialize(screen_size(), self.quit, self.wakeup, self.start_job, debug)
with handler:
while True:
has_data_to_write = bool(handler.write_buf)
if not has_data_to_write and not self.read_allowed:
break
if has_data_to_write != waiting_for_write:
waiting_for_write = has_data_to_write
self._modify_output_selector(waiting_for_write)
events = select()
for key, mask in events:
key.data(handler)

View File

@@ -4,8 +4,9 @@
import sys
from contextlib import contextmanager
from functools import wraps
from kitty.terminfo import string_capabilities
from kitty.rgb import Color, color_as_sharp, to_color
S7C1T = '\033 F'
SAVE_CURSOR = '\0337'
@@ -35,34 +36,69 @@ MODES = dict(
)
def set_mode(which, private=True):
def set_mode(which, private=True) -> str:
num, private = MODES[which]
return '\033[{}{}h'.format(private, num)
def reset_mode(which):
def reset_mode(which) -> str:
num, private = MODES[which]
return '\033[{}{}l'.format(private, num)
def clear_screen():
return string_capabilities['clear'].replace(r'\E', '\033')
def clear_screen() -> str:
return '\033[H\033[2J'
def set_window_title(value):
def clear_to_eol() -> str:
return '\033[K'
def bell() -> str:
return '\a'
def beep() -> str:
return '\a'
def set_window_title(value) -> str:
return ('\033]2;' + value.replace('\033', '').replace('\x9c', '') + '\033\\')
def set_line_wrapping(yes_or_no):
def set_line_wrapping(yes_or_no) -> str:
return (set_mode if yes_or_no else reset_mode)('DECAWM')
def set_cursor_visible(yes_or_no):
def set_cursor_visible(yes_or_no) -> str:
return (set_mode if yes_or_no else reset_mode)('DECTCEM')
def set_cursor_position(x, y) -> str: # (0, 0) is top left
return '\033[{};{}H'.format(y + 1, x + 1)
def set_scrolling_region(screen_size=None, top=None, bottom=None) -> str:
if screen_size is None:
return '\033[r'
if top is None:
top = 0
if bottom is None:
bottom = screen_size.rows - 1
if bottom < 0:
bottom = screen_size.rows - 1 + bottom
else:
bottom += 1
return '\033[{};{}r'.format(top + 1, bottom + 1)
def scroll_screen(amt=1) -> str:
return '\033[' + str(abs(amt)) + ('T' if amt < 0 else 'S')
STANDARD_COLORS = {name: i for i, name in enumerate(
'black red green yellow blue magenta cyan gray'.split())}
STANDARD_COLORS['white'] = STANDARD_COLORS['gray']
UNDERLINE_STYLES = {name: i + 1 for i, name in enumerate(
'straight double curly'.split())}
@@ -77,20 +113,20 @@ def color_code(color, intense=False, base=30):
return e
def sgr(*parts):
def sgr(*parts) -> str:
return '\033[{}m'.format(';'.join(parts))
def colored(text, color, intense=False, reset_to=None, reset_to_intense=False):
def colored(text, color, intense=False, reset_to=None, reset_to_intense=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):
def faint(text) -> 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):
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:
start, end = [], []
if fg is not None:
start.append(color_code(fg, fg_intense))
@@ -120,6 +156,26 @@ def styled(text, fg=None, bg=None, fg_intense=False, bg_intense=False, italic=No
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 gr_command(cmd, payload=None) -> str:
return serialize_gr_command(cmd, payload)
def clear_images_on_screen(delete_data=False) -> str:
return serialize_gr_command({'a': 'd', 'd': 'A' if delete_data else 'a'})
def init_state(alternate_screen=True):
ans = (
S7C1T + SAVE_CURSOR + SAVE_PRIVATE_MODE_VALUES + reset_mode('LNM') +
@@ -160,3 +216,44 @@ def alternate_screen(f=None):
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) -> str:
ans = ''
if fg is None:
ans += '\x1b]110\x1b\\'
else:
ans += '\x1b]10;{}\x1b\\'.format(color_as_sharp(fg if isinstance(fg, Color) else to_color(fg)))
if bg is None:
ans += '\x1b]111\x1b\\'
else:
ans += '\x1b]11;{}\x1b\\'.format(color_as_sharp(bg if isinstance(bg, Color) else to_color(bg)))
return ans
def write_to_clipboard(data, use_primary=False) -> str:
if isinstance(data, str):
data = data.encode('utf-8')
from base64 import standard_b64encode
return '\x1b]52;{};{}\x07'.format('p' if use_primary else 'c', standard_b64encode(data).decode('ascii'))
def request_from_clipboard(use_primary=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)
def writer(handler, func):
@wraps(func)
def f(self, *a, **kw):
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)()

View File

@@ -56,7 +56,7 @@ def name(cp):
@lru_cache(maxsize=256)
def codepoints_matching_search(parts):
ans = []
if parts and parts[0]:
if parts and parts[0] and len(parts[0]) > 1:
codepoints = points_for_word(parts[0])
for word in parts[1:]:
pts = points_for_word(word)
@@ -327,8 +327,7 @@ class UnicodeInput(Handler):
self.write(set_line_wrapping(False))
self.write(set_window_title(_('Unicode input')))
def initialize(self, *args):
Handler.initialize(self, *args)
def initialize(self):
self.init_terminal_state()
self.draw_screen()

File diff suppressed because one or more lines are too long

View File

@@ -9,32 +9,61 @@
static PyObject*
all_words(PYNOARG) {
PyObject *ans = PyTuple_New(arraysz(idx_to_word));
PyObject *ans = PyTuple_New(arraysz(all_words_map));
if (!ans) return NULL;
for (size_t i = 0; i < arraysz(idx_to_word); i++) {
PyObject *w = PyUnicode_FromString(idx_to_word[i]);
for (size_t i = 0; i < arraysz(all_words_map); i++) {
PyObject *w = PyUnicode_FromString(all_words_map[i]);
if (w == NULL) { Py_DECREF(ans); return NULL; }
PyTuple_SET_ITEM(ans, i, w);
}
return ans;
}
static inline void
add_matches(const word_trie *wt, char_type *codepoints, size_t *pos, const size_t sz) {
size_t num = mark_groups[wt->match_offset];
for (size_t i = wt->match_offset + 1; i < wt->match_offset + 1 + num && *pos < sz; i++, (*pos)++) {
codepoints[*pos] = mark_to_cp[mark_groups[i]];
}
}
static void
process_trie_node(const word_trie *wt, char_type *codepoints, size_t *pos, const size_t sz) {
if (wt->match_offset) add_matches(wt, codepoints, pos, sz);
size_t num_children = children_array[wt->children_offset];
if (!num_children) return;
for (size_t c = wt->children_offset + 1; c < wt->children_offset + 1 + num_children; c++) {
if (*pos > sz) return;
uint32_t x = children_array[c];
process_trie_node(&all_trie_nodes[x >> 8], codepoints, pos, sz);
}
}
static inline PyObject*
codepoints_for_word(const char *word, size_t len) {
PyObject *ans = PyFrozenSet_New(NULL); if (ans == NULL) return NULL;
const unsigned short *words = words_for_first_letter[(unsigned)*word];
if (words == NULL) return ans;
for (unsigned short i = 1; i <= words[0]; i++) {
unsigned short word_idx = words[i];
const char *w = idx_to_word[word_idx];
if (strncmp(word, w, len) == 0 && strlen(w) == len) {
const char_type* codepoints = codepoints_for_word_idx[word_idx];
for (char_type i = 1; i <= codepoints[0]; i++) {
PyObject *t = PyLong_FromUnsignedLong(codepoints[i]); if (t == NULL) { Py_DECREF(ans); return NULL; }
int ret = PySet_Add(ans, t); Py_DECREF(t); if (ret != 0) { Py_DECREF(ans); return NULL; }
const word_trie *wt = all_trie_nodes;
for (size_t i = 0; i < len; i++) {
unsigned char ch = word[i];
size_t num_children = children_array[wt->children_offset];
if (!num_children) return PyFrozenSet_New(NULL);
bool found = false;
for (size_t c = wt->children_offset + 1; c < wt->children_offset + 1 + num_children; c++) {
uint32_t x = children_array[c];
if ((x & 0xff) == ch) {
found = true;
wt = &all_trie_nodes[x >> 8];
break;
}
break;
}
if (!found) return PyFrozenSet_New(NULL);
}
static char_type codepoints[1024];
size_t cpos = 0;
process_trie_node(wt, codepoints, &cpos, arraysz(codepoints));
PyObject *ans = PyFrozenSet_New(NULL); if (ans == NULL) return NULL;
for (size_t i = 0; i < cpos; i++) {
PyObject *t = PyLong_FromUnsignedLong(codepoints[i]); if (t == NULL) { Py_DECREF(ans); return NULL; }
int ret = PySet_Add(ans, t); Py_DECREF(t); if (ret != 0) { Py_DECREF(ans); return NULL; }
}
return ans;
}
@@ -56,12 +85,13 @@ nfc(PyObject *self UNUSED, PyObject *args) {
}
static PyMethodDef module_methods[] = {
METHODB(all_words, METH_NOARGS),
{"all_words", (PyCFunction)all_words, METH_NOARGS, ""},
{"codepoints_for_word", (PyCFunction)cfw, METH_VARARGS, ""},
{"name_for_codepoint", (PyCFunction)nfc, METH_VARARGS, ""},
{NULL, NULL, 0, NULL} /* Sentinel */
};
#if PY_VERSION_HEX >= 0x03000000
static struct PyModuleDef module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "unicode_names", /* name of module */
@@ -79,3 +109,13 @@ PyInit_unicode_names(void) {
if (m == NULL) return NULL;
return m;
}
#else
EXPORTED
initunicode_names(void) {
PyObject *m;
m = Py_InitModule3("unicode_names", module_methods,
""
);
if (m == NULL) return;
}
#endif

View File

@@ -3,6 +3,7 @@ uniform uvec2 viewport;
uniform vec3 default_bg;
uniform vec3 active_border_color;
uniform vec3 inactive_border_color;
uniform vec3 bell_border_color;
in uvec4 rect; // left, top, right, bottom
in uint rect_color;
out vec3 color;
@@ -37,5 +38,5 @@ void main() {
gl_Position = vec4(to_opengl(rect[pos.x], rect[pos.y]), 0, 1);
int rc = int(rect_color);
vec3 window_bg = vec3(to_color(rect_color >> 24), to_color(rect_color >> 16), to_color(rect_color >> 8));
color = float(1 & rc) * default_bg + float((2 & rc) >> 1) * active_border_color + float((4 & rc) >> 2) * inactive_border_color + float((8 & rc) >> 3) * window_bg;
color = float(1 & rc) * default_bg + float((2 & rc) >> 1) * active_border_color + float((4 & rc) >> 2) * inactive_border_color + float((8 & rc) >> 3) * window_bg + float((16 & rc) >> 4) * bell_border_color;
}

View File

@@ -64,7 +64,7 @@ class Borders:
g = w.geometry
if bw > 0 and draw_window_borders:
# Draw the border rectangles
color = 2 if w is active_window else 4
color = 2 if w is active_window else (16 if w.needs_attention else 4)
border(
self.os_window_id, self.tab_id,
color, bw, g.left - fw, g.top - fw, g.right + fw,

View File

@@ -6,7 +6,6 @@ import atexit
import json
import os
import re
import socket
from functools import partial
from gettext import gettext as _
from weakref import WeakValueDictionary
@@ -15,9 +14,13 @@ from .cli import create_opts, parse_args
from .config import (
MINIMUM_FONT_SIZE, initial_window_size, prepare_config_file_for_editing
)
from .constants import appname, editor, set_boss, config_dir
from .config_utils import to_cmdline
from .constants import (
appname, config_dir, editor, set_boss, supports_primary_selection
)
from .fast_data_types import (
ChildMonitor, create_os_window, current_os_window, destroy_global_data,
ChildMonitor, background_opacity_of, change_background_opacity,
create_os_window, current_os_window, destroy_global_data,
destroy_sprite_map, get_clipboard_string, glfw_post_empty_event,
layout_sprite_map, mark_os_window_for_close, set_clipboard_string,
set_dpi_from_os_window, set_in_sequence_mode, show_window,
@@ -42,6 +45,7 @@ def initialize_renderer():
def listen_on(spec):
import socket
family, address, socket_path = parse_address_spec(spec)
s = socket.socket(family)
atexit.register(remove_socket_file, s, socket_path)
@@ -148,10 +152,20 @@ class Boss:
field, exp = match.split(':', 1)
except ValueError:
return
pat = re.compile(exp)
for window in self.all_windows:
if window.matches(field, pat):
yield window
if field == 'num':
tab = self.active_tab
if tab is not None:
try:
w = tab.get_nth_window(int(exp))
except Exception:
return
if w is not None:
yield w
else:
pat = re.compile(exp)
for window in self.all_windows:
if window.matches(field, pat):
yield window
def tab_for_window(self, window):
for tab in self.all_tabs:
@@ -348,6 +362,29 @@ class Boss:
def on_dpi_change(self, os_window_id):
self._change_font_size()
def _set_os_window_background_opacity(self, os_window_id, opacity):
change_background_opacity(os_window_id, max(0.1, min(opacity, 1.0)))
def set_background_opacity(self, opacity):
window = self.active_window
if window is None or not opacity:
return
if not self.opts.dynamic_background_opacity:
return self.show_error(
_('Cannot change background opacity'),
_('You must set the dynamic_background_opacity option in kitty.conf to be able to change background opacity'))
os_window_id = window.os_window_id
if opacity[0] in '+-':
opacity = background_opacity_of(os_window_id)
if opacity is None:
return
opacity += float(opacity)
elif opacity == 'default':
opacity = self.opts.background_opacity
else:
opacity = float(opacity)
self._set_os_window_background_opacity(os_window_id, opacity)
@property
def active_tab_manager(self):
os_window_id = current_os_window()
@@ -400,6 +437,25 @@ class Boss:
if matched_action is not None:
self.dispatch_action(matched_action)
def start_resizing_window(self):
w = self.active_window
if w is None:
return
overlay_window = self._run_kitten('resize_window', args=[
'--horizontal-increment={}'.format(self.opts.window_resize_step_cells),
'--vertical-increment={}'.format(self.opts.window_resize_step_lines)
])
if overlay_window is not None:
overlay_window.allow_remote_control = True
def resize_layout_window(self, window, increment, is_horizontal, reset=False):
tab = window.tabref()
if tab is None or not increment:
return False
if reset:
return tab.reset_window_sizes()
return tab.resize_window_by(window.id, increment, is_horizontal)
def default_bg_changed_for(self, window_id):
w = self.window_id_map.get(window_id)
if w is not None:
@@ -440,6 +496,12 @@ class Boss:
w = tm.active_window
if w is not None:
w.focus_changed(focused)
tm.mark_tab_bar_dirty()
def update_tab_bar_data(self, os_window_id):
tm = self.os_window_map.get(os_window_id)
if tm is not None:
tm.update_tab_bar_data()
def on_drop(self, os_window_id, paths):
tm = self.os_window_map.get(os_window_id)
@@ -479,39 +541,56 @@ class Boss:
output += str(s.linebuf.line(i))
return output
def _run_kitten(self, kitten, args=(), type_of_input='none'):
def _run_kitten(self, kitten, args=(), input_data=None):
w = self.active_window
tab = self.active_tab
if w is not None and tab is not None and w.overlay_for is None:
orig_args, args = list(args), list(args)
args[0:0] = [config_dir, kitten]
if type_of_input in ('text', 'history', 'ansi', 'ansi-history'):
data = w.as_text(as_ansi='ansi' in type_of_input, add_history='history' in type_of_input).encode('utf-8')
elif type_of_input == 'none':
data = None
else:
raise ValueError('Unknown type_of_input: {}'.format(type_of_input))
from kittens.runner import create_kitten_handler
end_kitten = create_kitten_handler(kitten, orig_args)
args[0:0] = [config_dir, kitten]
if input_data is None:
type_of_input = end_kitten.type_of_input
if type_of_input in ('text', 'history', 'ansi', 'ansi-history', 'screen', 'screen-history', 'screen-ansi', 'screen-ansi-history'):
data = w.as_text(
as_ansi='ansi' in type_of_input,
add_history='history' in type_of_input,
add_wrap_markers='screen' in type_of_input
).encode('utf-8')
elif type_of_input is None:
data = None
else:
raise ValueError('Unknown type_of_input: {}'.format(type_of_input))
else:
data = input_data
if isinstance(data, str):
data = data.encode('utf-8')
copts = {k: self.opts[k] for k in ('select_by_word_characters', 'open_url_with')}
overlay_window = tab.new_special_window(
SpecialWindow(
['kitty', '+runpy', 'from kittens.runner import main; main()'] + args,
stdin=data,
env={'KITTY_COMMON_OPTS': json.dumps(copts)},
env={
'KITTY_COMMON_OPTS': json.dumps(copts),
'PYTHONWARNINGS': 'ignore',
'OVERLAID_WINDOW_LINES': str(w.screen.lines),
'OVERLAID_WINDOW_COLS': str(w.screen.columns),
},
overlay_for=w.id))
overlay_window.action_on_close = partial(self.on_kitten_finish, w.id, end_kitten)
return overlay_window
def run_kitten(self, type_of_input, kitten, *args):
def kitten(self, kitten, *args):
import shlex
cmdline = args[0] if args else ''
args = shlex.split(cmdline) if cmdline else []
self._run_kitten(kitten, args, type_of_input)
self._run_kitten(kitten, args)
def on_kitten_finish(self, target_window_id, end_kitten, source_window):
output = self.get_output(source_window, num_lines=None)
if output.startswith('OK: '):
data = json.loads(output.partition(' ')[2].strip())
from kittens.runner import deserialize
data = deserialize(output)
if data is not None:
end_kitten(data, target_window_id, self)
def input_unicode_character(self):
@@ -523,6 +602,9 @@ class Boss:
args = ['--name=tab-title', '--message', _('Enter the new title for this tab below.'), 'do_set_tab_title', str(tab.id)]
self._run_kitten('ask', args)
def show_error(self, title, msg):
self._run_kitten('show_error', ['--title', title], input_data=msg)
def do_set_tab_title(self, title, tab_id):
tm = self.active_tab_manager
if tm is not None and title:
@@ -554,14 +636,12 @@ class Boss:
def switch_focus_to(self, window_idx):
tab = self.active_tab
tab.set_active_window_idx(window_idx)
old_focus = tab.active_window
if not old_focus.destroyed:
old_focus.focus_changed(False)
tab.active_window.focus_changed(True)
def open_url(self, url, program=None):
def open_url(self, url, program=None, cwd=None):
if url:
open_url(url, program or self.opts.open_url_with)
if isinstance(program, str):
program = to_cmdline(program)
open_url(url, program or self.opts.open_url_with, cwd=cwd)
def open_url_lines(self, lines, program=None):
self.open_url(''.join(lines), program)
@@ -587,7 +667,7 @@ class Boss:
self.paste_to_active_window(text)
def paste_from_selection(self):
text = get_primary_selection()
text = get_primary_selection() if supports_primary_selection else get_clipboard_string()
self.paste_to_active_window(text)
def set_primary_selection(self):

View File

@@ -19,7 +19,7 @@ in float bg_alpha;
#ifdef NEEDS_FOREGROUND
uniform sampler2DArray sprites;
uniform float inactive_text_alpha;
in float effective_text_alpha;
in vec3 sprite_pos;
in vec3 underline_pos;
in vec3 strike_pos;
@@ -61,7 +61,7 @@ vec4 calculate_foreground() {
// Since strike and text are the same color, we simply add the alpha values
float combined_alpha = min(text_alpha + strike_alpha, 1.0f);
// Underline color might be different, so alpha blend
return alpha_blend(decoration_fg, underline_alpha * inactive_text_alpha, fg, combined_alpha * inactive_text_alpha);
return alpha_blend(decoration_fg, underline_alpha * effective_text_alpha, fg, combined_alpha * effective_text_alpha);
}
#endif

View File

@@ -1,6 +1,10 @@
#version GLSL_VERSION
#define WHICH_PROGRAM
#define NOT_TRANSPARENT
#define DECORATION_SHIFT {DECORATION_SHIFT}
#define REVERSE_SHIFT {REVERSE_SHIFT}
#define STRIKE_SHIFT {STRIKE_SHIFT}
#define DIM_SHIFT {DIM_SHIFT}
// Inputs {{{
layout(std140) uniform CellRenderData {
@@ -45,12 +49,15 @@ out float bg_alpha;
#endif
#ifdef NEEDS_FOREGROUND
uniform float inactive_text_alpha;
uniform float dim_opacity;
out vec3 sprite_pos;
out vec3 underline_pos;
out vec3 strike_pos;
out vec3 foreground;
out vec3 decoration_fg;
out float colored_sprite;
out float effective_text_alpha;
#endif
@@ -63,9 +70,6 @@ const uint ONE = uint(1);
const uint TWO = uint(2);
const uint THREE = uint(3);
const uint FOUR = uint(4);
const uint DECORATION_MASK = uint(3);
const uint STRIKE_MASK = uint(1);
const uint REVERSE_MASK = uint(1);
vec3 color_to_vec(uint c) {
uint r, g, b;
@@ -134,7 +138,7 @@ void main() {
// set cell color indices {{{
uvec2 default_colors = uvec2(default_fg, default_bg);
uint text_attrs = sprite_coords[3];
uint is_inverted = ((text_attrs >> 6) & REVERSE_MASK) + inverted;
uint is_inverted = ((text_attrs >> REVERSE_SHIFT) & ONE) + inverted;
int fg_index = fg_index_map[is_inverted];
int bg_index = 1 - fg_index;
float cursor = is_cursor(c, r);
@@ -151,13 +155,15 @@ void main() {
// Foreground
uint resolved_fg = resolve_color(colors[fg_index], default_colors[fg_index]);
foreground = color_to_vec(resolved_fg);
float has_dim = float((text_attrs >> DIM_SHIFT) & ONE);
effective_text_alpha = inactive_text_alpha * mix(1.0, dim_opacity, has_dim);
// Selection
foreground = choose_color(float(is_selected & ONE), color_to_vec(highlight_fg), foreground);
// Underline and strike through (rendered via sprites)
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));
underline_pos = choose_color(in_url, to_sprite_pos(pos, url_style, ZERO, ZERO), to_sprite_pos(pos, (text_attrs >> 2) & DECORATION_MASK, ZERO, ZERO));
strike_pos = to_sprite_pos(pos, ((text_attrs >> 7) & STRIKE_MASK) * FOUR, ZERO, ZERO);
underline_pos = choose_color(in_url, to_sprite_pos(pos, url_style, ZERO, ZERO), to_sprite_pos(pos, (text_attrs >> DECORATION_SHIFT) & THREE, ZERO, ZERO));
strike_pos = to_sprite_pos(pos, ((text_attrs >> STRIKE_SHIFT) & ONE) * FOUR, ZERO, ZERO);
// Cursor
foreground = choose_color(cursor, bg, foreground);

View File

@@ -502,7 +502,7 @@ collect_cursor_info(CursorRenderInfo *ans, Window *w, double now, OSWindow *os_w
ans->is_visible = false;
if (rd->screen->scrolled_by || !screen_is_cursor_visible(rd->screen)) return;
double time_since_start_blink = now - os_window->cursor_blink_zero_time;
bool cursor_blinking = OPT(cursor_blink_interval) > 0 && os_window->is_focused && time_since_start_blink <= OPT(cursor_stop_blinking_after) ? true : false;
bool cursor_blinking = OPT(cursor_blink_interval) > 0 && os_window->is_focused && (OPT(cursor_stop_blinking_after) == 0 || time_since_start_blink <= OPT(cursor_stop_blinking_after));
bool do_draw_cursor = true;
if (cursor_blinking) {
int t = (int)(time_since_start_blink * 1000);
@@ -572,6 +572,10 @@ prepare_to_render_os_window(OSWindow *os_window, double now, unsigned int *activ
bool needs_render = os_window->needs_render;
os_window->needs_render = false;
if (TD.screen && os_window->num_tabs > 1) {
if (!os_window->tab_bar_data_updated) {
call_boss(update_tab_bar_data, "K", os_window->id);
os_window->tab_bar_data_updated = true;
}
if (send_cell_data_to_gpu(TD.vao_idx, 0, TD.xstart, TD.ystart, TD.dx, TD.dy, TD.screen, os_window)) needs_render = true;
}
if (OPT(mouse_hide_wait) > 0 && now - os_window->last_mouse_activity_at > OPT(mouse_hide_wait)) hide_mouse(os_window);
@@ -610,7 +614,7 @@ static inline void
render_os_window(OSWindow *os_window, double now, unsigned int active_window_id, color_type active_window_bg, unsigned int num_visible_windows) {
Tab *tab = os_window->tabs + os_window->active_tab;
BorderRects *br = &tab->border_rects;
draw_borders(br->vao_idx, br->num_border_rects, br->rect_buf, br->is_dirty, os_window->viewport_width, os_window->viewport_height, active_window_bg, num_visible_windows);
draw_borders(br->vao_idx, br->num_border_rects, br->rect_buf, br->is_dirty, os_window->viewport_width, os_window->viewport_height, active_window_bg, num_visible_windows, os_window);
if (TD.screen && os_window->num_tabs > 1) draw_cells(TD.vao_idx, 0, TD.xstart, TD.ystart, TD.dx, TD.dy, TD.screen, os_window, true);
for (unsigned int i = 0; i < tab->num_windows; i++) {
Window *w = tab->windows + i;

View File

@@ -4,13 +4,32 @@
import os
import re
import subprocess
import sys
from collections import deque
from .config import defaults, load_config
from .config_utils import resolve_config
from .constants import appname, defconf, is_macos, is_wayland, str_version
from .layout import all_layouts
CONFIG_HELP = '''\
Specify a path to the configuration file(s) to use. All configuration files are
merged onto the builtin {conf_name}.conf, overriding the builtin values. This option
can be specified multiple times to read multiple configuration files in
sequence, which are merged. Use the special value NONE to not load a config
file.
If this option is not specified, config files are searched for in the order:
"$XDG_CONFIG_HOME/{appname}/{conf_name}.conf", "~/.config/{appname}/{conf_name}.conf", {macos_confpath}
"$XDG_CONFIG_DIRS/{appname}/{conf_name}.conf". The first one that exists is used as the
config file.
If the environment variable "KITTY_CONFIG_DIRECTORY" is specified, that
directory is always used and the above searching does not happen.
If "/etc/xdg/{appname}/{conf_name}.conf" exists it is merged before (i.e. with lower
priority) than any user config files. It can be used to specify system-wide
defaults for all users.
'''.replace('{macos_confpath}', '~/Library/Preferences/{appname}/{conf_name}.conf, ' if is_macos else '')
OPTIONS = '''
--class
@@ -30,26 +49,9 @@ Set the window title. This will override any title set by the program running in
only use this if you are running a program that does not set titles.
--config
--config -c
type=list
Specify a path to the configuration file(s) to use. All configuration files are
merged onto the builtin kitty.conf, overriding the builtin values. This option
can be specified multiple times to read multiple configuration files in
sequence, which are merged. Use the special value NONE to not load a config
file.
If this option is not specified, config files are searched for in the order:
"$XDG_CONFIG_HOME/kitty/kitty.conf", "~/.config/kitty/kitty.conf", {macos_confpath}
"$XDG_CONFIG_DIRS/kitty/kitty.conf". The first one that exists is used as the
config file.
If the environment variable "KITTY_CONFIG_DIRECTORY" is specified, that
directory is always used and the above searching does not happen.
If "/etc/xdg/kitty/kitty.conf" exists it is merged before (i.e. with lower
priority) than any user config files. It can be used to specify system-wide
defaults for all users.
{config_help}
--override -o
type=list
@@ -57,10 +59,6 @@ Override individual configuration options, can be specified multiple times.
Syntax: |_ name=value|. For example: |_ -o font_size=20|
--cmd -c
Run python code in the kitty context
--directory -d
default=.
Change to the specified directory when launching
@@ -72,12 +70,6 @@ condition=not is_macos
Detach from the controlling terminal, if any
--window-layout
type=choices
choices={window_layout_choices}
The window layout to use on startup.
--session
Path to a file containing the startup |_ session| (tabs, windows, layout, programs).
See the README file for details and an example.
@@ -120,8 +112,10 @@ Output commands received from child process to stdout
--replay-commands
type=bool-set
Replay previously dumped commands
Replay previously dumped commands. Specify the path to a dump file previously created by --dump-commands. You
can open a new kitty window to replay the commands with:
kitty sh -c "kitty --replay-commands /path/to/dump/file; read"
--dump-bytes
@@ -141,6 +135,11 @@ Print out information about the selection of fallback fonts for characters not p
--debug-config
type=bool-set
Print out information about the system and kitty configuration.
--execute -e
type=bool-set
!
'''
@@ -289,8 +288,8 @@ def wrap(text, limit=80):
def print_help_for_seq(seq, usage, message, appname):
from kitty.icat import screen_size
screen_size.changed = True
from kitty.utils import screen_size_function
screen_size = screen_size_function()
try:
linesz = min(screen_size().cols, 76)
except EnvironmentError:
@@ -326,12 +325,15 @@ def print_help_for_seq(seq, usage, message, appname):
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 = opt['help'].replace('%default', str(defval))
t = help_text.replace('%default', str(defval))
wa(prettify(t.strip()), indent=4)
if defval is not None:
wa('Default: {}'.format(defval), indent=4)
@@ -341,6 +343,7 @@ def print_help_for_seq(seq, usage, message, appname):
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)
p.communicate(text.encode('utf-8'))
raise SystemExit(p.wait())
@@ -480,8 +483,8 @@ def parse_cmdline(oc, disabled, args=None):
def options_spec():
if not hasattr(options_spec, 'ans'):
options_spec.ans = OPTIONS.format(
appname=appname, macos_confpath='~/Library/Preferences/kitty/kitty.conf, ' if is_macos else '',
window_layout_choices=', '.join(all_layouts)
appname=appname, config_help=CONFIG_HELP.format(appname=appname, conf_name=appname)
)
return options_spec.ans
@@ -496,70 +499,82 @@ def parse_args(args=None, ospec=options_spec, usage=None, message=None, appname=
SYSTEM_CONF = '/etc/xdg/kitty/kitty.conf'
def resolve_config(config_files_on_cmd_line):
if config_files_on_cmd_line:
if 'NONE' not in config_files_on_cmd_line:
yield SYSTEM_CONF
for cf in config_files_on_cmd_line:
yield cf
else:
yield SYSTEM_CONF
yield defconf
def print_shortcut(key, action):
def print_shortcut(key_sequence, action):
if not getattr(print_shortcut, 'maps', None):
from kitty.keys import defines
v = vars(defines)
mmap = {m.split('_')[-1].lower(): x for m, x in v.items() if m.startswith('GLFW_MOD_')}
kmap = {k.split('_')[-1].lower(): x for k, x in v.items() if k.startswith('GLFW_KEY_')}
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
names = []
mods, key = key
for name, val in mmap.items():
if mods & val:
names.append(name)
if key:
names.append(krmap[key])
print('\t', '+'.join(names), action)
keys = []
for key in key_sequence:
names = []
mods, key = key
for name, val in mmap.items():
if mods & val:
names.append(name)
if key:
names.append(krmap[key])
keys.append('+'.join(names))
print('\t', ' > '.join(keys), action)
def print_shortcut_changes(defns, text, changes):
if changes:
print(title(text))
for k in sorted(changes):
print_shortcut(k, defns[k])
def compare_keymaps(final, initial):
added = set(final) - set(initial)
removed = set(initial) - set(final)
changed = {k for k in set(final) & set(initial) if final[k] != initial[k]}
if added:
print(title('Added shortcuts:'))
for k in added:
print_shortcut(k, final[k])
if removed:
print(title('Removed shortcuts:'))
for k in removed:
print_shortcut(k, initial[k])
if changed:
print(title('Changed shortcuts:'))
for k in changed:
print_shortcut(k, final[k])
print_shortcut_changes(final, 'Added shortcuts:', added)
print_shortcut_changes(initial, 'Removed shortcuts:', removed)
print_shortcut_changes(final, 'Changed shortcuts:', changed)
def flatten_sequence_map(m):
ans = {}
for k, rest_map in m.items():
for r, action in rest_map.items():
ans[(k,) + (r)] = action
return ans
def compare_opts(opts):
print('\nConfig options different from defaults:')
for f in sorted(defaults._fields):
if getattr(opts, f) != getattr(defaults, f):
if f == 'keymap':
compare_keymaps(opts.keymap, defaults.keymap)
else:
print(title('{:20s}'.format(f)), getattr(opts, f))
default_opts = load_config()
changed_opts = [
f for f in sorted(defaults._fields)
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
fmt = '{{:{:d}s}}'.format(field_len)
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_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):
config = tuple(resolve_config(args.config))
config = tuple(resolve_config(SYSTEM_CONF, defconf, args.config))
if debug_config:
print(version(add_rev=True))
print(' '.join(os.uname()))
if is_macos:
import subprocess
print(' '.join(subprocess.check_output(['sw_vers']).decode('utf-8').splitlines()).strip())
else:
print('Running under:', green('Wayland' if is_wayland else 'X11'))

View File

@@ -106,6 +106,14 @@ def screen_backspace():
write('\x08')
def screen_set_cursor(mode, secondary):
write(CSI + '%d q' % secondary)
def screen_insert_lines(num):
write(CSI + '%dL' % num)
def draw(*a):
write(' '.join(a))
@@ -130,7 +138,7 @@ set_dynamic_color = set_color_table_color = write_osc
def replay(raw):
for line in raw.splitlines():
if line.strip():
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)

View File

@@ -21,7 +21,12 @@ class MatchError(ValueError):
ValueError.__init__(self, 'No matching {} for expression: {}'.format(target, expression))
def cmd(short_desc, desc=None, options_spec=None, no_response=False, argspec='...'):
class OpacityError(ValueError):
hide_traceback = True
def cmd(short_desc, desc=None, options_spec=None, no_response=False, argspec='...', string_return_is_error=False, args_count=None):
def w(func):
func.short_desc = short_desc
@@ -32,6 +37,8 @@ def cmd(short_desc, desc=None, options_spec=None, no_response=False, argspec='..
func.is_cmd = True
func.impl = lambda: globals()[func.__name__[4:]]
func.no_response = no_response
func.string_return_is_error = string_return_is_error
func.args_count = 0 if not argspec else args_count
return func
return w
@@ -39,10 +46,13 @@ def cmd(short_desc, desc=None, options_spec=None, no_response=False, argspec='..
MATCH_WINDOW_OPTION = '''\
--match -m
The window to match. Match specifications are of the form:
|_ field:regexp|. Where field can be one of: id, title, pid, cwd, cmdline.
|_ field:regexp|. Where field can be one of: id, title, pid, cwd, cmdline, num.
You can use the |_ ls| command to get a list of windows. Note that for
numeric fields such as id and pid the expression is interpreted as a number,
not a regular expression.
numeric fields such as id, pid and num the expression is interpreted as a number,
not a regular expression. The field num refers to the window position in the current tab,
starting from zero and counting clockwise (this is the same as the order in which the
windows are reported by the |_ ls| command). The window id of the current window
is available as the KITTY_WINDOW_ID environment variable.
'''
MATCH_TAB_OPTION = '''\
--match -m
@@ -82,7 +92,7 @@ def ls(boss, window):
@cmd(
'Set the font size in all windows',
'Sets the font size to the specified size, in pts.',
argspec='FONT_SIZE'
argspec='FONT_SIZE', args_count=1
)
def cmd_set_font_size(global_opts, opts, args):
try:
@@ -272,6 +282,55 @@ def close_window(boss, window, payload):
# }}}
# resize_window {{{
@cmd(
'Resize the specified window',
'Resize the specified window in the current layout. Note that not all layouts can resize all windows in all directions.',
options_spec=MATCH_WINDOW_OPTION + '''\n
--increment -i
type=int
default=2
The number of cells to change the size by, can be negative to decrease the size.
--axis -a
type=choices
choices=horizontal,vertical,reset
default=horizontal
The axis along which to resize. If |_ horizontal|, it will make the window wider or narrower by the specified increment.
If |_ vertical|, it will make the window taller or shorter by the specified increment. The special value |_ reset| will
reset the layout to its default configuration.
--self
type=bool-set
If specified close the window this command is run in, rather than the active window.
''',
argspec='',
string_return_is_error=True
)
def cmd_resize_window(global_opts, opts, args):
return {'match': opts.match, 'increment': opts.increment, 'axis': opts.axis, 'self': opts.self}
def resize_window(boss, window, payload):
match = payload['match']
if match:
windows = tuple(boss.match_windows(match))
if not windows:
raise MatchError(match)
else:
windows = [window if window and payload['self'] else boss.active_window]
resized = False
if windows and windows[0]:
resized = boss.resize_layout_window(
windows[0], increment=payload['increment'], is_horizontal=payload['axis'] == 'horizontal',
reset=payload['axis'] == 'reset'
)
return resized
# }}}
# close_tab {{{
@cmd(
'Close the specified tab(s)',
@@ -372,6 +431,7 @@ def new_window(boss, window, payload):
# focus_window {{{
@cmd(
'Focus the specified window',
'Focus the specified window, if no window is specified, focus the window this command is run inside.',
options_spec=MATCH_WINDOW_OPTION,
argspec='',
)
@@ -532,11 +592,55 @@ def set_colors(boss, window, payload):
# }}}
# set_background_opacity {{{
@cmd(
'Set the background_opacity',
'Set the background opacity for the specified windows. This will only work if you have turned on'
' dynamic_background_opacity in kitty.conf. The background opacity affects all kitty windows in a'
' single os_window. For example: kitty @ set-background-opacity 0.5',
options_spec='''\
--all -a
type=bool-set
By default, colors are only changed for the currently active window. This option will
cause colors to be changed in all windows.
''' + '\n\n' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t'),
argspec='OPACITY',
args_count=1
)
def cmd_set_background_opacity(global_opts, opts, args):
opacity = max(0.1, min(float(args[0]), 1.0))
return {
'opacity': opacity, 'match_window': opts.match,
'all': opts.all,
}
def set_background_opacity(boss, window, payload):
if not boss.opts.dynamic_background_opacity:
raise OpacityError('You must turn on the dynamic_background_opacity option in kitty.conf to be able to set background opacity')
if payload['all']:
windows = tuple(boss.all_windows)
else:
windows = (window or boss.active_window,)
if payload['match_window']:
windows = tuple(boss.match_windows(payload['match_window']))
if not windows:
raise MatchError(payload['match_window'])
for os_window_id in {w.os_window_id for w in windows}:
boss._set_os_window_background_opacity(os_window_id, payload['opacity'])
# }}}
cmap = {v.name: v for v in globals().values() if hasattr(v, 'is_cmd')}
def parse_subcommand_cli(func, args):
opts, items = parse_args(args[1:], (func.options_spec or '\n').format, func.argspec, func.desc, '{} @ {}'.format(appname, func.name))
if func.args_count is not None and func.args_count != len(items):
if func.args_count == 0:
raise SystemExit('Unknown extra argument(s) supplied to {}'.format(func.name))
raise SystemExit('Must specify exactly {} argument(s) for {}'.format(func.args_count, func.name))
return opts, items

View File

@@ -23,8 +23,6 @@ typedef void* rusage_info_t; // needed for libproc.h
#endif
static NSMenuItem* title_menu = NULL;
static bool change_titlebar_color = false;
static color_type titlebar_color = 0;
static NSString*
@@ -207,32 +205,14 @@ cwd_of_process(PyObject *self UNUSED, PyObject *pid_) {
return PyUnicode_FromString(vpi.pvi_cdir.vip_path);
}
static inline color_type
color_as_int(PyObject *color) {
#define I(n, s) ((PyLong_AsUnsignedLong(PyTuple_GET_ITEM(color, n)) & 0xff) << s)
return (I(0, 16) | I(1, 8) | I(2, 0)) & 0xffffff;
#undef I
}
static PyObject*
macos_change_titlebar_color(PyObject *self UNUSED, PyObject *val) {
if (val == Py_None) change_titlebar_color = false;
else if (val == Py_True) {
change_titlebar_color = true;
titlebar_color = OPT(background);
} else {
if (!PyTuple_Check(val)) { PyErr_SetString(PyExc_TypeError, "Not a color tuple"); return NULL; }
change_titlebar_color = true;
titlebar_color = color_as_int(val);
}
Py_RETURN_NONE;
void
cocoa_set_hide_from_tasks(void) {
[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
}
void
cocoa_set_titlebar_color(void *w)
cocoa_set_titlebar_color(void *w, color_type titlebar_color)
{
if (!change_titlebar_color) return;
NSWindow *window = (NSWindow *)w;
double red = ((titlebar_color >> 16) & 0xFF) / 255.0;
@@ -259,7 +239,6 @@ cocoa_set_titlebar_color(void *w)
static PyMethodDef module_methods[] = {
{"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""},
{"cwd_of_process", (PyCFunction)cwd_of_process, METH_O, ""},
{"macos_change_titlebar_color", (PyCFunction)macos_change_titlebar_color, METH_O, ""},
{NULL, NULL, 0, NULL} /* Sentinel */
};

View File

@@ -2,25 +2,23 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import ast
import json
import os
import re
import shlex
import sys
import tempfile
from collections import namedtuple
from contextlib import contextmanager
from . import fast_data_types as defines
from .config_utils import (
init_config, parse_config_base, positive_float, positive_int, to_bool,
to_color, unit_float
init_config, key_func, load_config as _load_config, merge_dicts,
parse_config_base, positive_float, positive_int, python_string, to_bool,
to_cmdline, to_color, unit_float
)
from .constants import cache_dir, defconf
from .fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE
from .layout import all_layouts
from .rgb import color_from_int
from .rgb import color_as_int, color_from_int
from .utils import log_error
MINIMUM_FONT_SIZE = 4
@@ -48,18 +46,20 @@ def to_cursor_shape(x):
)
def parse_mods(parts):
mod_map = {'CTRL': 'CONTROL', 'CMD': 'SUPER', '': 'SUPER', '': 'ALT', 'OPTION': 'ALT', 'KITTY_MOD': 'KITTY'}
def parse_mods(parts, sc):
def map_mod(m):
return {'CTRL': 'CONTROL', 'CMD': 'SUPER', '': 'SUPER', '': 'ALT', 'OPTION': 'ALT'}.get(m, m)
return mod_map.get(m, m)
mods = 0
for m in parts:
try:
mods |= getattr(defines, 'GLFW_MOD_' + map_mod(m.upper()))
except AttributeError:
log_error('Shortcut: {} has unknown modifier, ignoring'.format(
parts.join('+')))
log_error('Shortcut: {} has unknown modifier, ignoring'.format(sc))
return
return mods
@@ -81,19 +81,72 @@ named_keys = {
def parse_shortcut(sc):
parts = sc.split('+')
mods = parse_mods(parts[:-1])
mods = parse_mods(parts[:-1], sc)
if mods is None:
return None, None
key = parts[-1].upper()
key = getattr(defines, 'GLFW_KEY_' + named_keys.get(key, key), None)
if key is not None:
return mods, key
return None, None
return mods, None
KeyAction = namedtuple('KeyAction', 'func args')
shlex_actions = {
func_with_args, args_funcs = key_func()
@func_with_args(
'pass_selection_to_program', 'new_window', 'new_tab', 'new_os_window',
'new_window_with_cwd', 'new_tab_with_cwd', 'new_os_window_with_cwd'
}
)
def shlex_parse(func, rest):
return func, to_cmdline(rest)
@func_with_args('combine')
def combine_parse(func, rest):
sep, rest = rest.split(' ', 1)
parts = re.split(r'\s*' + re.escape(sep) + r'\s*', rest)
args = tuple(map(parse_key_action, filter(None, parts)))
return func, args
@func_with_args('send_text')
def send_text_parse(func, rest):
args = rest.split(' ', 1)
if len(args) > 0:
try:
args[1] = parse_send_text_bytes(args[1])
except Exception:
log_error('Ignoring invalid send_text string: ' + args[1])
args[1] = ''
return func, args
@func_with_args('run_kitten', 'run_simple_kitten', 'kitten')
def kitten_parse(func, rest):
if func == 'kitten':
args = rest.split(' ', 1)
else:
args = rest.split(' ', 2)[1:]
func = 'kitten'
return func, args
@func_with_args('goto_tab')
def goto_tab_parse(func, rest):
args = (max(0, int(rest)), )
return func, args
@func_with_args('set_background_opacity', 'goto_layout', 'kitty_shell')
def simple_parse(func, rest):
return func, [rest]
@func_with_args('set_font_size')
def float_parse(func, rest):
return func, (float(rest),)
def parse_key_action(action):
@@ -102,24 +155,12 @@ def parse_key_action(action):
if len(parts) == 1:
return KeyAction(func, ())
rest = parts[1]
if func == 'combine':
sep, rest = rest.split(' ', 1)
parts = re.split(r'\s*' + re.escape(sep) + r'\s*', rest)
args = tuple(map(parse_key_action, filter(None, parts)))
elif func == 'send_text':
args = rest.split(' ', 1)
elif func in ('run_kitten', 'run_simple_kitten'):
if func == 'run_simple_kitten':
func = 'run_kitten'
args = rest.split(' ', 2)
elif func == 'goto_tab':
args = (max(0, int(rest)), )
elif func == 'goto_layout' or func == 'kitty_shell':
args = [rest]
elif func == 'set_font_size':
args = (float(rest),)
elif func in shlex_actions:
args = shlex.split(rest)
parser = args_funcs.get(func)
if parser is not None:
try:
func, args = parser(func, rest)
except Exception:
log_error('Ignoring invalid key action: {}'.format(action))
return KeyAction(func, args)
@@ -127,7 +168,20 @@ all_key_actions = set()
sequence_sep = '>'
def parse_key(val, keymap, sequence_map):
class KeyDefinition:
def __init__(self, is_sequence, action, mods, key, rest=()):
self.is_sequence = is_sequence
self.action = action
self.trigger = mods, key
self.rest = rest
def resolve(self, kitty_mod):
self.trigger = defines.resolve_key_mods(kitty_mod, self.trigger[0]), self.trigger[1]
self.rest = tuple((defines.resolve_key_mods(kitty_mod, mods), key) for mods, key in self.rest)
def parse_key(val, key_definitions):
sc, action = val.partition(' ')[::2]
sc, action = sc.strip().strip(sequence_sep), action.strip()
if not sc or not action:
@@ -139,8 +193,8 @@ def parse_key(val, keymap, sequence_map):
for part in sc.split(sequence_sep):
mods, key = parse_shortcut(part)
if key is None:
log_error('Shortcut: {} has unknown key, ignoring'.format(
sc))
if mods is not None:
log_error('Shortcut: {} has unknown key, ignoring'.format(sc))
return
if trigger is None:
trigger = mods, key
@@ -150,8 +204,8 @@ def parse_key(val, keymap, sequence_map):
else:
mods, key = parse_shortcut(sc)
if key is None:
log_error('Shortcut: {} has unknown key, ignoring'.format(
sc))
if mods is not None:
log_error('Shortcut: {} has unknown key, ignoring'.format(sc))
return
try:
paction = parse_key_action(action)
@@ -162,10 +216,9 @@ def parse_key(val, keymap, sequence_map):
if paction is not None:
all_key_actions.add(paction.func)
if is_sequence:
s = sequence_map.setdefault(trigger, {})
s[rest] = paction
key_definitions.append(KeyDefinition(True, paction, trigger[0], trigger[1], rest))
else:
keymap[(mods, key)] = paction
key_definitions.append(KeyDefinition(False, paction, mods, key))
def parse_symbol_map(val):
@@ -201,11 +254,10 @@ def parse_symbol_map(val):
def parse_send_text_bytes(text):
return ast.literal_eval("'''" + text.replace("'''", "'\\''") + "'''"
).encode('utf-8')
return python_string(text).encode('utf-8')
def parse_send_text(val, keymap, sequence_map):
def parse_send_text(val, key_definitions):
parts = val.split(' ')
def abort(msg):
@@ -218,21 +270,31 @@ def parse_send_text(val, keymap, sequence_map):
mode, sc = parts[:2]
text = ' '.join(parts[2:])
key_str = '{} send_text {} {}'.format(sc, mode, text)
return parse_key(key_str, keymap, sequence_map)
return parse_key(key_str, key_definitions)
def to_modifiers(val):
return parse_mods(val.split('+'))
return parse_mods(val.split('+'), val) or 0
def uniq(vals, result_type=list):
seen = set()
seen_add = seen.add
return result_type(x for x in vals if x not in seen and not seen_add(x))
def to_layout_names(raw):
parts = [x.strip().lower() for x in raw.split(',')]
if '*' in parts:
return sorted(all_layouts)
ans = []
for p in parts:
if p not in all_layouts:
if p == '*':
ans.extend(sorted(all_layouts))
continue
name = p.partition(':')[0]
if name not in all_layouts:
raise ValueError('The window layout {} is unknown'.format(p))
return parts
ans.append(p)
return uniq(ans)
def adjust_line_height(x):
@@ -244,10 +306,10 @@ def adjust_line_height(x):
def macos_titlebar_color(x):
x = x.strip('"')
if x == 'system':
return
return 0
if x == 'background':
return True
return to_color(x)
return 1
return (color_as_int(to_color(x)) << 8) | 2
def box_drawing_scale(x):
@@ -292,7 +354,8 @@ type_map = {
'adjust_line_height': adjust_line_height,
'adjust_column_width': adjust_line_height,
'scrollback_lines': positive_int,
'scrollback_pager': shlex.split,
'scrollback_pager': to_cmdline,
'open_url_with': to_cmdline,
'font_size': to_font_size,
'font_size_delta': positive_float,
'focus_follows_mouse': to_bool,
@@ -318,22 +381,32 @@ type_map = {
'initial_window_width': positive_int,
'initial_window_height': positive_int,
'macos_hide_titlebar': to_bool,
'macos_hide_from_tasks': to_bool,
'macos_option_as_alt': to_bool,
'macos_titlebar_color': macos_titlebar_color,
'box_drawing_scale': box_drawing_scale,
'dynamic_background_opacity': to_bool,
'background_opacity': unit_float,
'dim_opacity': unit_float,
'tab_separator': tab_separator,
'active_tab_font_style': tab_font_style,
'inactive_tab_font_style': tab_font_style,
'inactive_text_alpha': unit_float,
'url_style': url_style,
'copy_on_select': to_bool,
'window_alert_on_bell': to_bool,
'tab_bar_edge': tab_bar_edge,
'bell_on_tab': to_bool,
'kitty_mod': to_modifiers,
'clear_all_shortcuts': to_bool,
'clipboard_control': lambda x: frozenset(x.lower().split()),
'window_resize_step_cells': int,
'window_resize_step_lines': int,
}
for name in (
'foreground background cursor active_border_color inactive_border_color'
' selection_foreground selection_background url_color'
' selection_foreground selection_background url_color bell_border_color'
).split():
type_map[name] = to_color
for i in range(256):
@@ -345,15 +418,19 @@ for a in ('active', 'inactive'):
def special_handling(key, val, ans):
if key == 'map':
parse_key(val, ans['keymap'], ans['sequence_map'])
parse_key(val, ans['key_definitions'])
return True
if key == 'symbol_map':
ans['symbol_map'].update(parse_symbol_map(val))
return True
if key == 'send_text':
# For legacy compatibility
parse_send_text(val, ans['keymap'], ans['sequence_map'])
parse_send_text(val, ans['key_definitions'])
return True
if key == 'clear_all_shortcuts':
if to_bool(val):
ans['key_definitions'] = [None]
return
defaults = None
@@ -363,11 +440,7 @@ default_config_path = os.path.join(
def parse_config(lines, check_keys=True):
ans = {
'keymap': {},
'sequence_map': {},
'symbol_map': {},
}
ans = {'symbol_map': {}, 'keymap': {}, 'sequence_map': {}, 'key_definitions': []}
parse_config_base(
lines,
defaults,
@@ -397,74 +470,20 @@ actions = frozenset(all_key_actions) | frozenset(
no_op_actions = frozenset({'noop', 'no-op', 'no_op'})
def merge_keys(ans, defaults, newvals):
ans['keymap'] = defaults['keymap'].copy()
ans['sequence_map'] = {t: r.copy() for t, r in defaults['sequence_map'].items()}
# Merge the keymap
for k, v in newvals['keymap'].items():
ans['sequence_map'].pop(k, None)
f = v.func
if f in no_op_actions:
ans['keymap'].pop(k, None)
elif f in actions:
ans['keymap'][k] = v
# Merge the sequence map
for trigger, rest_map in newvals['sequence_map'].items():
ans['keymap'].pop(trigger, None)
if trigger in newvals['keymap']:
log_error('The shortcut for {} has conflicting definitions'.format(newvals['keymap'][trigger].func))
s = ans['sequence_map'].setdefault(trigger, {})
for k, v in rest_map.items():
f = v.func
if f in no_op_actions:
s.pop(k, None)
elif f in actions:
s[k] = v
ans['sequence_map'] = {k: v for k, v in ans['sequence_map'].items() if v}
def merge_dicts(defaults, newvals):
ans = defaults.copy()
ans.update(newvals)
return ans
def merge_configs(defaults, vals):
ans = {}
for k, v in defaults.items():
if isinstance(v, dict):
if k not in ('keymap', 'sequence_map'):
newvals = vals.get(k, {})
ans[k] = merge_dicts(v, newvals)
newvals = vals.get(k, {})
ans[k] = merge_dicts(v, newvals)
elif k == 'key_definitions':
ans['key_definitions'] = v + vals.get('key_definitions', [])
else:
ans[k] = vals.get(k, v)
merge_keys(
ans,
{'keymap': defaults.get('keymap', {}), 'sequence_map': defaults.get('sequence_map', {})},
{'keymap': vals.get('keymap', {}), 'sequence_map': vals.get('sequence_map', {})}
)
return ans
def load_config(*paths, overrides=None) -> Options:
ans = defaults._asdict()
for path in paths:
if not path:
continue
try:
f = open(path, encoding='utf-8', errors='replace')
except FileNotFoundError:
continue
with f:
vals = parse_config(f)
ans = merge_configs(ans, vals)
if overrides is not None:
vals = parse_config(overrides)
ans = merge_configs(ans, vals)
return Options(ans)
def build_ansi_color_table(opts: Options = defaults):
def build_ansi_color_table(opts=defaults):
def as_int(x):
return (x[0] << 16) | (x[1] << 8) | x[2]
@@ -476,6 +495,7 @@ def build_ansi_color_table(opts: Options = defaults):
def atomic_save(data, path):
import tempfile
fd, p = tempfile.mkstemp(dir=os.path.dirname(path), suffix='.tmp')
try:
with os.fdopen(fd, 'wb') as f:
@@ -547,3 +567,45 @@ def prepare_config_file_for_editing():
with open(defconf, 'w') as f:
f.write(commented_out_default_config())
return defconf
def finalize_keys(opts):
defns = []
for d in opts.key_definitions:
if d is None: # clear_all_shortcuts
defns = []
else:
defns.append(d)
for d in defns:
d.resolve(opts.kitty_mod)
keymap = {}
sequence_map = {}
for defn in defns:
is_no_op = defn.action.func in no_op_actions
if defn.is_sequence:
keymap.pop(defn.trigger, None)
s = sequence_map.setdefault(defn.trigger, {})
if is_no_op:
s.pop(defn.rest, None)
if not s:
del sequence_map[defn.trigger]
else:
s[defn.rest] = defn.action
else:
sequence_map.pop(defn.trigger, None)
if is_no_op:
keymap.pop(defn.trigger, None)
else:
keymap[defn.trigger] = defn.action
opts.keymap = keymap
opts.sequence_map = sequence_map
def load_config(*paths, overrides=None):
opts = _load_config(Options, defaults, parse_config, merge_configs, *paths, overrides=overrides)
finalize_keys(opts)
if opts.background_opacity < 1.0 and opts.macos_titlebar_color:
log_error('Cannot use both macos_titlebar_color and background_opacity')
opts.macos_titlebar_color = 0
return opts

View File

@@ -2,8 +2,10 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import ast
import os
import re
import shlex
from .rgb import to_color as as_color
from .utils import log_error
@@ -31,6 +33,16 @@ def to_bool(x):
return x.lower() in 'y yes true'.split()
def to_cmdline(x):
ans = shlex.split(x)
ans[0] = os.path.expandvars(os.path.expanduser(ans[0]))
return ans
def python_string(text):
return ast.literal_eval("'''" + text.replace("'''", "'\\''") + "'''")
def parse_line(line, type_map, special_handling, ans, all_keys, base_path_for_includes):
line = line.strip()
if not line or line.startswith('#'):
@@ -41,7 +53,7 @@ def parse_line(line, type_map, special_handling, ans, all_keys, base_path_for_in
if special_handling(key, val, ans):
return
if key == 'include':
val = val.strip()
val = os.path.expandvars(os.path.expanduser(val.strip()))
if not os.path.isabs(val):
val = os.path.join(base_path_for_includes, val)
try:
@@ -117,9 +129,114 @@ def create_options_class(keys):
return ans
def merge_dicts(defaults, newvals):
ans = defaults.copy()
ans.update(newvals)
return ans
def resolve_config(SYSTEM_CONF, defconf, config_files_on_cmd_line):
if config_files_on_cmd_line:
if 'NONE' not in config_files_on_cmd_line:
yield SYSTEM_CONF
for cf in config_files_on_cmd_line:
yield cf
else:
yield SYSTEM_CONF
yield defconf
def load_config(Options, defaults, parse_config, merge_configs, *paths, overrides=None):
ans = defaults._asdict()
for path in paths:
if not path:
continue
try:
f = open(path, encoding='utf-8', errors='replace')
except FileNotFoundError:
continue
with f:
vals = parse_config(f)
ans = merge_configs(ans, vals)
if overrides is not None:
vals = parse_config(overrides)
ans = merge_configs(ans, vals)
return Options(ans)
def init_config(defaults_path, parse_config):
with open(defaults_path, encoding='utf-8', errors='replace') as f:
defaults = parse_config(f, check_keys=False)
Options = create_options_class(defaults.keys())
defaults = Options(defaults)
return Options, defaults
def key_func():
ans = {}
def func_with_args(*names):
def w(f):
for name in names:
if ans.setdefault(name, f) is not f:
raise ValueError('the args_func {} is being redefined'.format(name))
return f
return w
return func_with_args, ans
def parse_kittens_shortcut(sc):
from kitty.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:
resolved_mods = 0
for mod in mods:
m = config_mod_map.get(mod.upper())
if m is None:
raise ValueError('Unknown shortcut modifiers: {}'.format(sc))
resolved_mods |= m
mods = resolved_mods
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:
raise ValueError('Unknown shortcut key: {}'.format(sc))
else:
is_text = True
rkey = tkey
return mods, rkey, is_text
def parse_kittens_func_args(action, args_funcs):
parts = action.split(' ', 1)
func = parts[0]
if len(parts) == 1:
return func, ()
rest = parts[1]
parser = args_funcs.get(func)
if parser is not None:
try:
func, args = parser(func, rest)
except Exception:
raise ValueError('Unknown key action: {}'.format(action))
if not isinstance(args, (list, tuple)):
args = (args,)
return func, tuple(args)
def parse_kittens_key(val, funcs_with_args):
sc, action = val.partition(' ')[::2]
if not sc or not action:
return
mods, key, is_text = parse_kittens_shortcut(sc)
action = parse_kittens_func_args(action, funcs_with_args)
return action, key, mods, is_text

View File

@@ -11,7 +11,7 @@ from collections import namedtuple
from .fast_data_types import set_boss as set_c_boss
appname = 'kitty'
version = (0, 9, 0)
version = (0, 10, 1)
str_version = '.'.join(map(str, version))
_plat = sys.platform.lower()
is_macos = 'darwin' in _plat
@@ -105,3 +105,6 @@ def glfw_path(module):
is_wayland = False
if os.environ.get('WAYLAND_DISPLAY') and 'KITTY_ENABLE_WAYLAND' in os.environ and os.path.exists(glfw_path('wayland')):
is_wayland = True
supports_primary_selection = not is_macos and not is_wayland

View File

@@ -24,7 +24,7 @@ dealloc(Cursor* self) {
#define EQ(x) (a->x == b->x)
static int __eq__(Cursor *a, Cursor *b) {
return EQ(bold) && EQ(italic) && EQ(strikethrough) && EQ(reverse) && EQ(decoration) && EQ(fg) && EQ(bg) && EQ(decoration_fg) && EQ(x) && EQ(y) && EQ(shape) && EQ(blink);
return EQ(bold) && EQ(italic) && EQ(strikethrough) && EQ(dim) && EQ(reverse) && EQ(decoration) && EQ(fg) && EQ(bg) && EQ(decoration_fg) && EQ(x) && EQ(y) && EQ(shape) && EQ(blink);
}
static const char* cursor_names[NUM_OF_CURSOR_SHAPES] = { "NO_SHAPE", "BLOCK", "BEAM", "UNDERLINE" };
@@ -33,16 +33,16 @@ static const char* cursor_names[NUM_OF_CURSOR_SHAPES] = { "NO_SHAPE", "BLOCK", "
static PyObject *
repr(Cursor *self) {
return PyUnicode_FromFormat(
"Cursor(x=%u, y=%u, shape=%s, blink=%R, fg=#%08x, bg=#%08x, bold=%R, italic=%R, reverse=%R, strikethrough=%R, decoration=%d, decoration_fg=#%08x)",
"Cursor(x=%u, y=%u, shape=%s, blink=%R, fg=#%08x, bg=#%08x, bold=%R, italic=%R, reverse=%R, strikethrough=%R, dim=%R, decoration=%d, decoration_fg=#%08x)",
self->x, self->y, (self->shape < NUM_OF_CURSOR_SHAPES ? cursor_names[self->shape] : "INVALID"),
BOOL(self->blink), self->fg, self->bg, BOOL(self->bold), BOOL(self->italic), BOOL(self->reverse), BOOL(self->strikethrough), self->decoration, self->decoration_fg
BOOL(self->blink), self->fg, self->bg, BOOL(self->bold), BOOL(self->italic), BOOL(self->reverse), BOOL(self->strikethrough), BOOL(self->dim), self->decoration, self->decoration_fg
);
}
void
cursor_reset_display_attrs(Cursor *self) {
self->bg = 0; self->fg = 0; self->decoration_fg = 0;
self->decoration = 0; self->bold = false; self->italic = false; self->reverse = false; self->strikethrough = false;
self->decoration = 0; self->bold = false; self->italic = false; self->reverse = false; self->strikethrough = false; self->dim = false;
}
@@ -85,6 +85,8 @@ START_ALLOW_CASE_RANGE
cursor_reset_display_attrs(self); break;
case 1:
self->bold = true; break;
case 2:
self->dim = true; break;
case 3:
self->italic = true; break;
case 4:
@@ -98,7 +100,7 @@ START_ALLOW_CASE_RANGE
case 21:
self->decoration = 2; break;
case 22:
self->bold = false; break;
self->bold = false; self->dim = false; break;
case 23:
self->italic = false; break;
case 24:
@@ -138,6 +140,7 @@ apply_sgr_to_cells(Cell *first_cell, unsigned int cell_count, unsigned int *para
#define RANGE for(unsigned c = 0; c < cell_count; c++, cell++)
#define SET(shift) RANGE { cell->attrs |= (1 << shift); } break;
#define RESET(shift) RANGE { cell->attrs &= ~(1 << shift); } break;
#define RESET2(shift1, shift2) RANGE { cell->attrs &= ~((1 << shift1) | (1 << shift2)); } break;
#define SETM(val, mask, shift) { RANGE { cell->attrs &= ~(mask << shift); cell->attrs |= ((val) << shift); } break; }
#define SET_COLOR(which) { color_type color = 0; parse_color(params, &i, count, &color); if (color) { RANGE { cell->which = color; }} } break;
#define SIMPLE(which, val) RANGE { cell->which = (val); } break;
@@ -153,6 +156,8 @@ apply_sgr_to_cells(Cell *first_cell, unsigned int cell_count, unsigned int *para
break;
case 1:
SET(BOLD_SHIFT);
case 2:
SET(DIM_SHIFT);
case 3:
SET(ITALIC_SHIFT);
case 4:
@@ -165,7 +170,7 @@ apply_sgr_to_cells(Cell *first_cell, unsigned int cell_count, unsigned int *para
case 21:
SETM(2, DECORATION_MASK, DECORATION_SHIFT);
case 22:
RESET(BOLD_SHIFT);
RESET2(DIM_SHIFT, BOLD_SHIFT);
case 23:
RESET(ITALIC_SHIFT);
case 24:
@@ -199,6 +204,7 @@ END_ALLOW_CASE_RANGE
}
}
#undef RESET
#undef RESET2
#undef SET_COLOR
#undef SET
#undef SETM
@@ -237,7 +243,11 @@ cursor_as_sgr(Cursor *self, Cursor *prev) {
#define SZ sizeof(buf) - (p - buf) - 2
#define P(fmt, ...) { p += snprintf(p, SZ, fmt ";", __VA_ARGS__); }
char *p = buf;
if (self->bold != prev->bold) P("%d", self->bold ? 1 : 22);
bool intensity_differs = self->bold != prev->bold || self->dim != prev->dim;
if (intensity_differs) {
if (!self->bold || !self->dim) { P("%d", 22); }
else P("%d;%d", 1, 2);
}
if (self->italic != prev->italic) P("%d", self->italic ? 3 : 23);
if (self->reverse != prev->reverse) P("%d", self->reverse ? 7 : 27);
if (self->strikethrough != prev->strikethrough) P("%d", self->strikethrough ? 9 : 29);
@@ -268,7 +278,7 @@ void cursor_reset(Cursor *self) {
void cursor_copy_to(Cursor *src, Cursor *dest) {
#define CCY(x) dest->x = src->x;
CCY(x); CCY(y); CCY(shape); CCY(blink);
CCY(bold); CCY(italic); CCY(strikethrough); CCY(reverse); CCY(decoration); CCY(fg); CCY(bg); CCY(decoration_fg);
CCY(bold); CCY(italic); CCY(strikethrough); CCY(dim); CCY(reverse); CCY(decoration); CCY(fg); CCY(bg); CCY(decoration_fg);
}
static PyObject*
@@ -281,6 +291,7 @@ BOOL_GETSET(Cursor, bold)
BOOL_GETSET(Cursor, italic)
BOOL_GETSET(Cursor, reverse)
BOOL_GETSET(Cursor, strikethrough)
BOOL_GETSET(Cursor, dim)
BOOL_GETSET(Cursor, blink)
static PyMemberDef members[] = {
@@ -299,6 +310,7 @@ static PyGetSetDef getseters[] = {
GETSET(italic)
GETSET(reverse)
GETSET(strikethrough)
GETSET(dim)
GETSET(blink)
{NULL} /* Sentinel */
};

View File

@@ -193,14 +193,11 @@ PyInit_fast_data_types(void) {
#endif
if (!init_fonts(m)) return NULL;
#define OOF(n) #n, offsetof(Cell, n)
if (PyModule_AddObject(m, "CELL", Py_BuildValue("{sI sI sI sI sI sI sI sI sI}",
OOF(ch), OOF(fg), OOF(bg), OOF(decoration_fg), OOF(cc_idx), OOF(sprite_x), OOF(sprite_y), OOF(sprite_z), "size", sizeof(Cell))) != 0) return NULL;
#undef OOF
PyModule_AddIntConstant(m, "BOLD", BOLD_SHIFT);
PyModule_AddIntConstant(m, "ITALIC", ITALIC_SHIFT);
PyModule_AddIntConstant(m, "REVERSE", REVERSE_SHIFT);
PyModule_AddIntConstant(m, "STRIKETHROUGH", STRIKE_SHIFT);
PyModule_AddIntConstant(m, "DIM", DIM_SHIFT);
PyModule_AddIntConstant(m, "DECORATION", DECORATION_SHIFT);
PyModule_AddStringMacro(m, ERROR_PREFIX);
#ifdef KITTY_VCS_REV

View File

@@ -17,6 +17,7 @@
// Required minimum OpenGL version
#define OPENGL_REQUIRED_VERSION_MAJOR 3
#define OPENGL_REQUIRED_VERSION_MINOR 3
#define GLFW_MOD_KITTY 1024
#define UNUSED __attribute__ ((unused))
#define PYNOARG PyObject *__a1 UNUSED, PyObject *__a2 UNUSED
#define EXPORTED __attribute__ ((visibility ("default")))
@@ -57,6 +58,7 @@ typedef enum MouseShapes { BEAM, HAND, ARROW } MouseShape;
#define BI_VAL(attrs) ((attrs >> 4) & 3)
#define REVERSE_SHIFT 6
#define STRIKE_SHIFT 7
#define DIM_SHIFT 8
#define COL_MASK 0xFFFFFFFF
#define UTF8_ACCEPT 0
#define UTF8_REJECT 1
@@ -70,11 +72,12 @@ typedef enum MouseShapes { BEAM, HAND, ARROW } MouseShape;
#define CURSOR_TO_ATTRS(c, w) \
((w) | (((c->decoration & 3) << DECORATION_SHIFT) | ((c->bold & 1) << BOLD_SHIFT) | \
((c->italic & 1) << ITALIC_SHIFT) | ((c->reverse & 1) << REVERSE_SHIFT) | ((c->strikethrough & 1) << STRIKE_SHIFT)))
((c->italic & 1) << ITALIC_SHIFT) | ((c->reverse & 1) << REVERSE_SHIFT) | \
((c->strikethrough & 1) << STRIKE_SHIFT) | ((c->dim & 1) << DIM_SHIFT)))
#define ATTRS_TO_CURSOR(a, c) \
(c)->decoration = (a >> DECORATION_SHIFT) & 3; (c)->bold = (a >> BOLD_SHIFT) & 1; (c)->italic = (a >> ITALIC_SHIFT) & 1; \
(c)->reverse = (a >> REVERSE_SHIFT) & 1; (c)->strikethrough = (a >> STRIKE_SHIFT) & 1;
(c)->reverse = (a >> REVERSE_SHIFT) & 1; (c)->strikethrough = (a >> STRIKE_SHIFT) & 1; (c)->dim = (a >> DIM_SHIFT) & 1;
#define COPY_CELL(src, s, dest, d) \
(dest)->cells[d] = (src)->cells[s];
@@ -160,21 +163,24 @@ typedef struct {
Line *line;
} LineBuf;
typedef struct {
Cell *cells;
line_attrs_type *line_attrs;
} HistoryBufSegment;
typedef struct {
PyObject_HEAD
Cell *buf;
index_type xnum, ynum;
index_type xnum, ynum, num_segments;
HistoryBufSegment* segments;
Line *line;
index_type start_of_data, count;
line_attrs_type *line_attrs;
} HistoryBuf;
typedef struct {
PyObject_HEAD
bool bold, italic, reverse, strikethrough, blink;
bool bold, italic, reverse, strikethrough, blink, dim;
unsigned int x, y;
uint8_t decoration;
CursorShape shape;

2
kitty/emoji.h generated
View File

@@ -1,4 +1,4 @@
// unicode data, built from the unicode standard on: 2018-02-09
// unicode data, built from the unicode standard on: 2018-05-01
// see gen-wcwidth.py
#pragma once
#include "data-types.h"

View File

@@ -424,6 +424,8 @@ START_ALLOW_CASE_RANGE
case 0x2574 ... 0x259f:
case 0xe0b0:
case 0xe0b2:
case 0xe0b4:
case 0xe0b6:
return BOX_FONT;
default:
ans = in_symbol_maps(cell->ch);
@@ -460,6 +462,10 @@ START_ALLOW_CASE_RANGE
return 0xfa;
case 0xe0b2:
return 0xfb;
case 0xe0b4:
return 0xfc;
case 0xe0b6:
return 0xfd;
default:
return 0xff;
}
@@ -720,7 +726,8 @@ shape_run(Cell *first_cell, index_type num_cells, Font *font) {
num_codepoints_used_by_glyph = UINT32_MAX;
} else {
next_cluster = G(info)[G(glyph_idx) + 1].cluster;
if (next_cluster > cluster) num_codepoints_used_by_glyph = next_cluster - cluster;
// RTL languages like Arabic have decreasing cluster numbers
if (next_cluster != cluster) num_codepoints_used_by_glyph = cluster > next_cluster ? cluster - next_cluster : next_cluster - cluster;
}
if (!current_group->num_glyphs) {
add_to_current_group = true;
@@ -852,7 +859,7 @@ test_shape(PyObject UNUSED *self, PyObject *args) {
PyObject *eg = PyTuple_New(MAX_NUM_EXTRA_GLYPHS);
for (size_t g = 0; g < MAX_NUM_EXTRA_GLYPHS; g++) PyTuple_SET_ITEM(eg, g, Py_BuildValue("H", g + 1 < group->num_glyphs ? G(info)[group->first_glyph_idx + g].codepoint : 0));
PyList_Append(ans, Py_BuildValue("IIIN", group->num_cells, group->num_glyphs, first_glyph, eg));
PyList_Append(ans, Py_BuildValue("IIHN", group->num_cells, group->num_glyphs, first_glyph, eg));
idx++;
}
if (face) { Py_CLEAR(face); free(font); }
@@ -897,6 +904,12 @@ render_line(Line *line) {
ssize_t cell_font_idx = font_for_cell(cell);
if (is_private_use(cell->ch) && i + 1 < line->xnum && (line->cells[i+1].ch == ' ' || line->cells[i+1].ch == 0) && cell_font_idx != BOX_FONT && cell_font_idx != MISSING_FONT) {
// We have a private use char followed by a space char, render it as a two cell ligature.
Cell *space_cell = line->cells + i+1;
// Ensure the space cell uses the foreground colors from the PUA cell
// This is needed because there are stupid applications like
// powerline that use PUA+space with different foreground colors
// for the space and the PUA. See for example: https://github.com/kovidgoyal/kitty/issues/467
space_cell->fg = cell->fg; space_cell->decoration_fg = cell->decoration_fg;
RENDER;
render_run(line->cells + i, 2, cell_font_idx, true);
run_font_idx = NO_FONT;
@@ -982,7 +995,7 @@ static PyObject*
test_sprite_position_for(PyObject UNUSED *self, PyObject *args) {
glyph_index glyph;
ExtraGlyphs extra_glyphs = {{0}};
if (!PyArg_ParseTuple(args, "H|I", &glyph, &extra_glyphs.data)) return NULL;
if (!PyArg_ParseTuple(args, "H|H", &glyph, &extra_glyphs.data)) return NULL;
int error;
SpritePosition *pos = sprite_position_for(&fonts.fonts[fonts.medium_font_idx], glyph, &extra_glyphs, 0, &error);
if (pos == NULL) { sprite_map_set_error(error); return NULL; }

View File

@@ -3,7 +3,6 @@
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import math
import os
from functools import partial as p
from itertools import repeat
@@ -138,6 +137,20 @@ def cross(*s, a=1, b=1, c=1, d=1):
half_vline(*s, level=d, which='bottom')
def fill_region(buf, width, height, xlimits):
for y in range(height):
offset = y * width
for x, (upper, lower) in enumerate(xlimits):
buf[x + offset] = 255 if upper <= y <= lower else 0
# Anti-alias the boundary, simple y-axis anti-aliasing
for x, limits in enumerate(xlimits):
for y in limits:
for ypx in range(int(math.floor(y)), int(math.ceil(y)) + 1):
if 0 <= ypx < height:
off = ypx * width + x
buf[off] = min(255, buf[off] + int((1 - abs(y - ypx)) * 255))
def line_equation(x1, y1, x2, y2):
m = (y2 - y1) / (x2 - x1)
c = y1 - m * x1
@@ -157,17 +170,91 @@ def triangle(buf, width, height, left=True):
uppery = line_equation(x1, ay1, x2, y2)
lowery = line_equation(x1, by1, x2, y2)
xlimits = [(uppery(x), lowery(x)) for x in range(width)]
for y in range(height):
offset = y * width
for x, (upper, lower) in zip(range(width), xlimits):
buf[x + offset] = 255 if upper <= y <= lower else 0
# Anti-alias the diagonals, simple y-axis anti-aliasing
for x in range(width):
for y in xlimits[x]:
for ypx in range(int(math.floor(y)), int(math.ceil(y)) + 1):
if 0 <= ypx < height:
off = ypx * width + x
buf[off] = min(255, buf[off] + int((1 - abs(y - ypx)) * 255))
fill_region(buf, width, height, xlimits)
def cubic_bezier(start, end, c1, c2):
def bezier_eq(p0, p1, p2, p3):
def f(t):
tm1 = 1 - t
tm1_3 = tm1 * tm1 * tm1
t_3 = t * t * t
return tm1_3 * p0 + 3 * t * tm1 * (tm1 * p1 + t * p2) + t_3 * p3
return f
bezier_x = bezier_eq(start[0], c1[0], c2[0], end[0])
bezier_y = bezier_eq(start[1], c1[1], c2[1], end[1])
return bezier_x, bezier_y
def find_bezier_for_D(width, height):
cx = last_cx = width - 1
start = (0, 0)
end = (0, height - 1)
while True:
c1 = cx, start[1]
c2 = cx, end[1]
bezier_x, bezier_y = cubic_bezier(start, end, c1, c2)
if bezier_x(0.5) > width - 1:
return last_cx
last_cx = cx
cx += 1
def get_bezier_limits(bezier_x, bezier_y):
start_x = bezier_x(0)
max_x = int(bezier_x(0.5))
last_t, t_limit = 0, 0.5
def find_t_for_x(x, start_t):
if abs(bezier_x(start_t) - x) < 0.1:
return start_t
increment = t_limit - start_t
if increment <= 0:
return start_t
while True:
q = bezier_x(start_t + increment)
if (abs(q - x) < 0.1):
return start_t + increment
if q > x:
increment /= 2
if increment < 1e-6:
raise ValueError('Failed to find t for x={}'.format(x))
else:
start_t += increment
increment = t_limit - start_t
if increment <= 0:
return start_t
for x in range(start_x, max_x + 1):
if x > start_x:
last_t = find_t_for_x(x, last_t)
upper, lower = bezier_y(last_t), bezier_y(1 - last_t)
if abs(upper - lower) <= 2: # avoid pip on end of D
break
yield upper, lower
def D(buf, width, height, left=True):
c1x = find_bezier_for_D(width, height)
start = (0, 0)
end = (0, height - 1)
c1 = c1x, start[1]
c2 = c1x, end[1]
bezier_x, bezier_y = cubic_bezier(start, end, c1, c2)
xlimits = list(get_bezier_limits(bezier_x, bezier_y))
if left:
fill_region(buf, width, height, xlimits)
else:
mbuf = bytearray(width * height)
fill_region(mbuf, width, height, xlimits)
for y in range(height):
offset = y * width
for src_x in range(width):
dest_x = width - 1 - src_x
buf[offset + dest_x] = mbuf[offset + src_x]
def half_dhline(buf, width, height, level=1, which='left', only=None):
@@ -289,16 +376,42 @@ def hblock(buf, width, height, frac=1, gravity='left'):
buf[c] = 255
def shade(buf, width, height, frac=1/4):
rand = bytearray(os.urandom(width * height))
cutoff = int(frac * 255)
def shade(buf, width, height, light=False, invert=False):
square_sz = max(1, width // 12)
number_of_rows = height // square_sz
number_of_cols = width // square_sz
nums = tuple(range(square_sz))
for r in range(height):
off = width * r
for c in range(width):
q = off + c
if rand[q] < cutoff:
buf[q] = 255
dest = bytearray(width * height) if invert else buf
for r in range(number_of_rows):
y = r * square_sz
is_odd = r % 2 != 0
if is_odd:
continue
fill_even = r % 4 == 0
for yr in nums:
y = r * square_sz + yr
if y >= height:
break
off = width * y
for c in range(number_of_cols):
if light:
fill = (c % 4) == (0 if fill_even else 2)
else:
fill = (c % 2 == 0) == fill_even
if fill:
for xc in nums:
x = (c * square_sz) + xc
if x >= width:
break
dest[off + x] = 255
if invert:
for y in range(height):
off = width * y
for x in range(width):
q = off + x
buf[q] = 255 - dest[q]
def quad(buf, width, height, x=0, y=0):
@@ -345,6 +458,8 @@ box_chars = {
'': [p(half_vline, level=3), p(half_vline, which='bottom')],
'': [triangle],
'': [p(triangle, left=False)],
'': [D],
'': [p(D, left=False)],
'': [dhline],
'': [dvline],
'': [vline, p(half_dhline, which='right')],
@@ -375,9 +490,9 @@ box_chars = {
'': [p(hblock, frac=1/4)],
'': [p(hblock, frac=1/8)],
'': [p(hblock, frac=1/2, gravity='right')],
'': [p(shade, frac=1/4)],
'': [p(shade, frac=2/4)],
'': [p(shade, frac=3/4)],
'': [p(shade, light=True)],
'': [shade],
'': [p(shade, invert=True)],
'': [p(vblock, frac=1/8)],
'': [p(hblock, frac=1/8, gravity='right')],
'': [p(quad, y=1)],
@@ -434,6 +549,26 @@ def render_missing_glyph(buf, width, height):
draw_vline(buf, width, vgap, height - vgap + 1, width - hgap, 0)
def test_char(ch, sz=48):
# kitty +runpy "from kitty.fonts.box_drawing import test_char; import sys; test_char('XXX')"
from .render import display_bitmap, setup_for_testing
from kitty.fast_data_types import concat_cells, set_send_sprite_to_gpu
width, height = setup_for_testing('monospace', sz)[1:]
buf = bytearray(width * height)
try:
render_box_char(ch, buf, width, height)
def join_cells(*cells):
cells = tuple(bytes(x) for x in cells)
return concat_cells(width, height, False, cells)
rgb_data = join_cells(buf)
display_bitmap(rgb_data, width, height)
print()
finally:
set_send_sprite_to_gpu(None)
def test_drawing(sz=48, family='monospace'):
from .render import display_bitmap, setup_for_testing
from kitty.fast_data_types import concat_cells, set_send_sprite_to_gpu

View File

@@ -220,7 +220,7 @@ def shape_string(text="abcd", family='monospace', size=11.0, dpi=96.0, path=None
def display_bitmap(rgb_data, width, height):
from tempfile import NamedTemporaryFile
from kitty.icat import detect_support, show
from kittens.icat.main import detect_support, show
if not hasattr(display_bitmap, 'detected') and not detect_support():
raise SystemExit('Your terminal does not support the graphics protocol')
display_bitmap.detected = True

View File

@@ -214,15 +214,20 @@ ft_face_from_path_and_psname(PyObject* path, const char* psname, void *extra_dat
PyObject*
face_from_descriptor(PyObject *descriptor) {
#define D(key, conv) { PyObject *t = PyDict_GetItemString(descriptor, #key); if (t == NULL) return NULL; key = conv(t); t = NULL; }
char *path;
long index;
bool hinting;
long hint_style;
D(path, PyUnicode_AsUTF8);
D(index, PyLong_AsLong);
D(hinting, PyObject_IsTrue);
D(hint_style, PyLong_AsLong);
#define D(key, conv, missing_ok) { \
PyObject *t = PyDict_GetItemString(descriptor, #key); \
if (t == NULL) { \
if (!missing_ok) { PyErr_SetString(PyExc_KeyError, "font descriptor is missing the key: " #key); return NULL; } \
} else key = conv(t); \
}
char *path = NULL;
long index = 0;
bool hinting = false;
long hint_style = 0;
D(path, PyUnicode_AsUTF8, false);
D(index, PyLong_AsLong, true);
D(hinting, PyObject_IsTrue, true);
D(hint_style, PyLong_AsLong, true);
#undef D
Face *self = (Face *)Face_Type.tp_alloc(&Face_Type, 0);
if (self != NULL) {
@@ -255,10 +260,11 @@ dealloc(Face* self) {
static PyObject *
repr(Face *self) {
const char *ps_name = FT_Get_Postscript_Name(self->face);
return PyUnicode_FromFormat(
"Face(family=%s, style=%s, ps_name=%s, path=%S, index=%d, is_scalable=%S, has_color=%S, ascender=%i, descender=%i, height=%i, underline_position=%i, underline_thickness=%i)",
self->face->family_name ? self->face->family_name : "", self->face->style_name ? self->face->style_name : "",
FT_Get_Postscript_Name(self->face),
ps_name ? ps_name: "",
self->path, self->index, self->is_scalable ? Py_True : Py_False, self->has_color ? Py_True : Py_False,
self->ascender, self->descender, self->height, self->underline_position, self->underline_thickness
);

6265
kitty/gl-wrapper.c generated

File diff suppressed because it is too large Load Diff

43
kitty/gl-wrapper.h generated
View File

@@ -1,7 +1,5 @@
/*
OpenGL loader generated by glad 0.1.15a0 on Wed Nov 8 14:58:53 2017.
OpenGL loader generated by glad 0.1.20a0 on Fri May 4 18:11:55 2018.
Language/Generator: C/C++ Debug
Specification: gl
APIs: gl=3.3
@@ -15,22 +13,17 @@
Loader: True
Local files: False
Omit khrplatform: False
Commandline:
--profile="core" --api="gl=3.3" --generator="c-debug" --spec="gl" --extensions="GL_ARB_copy_image,GL_ARB_multisample,GL_ARB_robustness,GL_ARB_texture_storage,GL_KHR_debug"
Online:
http://glad.dav1d.de/#profile=core&language=c-debug&specification=gl&loader=on&api=gl%3D3.3&extensions=GL_ARB_copy_image&extensions=GL_ARB_multisample&extensions=GL_ARB_robustness&extensions=GL_ARB_texture_storage&extensions=GL_KHR_debug
*/
#ifndef __glad_h_
#define __glad_h_
#ifdef __gl_h_
#error OpenGL header already included, remove this include, glad already provides it
#endif
#define __gl_h_
#if defined(_WIN32) && !defined(APIENTRY) && !defined(__CYGWIN__) && !defined(__SCITECH_SNAP__)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN 1
@@ -40,25 +33,20 @@
#endif
#include <windows.h>
#endif
#ifndef APIENTRY
#define APIENTRY
#endif
#ifndef APIENTRYP
#define APIENTRYP APIENTRY *
#endif
#ifdef __cplusplus
extern "C" {
#endif
struct gladGLversionStruct {
int major;
int minor;
};
typedef void* (* GLADloadproc)(const char *name);
#ifndef GLAPI
# if defined(GLAD_GLAPI_EXPORT)
# if defined(_WIN32) || defined(__CYGWIN__)
@@ -84,17 +72,11 @@ typedef void* (* GLADloadproc)(const char *name);
# define GLAPI extern
# endif
#endif
GLAPI struct gladGLversionStruct GLVersion;
GLAPI int gladLoadGL(void);
GLAPI int gladLoadGLLoader(GLADloadproc);
#define GLAD_DEBUG
typedef void (* GLADcallback)(const char *name, void *funcptr, int len_args, ...);
GLAPI void glad_set_pre_callback(GLADcallback cb);
GLAPI void glad_set_post_callback(GLADcallback cb);
#include <stddef.h>
@@ -1112,11 +1094,11 @@ typedef void (APIENTRYP PFNGLENABLEPROC)(GLenum cap);
GLAPI PFNGLENABLEPROC glad_glEnable;
GLAPI PFNGLENABLEPROC glad_debug_glEnable;
#define glEnable glad_debug_glEnable
typedef void (APIENTRYP PFNGLFINISHPROC)();
typedef void (APIENTRYP PFNGLFINISHPROC)(void);
GLAPI PFNGLFINISHPROC glad_glFinish;
GLAPI PFNGLFINISHPROC glad_debug_glFinish;
#define glFinish glad_debug_glFinish
typedef void (APIENTRYP PFNGLFLUSHPROC)();
typedef void (APIENTRYP PFNGLFLUSHPROC)(void);
GLAPI PFNGLFLUSHPROC glad_glFlush;
GLAPI PFNGLFLUSHPROC glad_debug_glFlush;
#define glFlush glad_debug_glFlush
@@ -1164,7 +1146,7 @@ typedef void (APIENTRYP PFNGLGETDOUBLEVPROC)(GLenum pname, GLdouble *data);
GLAPI PFNGLGETDOUBLEVPROC glad_glGetDoublev;
GLAPI PFNGLGETDOUBLEVPROC glad_debug_glGetDoublev;
#define glGetDoublev glad_debug_glGetDoublev
typedef GLenum (APIENTRYP PFNGLGETERRORPROC)();
typedef GLenum (APIENTRYP PFNGLGETERRORPROC)(void);
GLAPI PFNGLGETERRORPROC glad_glGetError;
GLAPI PFNGLGETERRORPROC glad_debug_glGetError;
#define glGetError glad_debug_glGetError
@@ -1204,7 +1186,7 @@ typedef GLboolean (APIENTRYP PFNGLISENABLEDPROC)(GLenum cap);
GLAPI PFNGLISENABLEDPROC glad_glIsEnabled;
GLAPI PFNGLISENABLEDPROC glad_debug_glIsEnabled;
#define glIsEnabled glad_debug_glIsEnabled
typedef void (APIENTRYP PFNGLDEPTHRANGEPROC)(GLdouble near, GLdouble far);
typedef void (APIENTRYP PFNGLDEPTHRANGEPROC)(GLdouble n, GLdouble f);
GLAPI PFNGLDEPTHRANGEPROC glad_glDepthRange;
GLAPI PFNGLDEPTHRANGEPROC glad_debug_glDepthRange;
#define glDepthRange glad_debug_glDepthRange
@@ -1484,7 +1466,7 @@ typedef void (APIENTRYP PFNGLCOMPILESHADERPROC)(GLuint shader);
GLAPI PFNGLCOMPILESHADERPROC glad_glCompileShader;
GLAPI PFNGLCOMPILESHADERPROC glad_debug_glCompileShader;
#define glCompileShader glad_debug_glCompileShader
typedef GLuint (APIENTRYP PFNGLCREATEPROGRAMPROC)();
typedef GLuint (APIENTRYP PFNGLCREATEPROGRAMPROC)(void);
GLAPI PFNGLCREATEPROGRAMPROC glad_glCreateProgram;
GLAPI PFNGLCREATEPROGRAMPROC glad_debug_glCreateProgram;
#define glCreateProgram glad_debug_glCreateProgram
@@ -1884,7 +1866,7 @@ typedef void (APIENTRYP PFNGLBEGINTRANSFORMFEEDBACKPROC)(GLenum primitiveMode);
GLAPI PFNGLBEGINTRANSFORMFEEDBACKPROC glad_glBeginTransformFeedback;
GLAPI PFNGLBEGINTRANSFORMFEEDBACKPROC glad_debug_glBeginTransformFeedback;
#define glBeginTransformFeedback glad_debug_glBeginTransformFeedback
typedef void (APIENTRYP PFNGLENDTRANSFORMFEEDBACKPROC)();
typedef void (APIENTRYP PFNGLENDTRANSFORMFEEDBACKPROC)(void);
GLAPI PFNGLENDTRANSFORMFEEDBACKPROC glad_glEndTransformFeedback;
GLAPI PFNGLENDTRANSFORMFEEDBACKPROC glad_debug_glEndTransformFeedback;
#define glEndTransformFeedback glad_debug_glEndTransformFeedback
@@ -1912,7 +1894,7 @@ typedef void (APIENTRYP PFNGLBEGINCONDITIONALRENDERPROC)(GLuint id, GLenum mode)
GLAPI PFNGLBEGINCONDITIONALRENDERPROC glad_glBeginConditionalRender;
GLAPI PFNGLBEGINCONDITIONALRENDERPROC glad_debug_glBeginConditionalRender;
#define glBeginConditionalRender glad_debug_glBeginConditionalRender
typedef void (APIENTRYP PFNGLENDCONDITIONALRENDERPROC)();
typedef void (APIENTRYP PFNGLENDCONDITIONALRENDERPROC)(void);
GLAPI PFNGLENDCONDITIONALRENDERPROC glad_glEndConditionalRender;
GLAPI PFNGLENDCONDITIONALRENDERPROC glad_debug_glEndConditionalRender;
#define glEndConditionalRender glad_debug_glEndConditionalRender
@@ -2678,7 +2660,7 @@ GLAPI PFNGLSAMPLECOVERAGEARBPROC glad_debug_glSampleCoverageARB;
#ifndef GL_ARB_robustness
#define GL_ARB_robustness 1
GLAPI int GLAD_GL_ARB_robustness;
typedef GLenum (APIENTRYP PFNGLGETGRAPHICSRESETSTATUSARBPROC)();
typedef GLenum (APIENTRYP PFNGLGETGRAPHICSRESETSTATUSARBPROC)(void);
GLAPI PFNGLGETGRAPHICSRESETSTATUSARBPROC glad_glGetGraphicsResetStatusARB;
GLAPI PFNGLGETGRAPHICSRESETSTATUSARBPROC glad_debug_glGetGraphicsResetStatusARB;
#define glGetGraphicsResetStatusARB glad_debug_glGetGraphicsResetStatusARB
@@ -2798,7 +2780,7 @@ typedef void (APIENTRYP PFNGLPUSHDEBUGGROUPPROC)(GLenum source, GLuint id, GLsiz
GLAPI PFNGLPUSHDEBUGGROUPPROC glad_glPushDebugGroup;
GLAPI PFNGLPUSHDEBUGGROUPPROC glad_debug_glPushDebugGroup;
#define glPushDebugGroup glad_debug_glPushDebugGroup
typedef void (APIENTRYP PFNGLPOPDEBUGGROUPPROC)();
typedef void (APIENTRYP PFNGLPOPDEBUGGROUPPROC)(void);
GLAPI PFNGLPOPDEBUGGROUPPROC glad_glPopDebugGroup;
GLAPI PFNGLPOPDEBUGGROUPPROC glad_debug_glPopDebugGroup;
#define glPopDebugGroup glad_debug_glPopDebugGroup
@@ -2842,7 +2824,7 @@ typedef void (APIENTRYP PFNGLPUSHDEBUGGROUPKHRPROC)(GLenum source, GLuint id, GL
GLAPI PFNGLPUSHDEBUGGROUPKHRPROC glad_glPushDebugGroupKHR;
GLAPI PFNGLPUSHDEBUGGROUPKHRPROC glad_debug_glPushDebugGroupKHR;
#define glPushDebugGroupKHR glad_debug_glPushDebugGroupKHR
typedef void (APIENTRYP PFNGLPOPDEBUGGROUPKHRPROC)();
typedef void (APIENTRYP PFNGLPOPDEBUGGROUPKHRPROC)(void);
GLAPI PFNGLPOPDEBUGGROUPKHRPROC glad_glPopDebugGroupKHR;
GLAPI PFNGLPOPDEBUGGROUPKHRPROC glad_debug_glPopDebugGroupKHR;
#define glPopDebugGroupKHR glad_debug_glPopDebugGroupKHR
@@ -2867,11 +2849,8 @@ GLAPI PFNGLGETPOINTERVKHRPROC glad_glGetPointervKHR;
GLAPI PFNGLGETPOINTERVKHRPROC glad_debug_glGetPointervKHR;
#define glGetPointervKHR glad_debug_glGetPointervKHR
#endif
#ifdef __cplusplus
}
#endif
#endif
int init_glad(GLADloadproc, int);

View File

@@ -59,6 +59,7 @@ gl_init() {
ARB_TEST(texture_storage);
#undef ARB_TEST
glad_loaded = true;
if (global_state.debug_gl) printf("GL version string: '%s' Detected version: %d.%d\n", glGetString(GL_VERSION), GLVersion.major, GLVersion.minor);
if (GLVersion.major < OPENGL_REQUIRED_VERSION_MAJOR || (GLVersion.major == OPENGL_REQUIRED_VERSION_MAJOR && GLVersion.minor < OPENGL_REQUIRED_VERSION_MINOR)) {
fatal("OpenGL version is %d.%d, version >= 3.3 required for kitty", GLVersion.major, GLVersion.minor);
}

2
kitty/glfw-wrapper.c generated
View File

@@ -357,6 +357,8 @@ load_glfw(const char* path) {
*(void **) (&glfwGetCocoaMonitor_impl) = dlsym(handle, "glfwGetCocoaMonitor");
*(void **) (&glfwSetCocoaTextInputFilter_impl) = dlsym(handle, "glfwSetCocoaTextInputFilter");
*(void **) (&glfwGetX11Display_impl) = dlsym(handle, "glfwGetX11Display");
*(void **) (&glfwGetX11Window_impl) = dlsym(handle, "glfwGetX11Window");

5
kitty/glfw-wrapper.h generated
View File

@@ -1,6 +1,7 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int);
/*! @name GLFW version macros
@@ -1825,6 +1826,10 @@ typedef uint32_t (*glfwGetCocoaMonitor_func)(GLFWmonitor*);
glfwGetCocoaMonitor_func glfwGetCocoaMonitor_impl;
#define glfwGetCocoaMonitor glfwGetCocoaMonitor_impl
typedef GLFWcocoatextinputfilterfun (*glfwSetCocoaTextInputFilter_func)(GLFWwindow*, GLFWcocoatextinputfilterfun);
glfwSetCocoaTextInputFilter_func glfwSetCocoaTextInputFilter_impl;
#define glfwSetCocoaTextInputFilter glfwSetCocoaTextInputFilter_impl
typedef void* (*glfwGetX11Display_func)();
glfwGetX11Display_func glfwGetX11Display_impl;
#define glfwGetX11Display glfwGetX11Display_impl

View File

@@ -9,7 +9,8 @@
#include "glfw-wrapper.h"
extern bool cocoa_make_window_resizable(void *w);
extern void cocoa_create_global_menu(void);
extern void cocoa_set_titlebar_color(void *w);
extern void cocoa_set_hide_from_tasks(void);
extern void cocoa_set_titlebar_color(void *w, color_type color);
#if GLFW_KEY_LAST >= MAX_KEY_COUNT
#error "glfw has too many keys, you should increase MAX_KEY_COUNT"
@@ -309,12 +310,29 @@ set_dpi_from_os_window(OSWindow *w) {
static bool is_first_window = true;
#ifdef __APPLE__
static int
filter_option(int key UNUSED, int mods, unsigned int scancode UNUSED) {
return ((mods == GLFW_MOD_ALT) || (mods == (GLFW_MOD_ALT | GLFW_MOD_SHIFT))) ? 1 : 0;
}
#endif
void
set_titlebar_color(OSWindow *w, color_type color) {
if (w->handle && (!w->last_titlebar_color || (w->last_titlebar_color & 0xffffff) != (color & 0xffffff))) {
w->last_titlebar_color = (1 << 24) | (color & 0xffffff);
#ifdef __APPLE__
cocoa_set_titlebar_color(glfwGetCocoaWindow(w->handle), color);
#endif
}
}
static PyObject*
create_os_window(PyObject UNUSED *self, PyObject *args) {
int width, height, x = -1, y = -1;
char *title, *wm_class_class, *wm_class_name;
PyObject *load_programs = NULL;
if (!PyArg_ParseTuple(args, "iisss|Oiii", &width, &height, &title, &wm_class_name, &wm_class_class, &load_programs, &x, &y)) return NULL;
if (!PyArg_ParseTuple(args, "iisss|Oii", &width, &height, &title, &wm_class_name, &wm_class_class, &load_programs, &x, &y)) return NULL;
if (is_first_window) {
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
@@ -347,7 +365,7 @@ create_os_window(PyObject UNUSED *self, PyObject *args) {
PyErr_SetString(PyExc_ValueError, "Too many windows");
return NULL;
}
bool want_semi_transparent = (1.0 - OPT(background_opacity) >= 0.01) ? true : false;
bool want_semi_transparent = (1.0 - OPT(background_opacity) >= 0.01) || OPT(dynamic_background_opacity);
glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, want_semi_transparent);
GLFWwindow *glfw_window = glfwCreateWindow(width, height, title, NULL, global_state.num_os_windows ? global_state.os_windows[0].handle : NULL);
if (glfw_window == NULL) {
@@ -367,9 +385,14 @@ create_os_window(PyObject UNUSED *self, PyObject *args) {
Py_DECREF(ret);
#ifdef __APPLE__
cocoa_create_global_menu();
// This needs to be done only after the first window has been created, because glfw only sets the activation policy once upon initialization.
if (OPT(macos_hide_from_tasks)) cocoa_set_hide_from_tasks();
#endif
is_first_window = false;
}
#ifdef __APPLE__
if (OPT(macos_option_as_alt)) glfwSetCocoaTextInputFilter(glfw_window, filter_option);
#endif
OSWindow *w = add_os_window();
w->handle = glfw_window;
@@ -395,7 +418,6 @@ create_os_window(PyObject UNUSED *self, PyObject *args) {
if (glfwGetCocoaWindow) { if (!cocoa_make_window_resizable(glfwGetCocoaWindow(glfw_window))) { PyErr_Print(); } }
else log_error("Failed to load glfwGetCocoaWindow");
}
cocoa_set_titlebar_color(glfwGetCocoaWindow(glfw_window));
#endif
double now = monotonic();
w->is_focused = true;
@@ -609,7 +631,7 @@ request_window_attention(id_type kitty_window_id, bool audio_bell) {
OSWindow *w = os_window_for_kitty_window(kitty_window_id);
if (w) {
if (audio_bell) ring_audio_bell(w);
glfwRequestWindowAttention(w->handle);
if (OPT(window_alert_on_bell)) glfwRequestWindowAttention(w->handle);
glfwPostEmptyEvent();
}
}
@@ -745,6 +767,16 @@ os_window_swap_buffers(PyObject UNUSED *self, PyObject *args) {
return NULL;
}
static PyObject*
ring_bell(PyObject UNUSED *self, PyObject *args) {
id_type os_window_id;
if (!PyArg_ParseTuple(args, "K", &os_window_id)) return NULL;
OSWindow *w = os_window_for_kitty_window(os_window_id);
if (w && w->handle) {
glfwWindowBell(w->handle);
}
Py_RETURN_NONE;
}
// Boilerplate {{{
static PyMethodDef module_methods[] = {
@@ -758,6 +790,7 @@ static PyMethodDef module_methods[] = {
METHODB(glfw_window_hint, METH_VARARGS),
METHODB(os_window_should_close, METH_VARARGS),
METHODB(os_window_swap_buffers, METH_VARARGS),
METHODB(ring_bell, METH_VARARGS),
METHODB(get_primary_selection, METH_NOARGS),
METHODB(x11_display, METH_NOARGS),
METHODB(x11_window_id, METH_O),
@@ -927,6 +960,7 @@ init_glfw(PyObject *m) {
ADDC(GLFW_MOD_CONTROL);
ADDC(GLFW_MOD_ALT);
ADDC(GLFW_MOD_SUPER);
ADDC(GLFW_MOD_KITTY);
// --- Mouse -------------------------------------------------------------------
ADDC(GLFW_MOUSE_BUTTON_1);

View File

@@ -873,9 +873,10 @@ new(PyTypeObject UNUSED *type, PyObject UNUSED *args, PyObject UNUSED *kwds) {
static inline PyObject*
image_as_dict(Image *img) {
#define U(x) #x, img->x
return Py_BuildValue("{sI sI sI sI sI sI sH sH sN}",
U(texture_id), U(client_id), U(width), U(height), U(internal_id), U(refcnt), U(data_loaded),
"is_4byte_aligned", img->load_data.is_4byte_aligned,
return Py_BuildValue("{sI sI sI sI sI sI sO sO sN}",
U(texture_id), U(client_id), U(width), U(height), U(internal_id), U(refcnt),
"data_loaded", img->data_loaded ? Py_True : Py_False,
"is_4byte_aligned", img->load_data.is_4byte_aligned ? Py_True : Py_False,
"data", Py_BuildValue("y#", img->load_data.data, img->load_data.data_sz)
);
#undef U

View File

@@ -10,10 +10,42 @@
#include <structmember.h>
extern PyTypeObject Line_Type;
#define SEGMENT_SIZE 2048
static inline void
add_segment(HistoryBuf *self) {
self->num_segments += 1;
self->segments = PyMem_Realloc(self->segments, sizeof(HistoryBufSegment) * self->num_segments);
if (self->segments == NULL) fatal("Out of memory allocating new history buffer segment");
HistoryBufSegment *s = self->segments + self->num_segments - 1;
s->cells = PyMem_Calloc(self->xnum * SEGMENT_SIZE, sizeof(Cell));
s->line_attrs = PyMem_Calloc(SEGMENT_SIZE, sizeof(line_attrs_type));
if (s->cells == NULL || s->line_attrs == NULL) fatal("Out of memory allocating new history buffer segment");
}
static inline index_type
segment_for(HistoryBuf *self, index_type y) {
index_type seg_num = y / SEGMENT_SIZE;
while (UNLIKELY(seg_num >= self->num_segments && SEGMENT_SIZE * self->num_segments < self->ynum)) add_segment(self);
if (UNLIKELY(seg_num >= self->num_segments)) fatal("Out of bounds access to history buffer line number: %u", y);
return seg_num;
}
#define seg_ptr(which, stride) { \
index_type seg_num = segment_for(self, y); \
y -= seg_num * SEGMENT_SIZE; \
return self->segments[seg_num].which + y * stride; \
}
static inline Cell*
lineptr(HistoryBuf *linebuf, index_type y) {
return linebuf->buf + y * linebuf->xnum;
lineptr(HistoryBuf *self, index_type y) {
seg_ptr(cells, self->xnum);
}
static inline line_attrs_type*
attrptr(HistoryBuf *self, index_type y) {
seg_ptr(line_attrs, 1);
}
static PyObject *
@@ -32,18 +64,12 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
if (self != NULL) {
self->xnum = xnum;
self->ynum = ynum;
self->buf = PyMem_Calloc(xnum * ynum, sizeof(Cell));
self->line_attrs = PyMem_Calloc(ynum, sizeof(line_attrs_type));
self->line = alloc_line();
if (self->buf == NULL || self->line == NULL || self->line_attrs == NULL) {
PyErr_NoMemory();
PyMem_Free(self->buf); Py_CLEAR(self->line); PyMem_Free(self->line_attrs);
Py_CLEAR(self);
} else {
self->line->xnum = xnum;
for(index_type y = 0; y < self->ynum; y++) {
clear_chars_in_line(lineptr(self, y), self->xnum, BLANK_CHAR);
}
self->num_segments = 0;
add_segment(self);
self->line->xnum = xnum;
for(index_type y = 0; y < self->ynum; y++) {
clear_chars_in_line(lineptr(self, y), self->xnum, BLANK_CHAR);
}
}
@@ -53,8 +79,11 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
static void
dealloc(HistoryBuf* self) {
Py_CLEAR(self->line);
PyMem_Free(self->buf);
PyMem_Free(self->line_attrs);
for (size_t i = 0; i < self->num_segments; i++) {
PyMem_Free(self->segments[i].cells);
PyMem_Free(self->segments[i].line_attrs);
}
PyMem_Free(self->segments);
Py_TYPE(self)->tp_free((PyObject*)self);
}
@@ -71,8 +100,8 @@ static inline void
init_line(HistoryBuf *self, index_type num, Line *l) {
// Initialize the line l, setting its pointer to the offsets for the line at index (buffer position) num
l->cells = lineptr(self, num);
l->continued = self->line_attrs[num] & CONTINUED_MASK;
l->has_dirty_text = self->line_attrs[num] & TEXT_DIRTY_MASK ? true : false;
l->continued = *attrptr(self, num) & CONTINUED_MASK;
l->has_dirty_text = *attrptr(self, num) & TEXT_DIRTY_MASK ? true : false;
}
void
@@ -82,12 +111,14 @@ historybuf_init_line(HistoryBuf *self, index_type lnum, Line *l) {
void
historybuf_mark_line_clean(HistoryBuf *self, index_type y) {
self->line_attrs[index_of(self, y)] &= ~TEXT_DIRTY_MASK;
line_attrs_type *p = attrptr(self, index_of(self, y));
*p &= ~TEXT_DIRTY_MASK;
}
void
historybuf_mark_line_dirty(HistoryBuf *self, index_type y) {
self->line_attrs[index_of(self, y)] |= TEXT_DIRTY_MASK;
line_attrs_type *p = attrptr(self, index_of(self, y));
*p |= TEXT_DIRTY_MASK;
}
inline void
@@ -105,43 +136,11 @@ historybuf_push(HistoryBuf *self) {
return idx;
}
bool
historybuf_resize(HistoryBuf *self, index_type lines) {
HistoryBuf t = {{0}};
t.xnum=self->xnum;
t.ynum=lines;
if (t.ynum > 0 && t.ynum != self->ynum) {
t.buf = PyMem_Calloc(t.xnum * t.ynum, sizeof(Cell));
if (t.buf == NULL) { PyErr_NoMemory(); return false; }
t.line_attrs = PyMem_Calloc(t.ynum, sizeof(line_attrs_type));
if (t.line_attrs == NULL) { PyMem_Free(t.buf); PyErr_NoMemory(); return false; }
t.count = MIN(self->count, t.ynum);
for (index_type s=0; s < t.count; s++) {
index_type si = index_of(self, s), ti = index_of(&t, s);
copy_cells(lineptr(self, si), lineptr(&t, ti), t.xnum);
t.line_attrs[ti] = self->line_attrs[si];
}
self->count = t.count;
self->start_of_data = t.start_of_data;
self->ynum = t.ynum;
PyMem_Free(self->buf); PyMem_Free(self->line_attrs);
self->buf = t.buf; self->line_attrs = t.line_attrs;
}
return true;
}
void
historybuf_add_line(HistoryBuf *self, const Line *line) {
index_type idx = historybuf_push(self);
copy_line(line, self->line);
self->line_attrs[idx] = (line->continued & CONTINUED_MASK) | (line->has_dirty_text ? TEXT_DIRTY_MASK : 0);
}
static PyObject*
change_num_of_lines(HistoryBuf *self, PyObject *val) {
#define change_num_of_lines_doc "Change the number of lines in this buffer"
if(!historybuf_resize(self, (index_type)PyLong_AsUnsignedLong(val))) return NULL;
Py_RETURN_NONE;
*attrptr(self, idx) = (line->continued & CONTINUED_MASK) | (line->has_dirty_text ? TEXT_DIRTY_MASK : 0);
}
static PyObject*
@@ -188,7 +187,7 @@ as_ansi(HistoryBuf *self, PyObject *callback) {
for(unsigned int i = 0; i < self->count; i++) {
init_line(self, i, &l);
if (i < self->count - 1) {
l.continued = self->line_attrs[index_of(self, i + 1)] & CONTINUED_MASK;
l.continued = *attrptr(self, index_of(self, i + 1)) & CONTINUED_MASK;
} else l.continued = false;
index_type num = line_as_ansi(&l, t, 5120);
if (!(l.continued) && num < 5119) t[num++] = 10; // 10 = \n
@@ -209,7 +208,7 @@ static PyObject*
as_text(HistoryBuf *self, PyObject *args) {
Line l = {.xnum=self->xnum};
#define gl(self, y) get_line(self, y, &l);
as_text_generic(args, self, gl, self->count, self->xnum, callback, as_ansi);
as_text_generic(args, self, gl, self->count, self->xnum);
#undef gl
}
@@ -219,7 +218,7 @@ dirty_lines(HistoryBuf *self, PyObject *a UNUSED) {
#define dirty_lines_doc "dirty_lines() -> Line numbers of all lines that have dirty text."
PyObject *ans = PyList_New(0);
for (index_type i = 0; i < self->ynum; i++) {
if (self->line_attrs[i] & TEXT_DIRTY_MASK) {
if (*attrptr(self, i) & TEXT_DIRTY_MASK) {
PyList_Append(ans, PyLong_FromUnsignedLong(i));
}
}
@@ -232,7 +231,6 @@ static PyObject* rewrap(HistoryBuf *self, PyObject *args);
#define rewrap_doc ""
static PyMethodDef methods[] = {
METHOD(change_num_of_lines, METH_O)
METHOD(line, METH_O)
METHOD(as_ansi, METH_O)
METHODB(as_text, METH_VARARGS),
@@ -275,19 +273,22 @@ HistoryBuf *alloc_historybuf(unsigned int lines, unsigned int columns) {
#define init_src_line(src_y) init_line(src, map_src_index(src_y), src->line);
#define is_src_line_continued(src_y) (map_src_index(src_y) < src->ynum - 1 ? (src->line_attrs[map_src_index(src_y + 1)] & CONTINUED_MASK) : false)
#define is_src_line_continued(src_y) (map_src_index(src_y) < src->ynum - 1 ? (*attrptr(src, map_src_index(src_y + 1)) & CONTINUED_MASK) : false)
#define next_dest_line(cont) dest->line_attrs[historybuf_push(dest)] = cont & CONTINUED_MASK; dest->line->continued = cont;
#define next_dest_line(cont) *attrptr(dest, historybuf_push(dest)) = cont & CONTINUED_MASK; dest->line->continued = cont;
#define first_dest_line next_dest_line(false);
#include "rewrap.h"
void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other) {
// Fast path
while(other->num_segments < self->num_segments) add_segment(other);
if (other->xnum == self->xnum && other->ynum == self->ynum) {
memcpy(other->buf, self->buf, sizeof(Cell) * self->xnum * self->ynum);
memcpy(other->line_attrs, self->line_attrs, sizeof(line_attrs_type) * self->ynum);
// Fast path
for (index_type i = 0; i < self->num_segments; i++) {
memcpy(other->segments[i].cells, self->segments[i].cells, SEGMENT_SIZE * self->xnum * sizeof(Cell));
memcpy(other->segments[i].line_attrs, self->segments[i].line_attrs, SEGMENT_SIZE * sizeof(line_attrs_type));
}
other->count = self->count; other->start_of_data = self->start_of_data;
return;
}
@@ -295,7 +296,7 @@ void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other) {
index_type x = 0, y = 0;
if (self->count > 0) {
rewrap_inner(self, other, self->count, NULL, &x, &y);
for (index_type i = 0; i < other->count; i++) other->line_attrs[(other->start_of_data + i) % other->ynum] |= TEXT_DIRTY_MASK;
for (index_type i = 0; i < other->count; i++) *attrptr(other, (other->start_of_data + i) % other->ynum) |= TEXT_DIRTY_MASK;
}
}

38
kitty/key_encoding.py generated
View File

@@ -254,6 +254,17 @@ KEY_MAP = {
# END_ENCODING }}}
text_keys = string.ascii_uppercase + string.ascii_lowercase + string.digits + '`~!@#$%^&*()_-+=[{]}\\|<,>./?;:\'" '
def text_match(key):
if key.upper() == 'SPACE':
return ' '
if key not in text_keys:
return
return key
def encode(
integer,
chars=string.ascii_uppercase + string.ascii_lowercase + string.digits +
@@ -312,13 +323,34 @@ type_map = {'p': PRESS, 't': REPEAT, 'r': RELEASE}
mod_map = {c: i for i, c in enumerate('ABCDEFGHIJKLMNOP')}
key_rmap = {}
g = globals()
config_key_map = {}
config_mod_map = {'SHIFT': SHIFT, 'ALT': ALT, 'OPTION': ALT, '': ALT, '': SUPER, 'CMD': SUPER, 'SUPER': SUPER, 'CTRL': CTRL, 'CONTROL': CTRL}
for key_name, enc in ENCODING.items():
key_name = key_name.replace(' ', '_')
g[key_name] = key_name
g[key_name] = config_key_map[key_name] = key_name
key_rmap[enc] = key_name
config_key_map.update({
'`': g['GRAVE_ACCENT'],
'-': g['MINUS'],
'=': g['EQUAL'],
'[': g['LEFT_BRACKET'],
']': g['RIGHT_BRACKET'],
'\\': g['BACKSLASH'],
';': g['SEMICOLON'],
"'": g['APOSTROPHE'],
',': g['COMMA'],
'.': g['PERIOD'],
'/': g['SLASH'],
'ESC': g['ESCAPE'],
})
enter_key = KeyEvent(PRESS, 0, g['ENTER'])
backspace_key = KeyEvent(PRESS, 0, g['BACKSPACE'])
del key_name, enc, g
enter_key = KeyEvent(PRESS, 0, ENCODING['ENTER'])
backspace_key = KeyEvent(PRESS, 0, ENCODING['BACKSPACE'])
def decode_key_event(text):

View File

@@ -43,22 +43,6 @@ active_window() {
return NULL;
}
void
on_text_input(unsigned int codepoint, int mods) {
Window *w = active_window();
static char buf[16];
unsigned int sz = 0;
if (w != NULL) {
bool is_text = mods <= GLFW_MOD_SHIFT;
if (is_text) sz = encode_utf8(codepoint, buf);
#ifdef __APPLE__
if (!OPT(macos_option_as_alt) && IS_ALT_MODS(mods)) sz = encode_utf8(codepoint, buf);
#endif
if (sz) schedule_write_to_child(w->id, buf, sz);
}
}
static inline bool
is_modifier_key(int key) {
switch(key) {
@@ -94,7 +78,7 @@ send_key_to_child(Window *w, int key, int mods, int action) {
static inline bool
is_ascii_control_char(char c) {
return (0 <= c && c <= 31) || c == 127;
return c == 0 || (1 <= c && c <= 31) || c == 127;
}
void
@@ -102,14 +86,14 @@ on_key_input(int key, int scancode, int action, int mods, const char* text, int
Window *w = active_window();
if (!w) return;
if (global_state.in_sequence_mode) {
if (action != GLFW_RELEASE) call_boss(process_sequence, "iiii", key, scancode, action, mods);
if (
action != GLFW_RELEASE &&
key != GLFW_KEY_LEFT_SHIFT && key != GLFW_KEY_RIGHT_SHIFT && key != GLFW_KEY_LEFT_ALT && key != GLFW_KEY_RIGHT_ALT && key != GLFW_KEY_LEFT_CONTROL && key != GLFW_KEY_RIGHT_CONTROL
) call_boss(process_sequence, "iiii", key, scancode, action, mods);
return;
}
Screen *screen = w->render_data.screen;
bool has_text = text && !is_ascii_control_char(text[0]);
#ifdef __APPLE__
if (has_text && IS_ALT_MODS(mods) && OPT(macos_option_as_alt)) has_text = false;
#endif
if (action == GLFW_PRESS || action == GLFW_REPEAT) {
uint16_t qkey = (0 <= key && key < (ssize_t)arraysz(key_map)) ? key_map[key] : UINT8_MAX;
bool special = false;

1744
kitty/keys.h generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,9 @@ def modify_complex_key(name, amt):
return modify_key_bytes(key_as_bytes(name), amt)
control_codes = {}
control_codes = {
defines.GLFW_KEY_BACKSPACE: b'\x08'
}
smkx_key_map = {}
alt_codes = {
defines.GLFW_KEY_TAB: b'\033\t',
@@ -37,6 +39,19 @@ shift_alt_codes[defines.GLFW_KEY_TAB] = key_as_bytes('kcbt')
alt_mods = (defines.GLFW_MOD_ALT, defines.GLFW_MOD_SHIFT | defines.GLFW_MOD_ALT)
ctrl_shift_mod = defines.GLFW_MOD_SHIFT | defines.GLFW_MOD_CONTROL
ctrl_alt_mod = defines.GLFW_MOD_ALT | defines.GLFW_MOD_CONTROL
SHIFTED_KEYS = {
defines.GLFW_KEY_TAB: key_as_bytes('kcbt'),
defines.GLFW_KEY_HOME: key_as_bytes('kHOM'),
defines.GLFW_KEY_END: key_as_bytes('kEND'),
defines.GLFW_KEY_LEFT: key_as_bytes('kLFT'),
defines.GLFW_KEY_RIGHT: key_as_bytes('kRIT'),
defines.GLFW_KEY_UP: key_as_bytes('kri'),
defines.GLFW_KEY_DOWN: key_as_bytes('kind'),
}
control_alt_codes = {
defines.GLFW_KEY_SPACE: b'\x1b\0',
}
for kf, kn in {
defines.GLFW_KEY_UP: 'kcuu1',
@@ -54,13 +69,20 @@ for kf, kn in {
alt_codes[kf] = modify_complex_key(kn, 3)
shift_alt_codes[kf] = modify_complex_key(kn, 4)
control_codes[kf] = modify_complex_key(kn, 5)
control_alt_codes[kf] = modify_complex_key(kn, 7)
for f in range(1, 13):
kf = getattr(defines, 'GLFW_KEY_F{}'.format(f))
kn = 'kf{}'.format(f)
smkx_key_map[kf] = key_as_bytes(kn)
SHIFTED_KEYS[kf] = modify_complex_key(kn, 2)
alt_codes[kf] = modify_complex_key(kn, 3)
shift_alt_codes[kf] = modify_complex_key(kn, 4)
control_codes[kf] = modify_complex_key(kn, 5)
control_alt_codes[kf] = modify_complex_key(kn, 7)
for f in range(13, 26):
kf = getattr(defines, 'GLFW_KEY_F{}'.format(f))
kn = 'kf{}'.format(f)
smkx_key_map[kf] = key_as_bytes(kn)
f = {k: k for k in '0123456789'}
f.update({
'COMMA': ',',
@@ -80,16 +102,6 @@ smkx_key_map[defines.GLFW_KEY_KP_ENTER] = b'\r'
smkx_key_map[defines.GLFW_KEY_BACKSPACE] = key_as_bytes('kbs')
smkx_key_map[defines.GLFW_KEY_TAB] = b'\t'
SHIFTED_KEYS = {
defines.GLFW_KEY_TAB: key_as_bytes('kcbt'),
defines.GLFW_KEY_HOME: key_as_bytes('kHOM'),
defines.GLFW_KEY_END: key_as_bytes('kEND'),
defines.GLFW_KEY_LEFT: key_as_bytes('kLFT'),
defines.GLFW_KEY_RIGHT: key_as_bytes('kRIT'),
defines.GLFW_KEY_UP: key_as_bytes('kri'),
defines.GLFW_KEY_DOWN: key_as_bytes('kind'),
}
control_codes.update({
k: (1 + i, )
for i, k in
@@ -206,6 +218,7 @@ ASCII_C0_SHIFTED = {
}
CTRL_SHIFT_KEYS = {getattr(defines, 'GLFW_KEY_' + k): v for k, v in ASCII_C0_SHIFTED.items()}
CTRL_ALT_KEYS = {getattr(defines, 'GLFW_KEY_' + k) for k in string.ascii_uppercase}
all_control_alt_keys = set(CTRL_ALT_KEYS) | set(control_alt_codes)
def key_to_bytes(key, smkx, extended, mods, action):
@@ -224,8 +237,11 @@ def key_to_bytes(key, smkx, extended, mods, action):
m = UN_SHIFTED_PRINTABLE if mods == defines.GLFW_MOD_ALT else SHIFTED_PRINTABLE
data.append(0o33)
data.extend(m[key])
elif mods == ctrl_alt_mod and key in CTRL_ALT_KEYS:
data.append(0x1b), data.extend(control_codes[key])
elif mods == ctrl_alt_mod and key in all_control_alt_keys:
if key in CTRL_ALT_KEYS:
data.append(0x1b), data.extend(control_codes[key])
else:
data.extend(control_alt_codes[key])
else:
key_map = cursor_key_mode_map[smkx]
x = key_map.get(key)
@@ -256,7 +272,7 @@ def shortcut_matches(s, mods, key, scancode):
def generate_key_table():
# To run this, use: python3 . -c "from kitty.keys import *; generate_key_table()"
# To run this, use: python3 . +runpy "from kitty.keys import *; generate_key_table()"
import os
from functools import partial
f = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'keys.h'), 'w')

43
kitty/khrplatform.h generated
View File

@@ -1,8 +1,7 @@
#ifndef __khrplatform_h_
#define __khrplatform_h_
/*
** Copyright (c) 2008-2009 The Khronos Group Inc.
** Copyright (c) 2008-2018 The Khronos Group Inc.
**
** Permission is hereby granted, free of charge, to any person obtaining a
** copy of this software and/or associated documentation files (the
@@ -23,21 +22,18 @@
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
*/
/* Khronos platform-specific types and definitions.
*
* $Revision: 32517 $ on $Date: 2016-03-11 02:41:19 -0800 (Fri, 11 Mar 2016) $
* The master copy of khrplatform.h is maintained in the Khronos EGL
* Registry repository at https://github.com/KhronosGroup/EGL-Registry
* The last semantic modification to khrplatform.h was at commit ID:
* 67a3e0864c2d75ea5287b9f3d2eb74a745936692
*
* Adopters may modify this file to suit their platform. Adopters are
* encouraged to submit platform specific modifications to the Khronos
* group so that they can be included in future versions of this file.
* Please submit changes by sending them to the public Khronos Bugzilla
* (http://khronos.org/bugzilla) by filing a bug against product
* "Khronos (general)" component "Registry".
*
* A predefined template which fills in some of the bug fields can be
* reached using http://tinyurl.com/khrplatform-h-bugreport, but you
* must create a Bugzilla login first.
* Please submit changes by filing pull requests or issues on
* the EGL Registry repository linked above.
*
*
* See the Implementer's Guidelines for information about where this file
@@ -91,7 +87,6 @@
* int arg1,
* int arg2) KHRONOS_APIATTRIBUTES;
*/
/*-------------------------------------------------------------------------
* Definition of KHRONOS_APICALL
*-------------------------------------------------------------------------
@@ -106,7 +101,6 @@
#else
# define KHRONOS_APICALL
#endif
/*-------------------------------------------------------------------------
* Definition of KHRONOS_APIENTRY
*-------------------------------------------------------------------------
@@ -119,7 +113,6 @@
#else
# define KHRONOS_APIENTRY
#endif
/*-------------------------------------------------------------------------
* Definition of KHRONOS_APIATTRIBUTES
*-------------------------------------------------------------------------
@@ -130,13 +123,10 @@
#else
#define KHRONOS_APIATTRIBUTES
#endif
/*-------------------------------------------------------------------------
* basic type definitions
*-----------------------------------------------------------------------*/
#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__GNUC__) || defined(__SCO__) || defined(__USLC__)
/*
* Using <stdint.h>
*/
@@ -147,9 +137,7 @@ typedef int64_t khronos_int64_t;
typedef uint64_t khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif defined(__VMS ) || defined(__sgi)
/*
* Using <inttypes.h>
*/
@@ -160,9 +148,7 @@ typedef int64_t khronos_int64_t;
typedef uint64_t khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif defined(_WIN32) && !defined(__SCITECH_SNAP__)
/*
* Win32
*/
@@ -172,9 +158,7 @@ typedef __int64 khronos_int64_t;
typedef unsigned __int64 khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif defined(__sun__) || defined(__digital__)
/*
* Sun or Digital
*/
@@ -189,9 +173,7 @@ typedef unsigned long long int khronos_uint64_t;
#endif /* __arch64__ */
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif 0
/*
* Hypothetical platform with no float or int64 support
*/
@@ -199,9 +181,7 @@ typedef int khronos_int32_t;
typedef unsigned int khronos_uint32_t;
#define KHRONOS_SUPPORT_INT64 0
#define KHRONOS_SUPPORT_FLOAT 0
#else
/*
* Generic fallback
*/
@@ -212,10 +192,7 @@ typedef int64_t khronos_int64_t;
typedef uint64_t khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#endif
/*
* Types that are (so far) the same on all platforms
*/
@@ -223,7 +200,6 @@ typedef signed char khronos_int8_t;
typedef unsigned char khronos_uint8_t;
typedef signed short int khronos_int16_t;
typedef unsigned short int khronos_uint16_t;
/*
* Types that differ between LLP64 and LP64 architectures - in LLP64,
* pointers are 64 bits, but 'long' is still 32 bits. Win64 appears
@@ -240,14 +216,12 @@ typedef unsigned long int khronos_uintptr_t;
typedef signed long int khronos_ssize_t;
typedef unsigned long int khronos_usize_t;
#endif
#if KHRONOS_SUPPORT_FLOAT
/*
* Float type
*/
typedef float khronos_float_t;
#endif
#if KHRONOS_SUPPORT_INT64
/* Time types
*
@@ -261,14 +235,12 @@ typedef float khronos_float_t;
typedef khronos_uint64_t khronos_utime_nanoseconds_t;
typedef khronos_int64_t khronos_stime_nanoseconds_t;
#endif
/*
* Dummy value used to pad enum types to 32 bits.
*/
#ifndef KHRONOS_MAX_ENUM
#define KHRONOS_MAX_ENUM 0x7FFFFFFF
#endif
/*
* Enumerated boolean type
*
@@ -280,5 +252,4 @@ typedef enum {
KHRONOS_TRUE = 1,
KHRONOS_BOOLEAN_ENUM_FORCE_SIZE = KHRONOS_MAX_ENUM
} khronos_boolean_enum_t;
#endif /* __khrplatform_h_ */

View File

@@ -2,7 +2,9 @@
# You can include secondary config files via the "include" directive.
# If you use a relative path for include, it is resolved with respect to the
# location od the current config file. For example:
# location of the current config file. Note that environment variables
# are expanded, so ${USER}.conf becomes name.conf if USER=name
# For example:
# include other.conf
# Fonts {{{
@@ -101,7 +103,7 @@ url_color #0087BD
url_style curly
# The modifier keys to press when clicking with the mouse on URLs to open the URL
open_url_modifiers ctrl+shift
open_url_modifiers kitty_mod
# The program with which to open URLs that are clicked on. The special value "default" means to
# use the operating system's default URL handler.
@@ -166,6 +168,14 @@ visual_bell_duration 0.0
# Enable/disable the audio bell. Useful in environments that require silence.
enable_audio_bell yes
# Request window attention on bell.
# Makes the dock icon bounce on macOS or the taskbar flash on linux.
window_alert_on_bell yes
# Show a bell symbol on the tab if a bell occurs in one of the windows in the
# tab and the window is not the currently focused window
bell_on_tab yes
# }}}
@@ -182,6 +192,12 @@ initial_window_height 400
# For a list of available layouts, see the README.
enabled_layouts *
# The step size (in units of cell width/cell height) to use when resizing
# windows. The cells value is used for horizontal resizing and the lines value
# for vertical resizing.
window_resize_step_cells 2
window_resize_step_lines 2
# The width (in pts) of window borders. Will be rounded to the nearest number of pixels based on screen resolution.
# Note that borders are displayed only when more than one window is visible. They are meant to separate multiple windows.
window_border_width 1
@@ -198,6 +214,9 @@ active_border_color #00ff00
# The color for the border of inactive windows
inactive_border_color #cccccc
# The color for the border of inactive windows in which a bell has occurred
bell_border_color #ff5a00
# Fade the text in inactive windows by the specified amount (a number between
# zero and one, with 0 being fully faded).
inactive_text_alpha 1.0
@@ -239,6 +258,13 @@ background #000000
# default colors in a shell script to launch your editor.
# Be aware that using a value less than 1.0 is a (possibly significant) performance hit.
background_opacity 1.0
# If you want to dynamically change transparency of windows set dynamic_background_opacity to yes
# (this is off by default as it has a performance cost)
dynamic_background_opacity no
# How much to dim text that has the DIM/FAINT attribute set. 1.0 means no dimming and
# 0.0 means fully dimmed (i.e. invisible).
dim_opacity 0.75
# The foreground for selections
selection_foreground #000000
@@ -307,6 +333,14 @@ close_on_child_death no
# Note that this even works over ssh connections.
allow_remote_control no
# 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
# The default is to allow writing to the clipboard and primary selection. Note
# that enabling the read functionality is a security risk as it means that any
# program, even one running on a remote server via SSH can read your clipboard.
clipboard_control write-clipboard write-primary
# The value of the TERM environment variable to set. Changing this can break
# many terminal programs, only change it if you know what you are doing, not
# because you read some advice on Stack Overflow to change it.
@@ -319,6 +353,11 @@ term xterm-kitty
# Keyboard shortcuts {{{
# For a list of key names, see: http://www.glfw.org/docs/latest/group__keys.html
# For a list of modifier names, see: http://www.glfw.org/docs/latest/group__mods.html
# The value of kitty_mod is used as the modifier for all default shortcuts, you
# can change it in your kitty.conf to change the modifiers for all the default
# shortcuts.
kitty_mod ctrl+shift
#
# You can use the special action no_op to unmap a keyboard shortcut that is
# assigned in the default configuration.
@@ -327,7 +366,7 @@ term xterm-kitty
# syntax below:
# map key combine <separator> action1 <separator> action2 <separator> action3 ...
# For example:
# map ctrl+shift+e combine : new_window : next_layout
# map kitty_mod+e combine : new_window : next_layout
# this will create a new window and switch to the next available layout
#
# You can use multi-key shortcuts using the syntax shown below:
@@ -337,59 +376,61 @@ term xterm-kitty
# this will change the font size to 20 points when you press ctrl+f and then 2
# Clipboard {{{
map ctrl+shift+v paste_from_clipboard
map ctrl+shift+s paste_from_selection
map ctrl+shift+c copy_to_clipboard
map shift+insert paste_from_selection
map kitty_mod+v paste_from_clipboard
map kitty_mod+s paste_from_selection
map kitty_mod+c copy_to_clipboard
map shift+insert paste_from_selection
# You can also pass the contents of the current selection to any program using
# pass_selection_to_program. By default, the system's open program is used, but
# you can specify your own, for example:
# map ctrl+shift+o pass_selection_to_program firefox
map ctrl+shift+o pass_selection_to_program
# map kitty_mod+o pass_selection_to_program firefox
map kitty_mod+o pass_selection_to_program
# }}}
# Scrolling {{{
map ctrl+shift+up scroll_line_up
map ctrl+shift+down scroll_line_down
map ctrl+shift+k scroll_line_up
map ctrl+shift+j scroll_line_down
map ctrl+shift+page_up scroll_page_up
map ctrl+shift+page_down scroll_page_down
map ctrl+shift+home scroll_home
map ctrl+shift+end scroll_end
map ctrl+shift+h show_scrollback
map kitty_mod+up scroll_line_up
map kitty_mod+down scroll_line_down
map kitty_mod+k scroll_line_up
map kitty_mod+j scroll_line_down
map kitty_mod+page_up scroll_page_up
map kitty_mod+page_down scroll_page_down
map kitty_mod+home scroll_home
map kitty_mod+end scroll_end
map kitty_mod+h show_scrollback
# }}}
# Window management {{{
map ctrl+shift+enter new_window
map ctrl+shift+n new_os_window
map ctrl+shift+w close_window
map ctrl+shift+] next_window
map ctrl+shift+[ previous_window
map ctrl+shift+f move_window_forward
map ctrl+shift+b move_window_backward
map ctrl+shift+` move_window_to_top
map ctrl+shift+1 first_window
map ctrl+shift+2 second_window
map ctrl+shift+3 third_window
map ctrl+shift+4 fourth_window
map ctrl+shift+5 fifth_window
map ctrl+shift+6 sixth_window
map ctrl+shift+7 seventh_window
map ctrl+shift+8 eighth_window
map ctrl+shift+9 ninth_window
map ctrl+shift+0 tenth_window
map kitty_mod+enter new_window
map kitty_mod+n new_os_window
map kitty_mod+w close_window
map kitty_mod+] next_window
map kitty_mod+[ previous_window
map kitty_mod+f move_window_forward
map kitty_mod+b move_window_backward
map kitty_mod+` move_window_to_top
map kitty_mod+r start_resizing_window
# Switching to a particular window
map kitty_mod+1 first_window
map kitty_mod+2 second_window
map kitty_mod+3 third_window
map kitty_mod+4 fourth_window
map kitty_mod+5 fifth_window
map kitty_mod+6 sixth_window
map kitty_mod+7 seventh_window
map kitty_mod+8 eighth_window
map kitty_mod+9 ninth_window
map kitty_mod+0 tenth_window
# You can open a new window running an arbitrary program, for example:
# map ctrl+shift+y new_window mutt
# map kitty_mod+y new_window mutt
#
# You can pass the current selection to the new program by using the @selection placeholder
# map ctrl+shift+y new_window less @selection
# map kitty_mod+y new_window less @selection
#
# You can even send the contents of the current screen + history buffer as stdin using
# the placeholders @text (which is the plain text) and @ansi (which includes text styling escape codes).
# For only the current screen, use @screen or @ansi_screen.
# For example, the following command opens the scrollback buffer in less in a new window.
# map ctrl+shift+y new_window @ansi less +G -R
# map kitty_mod+y new_window @ansi less +G -R
#
# You can open a new window with the current working directory set to the
# working directory of the current window using
@@ -397,14 +438,14 @@ map ctrl+shift+0 tenth_window
# }}}
# Tab management {{{
map ctrl+shift+right next_tab
map ctrl+shift+left previous_tab
map ctrl+shift+t new_tab
map ctrl+shift+q close_tab
map ctrl+shift+l next_layout
map ctrl+shift+. move_tab_forward
map ctrl+shift+, move_tab_backward
map ctrl+shift+alt+t set_tab_title
map kitty_mod+right next_tab
map kitty_mod+left previous_tab
map kitty_mod+t new_tab
map kitty_mod+q close_tab
map kitty_mod+l next_layout
map kitty_mod+. move_tab_forward
map kitty_mod+, move_tab_backward
map kitty_mod+alt+t set_tab_title
# You can also create shortcuts to go to specific tabs, with 1 being the first tab
# map ctrl+alt+1 goto_tab 1
# map ctrl+alt+2 goto_tab 2
@@ -420,12 +461,12 @@ map ctrl+shift+alt+t set_tab_title
# }}}
# Font sizes {{{
map ctrl+shift+equal increase_font_size
map ctrl+shift+minus decrease_font_size
map ctrl+shift+backspace restore_font_size
map kitty_mod+equal increase_font_size
map kitty_mod+minus decrease_font_size
map kitty_mod+backspace restore_font_size
# To setup shortcuts for specific font sizes, follow the example below:
# map ctrl+shift+f6 set_font_size 10.0
# map ctrl+shift+f7 set_font_size 20.5
# map kitty_mod+f6 set_font_size 10.0
# map kitty_mod+f7 set_font_size 20.5
# }}}
# Select and act on visible text {{{
@@ -434,33 +475,36 @@ map ctrl+shift+backspace restore_font_size
#
# Open a currently visible URL using the keyboard. The program used to open the
# URL is specified in open_url_with.
map ctrl+shift+e run_kitten text hints
map kitty_mod+e kitten hints
# Select a path/filename and insert it into the terminal. Useful, for instance to
# run git commands on a filename output from a previous git command.
map ctrl+shift+p>f run_kitten text hints --type path --program -
map kitty_mod+p>f kitten hints --type path --program -
# Select a path/filename and open it with the default open program.
map ctrl+shift+p>shift+f run_kitten text hints --type path
map kitty_mod+p>shift+f kitten hints --type path
# Select a line of text and insert it into the terminal. Use for the
# output of things like: ls -1
map ctrl+shift+p>l run_kitten text hints --type line --program -
map kitty_mod+p>l kitten hints --type line --program -
# Select words and insert into terminal.
map ctrl+shift+p>w run_kitten text hints --type word --program -
map kitty_mod+p>w kitten hints --type word --program -
# The hints kitten has many more modes of operation that you can map to different
# shortcuts. For a full description run: kitty +kitten hints --help
# }}}
# Miscellaneous {{{
map ctrl+shift+f11 toggle_fullscreen
map ctrl+shift+u input_unicode_character
map ctrl+shift+f2 edit_config_file
You can customize how the URLs are
map kitty_mod+f11 toggle_fullscreen
map kitty_mod+u input_unicode_character
map kitty_mod+f2 edit_config_file
# Open the kitty shell in a new window/tab/overlay/os_window to control kitty using commands.
map ctrl+shift+escape kitty_shell window
map kitty_mod+escape kitty_shell window
map kitty_mod+a>m set_background_opacity +0.1
map kitty_mod+a>l set_background_opacity -0.1
map kitty_mod+a>1 set_background_opacity 1
map kitty_mod+a>d set_background_opacity default
# Sending arbitrary text on shortcut key presses
# You can tell kitty to send arbitrary (UTF-8) encoded text to
@@ -480,6 +524,9 @@ map ctrl+shift+escape kitty_shell window
# map ctrl+alt+a send_text application Word\x1bOH
# }}}
# You can have kitty remove all shortcut definition seen up to this point. Useful, for
# instance, to remove the default shortcuts.
clear_all_shortcuts no
# }}}
@@ -487,8 +534,12 @@ map ctrl+shift+escape kitty_shell window
# Change the color of the kitty window's titlebar on macOS. A value of "system"
# means to use the default system color, a value of "background" means to use
# the default background color and finally you can use an arbitrary color, such
# as #12af59 or "red".
# the background color of the currently active window and finally you can use
# an arbitrary color, such as #12af59 or "red". WARNING: This option works by
# using a hack, as there is no proper Cocoa API for it. It sets the background
# color of the entire window and makes the titlebar transparent. As such it is
# incompatible with background_opacity. If you want to use both, you are
# probably better off just hiding the titlebar with macos_hide_titlebar.
macos_titlebar_color system
# Hide the kitty window's title bar on macOS.
@@ -499,4 +550,7 @@ macos_hide_titlebar no
# break any Alt+key keyboard shortcuts in your terminal programs, but you
# can use the macOS unicode input technique.
macos_option_as_alt yes
# Hide the kitty window from running tasks (alt-tab) on macOS.
macos_hide_from_tasks no
# }}}

View File

@@ -4,13 +4,14 @@
from collections import namedtuple
from functools import partial
from itertools import islice
from itertools import islice, repeat
from .constants import WindowGeometry
from .fast_data_types import (
Region, pt_to_px, set_active_window, swap_windows, viewport_for_window
Region, set_active_window, swap_windows, viewport_for_window
)
# Utils {{{
central = Region((0, 0, 199, 199, 200, 200))
cell_width = cell_height = 20
@@ -21,7 +22,7 @@ def idx_for_id(win_id, windows):
return i
def layout_dimension(start_at, length, cell_length, number_of_windows=1, border_length=0, margin_length=0, padding_length=0, left_align=False):
def layout_dimension(start_at, length, cell_length, number_of_windows=1, border_length=0, margin_length=0, padding_length=0, left_align=False, bias=None):
number_of_cells = length // cell_length
border_length += padding_length
space_needed_for_border = number_of_windows * 2 * border_length
@@ -37,8 +38,30 @@ def layout_dimension(start_at, length, cell_length, number_of_windows=1, border_
if not left_align:
pos += extra // 2
pos += border_length + margin_length
inner_length = cells_per_window * cell_length
window_length = 2 * (border_length + margin_length) + inner_length
def calc_window_length(cells_in_window):
inner_length = cells_in_window * cell_length
return 2 * (border_length + margin_length) + inner_length
if bias is not None and number_of_windows > 1 and len(bias) == number_of_windows and cells_per_window > 5:
cells_map = [int(b * number_of_cells) for b in bias]
while min(cells_map) < 5:
maxi, mini = map(cells_map.index, (max(cells_map), min(cells_map)))
if maxi == mini:
break
cells_map[mini] += 1
cells_map[maxi] -= 1
extra = number_of_cells - sum(cells_map)
if extra:
cells_map[-1] += extra
for cells_per_window in cells_map:
window_length = calc_window_length(cells_per_window)
yield pos, cells_per_window
pos += window_length
return
window_length = calc_window_length(cells_per_window)
extra = number_of_cells - (cells_per_window * number_of_windows)
while number_of_windows > 0:
number_of_windows -= 1
@@ -56,28 +79,140 @@ def process_overlaid_windows(all_windows):
return overlaid_windows, windows
class Layout:
def window_geometry(xstart, xnum, ystart, ynum):
return WindowGeometry(left=xstart, top=ystart, xnum=xnum, ynum=ynum, right=xstart + cell_width * xnum, bottom=ystart + cell_height * ynum)
def layout_single_window(margin_length, padding_length):
xstart, xnum = next(layout_dimension(central.left, central.width, cell_width, margin_length=margin_length, padding_length=padding_length))
ystart, ynum = next(layout_dimension(central.top, central.height, cell_height, margin_length=margin_length, padding_length=padding_length))
return window_geometry(xstart, xnum, ystart, ynum)
def left_blank_rect(w, rects):
lt = w.geometry.left
if lt > central.left:
rects.append(Rect(central.left, central.top, lt, central.bottom + 1))
def right_blank_rect(w, rects):
r = w.geometry.right
if r < central.right:
rects.append(Rect(r, central.top, central.right + 1, central.bottom + 1))
def top_blank_rect(w, rects):
t = w.geometry.top
if t > central.top:
rects.append(Rect(central.left, central.top, central.right + 1, t))
def bottom_blank_rect(w, rects):
b = w.geometry.bottom
# Need to use <= here as otherwise a single pixel row at the bottom of the
# window is sometimes not covered. See https://github.com/kovidgoyal/kitty/issues/506
if b <= central.bottom:
rects.append(Rect(central.left, b, central.right + 1, central.bottom + 1))
def blank_rects_for_window(w):
ans = []
left_blank_rect(w, ans), top_blank_rect(w, ans), right_blank_rect(w, ans), bottom_blank_rect(w, ans)
return ans
def safe_increment_bias(old_val, increment):
return max(0.1, min(old_val + increment, 0.9))
def normalize_biases(biases):
s = sum(biases)
if s == 1:
return biases
return [x/s for x in biases]
def distribute_indexed_bias(base_bias, index_bias_map):
if not index_bias_map:
return base_bias
ans = list(base_bias)
limit = len(ans)
for row, increment in index_bias_map.items():
if row >= limit or not increment:
continue
other_increment = -increment / (limit - 1)
ans = [safe_increment_bias(b, increment if i == row else other_increment) for i, b in enumerate(ans)]
return normalize_biases(ans)
def variable_bias(num_windows, candidate):
return distribute_indexed_bias(list(repeat(1/(num_windows), num_windows)), candidate)
# }}}
class Layout: # {{{
name = None
needs_window_borders = True
only_active_window_visible = False
def __init__(self, os_window_id, tab_id, opts, border_width):
def __init__(self, os_window_id, tab_id, margin_width, padding_width, border_width, layout_opts=''):
self.os_window_id = os_window_id
self.tab_id = tab_id
self.set_active_window_in_os_window = partial(set_active_window, os_window_id, tab_id)
self.swap_windows_in_os_window = partial(swap_windows, os_window_id, tab_id)
self.opts = opts
self.border_width = border_width
self.margin_width = pt_to_px(opts.window_margin_width)
self.padding_width = pt_to_px(opts.window_padding_width)
self.margin_width = margin_width
self.padding_width = padding_width
# A set of rectangles corresponding to the blank spaces at the edges of
# this layout, i.e. spaces that are not covered by any window
self.blank_rects = ()
self.blank_rects = []
self.layout_opts = self.parse_layout_opts(layout_opts)
self.full_name = self.name + ((':' + layout_opts) if layout_opts else '')
self.remove_all_biases()
def nth_window(self, all_windows, num):
def bias_increment_for_cell(self, is_horizontal):
self._set_dimensions()
if is_horizontal:
return (cell_width + 1) / central.width
return (cell_height + 1) / central.height
def apply_bias(self, idx, increment_as_percent, num_windows, is_horizontal):
return False
def remove_all_biases(self):
return False
def modify_size_of_window(self, all_windows, window_id, increment, is_horizontal=True):
idx = idx_for_id(window_id, all_windows)
if idx is None:
return False
w = all_windows[idx]
windows = process_overlaid_windows(all_windows)[1]
idx = idx_for_id(w.id, windows)
if idx is None:
idx = idx_for_id(w.overlay_window_id, windows)
if idx is not None:
return self.apply_bias(idx, increment, len(windows), is_horizontal)
return False
def parse_layout_opts(self, layout_opts):
if not layout_opts:
return {}
ans = {}
for x in layout_opts.split(';'):
k, v = x.partition('=')[::2]
if k and v:
ans[k] = v
return ans
def nth_window(self, all_windows, num, make_active=True):
windows = process_overlaid_windows(all_windows)[1]
w = windows[min(num, len(windows) - 1)]
if not make_active:
return w
active_window_idx = idx_for_id(w.id, all_windows)
return self.set_active_window(all_windows, active_window_idx)
@@ -178,10 +313,12 @@ class Layout:
self.set_active_window_in_os_window(active_window_idx)
return active_window_idx
def __call__(self, all_windows, active_window_idx):
def _set_dimensions(self):
global central, cell_width, cell_height
central, tab_bar, vw, vh, cell_width, cell_height = viewport_for_window(self.os_window_id)
def __call__(self, all_windows, active_window_idx):
self._set_dimensions()
active_window = all_windows[active_window_idx]
overlaid_windows, windows = process_overlaid_windows(all_windows)
if overlaid_windows:
@@ -193,19 +330,25 @@ class Layout:
else:
windows = all_windows
self.update_visibility(all_windows, active_window, overlaid_windows)
self.blank_rects = []
self.do_layout(windows, active_window_idx)
return idx_for_id(active_window.id, all_windows)
# Utils {{{
def xlayout(self, num):
def layout_single_window(self, w):
wg = layout_single_window(self.margin_width, self.padding_width)
w.set_geometry(0, wg)
self.blank_rects = blank_rects_for_window(w)
def xlayout(self, num, bias=None):
return layout_dimension(
central.left, central.width, cell_width, num, self.border_width,
margin_length=self.margin_width, padding_length=self.padding_width)
margin_length=self.margin_width, padding_length=self.padding_width, bias=bias)
def ylayout(self, num, left_align=True):
def ylayout(self, num, left_align=True, bias=None):
return layout_dimension(
central.top, central.height, cell_height, num, self.border_width, left_align=left_align,
margin_length=self.margin_width, padding_length=self.padding_width)
margin_length=self.margin_width, padding_length=self.padding_width, bias=bias)
def simple_blank_rects(self, first_window, last_window):
br = self.blank_rects
@@ -220,80 +363,81 @@ class Layout:
def do_layout(self, windows, active_window_idx):
raise NotImplementedError()
# }}}
def window_geometry(xstart, xnum, ystart, ynum):
return WindowGeometry(left=xstart, top=ystart, xnum=xnum, ynum=ynum, right=xstart + cell_width * xnum, bottom=ystart + cell_height * ynum)
def layout_single_window(margin_length, padding_length):
xstart, xnum = next(layout_dimension(central.left, central.width, cell_width, margin_length=margin_length, padding_length=padding_length))
ystart, ynum = next(layout_dimension(central.top, central.height, cell_height, margin_length=margin_length, padding_length=padding_length))
return window_geometry(xstart, xnum, ystart, ynum)
def left_blank_rect(w, rects):
lt = w.geometry.left
if lt > central.left:
rects.append(Rect(central.left, central.top, lt, central.bottom + 1))
def right_blank_rect(w, rects):
r = w.geometry.right
if r < central.right:
rects.append(Rect(r, central.top, central.right + 1, central.bottom + 1))
def top_blank_rect(w, rects):
t = w.geometry.top
if t > central.top:
rects.append(Rect(central.left, central.top, central.right + 1, t))
def bottom_blank_rect(w, rects):
b = w.geometry.bottom
if b < central.bottom:
rects.append(Rect(central.left, b, central.right + 1, central.bottom + 1))
def blank_rects_for_window(w):
ans = []
left_blank_rect(w, ans), top_blank_rect(w, ans), right_blank_rect(w, ans), bottom_blank_rect(w, ans)
return ans
class Stack(Layout):
class Stack(Layout): # {{{
name = 'stack'
needs_window_borders = False
only_active_window_visible = True
def do_layout(self, windows, active_window_idx):
self.blank_rects = []
wg = layout_single_window(self.margin_width, self.padding_width)
for i, w in enumerate(windows):
w.set_geometry(i, wg)
if w.is_visible_in_layout:
self.blank_rects = blank_rects_for_window(w)
# }}}
class Tall(Layout):
class Tall(Layout): # {{{
name = 'tall'
vlayout = Layout.ylayout
main_is_horizontal = True
def remove_all_biases(self):
self.main_bias = list(self.layout_opts['bias'])
self.biased_map = {}
return True
def variable_layout(self, num_windows, biased_map):
num_windows -= 1
return self.vlayout(num_windows, bias=variable_bias(num_windows, biased_map) if num_windows > 1 else None)
def apply_bias(self, idx, increment, num_windows, is_horizontal):
if self.main_is_horizontal == is_horizontal:
before = self.main_bias
if idx == 0:
self.main_bias = [safe_increment_bias(self.main_bias[0], increment), safe_increment_bias(self.main_bias[1], -increment)]
else:
self.main_bias = [safe_increment_bias(self.main_bias[0], -increment), safe_increment_bias(self.main_bias[1], increment)]
self.main_bias = normalize_biases(self.main_bias)
after = self.main_bias
else:
if idx == 0 or num_windows < 3:
return False
idx -= 1
before_layout = list(self.variable_layout(num_windows, self.biased_map))
candidate = self.biased_map.copy()
before = candidate.get(idx, 0)
candidate[idx] = after = before + increment
if before_layout == list(self.variable_layout(num_windows, candidate)):
return False
self.biased_map = candidate
return before != after
def parse_layout_opts(self, layout_opts):
ans = Layout.parse_layout_opts(self, layout_opts)
try:
ans['bias'] = int(ans.get('bias', 50)) / 100
except Exception:
ans['bias'] = 0.5
ans['bias'] = max(0.1, min(ans['bias'], 0.9))
ans['bias'] = ans['bias'], 1.0 - ans['bias']
return ans
def do_layout(self, windows, active_window_idx):
self.blank_rects = []
if len(windows) == 1:
wg = layout_single_window(self.margin_width, self.padding_width)
windows[0].set_geometry(0, wg)
self.blank_rects = blank_rects_for_window(windows[0])
return
xlayout = self.xlayout(2)
return self.layout_single_window(windows[0])
xlayout = self.xlayout(2, bias=self.main_bias)
xstart, xnum = next(xlayout)
ystart, ynum = next(self.ylayout(1))
ystart, ynum = next(self.vlayout(1))
windows[0].set_geometry(0, window_geometry(xstart, xnum, ystart, ynum))
xstart, xnum = next(xlayout)
ylayout = self.ylayout(len(windows) - 1)
ylayout = self.variable_layout(len(windows), self.biased_map)
for i, (w, (ystart, ynum)) in enumerate(zip(islice(windows, 1, None), ylayout)):
w.set_geometry(i + 1, window_geometry(xstart, xnum, ystart, ynum))
# right bottom blank rect
@@ -305,19 +449,98 @@ class Tall(Layout):
self.between_blank_rect(windows[0], windows[1])
# left bottom blank rect
self.bottom_blank_rect(windows[0])
# }}}
class Grid(Tall):
class Fat(Tall): # {{{
name = 'fat'
vlayout = Layout.xlayout
main_is_horizontal = False
def do_layout(self, windows, active_window_idx):
if len(windows) == 1:
return self.layout_single_window(windows[0])
xstart, xnum = next(self.xlayout(1))
ylayout = self.ylayout(2, bias=self.main_bias)
ystart, ynum = next(ylayout)
windows[0].set_geometry(0, window_geometry(xstart, xnum, ystart, ynum))
xlayout = self.variable_layout(len(windows), self.biased_map)
ystart, ynum = next(ylayout)
for i, (w, (xstart, xnum)) in enumerate(zip(islice(windows, 1, None), xlayout)):
w.set_geometry(i + 1, window_geometry(xstart, xnum, ystart, ynum))
if i > 0:
# bottom between blank rect
self.between_blank_rect(windows[i - 1], windows[i])
# left, top and right blank rects
self.simple_blank_rects(windows[0], windows[-1])
# top bottom blank rect
self.bottom_blank_rect(windows[0])
# bottom blank rect
self.blank_rects.append(Rect(windows[0].geometry.left, windows[0].geometry.bottom, windows[-1].geometry.right, central.bottom + 1))
# }}}
class Grid(Layout): # {{{
name = 'grid'
def do_layout(self, windows, active_window_idx):
n = len(windows)
if n < 4:
return Tall.do_layout(self, windows, active_window_idx)
self.blank_rects = []
def remove_all_biases(self):
self.biased_rows = {}
self.biased_cols = {}
return True
def variable_layout(self, layout_func, num_windows, biased_map):
return layout_func(num_windows, bias=variable_bias(num_windows, biased_map) if num_windows > 1 else None)
def apply_bias(self, idx, increment, num_windows, is_horizontal):
b = self.biased_cols if is_horizontal else self.biased_rows
ncols, nrows, special_rows, special_col = self.calc_grid_size(num_windows)
def position_for_window_idx(idx):
row_num = col_num = 0
def on_col_done(col_windows):
nonlocal col_num, row_num
row_num = 0
col_num += 1
for window_idx, xstart, xnum, ystart, ynum in self.layout_windows(
num_windows, nrows, ncols, special_rows, special_col, on_col_done):
if idx == window_idx:
return row_num, col_num
row_num += 1
row_num, col_num = position_for_window_idx(idx)
if is_horizontal:
b = self.biased_cols
if ncols < 2:
return False
bias_idx = col_num
layout_func = self.xlayout
attr = 'biased_cols'
else:
b = self.biased_rows
if max(nrows, special_rows) < 2:
return False
bias_idx = row_num
layout_func = self.ylayout
attr = 'biased_rows'
before_layout = list(self.variable_layout(layout_func, num_windows, b))
candidate = b.copy()
before = candidate.get(bias_idx, 0)
candidate[bias_idx] = before + increment
if before_layout == list(self.variable_layout(layout_func, num_windows, candidate)):
return False
setattr(self, attr, candidate)
return True
def calc_grid_size(self, n):
if n <= 5:
ncols = 2
ncols = 1 if n == 1 else 2
else:
for ncols in range(3, (n // 2) + 1):
if ncols * ncols >= n:
@@ -325,27 +548,44 @@ class Grid(Tall):
nrows = n // ncols
special_rows = n - (nrows * (ncols - 1))
special_col = 0 if special_rows < nrows else ncols - 1
return ncols, nrows, special_rows, special_col
def layout_windows(self, num_windows, nrows, ncols, special_rows, special_col, on_col_done=lambda col_windows: None):
# Distribute windows top-to-bottom, left-to-right (i.e. in columns)
xlayout = self.xlayout(ncols)
yvals_normal = tuple(self.ylayout(nrows))
yvals_special = yvals_normal if special_rows == nrows else tuple(self.ylayout(special_rows))
winmap = list(enumerate(windows))
xlayout = self.variable_layout(self.xlayout, ncols, self.biased_cols)
yvals_normal = tuple(self.variable_layout(self.ylayout, nrows, self.biased_rows))
yvals_special = yvals_normal if special_rows == nrows else tuple(self.variable_layout(self.ylayout, special_rows, self.biased_rows))
pos = 0
win_col_map = []
for col in range(ncols):
rows = special_rows if col == special_col else nrows
yl = yvals_special if col == special_col else yvals_normal
xstart, xnum = next(xlayout)
col_windows = []
for (ystart, ynum), (window_idx, w) in zip(yl, winmap[pos:pos + rows]):
w.set_geometry(window_idx, window_geometry(xstart, xnum, ystart, ynum))
col_windows.append(w)
# bottom blank rect
self.bottom_blank_rect(w)
for i, (ystart, ynum) in enumerate(yl):
window_idx = pos + i
yield window_idx, xstart, xnum, ystart, ynum
col_windows.append(window_idx)
pos += rows
on_col_done(col_windows)
def do_layout(self, windows, active_window_idx):
n = len(windows)
if n == 1:
return self.layout_single_window(windows[0])
ncols, nrows, special_rows, special_col = self.calc_grid_size(n)
win_col_map = []
def on_col_done(col_windows):
col_windows = [windows[i] for i in col_windows]
win_col_map.append(col_windows)
# bottom blank rect
self.bottom_blank_rect(col_windows[-1])
for window_idx, xstart, xnum, ystart, ynum in self.layout_windows(
len(windows), nrows, ncols, special_rows, special_col, on_col_done):
w = windows[window_idx]
w.set_geometry(window_idx, window_geometry(xstart, xnum, ystart, ynum))
# left, top and right blank rects
self.simple_blank_rects(windows[0], windows[-1])
@@ -358,23 +598,40 @@ class Grid(Tall):
class Vertical(Layout):
name = 'vertical'
main_is_horizontal = False
vlayout = Layout.ylayout
def variable_layout(self, num_windows, biased_map):
return self.vlayout(num_windows, bias=variable_bias(num_windows, biased_map) if num_windows else None)
def remove_all_biases(self):
self.biased_map = {}
return True
def apply_bias(self, idx, increment, num_windows, is_horizontal):
if self.main_is_horizontal != is_horizontal:
return False
if num_windows < 2:
return False
before_layout = list(self.variable_layout(num_windows, self.biased_map))
candidate = self.biased_map.copy()
before = candidate.get(idx, 0)
candidate[idx] = before + increment
if before_layout == list(self.variable_layout(num_windows, candidate)):
return False
self.biased_map = candidate
return True
def do_layout(self, windows, active_window_idx):
self.blank_rects = []
window_count = len(windows)
if window_count == 1:
wg = layout_single_window(self.margin_width, self.padding_width)
windows[0].set_geometry(0, wg)
self.blank_rects = blank_rects_for_window(windows[0])
return
return self.layout_single_window(windows[0])
xlayout = self.xlayout(1)
xstart, xnum = next(xlayout)
ylayout = self.ylayout(window_count)
for i in range(window_count):
ystart, ynum = next(ylayout)
windows[i].set_geometry(i, window_geometry(xstart, xnum, ystart, ynum))
ylayout = self.variable_layout(window_count, self.biased_map)
for i, (w, (ystart, ynum)) in enumerate(zip(windows, ylayout)):
w.set_geometry(i, window_geometry(xstart, xnum, ystart, ynum))
# bottom blank rect
self.bottom_blank_rect(windows[i])
@@ -382,26 +639,22 @@ class Vertical(Layout):
self.simple_blank_rects(windows[0], windows[-1])
class Horizontal(Layout):
class Horizontal(Vertical):
name = 'horizontal'
main_is_horizontal = True
vlayout = Layout.xlayout
def do_layout(self, windows, active_window_idx):
self.blank_rects = []
window_count = len(windows)
if window_count == 1:
wg = layout_single_window(self.margin_width, self.padding_width)
windows[0].set_geometry(0, wg)
self.blank_rects = blank_rects_for_window(windows[0])
return
return self.layout_single_window(windows[0])
xlayout = self.xlayout(window_count)
xlayout = self.variable_layout(window_count, self.biased_map)
ylayout = self.ylayout(1)
ystart, ynum = next(ylayout)
for i in range(window_count):
xstart, xnum = next(xlayout)
windows[i].set_geometry(i, window_geometry(xstart, xnum, ystart, ynum))
for i, (w, (xstart, xnum)) in enumerate(zip(windows, xlayout)):
w.set_geometry(i, window_geometry(xstart, xnum, ystart, ynum))
if i > 0:
# between blank rect
self.between_blank_rect(windows[i - 1], windows[i])
@@ -410,6 +663,29 @@ class Horizontal(Layout):
self.simple_blank_rects(windows[0], windows[-1])
# bottom blank rect
self.blank_rects.append(Rect(windows[0].geometry.left, windows[0].geometry.bottom, windows[-1].geometry.right, central.bottom + 1))
# }}}
# Instantiation {{{
all_layouts = {o.name: o for o in globals().values() if isinstance(o, type) and issubclass(o, Layout) and o is not Layout}
def create_layout_object_for(name, os_window_id, tab_id, margin_width, padding_width, border_width, layout_opts=''):
key = name, os_window_id, tab_id, margin_width, padding_width, border_width, layout_opts
ans = create_layout_object_for.cache.get(key)
if ans is None:
name, layout_opts = name.partition(':')[::2]
ans = create_layout_object_for.cache[key] = all_layouts[name](os_window_id, tab_id, margin_width, padding_width, border_width, layout_opts)
return ans
create_layout_object_for.cache = {}
def evict_cached_layouts(tab_id):
remove = [key for key in create_layout_object_for.cache if key[2] == tab_id]
for key in remove:
del create_layout_object_for.cache[key]
# }}}

View File

@@ -149,7 +149,7 @@ set_attribute(LineBuf *self, PyObject *args) {
#define set_attribute_doc "set_attribute(which, val) -> Set the attribute on all cells in the line."
unsigned int shift, val;
if (!PyArg_ParseTuple(args, "II", &shift, &val)) return NULL;
if (shift < DECORATION_SHIFT || shift > STRIKE_SHIFT) { PyErr_SetString(PyExc_ValueError, "Unknown attribute"); return NULL; }
if (shift < DECORATION_SHIFT || shift > DIM_SHIFT) { PyErr_SetString(PyExc_ValueError, "Unknown attribute"); return NULL; }
linebuf_set_attribute(self, shift, val);
Py_RETURN_NONE;
}
@@ -414,7 +414,7 @@ get_line(LineBuf *self, index_type y) {
static PyObject*
as_text(LineBuf *self, PyObject *args) {
as_text_generic(args, self, get_line, self->ynum, self->xnum, callback, as_ansi);
as_text_generic(args, self, get_line, self->ynum, self->xnum);
}

View File

@@ -515,7 +515,7 @@ set_attribute(Line *self, PyObject *args) {
#define set_attribute_doc "set_attribute(which, val) -> Set the attribute on all cells in the line."
unsigned int shift, val;
if (!PyArg_ParseTuple(args, "II", &shift, &val)) return NULL;
if (shift < DECORATION_SHIFT || shift > STRIKE_SHIFT) { PyErr_SetString(PyExc_ValueError, "Unknown attribute"); return NULL; }
if (shift < DECORATION_SHIFT || shift > DIM_SHIFT) { PyErr_SetString(PyExc_ValueError, "Unknown attribute"); return NULL; }
set_attribute_on_line(self->cells, shift, val, self->xnum);
Py_RETURN_NONE;
}

View File

@@ -72,7 +72,6 @@ void linebuf_mark_line_dirty(LineBuf *self, index_type y);
void linebuf_mark_line_clean(LineBuf *self, index_type y);
unsigned int linebuf_char_width_at(LineBuf *self, index_type x, index_type y);
void linebuf_refresh_sprite_positions(LineBuf *self);
bool historybuf_resize(HistoryBuf *self, index_type lines);
void historybuf_add_line(HistoryBuf *self, const Line *line);
void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other);
void historybuf_init_line(HistoryBuf *self, index_type num, Line *l);
@@ -82,14 +81,15 @@ void historybuf_refresh_sprite_positions(HistoryBuf *self);
void historybuf_clear(HistoryBuf *self);
#define as_text_generic(args, container, get_line, lines, columns, callback, as_ansi) { \
#define as_text_generic(args, container, get_line, lines, columns) { \
PyObject *callback; \
int as_ansi = 0; \
if (!PyArg_ParseTuple(args, "O|p", &callback, &as_ansi)) return NULL; \
int as_ansi = 0, insert_wrap_markers = 0; \
if (!PyArg_ParseTuple(args, "O|pp", &callback, &as_ansi, &insert_wrap_markers)) return NULL; \
PyObject *ret = NULL, *t = NULL; \
Py_UCS4 *buf = NULL; \
PyObject *nl = PyUnicode_FromString("\n"); \
if (nl == NULL) goto end; \
PyObject *cr = PyUnicode_FromString("\r"); \
if (nl == NULL || cr == NULL) goto end; \
if (as_ansi) { \
buf = malloc(sizeof(Py_UCS4) * columns * 100); \
if (buf == NULL) { PyErr_NoMemory(); goto end; } \
@@ -110,9 +110,14 @@ void historybuf_clear(HistoryBuf *self);
if (t == NULL) goto end; \
ret = PyObject_CallFunctionObjArgs(callback, t, NULL); \
Py_DECREF(t); if (ret == NULL) goto end; Py_DECREF(ret); \
if (insert_wrap_markers) { \
ret = PyObject_CallFunctionObjArgs(callback, cr, NULL); \
if (ret == NULL) goto end; \
Py_CLEAR(ret); \
}\
} \
end: \
Py_CLEAR(nl); free(buf); \
Py_CLEAR(nl); Py_CLEAR(cr); free(buf); \
if (PyErr_Occurred()) return NULL; \
Py_RETURN_NONE; \
}

View File

@@ -39,12 +39,10 @@ def init_graphics():
def run_app(opts, args):
set_scale(opts.box_drawing_scale)
set_options(opts, is_wayland, args.debug_gl, args.debug_font_fallback)
if is_macos:
from .fast_data_types import macos_change_titlebar_color
macos_change_titlebar_color(opts.macos_titlebar_color)
with cached_values_for('main') as cached_values:
w, h = initial_window_size(opts, cached_values)
with cached_values_for(run_app.cached_values_name) as cached_values:
w, h = run_app.initial_window_size(opts, cached_values)
window_id = create_os_window(w, h, appname, args.name or args.cls or appname, args.cls or appname, load_all_shaders)
run_app.first_window_callback(opts, window_id)
startup_ctx = init_startup_notification(window_id)
show_window(window_id)
if not is_wayland and not is_macos: # no window icons on wayland
@@ -59,6 +57,11 @@ def run_app(opts, args):
boss.destroy()
run_app.cached_values_name = 'main'
run_app.first_window_callback = lambda opts, window_id: None
run_app.initial_window_size = initial_window_size
def ensure_osx_locale():
# Ensure the LANG env var is set. See
# https://github.com/kovidgoyal/kitty/issues/90
@@ -154,9 +157,6 @@ def _main():
return
if getattr(args, 'detach', False):
detach()
if args.cmd:
exec(args.cmd)
return
if args.replay_commands:
from kitty.client import main
main(args.replay_commands)

View File

@@ -341,6 +341,9 @@ dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
case 119:
SET_COLOR(set_dynamic_color);
break;
case 52:
DISPATCH_OSC(clipboard_control);
break;
default:
REPORT_ERROR("Unknown OSC code: %u", code);
break;
@@ -766,17 +769,28 @@ dispatch_dcs(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
}
break;
case '@':
if (startswith(screen->parser_buf + 1, screen->parser_buf_pos - 2, "kitty-cmd{")) {
#define CMD_PREFIX "kitty-cmd{"
if (startswith(screen->parser_buf + 1, screen->parser_buf_pos - 2, CMD_PREFIX)) {
PyObject *cmd = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, screen->parser_buf + 10, screen->parser_buf_pos - 10);
if (cmd != NULL) {
REPORT_OSC2(screen_handle_cmd, (char)screen->parser_buf[0], cmd);
screen_handle_cmd(screen, cmd);
Py_DECREF(cmd);
} else PyErr_Clear();
#undef CMD_PREFIX
#define PRINT_PREFIX "kitty-print|"
} else if (startswith(screen->parser_buf + 1, screen->parser_buf_pos - 1, PRINT_PREFIX)) {
PyObject *msg = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, screen->parser_buf + sizeof(PRINT_PREFIX), screen->parser_buf_pos - sizeof(PRINT_PREFIX));
if (msg != NULL) {
REPORT_OSC2(screen_handle_print, (char)screen->parser_buf[0], msg);
screen_handle_print(screen, msg);
Py_DECREF(msg);
} else PyErr_Clear();
} else {
REPORT_ERROR("Unrecognized DCS @ code: 0x%x", screen->parser_buf[1]);
}
break;
#undef PRINT_PREFIX
default:
REPORT_ERROR("Unrecognized DCS code: 0x%x", screen->parser_buf[0]);
break;
@@ -816,7 +830,7 @@ parse_graphics_code(Screen *screen, PyObject UNUSED *dump_callback) {
enum KEYS key = 'a';
static GraphicsCommand g;
unsigned int i, code;
unsigned long lcode;
uint64_t lcode;
bool is_negative;
memset(&g, 0, sizeof(g));
static uint8_t payload[4096];

View File

@@ -4,7 +4,6 @@
import json
import re
import socket
import sys
import types
from functools import partial
@@ -40,9 +39,14 @@ will only work if this process is run within an existing kitty window.
'''.format, appname=appname)
def do_io(to, send, no_response):
def encode_send(send):
send = ('@kitty-cmd' + json.dumps(send)).encode('ascii')
send = b'\x1bP' + send + b'\x1b\\'
return b'\x1bP' + send + b'\x1b\\'
def do_io(to, send, no_response):
import socket
send = encode_send(send)
if to:
family, address = parse_address_spec(to)[:2]
s = socket.socket(family)
@@ -79,8 +83,10 @@ def do_io(to, send, no_response):
return response
def main(args):
all_commands = tuple(sorted(cmap))
all_commands = tuple(sorted(cmap))
def parse_rc_args(args):
cmds = (' |G {}|\n {}'.format(cmap[c].name, cmap[c].short_desc) for c in all_commands)
msg = (
'Control {appname} by sending it commands. Add'
@@ -90,7 +96,11 @@ def main(args):
'{appname} @ |_ command| -h'
).format(appname=appname, cmds='\n'.join(cmds))
global_opts, items = parse_args(args[1:], global_options_spec, 'command ...', msg, '{} @'.format(appname))
return parse_args(args[1:], global_options_spec, 'command ...', msg, '{} @'.format(appname))
def main(args):
global_opts, items = parse_rc_args(args)
if not items:
from kitty.shell import main
@@ -120,5 +130,8 @@ def main(args):
if response.get('tb'):
print(response['tb'], file=sys.stderr)
raise SystemExit(response['error'])
if 'data' in response:
print(response['data'])
data = response.get('data')
if data is not None:
if func.string_return_is_error and isinstance(data, str):
raise SystemExit(data)
print(data)

8
kitty/rgb.py generated
View File

@@ -35,6 +35,14 @@ def color_as_int(x):
return x.red << 16 | x.green << 8 | x.blue
def color_as_sharp(x):
return '#{:02x}{:02x}{:02x}'.format(*x)
def color_as_sgr(x):
return ':2:{}:{}:{}'.format(*x)
def to_color(raw, validate=False):
# See man XParseColor
x = raw.strip().lower()

View File

@@ -212,12 +212,6 @@ screen_rescale_images(Screen *self, unsigned int old_cell_width, unsigned int ol
}
static bool
screen_change_scrollback_size(Screen *self, unsigned int size) {
if (size != self->historybuf->ynum) return historybuf_resize(self->historybuf, size);
return true;
}
static PyObject*
reset_callbacks(Screen *self, PyObject *a UNUSED) {
Py_CLEAR(self->callbacks);
@@ -452,15 +446,21 @@ write_escape_code_to_child(Screen *self, unsigned char which, const char *data)
if (suffix[0]) write_to_child(self, suffix, strlen(suffix));
}
static inline bool
cursor_within_margins(Screen *self) {
return self->margin_top <= self->cursor->y && self->cursor->y >= self->margin_bottom;
}
void
screen_handle_graphics_command(Screen *self, const GraphicsCommand *cmd, const uint8_t *payload) {
unsigned int x = self->cursor->x, y = self->cursor->y;
const char *response = grman_handle_command(self->grman, cmd, payload, self->cursor, &self->is_dirty);
if (response != NULL) write_escape_code_to_child(self, APC, response);
if (x != self->cursor->x || y != self->cursor->y) {
bool in_margins = cursor_within_margins(self);
if (self->cursor->x >= self->columns) { self->cursor->x = 0; self->cursor->y++; }
if (self->cursor->y > self->margin_bottom) screen_scroll(self, self->cursor->y - self->margin_bottom);
screen_ensure_bounds(self, false);
screen_ensure_bounds(self, false, in_margins);
}
}
// }}}
@@ -664,7 +664,7 @@ screen_cursor_back(Screen *self, unsigned int count/*=1*/, int move_direction/*=
if (count == 0) count = 1;
if (move_direction < 0 && count > self->cursor->x) self->cursor->x = 0;
else self->cursor->x += move_direction * count;
screen_ensure_bounds(self, false);
screen_ensure_bounds(self, false, cursor_within_margins(self));
}
void
@@ -674,10 +674,11 @@ screen_cursor_forward(Screen *self, unsigned int count/*=1*/) {
void
screen_cursor_up(Screen *self, unsigned int count/*=1*/, bool do_carriage_return/*=false*/, int move_direction/*=-1*/) {
bool in_margins = cursor_within_margins(self);
if (count == 0) count = 1;
if (move_direction < 0 && count > self->cursor->y) self->cursor->y = 0;
else self->cursor->y += move_direction * count;
screen_ensure_bounds(self, true);
screen_ensure_bounds(self, true, in_margins);
if (do_carriage_return) self->cursor->x = 0;
}
@@ -701,7 +702,7 @@ screen_cursor_to_column(Screen *self, unsigned int column) {
unsigned int x = MAX(column, 1) - 1;
if (x != self->cursor->x) {
self->cursor->x = x;
screen_ensure_bounds(self, false);
screen_ensure_bounds(self, false, cursor_within_margins(self));
}
}
@@ -781,9 +782,10 @@ screen_carriage_return(Screen *self) {
void
screen_linefeed(Screen *self) {
bool in_margins = cursor_within_margins(self);
screen_index(self);
if (self->modes.mLNM) screen_carriage_return(self);
screen_ensure_bounds(self, false);
screen_ensure_bounds(self, false, in_margins);
}
#define buffer_push(self, ans) { \
@@ -844,7 +846,7 @@ screen_restore_cursor(Screen *self) {
set_mode_from_const(self, DECAWM, sp->mDECAWM);
set_mode_from_const(self, DECSCNM, sp->mDECSCNM);
cursor_copy_to(&(sp->cursor), self->cursor);
screen_ensure_bounds(self, false);
screen_ensure_bounds(self, false, false);
}
}
@@ -862,9 +864,9 @@ screen_restore_modes(Screen *self) {
}
void
screen_ensure_bounds(Screen *self, bool force_use_margins/*=false*/) {
screen_ensure_bounds(Screen *self, bool force_use_margins/*=false*/, bool in_margins) {
unsigned int top, bottom;
if (force_use_margins || self->modes.mDECOM) {
if (in_margins && (force_use_margins || self->modes.mDECOM)) {
top = self->margin_top; bottom = self->margin_bottom;
} else {
top = 0; bottom = self->lines - 1;
@@ -875,6 +877,7 @@ screen_ensure_bounds(Screen *self, bool force_use_margins/*=false*/) {
void
screen_cursor_position(Screen *self, unsigned int line, unsigned int column) {
bool in_margins = cursor_within_margins(self);
line = (line == 0 ? 1 : line) - 1;
column = (column == 0 ? 1: column) - 1;
if (self->modes.mDECOM) {
@@ -882,7 +885,7 @@ screen_cursor_position(Screen *self, unsigned int line, unsigned int column) {
line = MAX(self->margin_top, MIN(line, self->margin_bottom));
}
self->cursor->x = column; self->cursor->y = line;
screen_ensure_bounds(self, false);
screen_ensure_bounds(self, false, in_margins);
}
void
@@ -939,7 +942,7 @@ void
screen_erase_in_display(Screen *self, unsigned int how, bool private) {
/* Erases display in a specific way.
:param int how: defines the way the line should be erased in:
:param int how: defines the way the screen should be erased:
* ``0`` -- Erases from cursor to end of screen, including
cursor position.
@@ -1077,6 +1080,7 @@ void
screen_bell(Screen *self) {
request_window_attention(self->window_id, OPT(enable_audio_bell));
if (OPT(visual_bell_duration) > 0.0f) self->start_visual_bell_at = monotonic();
CALLBACK("on_bell", NULL);
}
void
@@ -1199,6 +1203,11 @@ set_dynamic_color(Screen *self, unsigned int code, PyObject *color) {
else { CALLBACK("set_dynamic_color", "IO", code, color); }
}
void
clipboard_control(Screen *self, PyObject *data) {
CALLBACK("clipboard_control", "O", data);
}
void
set_color_table_color(Screen *self, unsigned int code, PyObject *color) {
if (color == NULL) { CALLBACK("set_color_table_color", "Is", code, ""); }
@@ -1210,6 +1219,11 @@ screen_handle_cmd(Screen *self, PyObject *cmd) {
CALLBACK("handle_remote_cmd", "O", cmd);
}
void
screen_handle_print(Screen *self, PyObject *msg) {
CALLBACK("handle_remote_print", "O", msg);
}
void
screen_request_capabilities(Screen *self, char c, PyObject *q) {
static char buf[128];
@@ -1462,7 +1476,12 @@ screen_open_url(Screen *self) {
static PyObject*
as_text(Screen *self, PyObject *args) {
as_text_generic(args, self, visual_line_, self->lines, self->columns, callback, as_ansi);
as_text_generic(args, self, visual_line_, self->lines, self->columns);
}
static PyObject*
as_text_non_visual(Screen *self, PyObject *args) {
as_text_generic(args, self, range_line_, self->lines, self->columns);
}
static PyObject*
@@ -1516,6 +1535,52 @@ screen_wcswidth(PyObject UNUSED *self, PyObject *str) {
return PyLong_FromUnsignedLong(ans);
}
static PyObject*
screen_truncate_point_for_length(PyObject UNUSED *self, PyObject *args) {
PyObject *str; unsigned int num_cells, start_pos = 0;
if (!PyArg_ParseTuple(args, "OI|I", &str, &num_cells, &start_pos)) return NULL;
if (PyUnicode_READY(str) != 0) return NULL;
int kind = PyUnicode_KIND(str);
void *data = PyUnicode_DATA(str);
Py_ssize_t len = PyUnicode_GET_LENGTH(str), i;
char_type prev_ch = 0;
int prev_width = 0;
bool in_sgr = false;
unsigned long width_so_far = 0;
for (i = start_pos; i < len && width_so_far < num_cells; i++) {
char_type ch = PyUnicode_READ(kind, data, i);
if (in_sgr) {
if (ch == 'm') in_sgr = false;
continue;
}
if (ch == 0x1b && i + 1 < len && PyUnicode_READ(kind, data, i + 1) == '[') { in_sgr = true; continue; }
if (ch == 0xfe0f) {
if (is_emoji_presentation_base(prev_ch) && prev_width == 1) {
width_so_far += 1;
prev_width = 2;
} else prev_width = 0;
} else {
int w = wcwidth_std(ch);
switch(w) {
case -1:
case 0:
prev_width = 0; break;
case 2:
prev_width = 2; break;
default:
prev_width = 1; break;
}
if (width_so_far + prev_width > num_cells) { break; }
width_so_far += prev_width;
}
prev_ch = ch;
}
return PyLong_FromUnsignedLong(i);
}
static PyObject*
line(Screen *self, PyObject *val) {
unsigned long y = PyLong_AsUnsignedLong(val);
@@ -1633,19 +1698,11 @@ static PyObject*
start_selection(Screen *self, PyObject *args) {
unsigned int x, y;
int rectangle_select = 0, extend_mode = EXTEND_CELL;
if (!PyArg_ParseTuple(args, "II|pp", &x, &y, &rectangle_select, &extend_mode)) return NULL;
if (!PyArg_ParseTuple(args, "II|pi", &x, &y, &rectangle_select, &extend_mode)) return NULL;
screen_start_selection(self, x, y, rectangle_select, extend_mode);
Py_RETURN_NONE;
}
static PyObject*
change_scrollback_size(Screen *self, PyObject *args) {
unsigned int count = 1;
if (!PyArg_ParseTuple(args, "|I", &count)) return NULL;
if (!screen_change_scrollback_size(self, MAX(self->lines, count))) return NULL;
Py_RETURN_NONE;
}
static PyObject*
text_for_selection(Screen *self, PyObject *a UNUSED) {
FullSelectionBoundary start, end;
@@ -1801,19 +1858,29 @@ screen_update_selection(Screen *self, index_type x, index_type y, bool ended) {
}
break;
}
case EXTEND_LINE:
if (extending_leftwards) {
found = screen_selection_range_for_line(self, self->selection.end_y, &start, &end);
if (found) { self->selection.end_x = start; }
found = screen_selection_range_for_line(self, self->selection.start_y, &start, &end);
if (found) { self->selection.start_x = end; }
} else {
found = screen_selection_range_for_line(self, self->selection.start_y, &start, &end);
if (found) { self->selection.start_x = start; }
found = screen_selection_range_for_line(self, self->selection.end_y, &start, &end);
if (found) { self->selection.end_x = end; }
case EXTEND_LINE: {
index_type top_line = extending_leftwards ? self->selection.end_y : self->selection.start_y;
index_type bottom_line = extending_leftwards ? self->selection.start_y : self->selection.end_y;
while(top_line > 0 && visual_line_(self, top_line)->continued) top_line--;
while(bottom_line < self->lines - 1 && visual_line_(self, bottom_line + 1)->continued) bottom_line++;
found = screen_selection_range_for_line(self, top_line, &start, &end);
if (found) {
if (extending_leftwards) {
self->selection.end_x = start; self->selection.end_y = top_line;
} else {
self->selection.start_x = start; self->selection.start_y = top_line;
}
}
found = screen_selection_range_for_line(self, bottom_line, &start, &end);
if (found) {
if (extending_leftwards) {
self->selection.start_x = end; self->selection.start_y = bottom_line;
} else {
self->selection.end_x = end; self->selection.end_y = bottom_line;
}
}
break;
}
case EXTEND_CELL:
break;
}
@@ -1903,7 +1970,6 @@ static PyMethodDef methods[] = {
MND(delete_lines, METH_VARARGS)
MND(insert_characters, METH_VARARGS)
MND(delete_characters, METH_VARARGS)
MND(change_scrollback_size, METH_VARARGS)
MND(erase_characters, METH_VARARGS)
MND(cursor_up, METH_VARARGS)
MND(cursor_up1, METH_VARARGS)
@@ -1912,6 +1978,7 @@ static PyMethodDef methods[] = {
MND(cursor_forward, METH_VARARGS)
{"index", (PyCFunction)xxx_index, METH_VARARGS, ""},
MND(as_text, METH_VARARGS)
MND(as_text_non_visual, METH_VARARGS)
MND(refresh_sprite_positions, METH_NOARGS)
MND(tab, METH_NOARGS)
MND(backspace, METH_NOARGS)
@@ -1987,6 +2054,7 @@ PyTypeObject Screen_Type = {
static PyMethodDef module_methods[] = {
{"wcwidth", (PyCFunction)wcwidth_wrap, METH_O, ""},
{"wcswidth", (PyCFunction)screen_wcswidth, METH_O, ""},
{"truncate_point_for_length", (PyCFunction)screen_truncate_point_for_length, METH_VARARGS, ""},
{NULL} /* Sentinel */
};

View File

@@ -104,7 +104,7 @@ void screen_cursor_back(Screen *self, unsigned int count/*=1*/, int move_directi
void screen_erase_in_line(Screen *, unsigned int, bool);
void screen_erase_in_display(Screen *, unsigned int, bool);
void screen_draw(Screen *screen, uint32_t codepoint);
void screen_ensure_bounds(Screen *self, bool use_margins);
void screen_ensure_bounds(Screen *self, bool use_margins, bool cursor_was_within_margins);
void screen_toggle_screen_buffer(Screen *self);
void screen_normal_keypad_mode(Screen *self);
void screen_alternate_keypad_mode(Screen *self);
@@ -138,11 +138,13 @@ void screen_erase_characters(Screen *self, unsigned int count);
void screen_set_margins(Screen *self, unsigned int top, unsigned int bottom);
void screen_change_charset(Screen *, uint32_t to);
void screen_handle_cmd(Screen *, PyObject *cmd);
void screen_handle_print(Screen *, PyObject *cmd);
void screen_designate_charset(Screen *, uint32_t which, uint32_t as);
void screen_use_latin1(Screen *, bool);
void set_title(Screen *self, PyObject*);
void set_icon(Screen *self, PyObject*);
void set_dynamic_color(Screen *self, unsigned int code, PyObject*);
void clipboard_control(Screen *self, PyObject*);
void set_color_table_color(Screen *self, unsigned int code, PyObject*);
uint32_t* translation_table(uint32_t which);
void screen_request_capabilities(Screen *, char, PyObject *);

View File

@@ -110,12 +110,7 @@ def create_session(opts, args=None, special_window=None, cwd_from=None, respect_
with open(args.session) as f:
return parse_session(f.read(), opts)
ans = Session()
if args and args.window_layout:
if args.window_layout not in opts.enabled_layouts:
opts.enabled_layouts.insert(0, args.window_layout)
current_layout = args.window_layout
else:
current_layout = opts.enabled_layouts[0] if opts.enabled_layouts else 'tall'
current_layout = opts.enabled_layouts[0] if opts.enabled_layouts else 'tall'
ans.add_tab(opts)
ans.tabs[-1].layout = current_layout
if special_window is None:

View File

@@ -7,7 +7,6 @@
#include "gl.h"
#include "fonts.h"
#include <sys/sysctl.h>
enum { CELL_PROGRAM, CELL_BG_PROGRAM, CELL_SPECIAL_PROGRAM, CELL_FG_PROGRAM, CURSOR_PROGRAM, BORDERS_PROGRAM, GRAPHICS_PROGRAM, GRAPHICS_PREMULT_PROGRAM, BLIT_PROGRAM, NUM_PROGRAMS };
enum { SPRITE_MAP_UNIT, GRAPHICS_UNIT, BLIT_UNIT };
@@ -210,7 +209,7 @@ struct CellUniformData {
static struct CellUniformData cell_uniform_data = {0, .prev_inactive_text_alpha=-1};
static inline void
cell_update_uniform_block(ssize_t vao_idx, Screen *screen, int uniform_buffer, GLfloat xstart, GLfloat ystart, GLfloat dx, GLfloat dy, CursorRenderInfo *cursor, bool inverted) {
cell_update_uniform_block(ssize_t vao_idx, Screen *screen, int uniform_buffer, GLfloat xstart, GLfloat ystart, GLfloat dx, GLfloat dy, CursorRenderInfo *cursor, bool inverted, OSWindow *os_window) {
struct CellRenderData {
GLfloat xstart, ystart, dx, dy, sprite_dx, sprite_dy, background_opacity;
@@ -240,7 +239,7 @@ cell_update_uniform_block(ssize_t vao_idx, Screen *screen, int uniform_buffer, G
sprite_tracker_current_layout(&x, &y, &z);
rd->sprite_dx = 1.0f / (float)x; rd->sprite_dy = 1.0f / (float)y;
rd->inverted = inverted ? 1 : 0;
rd->background_opacity = OPT(background_opacity);
rd->background_opacity = os_window->background_opacity;
#define COLOR(name) colorprofile_to_color(screen->color_profile, screen->color_profile->overridden.name, screen->color_profile->configured.name)
rd->default_fg = COLOR(default_fg); rd->default_bg = COLOR(default_bg); rd->highlight_fg = COLOR(highlight_fg); rd->highlight_bg = COLOR(highlight_bg);
@@ -395,9 +394,10 @@ set_cell_uniforms(float current_inactive_text_alpha) {
cell_uniform_data.gpploc = glGetUniformLocation(program_id(GRAPHICS_PREMULT_PROGRAM), "inactive_text_alpha");
cell_uniform_data.cploc = glGetUniformLocation(program_id(CELL_PROGRAM), "inactive_text_alpha");
cell_uniform_data.cfploc = glGetUniformLocation(program_id(CELL_FG_PROGRAM), "inactive_text_alpha");
#define S(prog, name, val) { bind_program(prog); glUniform1i(glGetUniformLocation(program_id(prog), #name), val); }
S(GRAPHICS_PROGRAM, image, GRAPHICS_UNIT); S(GRAPHICS_PREMULT_PROGRAM, image, GRAPHICS_UNIT);
S(CELL_PROGRAM, sprites, SPRITE_MAP_UNIT); S(CELL_FG_PROGRAM, sprites, SPRITE_MAP_UNIT);
#define S(prog, name, val, type) { bind_program(prog); glUniform##type(glGetUniformLocation(program_id(prog), #name), val); }
S(GRAPHICS_PROGRAM, image, GRAPHICS_UNIT, 1i); S(GRAPHICS_PREMULT_PROGRAM, image, GRAPHICS_UNIT, 1i);
S(CELL_PROGRAM, sprites, SPRITE_MAP_UNIT, 1i); S(CELL_FG_PROGRAM, sprites, SPRITE_MAP_UNIT, 1i);
S(CELL_PROGRAM, dim_opacity, OPT(dim_opacity), 1f); S(CELL_FG_PROGRAM, dim_opacity, OPT(dim_opacity), 1f);
#undef S
cell_uniform_data.constants_set = true;
}
@@ -415,7 +415,7 @@ send_cell_data_to_gpu(ssize_t vao_idx, ssize_t gvao_idx, GLfloat xstart, GLfloat
if (os_window->clear_count < 2) {
os_window->clear_count++;
#define C(shift) (((GLfloat)((OPT(background) >> shift) & 0xFF)) / 255.0f)
glClearColor(C(16), C(8), C(0), os_window->is_semi_transparent ? OPT(background_opacity) : 1.0f);
glClearColor(C(16), C(8), C(0), os_window->is_semi_transparent ? os_window->background_opacity : 1.0f);
#undef C
glClear(GL_COLOR_BUFFER_BIT);
changed = true;
@@ -430,7 +430,7 @@ draw_cells(ssize_t vao_idx, ssize_t gvao_idx, GLfloat xstart, GLfloat ystart, GL
CELL_BUFFERS;
bool inverted = screen_invert_colors(screen);
cell_update_uniform_block(vao_idx, screen, uniform_buffer, xstart, ystart, dx, dy, &screen->cursor_render_info, inverted);
cell_update_uniform_block(vao_idx, screen, uniform_buffer, xstart, ystart, dx, dy, &screen->cursor_render_info, inverted, os_window);
bind_vao_uniform_buffer(vao_idx, uniform_buffer, cell_program_layouts[CELL_PROGRAM].render_data.index);
bind_vertex_array(vao_idx);
@@ -487,7 +487,7 @@ draw_cursor(CursorRenderInfo *cursor, bool is_focused) {
// }}}
// Borders {{{
enum BorderUniforms { BORDER_viewport, BORDER_background_opacity, BORDER_default_bg, BORDER_active_border_color, BORDER_inactive_border_color, NUM_BORDER_UNIFORMS };
enum BorderUniforms { BORDER_viewport, BORDER_background_opacity, BORDER_default_bg, BORDER_active_border_color, BORDER_inactive_border_color, BORDER_bell_border_color, NUM_BORDER_UNIFORMS };
static GLint border_uniform_locations[NUM_BORDER_UNIFORMS] = {0};
static void
@@ -501,6 +501,7 @@ init_borders_program() {
else if SET_LOC(default_bg);
else if SET_LOC(active_border_color);
else if SET_LOC(inactive_border_color);
else if SET_LOC(bell_border_color);
else { fatal("Unknown uniform in borders program: %s", p->uniforms[i].name); return; }
}
if (left) { fatal("Left over uniforms in borders program"); return; }
@@ -521,7 +522,7 @@ create_border_vao() {
}
void
draw_borders(ssize_t vao_idx, unsigned int num_border_rects, BorderRect *rect_buf, bool rect_data_is_dirty, uint32_t viewport_width, uint32_t viewport_height, color_type active_window_bg, unsigned int num_visible_windows) {
draw_borders(ssize_t vao_idx, unsigned int num_border_rects, BorderRect *rect_buf, bool rect_data_is_dirty, uint32_t viewport_width, uint32_t viewport_height, color_type active_window_bg, unsigned int num_visible_windows, OSWindow *w) {
if (num_border_rects) {
if (rect_data_is_dirty) {
size_t sz = sizeof(GLuint) * 5 * num_border_rects;
@@ -534,9 +535,13 @@ draw_borders(ssize_t vao_idx, unsigned int num_border_rects, BorderRect *rect_bu
#define CV3(x) (((float)((x >> 16) & 0xff))/255.f), (((float)((x >> 8) & 0xff))/255.f), (((float)(x & 0xff))/255.f)
if (!constants_set) {
constants_set = true;
glUniform1f(border_uniform_locations[BORDER_background_opacity], OPT(background_opacity));
glUniform1f(border_uniform_locations[BORDER_background_opacity], w->background_opacity);
glUniform3f(border_uniform_locations[BORDER_active_border_color], CV3(OPT(active_border_color)));
glUniform3f(border_uniform_locations[BORDER_inactive_border_color], CV3(OPT(inactive_border_color)));
glUniform3f(border_uniform_locations[BORDER_bell_border_color], CV3(OPT(bell_border_color)));
}
if (OPT(dynamic_background_opacity)) {
glUniform1f(border_uniform_locations[BORDER_background_opacity], w->background_opacity);
}
glUniform2ui(border_uniform_locations[BORDER_viewport], viewport_width, viewport_height);
color_type default_bg = num_visible_windows > 1 ? OPT(background) : active_window_bg;

View File

@@ -14,12 +14,24 @@ from .cli import (
emph, green, italic, parse_option_spec, print_help_for_seq, title
)
from .cmds import cmap, display_subcommand_help, parse_subcommand_cli
from .constants import cache_dir, version
from .constants import cache_dir, is_macos, version
all_commands = tuple(sorted(cmap))
match_commands = tuple(sorted(all_commands + ('exit', 'help', 'quit')))
def init_readline(readline):
try:
readline.read_init_file()
except EnvironmentError:
if not is_macos:
raise
if 'libedit' in readline.__doc__:
readline.parse_and_bind("bind ^I rl_complete")
else:
readline.parse_and_bind('tab: complete')
def cmd_names_matching(prefix):
for cmd in match_commands:
if not prefix or cmd.startswith(prefix):
@@ -76,7 +88,6 @@ class Completer:
if os.path.exists(self.history_path):
readline.read_history_file(self.history_path)
readline.set_completer(self.complete)
readline.parse_and_bind('tab: complete')
delims = readline.get_completer_delims()
readline.set_completer_delims(delims.replace('-', ''))
return self
@@ -141,7 +152,7 @@ def run_cmd(global_opts, cmd, func, opts, items):
def real_main(global_opts):
readline.read_init_file()
init_readline(readline)
print_help_for_seq.allow_pager = False
print('Welcome to the kitty shell!')
print('Use {} for assistance or {} to quit'.format(green('help'), green('exit')))

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