Compare commits

...

182 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
87 changed files with 3744 additions and 862 deletions

1
.ignore Normal file
View File

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

View File

@@ -3,6 +3,87 @@ 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]
------------------------------

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,6 +111,7 @@ 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`, `libxi`, `xinerama`, `libgl1-mesa` and `xkbcommon-x11`, if they are not already installed by your distro.
@@ -152,7 +158,7 @@ just `kitty`.
* openSUSE: https://build.opensuse.org/package/show/X11:terminals/kitty
* Debian: https://packages.qa.debian.org/k/kitty/news/20180419T070013Z.html
* Debian: https://packages.debian.org/buster/kitty
=== macOS packages
@@ -260,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}
|===
@@ -292,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
@@ -311,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
@@ -334,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
@@ -342,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!
@@ -375,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
@@ -539,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
@@ -550,17 +588,6 @@ Really, the correct solution for this is to convince the OpenSSH maintainers to
have ssh do this automatically when connecting to a server, so that all
terminals work transparently.
In the meantime, you can automate it by using a simple script called, for example,
`~/bin/myssh`
....
#!/bin/sh
infocmp xterm-kitty | ssh $1 tic -x -o \~/.terminfo /dev/stdin && ssh $1
....
Then you can use `myssh server` to log in to `server` with the terminfo file
being automatically available.
=== How do I change the colors in a running kitty instance?
@@ -584,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

@@ -219,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:
@@ -261,7 +263,7 @@ def gen_ucd():
rmap[0xfe0e], rmap[0xfe0f]
))
with open('kittens/hints/url_regex.py', 'w') as f:
f.write("url_delimiters = '{}' # noqa".format(''.join(classes_to_regex(cz))))
f.write("url_delimiters = '{}' # noqa".format(''.join(classes_to_regex(cz, exclude='\n'))))
def gen_names():

5
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
@@ -1176,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

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

5
glfw/x11_window.c vendored
View File

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

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

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

View File

@@ -11,6 +11,20 @@ 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):
@@ -20,6 +34,7 @@ class Collection:
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
@@ -35,11 +50,15 @@ class Collection:
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)
@@ -47,8 +66,16 @@ class Collection:
def __iter__(self):
for path in self.all_paths:
typ = self.type_map[path]
data = self.changes[path] if typ == 'diff' else None
yield path, self.type_map[path], data
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):
@@ -76,7 +103,7 @@ def collect_files(collection, left, right):
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[name])
collection.add_rename(left_path_map[name], right_path_map[n])
added.discard(n)
break
else:
@@ -86,7 +113,7 @@ def collect_files(collection, left, right):
collection.add_add(right_path_map[name])
sanitize_pat = re.compile('[\x00-\x1f\x7f\x80-\x9f]')
sanitize_pat = re.compile('[\x00-\x09\x0b-\x1f\x7f\x80-\x9f]')
def sanitize_sub(m):
@@ -103,10 +130,19 @@ def mime_type_for_path(path):
@lru_cache(maxsize=1024)
def data_for_path(path):
def raw_data_for_path(path):
with open(path, 'rb') as f:
ans = f.read()
if not mime_type_for_path(path).startswith('image/'):
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:
@@ -116,13 +152,16 @@ def data_for_path(path):
@lru_cache(maxsize=1024)
def lines_for_path(path):
data = data_for_path(path)
return tuple(map(sanitize, data.splitlines()))
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):
md5(data_for_path(path)).digest()
return md5(raw_data_for_path(path)).digest()
def create_collection(left, right):
@@ -131,8 +170,20 @@ def create_collection(left, right):
collect_files(collection, left, right)
else:
pl, pr = os.path.abspath(left), os.path.abspath(right)
path_name_map[left] = pl
path_name_map[right] = pr
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, [])

View File

@@ -5,8 +5,9 @@
import os
from kitty.config_utils import (
init_config, load_config as _load_config, merge_dicts, parse_config_base,
resolve_config, to_color
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
@@ -24,13 +25,13 @@ formats = {
def set_formats(opts):
formats['text'] = '38' + color_as_sgr(opts.foreground) + ';48' + color_as_sgr(opts.background)
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'] = '38' + color_as_sgr(opts.foreground) + ';48' + color_as_sgr(opts.added_bg)
formats['removed'] = '38' + color_as_sgr(opts.foreground) + ';48' + color_as_sgr(opts.removed_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)
@@ -38,21 +39,66 @@ def set_formats(opts):
formats['added_highlight'] = '48' + color_as_sgr(opts.highlight_added_bg)
type_map = {}
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()
def special_handling(*a):
pass
@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 = {}
ans = {'key_definitions': {}}
parse_config_base(
lines,
defaults,

View File

@@ -1,3 +1,26 @@
# 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
@@ -13,3 +36,23 @@ 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)

View File

@@ -4,21 +4,31 @@
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.key_encoding import ESCAPE
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 (
clear_screen, set_default_colors, set_line_wrapping, set_window_title
from ..tui.operations import styled
from .collect import (
create_collection, data_for_path, lines_for_path, set_highlight_data
)
from .collect import create_collection, data_for_path
from .config import init_config
from .git import Differ
from .render import render_diff
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)
@@ -37,60 +47,261 @@ def generate_diff(collection, 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 = 0
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.args.context)
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.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.write(set_line_wrapping(False))
self.write(set_window_title('kitty +diff'))
self.write(set_default_colors(self.opts.foreground, self.opts.background))
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 finalize(self):
self.write(set_default_colors())
def enforce_cursor_state(self):
self.cmd.set_cursor_visible(self.state > DIFFED)
def draw_screen(self):
if self.state < DIFFED:
self.write(clear_screen())
self.write(_('Calculating diff, please wait...'))
return
self.write(clear_screen())
for i in range(self.screen_size.rows - 1):
lpos = self.scroll_pos + i
if lpos >= len(self.diff_lines):
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:
text = self.diff_lines[lpos].text
self.write(text)
self.write('\x1b[0m\n\r')
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 self.state is INITIALIZING:
if key_event.key is ESCAPE:
self.quit_loop(0)
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()
@@ -101,6 +312,7 @@ class DiffHandler(Handler):
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']
@@ -110,6 +322,30 @@ class DiffHandler(Handler):
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()
@@ -123,8 +359,9 @@ class DiffHandler(Handler):
OPTIONS = partial('''\
--context
type=int
default=3
Number of lines of context to show between changes.
default=-1
Number of lines of context to show between changes. Negative values
use the number set in diff.conf
--config
@@ -140,28 +377,40 @@ 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)
def handle_result(args, current_char, target_window_id, boss):
pass
if __name__ == '__main__':
main(sys.argv)

View File

@@ -4,20 +4,45 @@
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
p = subprocess.Popen([
'git', 'diff', '--no-color', '--no-ext-diff', '--exit-code', '-U' + str(context), '--no-index', '--'
] + [file1, file2],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL)
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):
@@ -48,7 +73,10 @@ class Chunk:
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))
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(
@@ -63,6 +91,7 @@ class Hunk:
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)
@@ -94,10 +123,12 @@ class Hunk:
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()
@@ -138,6 +169,8 @@ 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)
@@ -148,20 +181,23 @@ class Patch:
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 not all_hunks:
if current_hunk is None:
continue
q = line[0]
if q == '+':
all_hunks[-1].add_line()
current_hunk.add_line()
elif q == '-':
all_hunks[-1].remove_line()
current_hunk.remove_line()
elif q == '\\':
continue
else:
all_hunks[-1].context_line()
current_hunk.context_line()
for h in all_hunks:
h.finalize()
return Patch(all_hunks)
@@ -199,7 +235,7 @@ class Differ:
patch = parse_patch(output)
except Exception:
import traceback
return traceback.format_exc() + '\nParsing diff for {} vs. {} failed'.format(key[0], key[1])
return traceback.format_exc() + '\nParsing diff for {} vs. {} failed'.format(left_path, right_path)
else:
ans[key] = patch
return ans

View File

@@ -2,55 +2,78 @@
# 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
from itertools import repeat, zip_longest
from math import ceil
from kitty.fast_data_types import truncate_point_for_length, wcswidth
from .collect import data_for_path, lines_for_path, path_name_map, sanitize
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 HunkRef:
__slots__ = ('hunk_num', 'line_num', 'chunk_num')
def __init__(self, hunk_num, chunk_num=None, line_num=None):
self.hunk_num = hunk_num
self.chunk_num = chunk_num
self.line_num = line_num
class ImageSupportWarning(Warning):
pass
class LineRef:
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):
self.src_line_number = sln
self.wrapped_line_idx = wli
def __init__(self, sln, wli=0):
object.__setattr__(self, 'src_line_number', sln)
object.__setattr__(self, 'wrapped_line_idx', wli)
class Reference:
class Reference(Ref):
__slots__ = ('path', 'extra')
def __init__(self, path, extra=None):
self.path = path
self.extra = extra
object.__setattr__(self, 'path', path)
object.__setattr__(self, 'extra', extra)
class Line:
__slots__ = ('text', 'ref')
__slots__ = ('text', 'ref', 'is_change_start', 'image_data')
def __init__(self, text, ref):
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):
def yield_lines_from(iterator, reference, is_change_start=True):
for text in iterator:
yield Line(text, reference)
yield Line(text, reference, is_change_start)
is_change_start = False
def human_readable(size, sep=' '):
@@ -116,21 +139,38 @@ def highlight_boundaries(ltype):
return start, stop
def title_lines(left_path, args, columns, margin_size):
name = fit_in(sanitize(path_name_map[left_path]), columns - 2 * margin_size)
yield title_format(place_in(' ' + name, columns))
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):
def fl(path, fmt):
text = template.format(human_readable(len(data_for_path(path))))
text = place_in(text, columns // 2 - margin_size)
return margin_format(' ' * margin_size) + text_format(text)
text = place_in(text, available_cols)
return margin_format(' ' * margin_size) + fmt(text)
return fl(path) + fl(other_path)
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):
@@ -142,38 +182,20 @@ def split_to_size(line, width):
line = line[p:]
def split_to_size_with_center(line, width, prefix_count, suffix_count, start, stop):
def truncate_points(line, width):
pos = 0
sz = len(line)
if prefix_count + suffix_count == sz:
yield from split_to_size(line, width)
return
suffix_pos = sz - suffix_count
pos = state = 0
while line:
p = truncate_point_for_length(line, width)
if state is 0:
if pos + p > prefix_count:
state = 1
a, line = line[:p], line[p:]
if pos + p > suffix_pos:
a = a[:suffix_pos - pos] + stop + a[suffix_pos - pos:]
state = 2
yield a[:prefix_count - pos] + start + a[prefix_count - pos:]
else:
yield line[:p]
line = line[p:]
elif state is 1:
if pos + p > suffix_pos:
state = 2
a, line = line[:p], line[p:]
yield start + a[:suffix_pos - pos] + stop + a[suffix_pos - pos:]
else:
yield start + line[:p]
line = line[p:]
elif state is 2:
yield line[:p]
line = line[p:]
pos += p
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}
@@ -190,6 +212,18 @@ class DiffData:
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):
@@ -213,12 +247,21 @@ def hunk_title(hunk_num, hunk, margin_size, available_cols):
return m + hunk_format(place_in(t, available_cols))
def render_half_line(line_number, src, ltype, margin_size, available_cols, changed_center=None):
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]:
start, stop = highlight_boundaries(ltype)
lines = split_to_size_with_center(src[line_number], available_cols, changed_center[0], changed_center[1], start, stop)
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(src[line_number], available_cols)
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)
@@ -228,33 +271,42 @@ def render_half_line(line_number, src, ltype, margin_size, available_cols, chang
def lines_for_chunk(data, hunk_num, chunk, chunk_num):
if chunk.is_context:
for i in range(chunk.left_count):
left_line_number = chunk.left_start + i
left_line_number = line_ref = chunk.left_start + i
right_line_number = chunk.right_start + i
lines = split_to_size(data.left_lines[left_line_number], data.available_cols)
ref = Reference(data.left_path, HunkRef(hunk_num, chunk_num, 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 text in lines:
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)):
ref = Reference(data.left_path, HunkRef(hunk_num, chunk_num, i))
ll, rl = [], []
if i < chunk.left_count:
rln = ref_ln = chunk.left_start + i
ll.extend(render_half_line(
chunk.left_start + i, data.left_lines, 'remove', data.margin_size,
data.available_cols, None if chunk.centers is None else chunk.centers[i]))
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(
chunk.right_start + i, data.right_lines, 'add', data.margin_size,
data.available_cols, None if chunk.centers is None else chunk.centers[i]))
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:
@@ -270,8 +322,9 @@ def lines_for_chunk(data, hunk_num, chunk, chunk_num):
else:
x, count = ll, len(rl)
x.extend(repeat(data.filler_line, count))
for left_line, right_line in zip(ll, rl):
yield Line(left_line + right_line, ref)
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):
@@ -279,7 +332,7 @@ def lines_for_diff(left_path, right_path, hunks, args, columns, 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, HunkRef(hunk_num)))
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)
@@ -289,22 +342,105 @@ def all_lines(path, args, columns, margin_size, is_add=True):
ltype = 'add' if is_add else 'remove'
lines = lines_for_path(path)
filler = render_diff_line('', '', 'filler', margin_size, available_cols)
for line_number in range(len(lines)):
h = render_half_line(line_number, lines, ltype, 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))
text = (filler + h) if is_add else (h + filler)
yield Line(text, ref)
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(margin_size + _('The file {0} was renamed to {1}').format(
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
def render_diff(collection, diff_map, args, columns):
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':
@@ -313,22 +449,41 @@ def render_diff(collection, diff_map, args, columns):
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 path, item_type, other_path in collection:
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':
yield from yield_lines_from(title_lines(path, args, columns, margin_size), item_ref)
is_binary = isinstance(data_for_path(path), bytes)
if is_binary:
yield from yield_lines_from(binary_lines(path, other_path, columns, margin_size), item_ref)
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:
yield from lines_for_diff(path, other_path, diff_map[path], args, columns, margin_size)
ans = lines_for_diff(path, other_path, diff_map[path], args, columns, margin_size)
elif item_type == 'add':
yield from yield_lines_from(title_lines(other_path, args, columns, margin_size), item_ref)
yield from all_lines(other_path, args, columns, margin_size, is_add=True)
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':
yield from yield_lines_from(title_lines(path, args, columns, margin_size), item_ref)
yield from all_lines(path, args, columns, margin_size, is_add=False)
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':
yield from yield_lines_from(title_lines(path, args, columns, margin_size), item_ref)
yield from yield_lines_from(rename_lines(path, other_path, args, columns, margin_size))
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)

View File

@@ -29,8 +29,135 @@ changed_center(PyObject *self UNUSED, PyObject *args) {
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 */
};

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,8 +84,9 @@ 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):
self.init_terminal_state()
@@ -128,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)
@@ -146,50 +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
closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>'}
opening_brackets = ''.join(closing_bracket_map)
postprocessor_map = {}
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
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
# Detect a bracketed URL
if s > 0 and e > s + 4 and line[s-1] in '({[<' and line[e-1] == closing_bracket_map[line[s-1]]:
return s, e
@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
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
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, lines, index_map):
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}
@@ -200,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(r'(?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
@@ -278,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:
@@ -290,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)
@@ -319,8 +382,12 @@ def handle_result(args, data, target_window_id, boss):
boss.open_url(data['match'], None if program == 'default' else program, cwd=cwd)
handle_result.type_of_input = 'screen'
if __name__ == '__main__':
# Run with kitty +kitten hints
ans = main(sys.argv)
if ans:
print(ans)
# }}}

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,6 +58,7 @@ 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)
try:
result = m['start'](args)
@@ -73,6 +83,7 @@ def deserialize(output):
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,19 +3,46 @@
# 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, start_job):
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
@@ -50,6 +77,12 @@ class Handler:
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

@@ -18,23 +18,29 @@ 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 clear_screen, init_state, reset_state
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):
@@ -53,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)
@@ -67,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
@@ -122,7 +131,10 @@ class UnhandledException(Handler):
self.tb = tb
def initialize(self):
self.write(clear_screen())
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)
@@ -228,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]
@@ -251,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'):
@@ -268,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:
@@ -356,9 +381,12 @@ 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, self.start_job)
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)
@@ -376,12 +404,10 @@ class Loop:
self.return_code = 1
keep_going = False
finalize_output = b''.join(handler.write_buf).decode('utf-8')
term_manager.extra_finalize = b''.join(handler.write_buf).decode('utf-8')
if tb is not None:
self._report_error_loop(finalize_output + tb, term_manager)
if tb is None:
os.write(self.output_fd, finalize_output.encode('utf-8'))
self._report_error_loop(tb, term_manager)
def _report_error_loop(self, tb, term_manager):
select = self.sel.select
@@ -389,7 +415,7 @@ class Loop:
handler = UnhandledException(tb)
handler.write_buf = []
handler._term_manager = term_manager
handler._initialize(screen_size(), self.quit, self.wakeup, self.start_job)
handler._initialize(screen_size(), self.quit, self.wakeup, self.start_job, debug)
with handler:
while True:
has_data_to_write = bool(handler.write_buf)

View File

@@ -4,9 +4,9 @@
import sys
from contextlib import contextmanager
from functools import wraps
from kitty.rgb import Color, color_as_sharp, to_color
from kitty.terminfo import string_capabilities
S7C1T = '\033 F'
SAVE_CURSOR = '\0337'
@@ -36,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())}
@@ -78,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))
@@ -121,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') +
@@ -163,7 +218,7 @@ def alternate_screen(f=None):
print(reset_mode('ALTERNATE_SCREEN'), end='', file=f)
def set_default_colors(fg=None, bg=None):
def set_default_colors(fg=None, bg=None) -> str:
ans = ''
if fg is None:
ans += '\x1b]110\x1b\\'
@@ -174,3 +229,31 @@ def set_default_colors(fg=None, bg=None):
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

@@ -15,9 +15,12 @@ from .config import (
MINIMUM_FONT_SIZE, initial_window_size, prepare_config_file_for_editing
)
from .config_utils import to_cmdline
from .constants import appname, config_dir, editor, set_boss
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,
@@ -359,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()
@@ -411,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:
@@ -496,34 +541,50 @@ 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), 'PYTHONWARNINGS': 'ignore'},
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)
@@ -541,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:
@@ -603,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);
@@ -614,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

@@ -10,7 +10,6 @@ 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
@@ -71,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.
@@ -119,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
@@ -293,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:
@@ -488,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, config_help=CONFIG_HELP.format(appname=appname, conf_name=appname),
window_layout_choices=', '.join(all_layouts)
appname=appname, config_help=CONFIG_HELP.format(appname=appname, conf_name=appname)
)
return options_spec.ans
@@ -555,12 +550,14 @@ def flatten_sequence_map(m):
def compare_opts(opts):
print('\nConfig options different from defaults:')
default_opts = load_config()
for f in sorted(defaults._fields):
if getattr(opts, f) != getattr(defaults, f):
if f in ('key_definitions', 'keymap', 'sequence_map'):
continue
print(title('{:20s}'.format(f)), getattr(opts, f))
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()}

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
@@ -44,7 +51,8 @@ You can use the |_ ls| command to get a list of windows. Note that for
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).
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
@@ -84,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:
@@ -274,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)',
@@ -374,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='',
)
@@ -534,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

@@ -2,7 +2,6 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import ast
import json
import os
import re
@@ -12,8 +11,9 @@ from contextlib import contextmanager
from . import fast_data_types as defines
from .config_utils import (
init_config, load_config as _load_config, merge_dicts, parse_config_base,
positive_float, positive_int, to_bool, to_cmdline, 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
@@ -92,10 +92,61 @@ def parse_shortcut(sc):
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):
@@ -104,30 +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)
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] = ''
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 = to_cmdline(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)
@@ -221,8 +254,7 @@ 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, key_definitions):
@@ -245,14 +277,24 @@ def to_modifiers(val):
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):
@@ -343,7 +385,9 @@ type_map = {
'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,
@@ -355,6 +399,9 @@ type_map = {
'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 (

View File

@@ -2,6 +2,7 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import ast
import os
import re
import shlex
@@ -38,6 +39,10 @@ def to_cmdline(x):
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('#'):
@@ -48,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:
@@ -165,3 +170,73 @@ def init_config(defaults_path, parse_config):
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, 1)
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

@@ -197,6 +197,7 @@ PyInit_fast_data_types(void) {
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

@@ -58,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
@@ -71,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];
@@ -178,7 +180,7 @@ typedef struct {
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;

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;

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

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

View File

@@ -365,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) {
@@ -385,12 +385,14 @@ create_os_window(PyObject UNUSED *self, PyObject *args) {
Py_DECREF(ret);
#ifdef __APPLE__
cocoa_create_global_menu();
if (OPT(macos_option_as_alt)) glfwSetCocoaTextInputFilter(glfw_window, filter_option);
// 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;
@@ -765,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[] = {
@@ -778,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),

View File

@@ -208,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
}

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

192
kitty/keys.h generated
View File

@@ -761,7 +761,7 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
case 52: // TAB
return "\x01\x09";
case 53: // BACKSPACE
return "\x01\x7f";
return "\x01\x08";
case 54: // INSERT
return "\x06\x1b\x5b\x32\x3b\x35\x7e";
case 55: // DELETE
@@ -1230,6 +1230,8 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
} // end switch(key)
case 0x6:
switch(key & 0x7f) { default: return NULL;
case 0: // SPACE
return "\x02\x1b\x00";
case 18: // A
return "\x02\x1b\x01";
case 19: // B
@@ -1291,49 +1293,49 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
case 53: // BACKSPACE
return "\x01\x7f";
case 54: // INSERT
return "\x04\x1b\x5b\x32\x7e";
return "\x06\x1b\x5b\x32\x3b\x37\x7e";
case 55: // DELETE
return "\x04\x1b\x5b\x33\x7e";
return "\x06\x1b\x5b\x33\x3b\x37\x7e";
case 56: // RIGHT
return "\x03\x1b\x5b\x43";
return "\x06\x1b\x5b\x31\x3b\x37\x43";
case 57: // LEFT
return "\x03\x1b\x5b\x44";
return "\x06\x1b\x5b\x31\x3b\x37\x44";
case 58: // DOWN
return "\x03\x1b\x5b\x42";
return "\x06\x1b\x5b\x31\x3b\x37\x42";
case 59: // UP
return "\x03\x1b\x5b\x41";
return "\x06\x1b\x5b\x31\x3b\x37\x41";
case 60: // PAGE_UP
return "\x04\x1b\x5b\x35\x7e";
return "\x06\x1b\x5b\x35\x3b\x37\x7e";
case 61: // PAGE_DOWN
return "\x04\x1b\x5b\x36\x7e";
return "\x06\x1b\x5b\x36\x3b\x37\x7e";
case 62: // HOME
return "\x03\x1b\x5b\x48";
return "\x06\x1b\x5b\x31\x3b\x37\x48";
case 63: // END
return "\x03\x1b\x5b\x46";
return "\x06\x1b\x5b\x31\x3b\x37\x46";
case 69: // F1
return "\x03\x1b\x4f\x50";
return "\x06\x1b\x5b\x31\x3b\x37\x50";
case 70: // F2
return "\x03\x1b\x4f\x51";
return "\x06\x1b\x5b\x31\x3b\x37\x51";
case 71: // F3
return "\x03\x1b\x4f\x52";
return "\x06\x1b\x5b\x31\x3b\x37\x52";
case 72: // F4
return "\x03\x1b\x4f\x53";
return "\x06\x1b\x5b\x31\x3b\x37\x53";
case 73: // F5
return "\x05\x1b\x5b\x31\x35\x7e";
return "\x07\x1b\x5b\x31\x35\x3b\x37\x7e";
case 74: // F6
return "\x05\x1b\x5b\x31\x37\x7e";
return "\x07\x1b\x5b\x31\x37\x3b\x37\x7e";
case 75: // F7
return "\x05\x1b\x5b\x31\x38\x7e";
return "\x07\x1b\x5b\x31\x38\x3b\x37\x7e";
case 76: // F8
return "\x05\x1b\x5b\x31\x39\x7e";
return "\x07\x1b\x5b\x31\x39\x3b\x37\x7e";
case 77: // F9
return "\x05\x1b\x5b\x32\x30\x7e";
return "\x07\x1b\x5b\x32\x30\x3b\x37\x7e";
case 78: // F10
return "\x05\x1b\x5b\x32\x31\x7e";
return "\x07\x1b\x5b\x32\x31\x3b\x37\x7e";
case 79: // F11
return "\x05\x1b\x5b\x32\x33\x7e";
return "\x07\x1b\x5b\x32\x33\x3b\x37\x7e";
case 80: // F12
return "\x05\x1b\x5b\x32\x34\x7e";
return "\x07\x1b\x5b\x32\x34\x3b\x37\x7e";
case 81: // F13
return "\x06\x1b\x5b\x31\x3b\x32\x50";
case 82: // F14
@@ -2384,7 +2386,7 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
case 52: // TAB
return "\x01\x09";
case 53: // BACKSPACE
return "\x01\x7f";
return "\x01\x08";
case 54: // INSERT
return "\x06\x1b\x5b\x32\x3b\x35\x7e";
case 55: // DELETE
@@ -2853,6 +2855,8 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
} // end switch(key)
case 0x6:
switch(key & 0x7f) { default: return NULL;
case 0: // SPACE
return "\x02\x1b\x00";
case 18: // A
return "\x02\x1b\x01";
case 19: // B
@@ -2914,49 +2918,49 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
case 53: // BACKSPACE
return "\x01\x7f";
case 54: // INSERT
return "\x04\x1b\x5b\x32\x7e";
return "\x06\x1b\x5b\x32\x3b\x37\x7e";
case 55: // DELETE
return "\x04\x1b\x5b\x33\x7e";
return "\x06\x1b\x5b\x33\x3b\x37\x7e";
case 56: // RIGHT
return "\x03\x1b\x5b\x43";
return "\x06\x1b\x5b\x31\x3b\x37\x43";
case 57: // LEFT
return "\x03\x1b\x5b\x44";
return "\x06\x1b\x5b\x31\x3b\x37\x44";
case 58: // DOWN
return "\x03\x1b\x5b\x42";
return "\x06\x1b\x5b\x31\x3b\x37\x42";
case 59: // UP
return "\x03\x1b\x5b\x41";
return "\x06\x1b\x5b\x31\x3b\x37\x41";
case 60: // PAGE_UP
return "\x04\x1b\x5b\x35\x7e";
return "\x06\x1b\x5b\x35\x3b\x37\x7e";
case 61: // PAGE_DOWN
return "\x04\x1b\x5b\x36\x7e";
return "\x06\x1b\x5b\x36\x3b\x37\x7e";
case 62: // HOME
return "\x03\x1b\x5b\x48";
return "\x06\x1b\x5b\x31\x3b\x37\x48";
case 63: // END
return "\x03\x1b\x5b\x46";
return "\x06\x1b\x5b\x31\x3b\x37\x46";
case 69: // F1
return "\x03\x1b\x4f\x50";
return "\x06\x1b\x5b\x31\x3b\x37\x50";
case 70: // F2
return "\x03\x1b\x4f\x51";
return "\x06\x1b\x5b\x31\x3b\x37\x51";
case 71: // F3
return "\x03\x1b\x4f\x52";
return "\x06\x1b\x5b\x31\x3b\x37\x52";
case 72: // F4
return "\x03\x1b\x4f\x53";
return "\x06\x1b\x5b\x31\x3b\x37\x53";
case 73: // F5
return "\x05\x1b\x5b\x31\x35\x7e";
return "\x07\x1b\x5b\x31\x35\x3b\x37\x7e";
case 74: // F6
return "\x05\x1b\x5b\x31\x37\x7e";
return "\x07\x1b\x5b\x31\x37\x3b\x37\x7e";
case 75: // F7
return "\x05\x1b\x5b\x31\x38\x7e";
return "\x07\x1b\x5b\x31\x38\x3b\x37\x7e";
case 76: // F8
return "\x05\x1b\x5b\x31\x39\x7e";
return "\x07\x1b\x5b\x31\x39\x3b\x37\x7e";
case 77: // F9
return "\x05\x1b\x5b\x32\x30\x7e";
return "\x07\x1b\x5b\x32\x30\x3b\x37\x7e";
case 78: // F10
return "\x05\x1b\x5b\x32\x31\x7e";
return "\x07\x1b\x5b\x32\x31\x3b\x37\x7e";
case 79: // F11
return "\x05\x1b\x5b\x32\x33\x7e";
return "\x07\x1b\x5b\x32\x33\x3b\x37\x7e";
case 80: // F12
return "\x05\x1b\x5b\x32\x34\x7e";
return "\x07\x1b\x5b\x32\x34\x3b\x37\x7e";
case 81: // F13
return "\x06\x1b\x5b\x31\x3b\x32\x50";
case 82: // F14
@@ -4016,7 +4020,7 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
case 52: // TAB
return "\x01\x09";
case 53: // BACKSPACE
return "\x01\x7f";
return "\x01\x08";
case 54: // INSERT
return "\x06\x1b\x5b\x32\x3b\x35\x7e";
case 55: // DELETE
@@ -4485,6 +4489,8 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
} // end switch(key)
case 0x6:
switch(key & 0x7f) { default: return NULL;
case 0: // SPACE
return "\x02\x1b\x00";
case 18: // A
return "\x02\x1b\x01";
case 19: // B
@@ -4546,49 +4552,49 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
case 53: // BACKSPACE
return "\x01\x7f";
case 54: // INSERT
return "\x04\x1b\x5b\x32\x7e";
return "\x06\x1b\x5b\x32\x3b\x37\x7e";
case 55: // DELETE
return "\x04\x1b\x5b\x33\x7e";
return "\x06\x1b\x5b\x33\x3b\x37\x7e";
case 56: // RIGHT
return "\x03\x1b\x4f\x43";
return "\x06\x1b\x5b\x31\x3b\x37\x43";
case 57: // LEFT
return "\x03\x1b\x4f\x44";
return "\x06\x1b\x5b\x31\x3b\x37\x44";
case 58: // DOWN
return "\x03\x1b\x4f\x42";
return "\x06\x1b\x5b\x31\x3b\x37\x42";
case 59: // UP
return "\x03\x1b\x4f\x41";
return "\x06\x1b\x5b\x31\x3b\x37\x41";
case 60: // PAGE_UP
return "\x04\x1b\x5b\x35\x7e";
return "\x06\x1b\x5b\x35\x3b\x37\x7e";
case 61: // PAGE_DOWN
return "\x04\x1b\x5b\x36\x7e";
return "\x06\x1b\x5b\x36\x3b\x37\x7e";
case 62: // HOME
return "\x03\x1b\x4f\x48";
return "\x06\x1b\x5b\x31\x3b\x37\x48";
case 63: // END
return "\x03\x1b\x4f\x46";
return "\x06\x1b\x5b\x31\x3b\x37\x46";
case 69: // F1
return "\x03\x1b\x4f\x50";
return "\x06\x1b\x5b\x31\x3b\x37\x50";
case 70: // F2
return "\x03\x1b\x4f\x51";
return "\x06\x1b\x5b\x31\x3b\x37\x51";
case 71: // F3
return "\x03\x1b\x4f\x52";
return "\x06\x1b\x5b\x31\x3b\x37\x52";
case 72: // F4
return "\x03\x1b\x4f\x53";
return "\x06\x1b\x5b\x31\x3b\x37\x53";
case 73: // F5
return "\x05\x1b\x5b\x31\x35\x7e";
return "\x07\x1b\x5b\x31\x35\x3b\x37\x7e";
case 74: // F6
return "\x05\x1b\x5b\x31\x37\x7e";
return "\x07\x1b\x5b\x31\x37\x3b\x37\x7e";
case 75: // F7
return "\x05\x1b\x5b\x31\x38\x7e";
return "\x07\x1b\x5b\x31\x38\x3b\x37\x7e";
case 76: // F8
return "\x05\x1b\x5b\x31\x39\x7e";
return "\x07\x1b\x5b\x31\x39\x3b\x37\x7e";
case 77: // F9
return "\x05\x1b\x5b\x32\x30\x7e";
return "\x07\x1b\x5b\x32\x30\x3b\x37\x7e";
case 78: // F10
return "\x05\x1b\x5b\x32\x31\x7e";
return "\x07\x1b\x5b\x32\x31\x3b\x37\x7e";
case 79: // F11
return "\x05\x1b\x5b\x32\x33\x7e";
return "\x07\x1b\x5b\x32\x33\x3b\x37\x7e";
case 80: // F12
return "\x05\x1b\x5b\x32\x34\x7e";
return "\x07\x1b\x5b\x32\x34\x3b\x37\x7e";
case 81: // F13
return "\x06\x1b\x5b\x31\x3b\x32\x50";
case 82: // F14
@@ -5639,7 +5645,7 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
case 52: // TAB
return "\x01\x09";
case 53: // BACKSPACE
return "\x01\x7f";
return "\x01\x08";
case 54: // INSERT
return "\x06\x1b\x5b\x32\x3b\x35\x7e";
case 55: // DELETE
@@ -6108,6 +6114,8 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
} // end switch(key)
case 0x6:
switch(key & 0x7f) { default: return NULL;
case 0: // SPACE
return "\x02\x1b\x00";
case 18: // A
return "\x02\x1b\x01";
case 19: // B
@@ -6169,49 +6177,49 @@ key_lookup(uint8_t key, KeyboardMode mode, uint8_t mods, uint8_t action) {
case 53: // BACKSPACE
return "\x01\x7f";
case 54: // INSERT
return "\x04\x1b\x5b\x32\x7e";
return "\x06\x1b\x5b\x32\x3b\x37\x7e";
case 55: // DELETE
return "\x04\x1b\x5b\x33\x7e";
return "\x06\x1b\x5b\x33\x3b\x37\x7e";
case 56: // RIGHT
return "\x03\x1b\x4f\x43";
return "\x06\x1b\x5b\x31\x3b\x37\x43";
case 57: // LEFT
return "\x03\x1b\x4f\x44";
return "\x06\x1b\x5b\x31\x3b\x37\x44";
case 58: // DOWN
return "\x03\x1b\x4f\x42";
return "\x06\x1b\x5b\x31\x3b\x37\x42";
case 59: // UP
return "\x03\x1b\x4f\x41";
return "\x06\x1b\x5b\x31\x3b\x37\x41";
case 60: // PAGE_UP
return "\x04\x1b\x5b\x35\x7e";
return "\x06\x1b\x5b\x35\x3b\x37\x7e";
case 61: // PAGE_DOWN
return "\x04\x1b\x5b\x36\x7e";
return "\x06\x1b\x5b\x36\x3b\x37\x7e";
case 62: // HOME
return "\x03\x1b\x4f\x48";
return "\x06\x1b\x5b\x31\x3b\x37\x48";
case 63: // END
return "\x03\x1b\x4f\x46";
return "\x06\x1b\x5b\x31\x3b\x37\x46";
case 69: // F1
return "\x03\x1b\x4f\x50";
return "\x06\x1b\x5b\x31\x3b\x37\x50";
case 70: // F2
return "\x03\x1b\x4f\x51";
return "\x06\x1b\x5b\x31\x3b\x37\x51";
case 71: // F3
return "\x03\x1b\x4f\x52";
return "\x06\x1b\x5b\x31\x3b\x37\x52";
case 72: // F4
return "\x03\x1b\x4f\x53";
return "\x06\x1b\x5b\x31\x3b\x37\x53";
case 73: // F5
return "\x05\x1b\x5b\x31\x35\x7e";
return "\x07\x1b\x5b\x31\x35\x3b\x37\x7e";
case 74: // F6
return "\x05\x1b\x5b\x31\x37\x7e";
return "\x07\x1b\x5b\x31\x37\x3b\x37\x7e";
case 75: // F7
return "\x05\x1b\x5b\x31\x38\x7e";
return "\x07\x1b\x5b\x31\x38\x3b\x37\x7e";
case 76: // F8
return "\x05\x1b\x5b\x31\x39\x7e";
return "\x07\x1b\x5b\x31\x39\x3b\x37\x7e";
case 77: // F9
return "\x05\x1b\x5b\x32\x30\x7e";
return "\x07\x1b\x5b\x32\x30\x3b\x37\x7e";
case 78: // F10
return "\x05\x1b\x5b\x32\x31\x7e";
return "\x07\x1b\x5b\x32\x31\x3b\x37\x7e";
case 79: // F11
return "\x05\x1b\x5b\x32\x33\x7e";
return "\x07\x1b\x5b\x32\x33\x3b\x37\x7e";
case 80: // F12
return "\x05\x1b\x5b\x32\x34\x7e";
return "\x07\x1b\x5b\x32\x34\x3b\x37\x7e";
case 81: // F13
return "\x06\x1b\x5b\x31\x3b\x32\x50";
case 82: // F14

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',
@@ -47,6 +49,9 @@ SHIFTED_KEYS = {
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',
@@ -64,6 +69,7 @@ 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)
@@ -72,6 +78,7 @@ for f in range(1, 13):
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)
@@ -211,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):
@@ -229,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)
@@ -261,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')

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 of 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 {{{
@@ -190,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
@@ -250,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
@@ -318,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.
@@ -356,7 +379,7 @@ kitty_mod ctrl+shift
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
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:
@@ -385,6 +408,8 @@ 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
@@ -450,21 +475,21 @@ map kitty_mod+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 kitty_mod+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 kitty_mod+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 kitty_mod+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 kitty_mod+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 kitty_mod+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
@@ -474,9 +499,12 @@ map kitty_mod+p>w run_kitten text hints --type word --program -
map kitty_mod+f11 toggle_fullscreen
map kitty_mod+u input_unicode_character
map kitty_mod+f2 edit_config_file
You can customize how the URLs are
# Open the kitty shell in a new window/tab/overlay/os_window to control kitty using commands.
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
@@ -496,7 +524,7 @@ map kitty_mod+escape kitty_shell window
# map ctrl+alt+a send_text application Word\x1bOH
# }}}
# You can have kitty remove all shortcut definition seen upto this point. Useful for
# 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
# }}}

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,24 +79,134 @@ 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 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]
@@ -180,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:
@@ -195,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
@@ -222,82 +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
# 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
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
@@ -309,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:
@@ -329,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])
@@ -362,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])
@@ -386,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])
@@ -414,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

@@ -81,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; } \
@@ -109,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,9 +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)
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
@@ -56,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

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;

View File

@@ -39,10 +39,14 @@ will only work if this process is run within an existing kitty window.
'''.format, appname=appname)
def encode_send(send):
send = ('@kitty-cmd' + json.dumps(send)).encode('ascii')
return b'\x1bP' + send + b'\x1b\\'
def do_io(to, send, no_response):
import socket
send = ('@kitty-cmd' + json.dumps(send)).encode('ascii')
send = b'\x1bP' + send + b'\x1b\\'
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)

View File

@@ -446,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);
}
}
// }}}
@@ -658,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
@@ -668,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;
}
@@ -695,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));
}
}
@@ -775,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) { \
@@ -838,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);
}
}
@@ -856,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;
@@ -869,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) {
@@ -876,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
@@ -933,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.
@@ -1194,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, ""); }
@@ -1205,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];
@@ -1457,12 +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, callback, as_ansi);
as_text_generic(args, self, range_line_, self->lines, self->columns);
}
static PyObject*
@@ -1519,8 +1538,8 @@ screen_wcswidth(PyObject UNUSED *self, PyObject *str) {
static PyObject*
screen_truncate_point_for_length(PyObject UNUSED *self, PyObject *args) {
PyObject *str; unsigned int num_cells;
if (!PyArg_ParseTuple(args, "OI", &str, &num_cells)) return NULL;
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);
@@ -1529,7 +1548,7 @@ screen_truncate_point_for_length(PyObject UNUSED *self, PyObject *args) {
int prev_width = 0;
bool in_sgr = false;
unsigned long width_so_far = 0;
for (i = 0; i < len && width_so_far < num_cells; i++) {
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;

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

@@ -209,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;
@@ -239,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);
@@ -394,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;
}
@@ -414,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;
@@ -429,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);
@@ -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,11 +535,14 @@ 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;
glUniform3f(border_uniform_locations[BORDER_default_bg], CV3(default_bg));

View File

@@ -81,6 +81,7 @@ add_os_window() {
memset(ans, 0, sizeof(OSWindow));
ans->id = ++global_state.os_window_id_counter;
ans->tab_bar_render_data.vao_idx = create_cell_vao();
ans->background_opacity = OPT(background_opacity);
END_WITH_OS_WINDOW_REFS
return ans;
}
@@ -328,6 +329,10 @@ set_special_keys(PyObject *dict) {
}}
}
PYWRAP0(next_window_id) {
return PyLong_FromUnsignedLongLong(global_state.window_id_counter + 1);
}
PYWRAP1(handle_for_window_id) {
id_type os_window_id;
PA("K", &os_window_id);
@@ -355,6 +360,8 @@ PYWRAP1(set_options) {
S(cursor_blink_interval, PyFloat_AsDouble);
S(cursor_stop_blinking_after, PyFloat_AsDouble);
S(background_opacity, PyFloat_AsDouble);
S(dim_opacity, PyFloat_AsDouble);
S(dynamic_background_opacity, PyObject_IsTrue);
S(inactive_text_alpha, PyFloat_AsDouble);
S(window_padding_width, PyFloat_AsDouble);
S(cursor_shape, PyLong_AsLong);
@@ -498,6 +505,26 @@ PYWRAP1(mark_tab_bar_dirty) {
Py_RETURN_NONE;
}
PYWRAP1(change_background_opacity) {
id_type os_window_id;
float opacity;
PA("Kf", &os_window_id, &opacity);
WITH_OS_WINDOW(os_window_id)
os_window->background_opacity = opacity;
os_window->is_damaged = true;
Py_RETURN_TRUE;
END_WITH_OS_WINDOW
Py_RETURN_FALSE;
}
PYWRAP1(background_opacity_of) {
id_type os_window_id = PyLong_AsUnsignedLongLong(args);
WITH_OS_WINDOW(os_window_id)
return PyFloat_FromDouble((double)os_window->background_opacity);
END_WITH_OS_WINDOW
Py_RETURN_NONE;
}
static inline bool
fix_window_idx(Tab *tab, id_type window_id, unsigned int *window_idx) {
for (id_type fix = 0; fix < tab->num_windows; fix++) {
@@ -600,6 +627,7 @@ KK5I(add_borders_rect)
static PyMethodDef module_methods[] = {
MW(current_os_window, METH_NOARGS),
MW(next_window_id, METH_NOARGS),
MW(set_options, METH_VARARGS),
MW(set_in_sequence_mode, METH_O),
MW(resolve_key_mods, METH_VARARGS),
@@ -624,6 +652,8 @@ static PyMethodDef module_methods[] = {
MW(mark_os_window_for_close, METH_VARARGS),
MW(set_titlebar_color, METH_VARARGS),
MW(mark_tab_bar_dirty, METH_O),
MW(change_background_opacity, METH_VARARGS),
MW(background_opacity_of, METH_O),
MW(update_window_visibility, METH_VARARGS),
MW(set_boss, METH_O),
MW(set_display_state, METH_VARARGS),

View File

@@ -26,7 +26,8 @@ typedef struct {
bool macos_option_as_alt, macos_hide_titlebar, macos_hide_from_tasks;
int adjust_line_height_px, adjust_column_width_px;
float adjust_line_height_frac, adjust_column_width_frac;
float background_opacity;
float background_opacity, dim_opacity;
bool dynamic_background_opacity;
float inactive_text_alpha;
float window_padding_width;
Edge tab_bar_edge;
@@ -118,6 +119,7 @@ typedef struct {
uint32_t offscreen_texture_id;
unsigned int clear_count;
color_type last_titlebar_color;
float background_opacity;
} OSWindow;
@@ -171,7 +173,7 @@ OSWindow* add_os_window();
OSWindow* current_os_window();
void os_window_regions(OSWindow*, Region *main, Region *tab_bar);
bool drag_scroll(Window *, OSWindow*);
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, unsigned int);
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, unsigned int, OSWindow *w);
ssize_t create_cell_vao();
ssize_t create_graphics_vao();
ssize_t create_border_vao();

View File

@@ -12,10 +12,10 @@ from .config import build_ansi_color_table
from .constants import WindowGeometry, appname, get_boss, is_macos, is_wayland
from .fast_data_types import (
DECAWM, Screen, add_tab, glfw_post_empty_event, mark_tab_bar_dirty,
remove_tab, remove_window, set_active_tab, set_tab_bar_render_data,
swap_tabs, viewport_for_window, x11_window_id
next_window_id, pt_to_px, remove_tab, remove_window, set_active_tab,
set_tab_bar_render_data, swap_tabs, viewport_for_window, x11_window_id
)
from .layout import Rect, all_layouts
from .layout import Rect, create_layout_object_for, evict_cached_layouts
from .session import resolved_shell
from .utils import color_as_int, log_error
from .window import Window, calculate_gl_geometry
@@ -38,6 +38,8 @@ class Tab: # {{{
if not self.id:
raise Exception('No OS window with id {} found, or tab counter has wrapped'.format(self.os_window_id))
self.opts, self.args = tab_manager.opts, tab_manager.args
self.margin_width, self.padding_width = map(pt_to_px, (
self.opts.window_margin_width, self.opts.window_padding_width))
self.name = getattr(session_tab, 'name', '')
self.enabled_layouts = [x.lower() for x in getattr(session_tab, 'enabled_layouts', None) or self.opts.enabled_layouts]
self.borders = Borders(self.os_window_id, self.id, self.opts)
@@ -137,14 +139,16 @@ class Tab: # {{{
if w is not None:
w.change_titlebar_color()
def create_layout_object(self, idx):
return all_layouts[idx](self.os_window_id, self.id, self.opts, self.borders.border_width)
def create_layout_object(self, name):
return create_layout_object_for(name, self.os_window_id, self.id, self.margin_width, self.padding_width, self.borders.border_width)
def next_layout(self):
if len(self.enabled_layouts) > 1:
try:
idx = self.enabled_layouts.index(self.current_layout.name)
except Exception:
for i, layout_name in enumerate(self.enabled_layouts):
if layout_name == self.current_layout.full_name:
idx = i
break
else:
idx = -1
nl = self.enabled_layouts[(idx + 1) % len(self.enabled_layouts)]
self.current_layout = self.create_layout_object(nl)
@@ -158,6 +162,17 @@ class Tab: # {{{
self.current_layout = self.create_layout_object(layout_name)
self.relayout()
def resize_window_by(self, window_id, increment, is_horizontal):
increment_as_percent = self.current_layout.bias_increment_for_cell(is_horizontal) * increment
if self.current_layout.modify_size_of_window(self.windows, window_id, increment_as_percent, is_horizontal):
self.relayout()
return
return 'Could not resize'
def reset_window_sizes(self):
if self.current_layout.remove_all_biases():
self.relayout()
def launch_child(self, use_shell=False, cmd=None, stdin=None, cwd_from=None, cwd=None, env=None):
if cmd is None:
if use_shell:
@@ -167,6 +182,7 @@ class Tab: # {{{
fenv = {}
if env:
fenv.update(env)
fenv['KITTY_WINDOW_ID'] = str(next_window_id())
if not is_macos and not is_wayland:
try:
fenv['WINDOWID'] = str(x11_window_id(self.os_window_id))
@@ -274,6 +290,7 @@ class Tab: # {{{
return window in self.windows
def destroy(self):
evict_cached_layouts(self.id)
for w in self.windows:
w.destroy()
self.windows = deque()

View File

@@ -13,7 +13,9 @@ import sys
from contextlib import contextmanager
from time import monotonic
from .constants import appname, is_macos, is_wayland
from .constants import (
appname, is_macos, is_wayland, supports_primary_selection
)
from .fast_data_types import (
GLSL_VERSION, log_error_string, redirect_std_streams, x11_display,
x11_window_id
@@ -76,8 +78,50 @@ def parse_color_set(raw):
continue
def screen_size_function(fd=None):
ans = getattr(screen_size_function, 'ans', None)
if ans is None:
from collections import namedtuple
import array
import fcntl
import termios
Size = namedtuple('Size', 'rows cols width height cell_width cell_height')
if fd is None:
fd = sys.stdout
def screen_size():
if screen_size.changed:
buf = array.array('H', [0, 0, 0, 0])
fcntl.ioctl(fd, termios.TIOCGWINSZ, buf)
rows, cols, width, height = tuple(buf)
cell_width, cell_height = width // (cols or 1), height // (rows or 1)
screen_size.ans = Size(rows, cols, width, height, cell_width, cell_height)
screen_size.changed = False
return screen_size.ans
screen_size.changed = True
screen_size.Size = Size
ans = screen_size_function.ans = screen_size
return ans
def fit_image(width, height, pwidth, pheight):
from math import floor
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 set_primary_selection(text):
if is_macos or is_wayland:
if not supports_primary_selection:
return # There is no primary selection
if isinstance(text, bytes):
text = text.decode('utf-8')
@@ -86,7 +130,7 @@ def set_primary_selection(text):
def get_primary_selection():
if is_macos or is_wayland:
if not supports_primary_selection:
return '' # There is no primary selection
from kitty.fast_data_types import get_primary_selection
return (get_primary_selection() or b'').decode('utf-8', 'replace')
@@ -291,7 +335,7 @@ def make_fd_non_blocking(fd):
@contextmanager
def non_blocking_read(src=sys.stdin):
def non_blocking_read(src=sys.stdin, disable_echo=False):
import termios
import tty
import fcntl
@@ -299,6 +343,10 @@ def non_blocking_read(src=sys.stdin):
if src.isatty():
old = termios.tcgetattr(fd)
tty.setraw(fd)
if disable_echo:
new = list(old)
new[3] |= termios.ECHO
termios.tcsetattr(fd, termios.TCSANOW, new)
oldfl = make_fd_non_blocking(fd)
yield fd
if src.isatty():

View File

@@ -15,19 +15,20 @@ from .constants import (
)
from .fast_data_types import (
BLIT_PROGRAM, CELL_BG_PROGRAM, CELL_FG_PROGRAM, CELL_PROGRAM,
CELL_SPECIAL_PROGRAM, CSI, CURSOR_PROGRAM, DCS, GRAPHICS_PREMULT_PROGRAM,
GRAPHICS_PROGRAM, OSC, SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE, Screen,
add_window, compile_program, glfw_post_empty_event, init_cell_program,
init_cursor_program, set_clipboard_string, set_titlebar_color,
set_window_render_data, update_window_title, update_window_visibility,
viewport_for_window
CELL_SPECIAL_PROGRAM, CSI, CURSOR_PROGRAM, DCS, DECORATION, DIM,
GRAPHICS_PREMULT_PROGRAM, GRAPHICS_PROGRAM, OSC, REVERSE, SCROLL_FULL,
SCROLL_LINE, SCROLL_PAGE, STRIKETHROUGH, Screen, add_window,
compile_program, get_clipboard_string, glfw_post_empty_event,
init_cell_program, init_cursor_program, set_clipboard_string,
set_titlebar_color, set_window_render_data, update_window_title,
update_window_visibility, viewport_for_window
)
from .keys import keyboard_mode_name
from .rgb import to_color
from .terminfo import get_capabilities
from .utils import (
color_as_int, load_shaders, open_cmd, open_url, parse_color_set,
sanitize_title
color_as_int, get_primary_selection, load_shaders, log_error, open_cmd,
open_url, parse_color_set, sanitize_title, set_primary_selection
)
@@ -64,6 +65,8 @@ def load_shader_programs(semi_transparent=0):
'FOREGROUND': CELL_FG_PROGRAM,
}.items():
vv, ff = v.replace('WHICH_PROGRAM', which), f.replace('WHICH_PROGRAM', which)
for gln, pyn in {'REVERSE_SHIFT': REVERSE, 'STRIKE_SHIFT': STRIKETHROUGH, 'DIM_SHIFT': DIM, 'DECORATION_SHIFT': DECORATION}.items():
vv = vv.replace('{{{}}}'.format(gln), str(pyn), 1)
if semi_transparent:
vv = vv.replace('#define NOT_TRANSPARENT', '#define TRANSPARENT')
ff = ff.replace('#define NOT_TRANSPARENT', '#define TRANSPARENT')
@@ -328,9 +331,46 @@ class Window:
def handle_remote_cmd(self, cmd):
get_boss().handle_remote_cmd(cmd, self)
def handle_remote_print(self, msg):
from base64 import standard_b64decode
msg = standard_b64decode(msg).decode('utf-8')
print(msg, end='', file=sys.stderr)
sys.stderr.flush()
def send_cmd_response(self, response):
self.screen.send_escape_code_to_child(DCS, '@kitty-cmd' + json.dumps(response))
def clipboard_control(self, data):
where, text = data.partition(';')[::2]
if text == '?':
response = None
if 's' in where or 'c' in where:
response = get_clipboard_string() if 'read-clipboard' in self.opts.clipboard_control else ''
loc = 'c'
elif 'p' in where:
response = get_primary_selection() if 'read-primary' in self.opts.clipboard_control else ''
loc = 'p'
response = response or ''
from base64 import standard_b64encode
self.screen.send_escape_code_to_child(OSC, '52;{};{}'.format(
loc, standard_b64encode(response.encode('utf-8')).decode('ascii')))
else:
from base64 import standard_b64decode
try:
text = standard_b64decode(text).decode('utf-8')
except Exception:
log_error('Invalid data to write to clipboard received, ignoring')
return
if 's' in where or 'c' in where:
if 'write-clipboard' not in self.opts.clipboard_control:
set_clipboard_string(text)
if 'p' in where:
if self.opts.copy_on_select:
if 'write-clipboard' in self.opts.clipboard_control:
set_clipboard_string(text)
if 'write-primary' in self.opts.clipboard_control:
set_primary_selection(text)
# }}}
def text_for_selection(self):
@@ -343,11 +383,11 @@ class Window:
self.screen.reset_callbacks()
self.screen = None
def as_text(self, as_ansi=False, add_history=False):
def as_text(self, as_ansi=False, add_history=False, add_wrap_markers=False):
lines = []
add_history = add_history and not self.screen.is_using_alternate_linebuf()
f = self.screen.as_text_non_visual if add_history else self.screen.as_text
f(lines.append, as_ansi)
f(lines.append, as_ansi, add_wrap_markers)
if add_history:
h = []
self.screen.historybuf.as_text(h.append, as_ansi)

View File

@@ -52,7 +52,7 @@ def filled_line_buf(ynum=5, xnum=5, cursor=Cursor()):
def filled_cursor():
ans = Cursor()
ans.bold = ans.italic = ans.reverse = ans.strikethrough = True
ans.bold = ans.italic = ans.reverse = ans.strikethrough = ans.dim = True
ans.fg = 0x101
ans.bg = 0x201
ans.decoration_fg = 0x301

View File

@@ -179,7 +179,7 @@ class TestDataTypes(BaseTest):
self.assertEqual(str(l0), 'a' + t[1:])
c = C(3, 5)
c.bold = c.italic = c.reverse = c.strikethrough = True
c.bold = c.italic = c.reverse = c.strikethrough = c.dim = True
c.fg = c.bg = c.decoration_fg = 0x0101
self.ae(c, c)
c2, c3 = c.copy(), c.copy()
@@ -219,7 +219,7 @@ class TestDataTypes(BaseTest):
l3.set_text(t, 0, len(t), C())
q = C()
q.bold = q.italic = q.reverse = q.strikethrough = True
q.bold = q.italic = q.reverse = q.strikethrough = c.dim = True
q.decoration = 2
c = C()
c.x = 3
@@ -450,12 +450,12 @@ class TestDataTypes(BaseTest):
self.ae(a, ['\x1b[0m' + str(lb.line(i)) + '\n' for i in range(lb.ynum)])
l2 = lb.line(0)
c = C()
c.bold = c.italic = c.reverse = c.strikethrough = True
c.bold = c.italic = c.reverse = c.strikethrough = c.dim = True
c.fg = (4 << 8) | 1
c.bg = (1 << 24) | (2 << 16) | (3 << 8) | 2
c.decoration_fg = (5 << 8) | 1
l2.set_text('1', 0, 1, c)
self.ae(l2.as_ansi(), '\x1b[0m\x1b[1;3;7;9;34;48:2:1:2:3;58:5:5m' '1'
self.ae(l2.as_ansi(), '\x1b[0m\x1b[1;2;3;7;9;34;48:2:1:2:3;58:5:5m' '1'
'\x1b[22;23;27;29;39;49;59m' '0000')
lb = filled_line_buf()
for i in range(lb.ynum):

View File

@@ -22,13 +22,25 @@ class TestDiff(BaseTest):
for src in (left, right):
self.assertEqual((prefix, suffix), (src[:pc], src[-sc:] if sc else ''))
def test_split_to_size(self):
from kittens.diff.render import split_to_size_with_center
def test_split_with_highlights(self):
from kittens.diff.render import split_with_highlights, Segment, truncate_points
self.ae(list(truncate_points('1234567890ab', 3)), [3, 6, 9])
for line, width, prefix_count, suffix_count, expected in [
('abcdefgh', 20, 2, 3, ('abSScdeEEfgh',)),
('abcdefgh', 20, 2, 0, ('abSScdefgh',)),
('abcdefgh', 3, 2, 3, ('abSSc', 'SSdeEEf', 'gh')),
('abcdefgh', 2, 4, 1, ('ab', 'cd', 'SSef', 'SSgEEh')),
('abcdefgh', 20, 2, 0, ('abSScdefghEE',)),
('abcdefgh', 3, 2, 3, ('abSScEE', 'SSdeEEf', 'gh')),
('abcdefgh', 2, 4, 1, ('ab', 'cd', 'SSefEE', 'SSgEEh')),
]:
self.ae(expected, tuple(split_to_size_with_center(
line, width, prefix_count, suffix_count, 'SS', 'EE')))
seg = Segment(prefix_count, 'SS')
seg.end = len(line) - suffix_count
seg.end_code = 'EE'
self.ae(expected, tuple(split_with_highlights(line, width, [], seg)))
def h(s, e, w):
ans = Segment(s, 'S{}S'.format(w))
ans.end = e
ans.end_code = 'E{}E'.format(w)
return ans
highlights = [h(0, 1, 1), h(1, 3, 2)]
self.ae(['S1SaE1ES2SbcE2Ed'], split_with_highlights('abcd', 10, highlights))

31
kitty_tests/hints.py Normal file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
from . import BaseTest
class TestHints(BaseTest):
def test_url_hints(self):
from kittens.hints.main import parse_hints_args, functions_for, mark, convert_text
args = parse_hints_args([])[0]
pattern, post_processors = functions_for(args)
def create_marks(text, cols=20):
text = convert_text(text, cols)
return tuple(mark(pattern, post_processors, text, args))
def t(text, url, cols=20):
marks = create_marks(text, cols)
urls = [m.text for m in marks]
self.ae(urls, [url])
u = 'http://test.me/'
t(u, 'http://test.me/')
t('"{}"'.format(u), u)
t('({})'.format(u), u)
t(u + '\nxxx', u + 'xxx', len(u))
t('link:{}[xxx]'.format(u), u)
t('`xyz <{}>`_.'.format(u), u)

View File

@@ -5,6 +5,7 @@
from . import BaseTest
from kitty.config import defaults
from kitty.layout import Stack, Horizontal, idx_for_id
from kitty.fast_data_types import pt_to_px
class Window:
@@ -25,7 +26,8 @@ class Window:
def create_layout(cls, opts=None, border_width=2):
if opts is None:
opts = defaults
ans = cls(1, 1, opts, border_width)
mw, pw = map(pt_to_px, (opts.window_margin_width, opts.window_padding_width))
ans = cls(1, 1, mw, pw, border_width)
ans.set_active_window_in_os_window = lambda idx: None
ans.swap_windows_in_os_window = lambda a, b: None
return ans

View File

@@ -118,8 +118,8 @@ class TestParser(BaseTest):
def sgr(params):
return (('select_graphic_rendition', '{} '.format(x)) for x in params.split())
pb('\033[1;3;4;7;9;34;44m', *sgr('1 3 4 7 9 34 44'))
for attr in 'bold italic reverse strikethrough'.split():
pb('\033[1;2;3;4;7;9;34;44m', *sgr('1 2 3 4 7 9 34 44'))
for attr in 'bold italic reverse strikethrough dim'.split():
self.assertTrue(getattr(s.cursor, attr))
self.ae(s.cursor.decoration, 1)
self.ae(s.cursor.fg, 4 << 8 | 1)
@@ -202,7 +202,7 @@ class TestParser(BaseTest):
c.clear()
pb('\033P$qm\033\\', ('screen_request_capabilities', ord('$'), 'm'))
self.ae(c.wtcbuf, b'\033P1$rm\033\\')
for sgr in '0;34;102;1;3;4 0;38:5:200;58:2:10:11:12'.split():
for sgr in '0;34;102;1;2;3;4 0;38:5:200;58:2:10:11:12'.split():
expected = set(sgr.split(';')) - {'0'}
c.clear()
parse_bytes(s, '\033[{}m\033P$qm\033\\'.format(sgr).encode('ascii'))

View File

@@ -317,6 +317,13 @@ class TestScreen(BaseTest):
c = chr(ord('I') + l - 2)
self.ae(c + ' ' * (s.columns - 2) + c.lower(), str(s.line(l)))
s.reset_mode(DECOM)
# Test that moving cursor outside the margins works as expected
s = self.create_screen(10, 10)
s.set_margins(5, 7)
s.cursor_position(0, 0)
self.ae(s.cursor.y, 0)
nl()
self.ae(s.cursor.y, 1)
def test_sgr(self):
s = self.create_screen()

View File

@@ -17,7 +17,7 @@ for line in open('kitty/kitty.conf'):
if line.startswith('map '):
_, sc, name = line.split(maxsplit=2)
sc = sc.replace('kitty_mod', 'ctrl+shift')
name = name.rstrip().replace(' ', '_').replace('-', '_').replace('___', '_').replace('__', '_').strip('_')
name = name.rstrip().replace(' ', '_').replace('-', '_').replace('+', 'plus').replace('.', '_').replace('___', '_').replace('__', '_').strip('_')
defns[name].append('`' + sc.replace('>', '') + '`')
defns = [
@@ -38,7 +38,7 @@ if raw != nraw:
open('README.asciidoc', 'w').write(nraw)
raw = subprocess.check_output([
'kitty', '-c',
'kitty', '+runpy',
'from kitty.key_encoding import *; import json; print(json.dumps(ENCODING))'
]).decode('utf-8')
key_map = json.loads(raw)

View File

@@ -38,7 +38,7 @@ def call(*cmd):
def run_build(args):
os.chdir(build_path)
call('./osx kitty')
call('./osx kitty --sign-installers')
call('./osx shutdown')

BIN
screenshots/diff.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB