Compare commits
182 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4af0599cc | ||
|
|
de4384a420 | ||
|
|
a33ebce3c9 | ||
|
|
219d3161d9 | ||
|
|
2e07159461 | ||
|
|
23d0f5ce32 | ||
|
|
f4792a1e3d | ||
|
|
2a5ef628fa | ||
|
|
4b259dd719 | ||
|
|
c127476c4e | ||
|
|
beded75547 | ||
|
|
74a6cdb3bb | ||
|
|
ba554fb01d | ||
|
|
39c98e83da | ||
|
|
73660f7119 | ||
|
|
46b4f33850 | ||
|
|
ad8e16aac0 | ||
|
|
ae6e082419 | ||
|
|
f46d3057ba | ||
|
|
8e0e4b8fd1 | ||
|
|
76da315f81 | ||
|
|
95de39feb9 | ||
|
|
c0f790c928 | ||
|
|
6fbb953aa4 | ||
|
|
30b38e9fa0 | ||
|
|
d20e801793 | ||
|
|
fe65b346c1 | ||
|
|
c5da1c0f1b | ||
|
|
3fd2f03f11 | ||
|
|
4ad4e28a9c | ||
|
|
d3edd2e73f | ||
|
|
3d37348c2b | ||
|
|
8a3e81b7ba | ||
|
|
a29c317217 | ||
|
|
48847cdaf8 | ||
|
|
a59cda8b60 | ||
|
|
e70b5114fb | ||
|
|
d2ab7fbb36 | ||
|
|
70352ca722 | ||
|
|
be28e7bf46 | ||
|
|
6b2b8e0208 | ||
|
|
c997dc8ea7 | ||
|
|
f52afbb396 | ||
|
|
a20e7e2a6b | ||
|
|
9903b8e8db | ||
|
|
1d18f63eb5 | ||
|
|
a63682b160 | ||
|
|
812f3e60ca | ||
|
|
f8f5838ab5 | ||
|
|
07782e071a | ||
|
|
624dfdf913 | ||
|
|
a01dad8c3c | ||
|
|
b6baa99fce | ||
|
|
912b395316 | ||
|
|
b1b90d2bf7 | ||
|
|
37bd964fee | ||
|
|
fa3e173c18 | ||
|
|
2a9dc23c61 | ||
|
|
2885f54360 | ||
|
|
ac2f4c8ab6 | ||
|
|
7282d8edc1 | ||
|
|
6f2d63eb87 | ||
|
|
9cab8a2de5 | ||
|
|
709d19119d | ||
|
|
029b82807c | ||
|
|
67c0942e01 | ||
|
|
fb57653665 | ||
|
|
646a349d82 | ||
|
|
ad1109b6fe | ||
|
|
c2bd6a66d9 | ||
|
|
ba9af3df8c | ||
|
|
5d4e968f50 | ||
|
|
0e327f3d13 | ||
|
|
bb06bfa627 | ||
|
|
2a713cab60 | ||
|
|
26df57a1c7 | ||
|
|
18893f86ce | ||
|
|
8fead5ebe8 | ||
|
|
6020aadbf2 | ||
|
|
f8b6b532e0 | ||
|
|
05ec174868 | ||
|
|
3875ee021a | ||
|
|
2931afa0ae | ||
|
|
2803e45380 | ||
|
|
965dde599e | ||
|
|
229ea45d81 | ||
|
|
8ea84c97d5 | ||
|
|
7c47bd547f | ||
|
|
6ae58e6882 | ||
|
|
3bc0f90a82 | ||
|
|
2a99ef254f | ||
|
|
5bc811f26e | ||
|
|
83893d762a | ||
|
|
24bed6273b | ||
|
|
2a5b314f67 | ||
|
|
e3c9fc3fba | ||
|
|
b81700a8fd | ||
|
|
b319fd4b93 | ||
|
|
23d6995558 | ||
|
|
190612d507 | ||
|
|
cddfe425e1 | ||
|
|
bc6a03a41b | ||
|
|
e053d1f566 | ||
|
|
e429b8484c | ||
|
|
22159c5eb4 | ||
|
|
903df9972a | ||
|
|
4dcbb1f077 | ||
|
|
5158d49781 | ||
|
|
3fdf47c535 | ||
|
|
b59d1bda8b | ||
|
|
1760fb9db2 | ||
|
|
082c771fc0 | ||
|
|
7ec989d485 | ||
|
|
0f9613eb9e | ||
|
|
85a354351a | ||
|
|
24db8cef4f | ||
|
|
9908eee40c | ||
|
|
b6517758d9 | ||
|
|
029500f3ed | ||
|
|
686727a7c3 | ||
|
|
91a1d61cc6 | ||
|
|
b8e9921f3d | ||
|
|
e0ce5d5629 | ||
|
|
ed511c296c | ||
|
|
eab1fbbbb0 | ||
|
|
ee3b052a50 | ||
|
|
5b98f15d9b | ||
|
|
51d76e5a2b | ||
|
|
a6bce0b221 | ||
|
|
fb5dc8a2ba | ||
|
|
d571ee0d01 | ||
|
|
5d7ed67316 | ||
|
|
9f6c754339 | ||
|
|
2819a4ebf3 | ||
|
|
dec1203ea8 | ||
|
|
87ab0d7b18 | ||
|
|
043530409c | ||
|
|
9710282d22 | ||
|
|
5e8e141fb5 | ||
|
|
2e313fbdd2 | ||
|
|
5c3e4db05f | ||
|
|
2be2f6aa0c | ||
|
|
6d038f5cdf | ||
|
|
27feccba39 | ||
|
|
7d39d5c088 | ||
|
|
2e69b904da | ||
|
|
79dcc8c771 | ||
|
|
683e5a4754 | ||
|
|
d51a424b98 | ||
|
|
e07d216a11 | ||
|
|
b9fa8dbe42 | ||
|
|
3820c46801 | ||
|
|
b663160bc2 | ||
|
|
a65c807a4a | ||
|
|
3a1f85cb69 | ||
|
|
196bd9c22b | ||
|
|
9bf2087b55 | ||
|
|
51e589c7e6 | ||
|
|
7365ed9b05 | ||
|
|
9cb7a6b2c7 | ||
|
|
9ff7eae1af | ||
|
|
61e1b74434 | ||
|
|
324c223d54 | ||
|
|
96793a296c | ||
|
|
7b1626a38f | ||
|
|
09a3a3dafb | ||
|
|
0e21708f25 | ||
|
|
89fa2343a6 | ||
|
|
03256d8ac9 | ||
|
|
889ca77912 | ||
|
|
23ee6b3109 | ||
|
|
d133ffac25 | ||
|
|
ffc6e65392 | ||
|
|
d61086907d | ||
|
|
5b58c2892d | ||
|
|
67379c67cc | ||
|
|
4691257233 | ||
|
|
d46a8c893a | ||
|
|
577ba0b39d | ||
|
|
f761245280 | ||
|
|
f1b4e585aa | ||
|
|
255db2cb66 |
@@ -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]
|
||||
------------------------------
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,7 +6,7 @@ import sys
|
||||
|
||||
|
||||
def icat(args):
|
||||
from kitty.icat import main
|
||||
from kittens.icat.main import main
|
||||
main(args)
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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];
|
||||
|
||||
|
||||
4
glfw/win32_platform.h
vendored
@@ -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
@@ -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);
|
||||
|
||||
@@ -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::[]
|
||||
|
||||
|
||||
0
kittens/clipboard/__init__.py
Normal file
76
kittens/clipboard/main.py
Normal 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)
|
||||
127
kittens/diff/README.asciidoc
Normal 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 :)
|
||||
@@ -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, [])
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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 */
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
# }}}
|
||||
|
||||
@@ -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
@@ -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:
|
||||
0
kittens/resize_window/__init__.py
Normal file
131
kittens/resize_window/main.py
Normal 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)
|
||||
@@ -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__')
|
||||
|
||||
|
||||
|
||||
0
kittens/show_error/__init__.py
Normal file
42
kittens/show_error/main.py
Normal 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
51
kittens/ssh/main.py
Normal 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)
|
||||
@@ -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
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
35
kitty/cli.py
@@ -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()}
|
||||
|
||||
@@ -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)
|
||||
|
||||
108
kitty/cmds.py
@@ -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
|
||||
|
||||
|
||||
|
||||
117
kitty/config.py
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
17
kitty/glfw.c
@@ -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),
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
# }}}
|
||||
|
||||
488
kitty/layout.py
@@ -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]
|
||||
|
||||
# }}}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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; \
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 *);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |