Compare commits

...

256 Commits

Author SHA1 Message Date
Kovid Goyal
e06ef732d8 version 0.19.2 2020-11-13 09:32:53 +05:30
Kovid Goyal
792a3e7db8 Dont mix the raw/non-raw malloc/free for pagerhist 2020-11-13 08:12:26 +05:30
Kovid Goyal
9b32842f66 Fix #3096 2020-11-13 07:47:40 +05:30
Kovid Goyal
9816979169 Don't restrict the DCH control code to only the current scroll region
Matches behavior of other terminal emulators.  Fixes #3090
2020-11-12 11:44:09 +05:30
Kovid Goyal
a74679dd95 Fix last character of URL not being detected if it is the only character on a new line
Fixes #3088
2020-11-11 14:00:33 +05:30
Kovid Goyal
a40059729e Fix the send_text action not working in URL handlers
send_text keeps its args as byte string, so variable expansion was not
working. Fixes #3081
2020-11-11 07:51:43 +05:30
Kovid Goyal
6bab967586 Merge branch 'memory-leak' of https://github.com/Luflosi/kitty 2020-11-05 18:15:44 +05:30
Luflosi
bb88b33469 Fix memory leak
Found with the Clang Static Analyzer.
2020-11-05 13:15:19 +01:00
Kovid Goyal
c440470276 Entry point to edit config file 2020-11-04 16:30:29 +05:30
Kovid Goyal
ef6164eecb Merge branch 'incorrect-reference-count-decrement' of https://github.com/Luflosi/kitty 2020-11-04 07:44:00 +05:30
Luflosi
7ad360d872 Fix incorrect decrement of the reference counts of objects
Found with the Clang Static Analyzer.
The function `schedule_notification()` for the new notification API also does not `release` similar objects.
2020-11-03 18:17:58 +01:00
Kovid Goyal
0870a33956 Merge branch 'memory-leak' of https://github.com/Luflosi/kitty 2020-11-01 18:47:31 +05:30
Luflosi
3ccc2f538d Fix memory leak in unlikely failure condition
Found with the Clang Static Analyzer.
2020-11-01 13:41:52 +01:00
Kovid Goyal
17375f0be2 Add a link to nix-shell 2020-10-31 09:10:50 +05:30
Kovid Goyal
5a987a162e Merge branch 'add-shell.nix' of https://github.com/Luflosi/kitty 2020-10-31 07:31:19 +05:30
Luflosi
5058fb9919 Add shell.nix
This makes it possible to execute `nix-shell` to create the correct environment to build kitty. Use `nix-shell --pure` to eliminate most of the influence of the outside system, e.g. globally installed packages.
This works on NixOS and any Linux or macOS system with the Nix package manager installed.
The build inputs are split into `buildInputs`, `nativeBuildInputs`, `propagatedBuildInputs` and `checkInputs` so it closely resembles 2bb3a9da24/pkgs/applications/terminal-emulators/kitty/default.nix. This makes it easy to port changes between the two files.
2020-10-31 01:44:33 +01:00
Luflosi
3335f8602b Allow reading some build system flags as environment variables 2020-10-29 12:20:11 +01:00
Kovid Goyal
87ab1bbe54 Add a changelog entry for previous merge 2020-10-29 07:20:45 +05:30
Kovid Goyal
f8d52b33f4 Merge branch 'wl_fix_destroy_csds' of https://github.com/LeonardKoenig/kitty 2020-10-29 07:19:57 +05:30
Leonard König
bc2dcdf9db wl: Fix destroying CSDs in correct order (#3051)
On Wayland, some compositors refuse to implement SSDs in which case we draw
primitive CSDs.  However, the destructors were called in the wrong order,
leading to a dereference of an already freed object.
2020-10-28 18:57:37 +01:00
Kovid Goyal
93d1aacfb6 macOS: Fix kitty not being added to PATH automatically
Fixes #3063. Apparently in newer Python, Py_Initialize() nukes
sys._xoptions when the isolate flag is set.
2020-10-27 10:38:33 +05:30
Kovid Goyal
3ddaa4aff9 Make it clear that remote control mappings dont need remote control enabled 2020-10-26 16:51:53 +05:30
Kovid Goyal
75488d568a Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2020-10-26 16:34:30 +05:30
Luflosi
3df80545bd Reduce the difference of GLFW to upstream 2020-10-26 11:02:55 +01:00
Kovid Goyal
c605fd9783 Merge branch 'remove-extra-space' of https://github.com/Luflosi/kitty 2020-10-26 15:03:25 +05:30
Luflosi
60719e7bff Remove extra space 2020-10-26 10:22:14 +01:00
Kovid Goyal
e9faa3277a Merge branch 'fix-memory-leak' of https://github.com/Luflosi/kitty 2020-10-26 07:33:06 +05:30
Luflosi
35f7388725 Fix memory leaks in coretext_all_fonts()
Found with the Clang Static Analyzer.
2020-10-25 17:40:51 +01:00
Kovid Goyal
ef1486a779 Add .js and .json to known extensions 2020-10-25 20:25:31 +05:30
Kovid Goyal
95c44165e9 Merge branch 'fix-clang-analyze-error' of https://github.com/Luflosi/kitty 2020-10-25 20:24:28 +05:30
Kovid Goyal
7b816bb96f Font independent rendering for braille characters, which ensures they are properly aligned at all font sizes. 2020-10-25 20:20:22 +05:30
Luflosi
d2ac51aa4a Fix Clang Static Analyzer complaining about Apple coding conventions
The error message was: "Method accepting NSError** should have a non-void return value to indicate whether or not an error occurred".
2020-10-25 15:47:43 +01:00
Kovid Goyal
26974b1f3a Dont ignore --title when using an empty session
Fixes #3055
2020-10-25 18:53:57 +05:30
Kovid Goyal
3960c6678d Override MIME for some types that are actually text 2020-10-25 15:11:44 +05:30
Kovid Goyal
30b8991a73 DRYer 2020-10-25 13:47:12 +05:30
Kovid Goyal
75a94bcd96 Improving MIME type detection for some common file types when they are missing from the system MIME database
Also allow the user to specify their own database via mime.types in the
kitty config directory. See #3056
2020-10-25 13:42:11 +05:30
Kovid Goyal
e160cbf32b ssh kitten: Fix parsing of combined single letter options to more closely match ssh. Fixes #3053 2020-10-23 16:55:53 +05:30
Kovid Goyal
2da2e63245 Add another integration 2020-10-23 09:36:35 +05:30
Kovid Goyal
f160ea6012 Fix #3048 2020-10-21 21:57:35 +05:30
Kovid Goyal
8057bc639d Add diff context parser defs 2020-10-20 11:37:48 +05:30
Kovid Goyal
6f5ec5aa03 Merge branch 'fix-mistake-in-previous-pr' of https://github.com/Luflosi/kitty into master 2020-10-19 16:41:01 +05:30
Luflosi
e343c9445c Creating a temporary directory is not actually required
While developing the previous PR (https://github.com/kovidgoyal/kitty/pull/3043), I was using more compiler command line arguments, one of which was `-MMD`. This caused the .d file to be created. Without this argument, there is no need to create a temporary directory.
2020-10-19 13:08:32 +02:00
Kovid Goyal
c73e374f1a Remove unneeded space 2020-10-19 16:02:49 +05:30
Kovid Goyal
d9b292f808 Merge branch 'fix-macos-notification-api-detection' of https://github.com/Luflosi/kitty into master 2020-10-19 16:00:38 +05:30
Luflosi
46ef2f313b macOS: Fix detection for notification API
For some reason it can happen that the `UserNotifications` framework exists but the `UserNotifications/UserNotifications.h` header doesn't.
Closes https://github.com/kovidgoyal/kitty/issues/3042.
2020-10-19 12:19:51 +02:00
Luflosi
92a705c79e Add language parameter to first_successful_compile() and test_compile()
This allows compiling Objective-C files.
The temporary directory is needed because clang would try to write to `/dev/null.d`, which would obviously fail.
2020-10-18 23:08:19 +02:00
Kovid Goyal
12ebdd2574 Add a changelog entry for the last PR 2020-10-18 08:48:07 +05:30
Kovid Goyal
ed2e951032 Merge branch 'pointer_shape_when_dragging' of https://github.com/jaeheum/kitty into master 2020-10-18 08:46:18 +05:30
Kovid Goyal
01a34a8846 Add links to viu that supports the graphics protocol 2020-10-18 08:42:59 +05:30
Jay Han
b13174cb9c pointer_shape_when_dragging option + set_mouse_cursor_when_dragging() 2020-10-17 09:52:44 -07:00
Jay Han
03ac6cab99 default_pointer_shape option 2020-10-17 09:28:04 -07:00
Kovid Goyal
532eebaefb Merge branch 'fix-garbage-pointer-dereference' of https://github.com/Luflosi/kitty into master 2020-10-16 07:35:31 +05:30
Luflosi
f825345b14 Fix potential garbage pointer dereference
`create_os_window()` calls `get_window_content_scale()` without initializing the `xscale` and `yscale` variables. Instead of initializing them everywhere, where this function is called, just do it once in the function itself.
Found with the Clang Static Analyzer.
2020-10-15 20:41:16 +02:00
Kovid Goyal
1409477ed7 Merge branch 'double-free' of https://github.com/Luflosi/kitty into master 2020-10-15 19:11:31 +05:30
Luflosi
d8e4294432 Fix double free and memory leak
Found with the Clang Static Analyzer.
This is a copy & paste error. This bug was introduced in 72e2307c16 in Jul 2018.
2020-10-15 15:29:44 +02:00
Kovid Goyal
5d13908776 github actions CI updated to python 3.9
Pillow wheel currently not available for python 3.9. So tries to install
from source and fails on missing zlib. So install zlib in brew and make
it available via PKG_CONFIG_PATH. Apparently, this wont be necessary
after Pillow 8, but who knows.
2020-10-14 19:13:46 +05:30
Luflosi
2832a73285 GLFW: Wayland: Assume 96 DPI if physical size is <= 0
From upstream: 0ef149c8f2.
2020-10-14 14:43:40 +02:00
Kovid Goyal
4152ed5809 Update changelog 2020-10-14 08:51:07 +05:30
Kovid Goyal
8a90ed70ee Merge branch 'fix-upwards-selection-clearing' of https://github.com/Luflosi/kitty into master 2020-10-14 05:31:08 +05:30
Luflosi
e2646c08a5 Fix upwards selection not being cleared when lines change
When selecting text by dragging upwards, the "end" of the selection is above the "start". This causes the `selection_has_screen_line()` to never return `true` in this case.
In practice this bug causes the selection to not be cleared when the screen contents under the selection change if it was made by dragging upwards.
2020-10-13 21:40:03 +02:00
Kovid Goyal
7122ba820f Fix eight bar unicode chars not being rendered at very small font sizes
Fixes #3025
2020-10-13 20:00:25 +05:30
Kovid Goyal
efca2658ea Fix typing issues pointed out by updated mypy 2020-10-12 23:03:07 +05:30
Kovid Goyal
91a714947c Fix encoding error when reporting capabilities for number of colors 2020-10-12 22:39:05 +05:30
Kovid Goyal
5d3ac649ba Fix #3017 2020-10-08 17:39:44 +05:30
Kovid Goyal
6443e726a6 Add a note about how the query_terminal kitten works 2020-10-08 07:42:47 +05:30
Kovid Goyal
6a755bda45 A new kitten to easily query kitty for config values 2020-10-07 13:35:16 +05:30
Kovid Goyal
dab555ea3b kittens: Dont discard pending input on startup
Fixes #3012
2020-10-06 16:39:00 +05:30
Kovid Goyal
a3bbad0060 Allow specifying the optional actions for tcsetattr 2020-10-06 16:30:23 +05:30
Kovid Goyal
73db717a0c version 0.19.1 2020-10-06 14:03:25 +05:30
Kovid Goyal
f1552c8354 Avoid alias related complications with completion delegation for hyperlinked_grep 2020-10-06 13:53:41 +05:30
Kovid Goyal
fb87fc32f0 Fix a regression that caused a segfault when using scrollback_pager_history_size
Fixes #3011
2020-10-06 13:28:48 +05:30
Kovid Goyal
b04b0c670d Fix #3010
Probably ancient version of Python
2020-10-06 12:09:47 +05:30
Kovid Goyal
b33845df8a Update Changelog 2020-10-06 06:50:29 +05:30
Kovid Goyal
12c9d8d8f8 Delay load the ipaddress module 2020-10-06 06:50:17 +05:30
Kovid Goyal
fe97bbcbbf Merge branch 'master' of https://github.com/Sighery/kitty into master 2020-10-06 06:45:05 +05:30
Sighery
10533c3eba Hints kitten: validate IPs with ipaddress
On the initial commit of this feature, IPs were just matched with a
very simple regex that prioritised simplicity/readability over
accuracy.

This commit adds a postprocessor for ip matches that makes use of
Python's `ipaddress` in the standard library to validate all the IP
matches.

This way we don't need huge and complex regex patterns to match _and_
validate the IPs, and we can just use `ipaddress` to abstract us from
implementing all the validation logic into the regex pattern.
2020-10-05 20:11:25 +02:00
Sighery
b4415c90f9 Hints kitten: add support for IPs (v4 and v6)
It selects v4 and v6 IPs with a simple regex that doesn't actually
check for the validity of the IPs.
2020-10-05 20:11:22 +02:00
Kovid Goyal
2970bbdf6f Update GA tags
Also auto gen docs if running publish only for website
2020-10-05 20:04:32 +05:30
Kovid Goyal
95a420534d Remove the old asciidoc files
By now hopefully the kitty website is well established
2020-10-05 19:34:02 +05:30
Kovid Goyal
be56a21574 ... 2020-10-05 19:33:09 +05:30
Kovid Goyal
3741a235fe Merge branch 'patch-1' of https://github.com/Nudin/kitty into master 2020-10-05 19:28:22 +05:30
Michael F. Schönitzer
523b6c4c2d Fix wrong kittenname in documentation
It's called `hyperlinked_grep` not `hyperlink_grep`.
2020-10-05 15:54:11 +02:00
Kovid Goyal
c3b2300310 Merge branch 'remove-outdated-check' of https://github.com/Luflosi/kitty into master 2020-10-05 18:16:06 +05:30
Luflosi
1c4a20d86f Remove outdated Python version check
The Python version is already checked at the top of the file. That check was added in 81a58186c6.
2020-10-05 14:41:07 +02:00
Kovid Goyal
f5af475012 Merge branch 'notification_fix' of https://github.com/nertpinx/kitty into master 2020-10-05 18:09:35 +05:30
Martin Kletzander
f149b74332 Fix repeated version notifications
Without this fix the file with the list of notifications looks like this:
```
0.19.0,1601899026.4373078,<built-in method count of Notification object at 0x7f8c582193b0>
```
which ends up notifying about a new version all the time.
2020-10-05 14:27:47 +02:00
Kovid Goyal
8cd51386cb Fix some typos in the OSC 99 spec 2020-10-05 17:27:41 +05:30
Kovid Goyal
fb2cca88df Merge branch 'pip3-in-ci' of https://github.com/Luflosi/kitty into master 2020-10-04 21:23:29 +05:30
Luflosi
031d4dc85a Use pip3 instead of pip for CI
There was a warning when the CI executed the "macOS Brew" job:
```
DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 won't be maintained after that date. A future version of pip will drop support for Python 2.7. More details about Python 2 support in pip, can be found at https://pip.pypa.io/en/latest/development/release-process/#python-2-support
```
To fix this, use pip3 instead of pip.
2020-10-04 17:47:41 +02:00
Kovid Goyal
bf66d1c0f5 Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty into master 2020-10-04 21:13:48 +05:30
Luflosi
0edfa88755 Reduce the difference of GLFW to upstream 2020-10-04 17:34:51 +02:00
Kovid Goyal
dba1f3bcbc Merge branch 'add-comment' of https://github.com/Luflosi/kitty into master 2020-10-04 21:01:12 +05:30
Luflosi
e204cc8f12 Add comment on how to call send_test_notification() 2020-10-04 17:23:50 +02:00
Kovid Goyal
9e3c60f43f Make ancient gcc happy 2020-10-04 19:56:10 +05:30
Kovid Goyal
b03dd5b6b2 ... 2020-10-04 18:43:59 +05:30
Kovid Goyal
2aa9886ebb version 0.19.0 2020-10-04 18:42:21 +05:30
Kovid Goyal
225e52b8b9 Merge branch 're-add-deprecated-macos-notifications' of https://github.com/Luflosi/kitty into master 2020-10-04 18:16:09 +05:30
Luflosi
0ae1f9906f macOS: re-add deprecated notification API
The new User Notifications Framework is only available on macOS 10.14 and above, while the old NSUserNotification API is deprecated in macOS 11 (Big Sur) and will probably be removed in the future.
This commit compiles a simple test program to see if the Framework is available and then uses either the new or the old API.
2020-10-04 13:32:28 +02:00
Kovid Goyal
a5918b50f8 Merge branch 'better-build-output-for-pipes' of https://github.com/Luflosi/kitty into master 2020-10-04 15:50:39 +05:30
Luflosi
fa0374ee61 Better build system output when stdout is not a tty
When piping the output of `setup.py` to another program, that program cannot usually deal with escape sequences well. To fix this, output the compilation progress on new lines instead of overwriting the current line.
2020-10-04 12:02:49 +02:00
Kovid Goyal
5b1e1aee3d Merge branch 'flag-to-disable-lto' of https://github.com/Luflosi/kitty into master 2020-10-04 15:00:10 +05:30
Luflosi
3ba11d08fb Add flag to disable LTO
Disabling Link Time Optimization is useful for Nix on Darwin because LTO is broken there and has been for a long time, see https://github.com/NixOS/nixpkgs/pull/19312.
This is currently worked around in the Nix package with a patch that removes the lines that add -flto to the compiler flags.
2020-10-04 10:50:42 +02:00
Kovid Goyal
35d0f42d07 Add a note about creating self-signed app bundles 2020-10-04 08:25:58 +05:30
Kovid Goyal
f08f222337 Merge branch 'macos-notifications-add-subtitle' of https://github.com/Luflosi/kitty into master 2020-10-03 21:31:10 +05:30
Luflosi
7c4ad278d5 macOS: add ability to show subtitles in notifications
This used to be implemented before 4e3c6e52aa, when the now deprecated notifications framework was still being used.
Implement it again for feature parity.
2020-10-03 16:04:32 +02:00
Kovid Goyal
d3d2930bd2 typo 2020-10-01 20:19:14 +05:30
Kovid Goyal
b571c20acc Clean up docs for --copy-env 2020-10-01 12:43:26 +05:30
Kovid Goyal
f6cc0302c4 Remove trailing whitespace 2020-10-01 08:34:14 +05:30
Kovid Goyal
d9abcf0c83 Merge branch 'launch-doc' of https://github.com/poliorcetics/kitty into master 2020-10-01 08:22:30 +05:30
Alexis Bourget
5346849439 Add example of remote_control use with launch --copy-env 2020-10-01 01:01:56 +02:00
Alexis Bourget
9742b0c5b1 Add docs precisions for launch and --copy-env 2020-09-30 20:40:14 +02:00
Alexis Bourget
41cfe26898 use :code: block for @... in --stdin-source doc 2020-09-30 20:31:04 +02:00
Kovid Goyal
05a254eee9 Merge branch 'simplify' of https://github.com/Luflosi/kitty into master 2020-09-28 16:20:01 +05:30
Luflosi
8ef53aa363 Remove unnecessary condition
According to the python documentation, `Py_CLEAR()` has no effect when the argument is `NULL`, see https://docs.python.org/3/c-api/refcounting.html#c.Py_CLEAR.
2020-09-28 12:21:23 +02:00
Kovid Goyal
470abc25e4 Changelog entry for hyperlinks with hints kitten 2020-09-25 21:02:27 +05:30
Kovid Goyal
7fe6c7e78b Also handle EOFError 2020-09-25 18:58:03 +05:30
Kovid Goyal
6c8a4f8d9f Handle SIGINT more gracefully 2020-09-25 18:55:43 +05:30
Kovid Goyal
2a8c8c0cbb Fix #2980 2020-09-25 16:06:07 +05:30
Kovid Goyal
12bbe5459d Allow selecting hyperlinks via the hints kitten 2020-09-24 13:21:15 +05:30
Kovid Goyal
e0d9e05fd4 Preserve rg exit code 2020-09-23 20:33:03 +05:30
Kovid Goyal
8bea61cb99 hyperlinked_grep: Ensure filename is always printed
Fixes hyperlinks not working when a single file is searched
2020-09-23 19:11:09 +05:30
Kovid Goyal
d09666aba9 Unicode input kitten: Add symbols from NERD font
These are mostly Private Use symbols not in any standard,
however they are common enough to be useful.

Fixes #2972
2020-09-22 19:47:39 +05:30
Kovid Goyal
48e0ba1d3d Add a note about completion for the hyperlinked_grep kitten 2020-09-22 19:07:36 +05:30
Kovid Goyal
fb72be41f4 Add a test for serializing id less hyperlinks 2020-09-22 17:52:40 +05:30
Kovid Goyal
ef1db6d089 macOS: Add some keys to Info.plist to allow programs running inside kitty to access protected Cocoa APIs. 2020-09-22 16:32:40 +05:30
Kovid Goyal
9d4246a285 Output hyperlink markup when serializing as ANSI 2020-09-22 16:18:22 +05:30
Kovid Goyal
e0f5c39297 Get rid of unneeded malloc in as_text_generic 2020-09-22 09:57:45 +05:30
Kovid Goyal
e4d353b105 Ignore undecodeable bytes in pagerhist
Also add API to get pagerhist as bytes
2020-09-22 09:33:48 +05:30
Kovid Goyal
0d665495b8 Make getting hyperlinks from ids useable throughtout the codebase 2020-09-22 09:25:16 +05:30
Kovid Goyal
fdaf857885 Add more tests for pagerhist 2020-09-22 09:05:40 +05:30
Kovid Goyal
987b41d2cb Implement rewrp for pagerhist using the streaming wcswidth 2020-09-21 19:05:23 +05:30
Kovid Goyal
444080f320 Make wcswidth useable in a streaming fashion 2020-09-21 15:55:22 +05:30
Kovid Goyal
b5007ba276 Store pager history in UTF-8
Reduces storage requirement by a fator of 4 at the cost of some CPU
usage, to encode to UTF-8
2020-09-21 12:29:10 +05:30
Kovid Goyal
bf19d2b156 Add some tests for the pagerhist code 2020-09-21 09:08:46 +05:30
Kovid Goyal
a78515e5bf Remove the max size limit for line_as_ansi
Needed for output of hyperlinks, also more efficient, since avoids
malloc per line. Also fix pagerhist not having SGR reset at the start of
every line.
2020-09-20 11:26:59 +05:30
Kovid Goyal
581126c748 More powerful match criteria for fragments 2020-09-19 07:31:17 +05:30
Kovid Goyal
7b3e345a2a Document the framework for customizing URL open actions 2020-09-18 21:33:30 +05:30
Kovid Goyal
cc7cefd3ed Use unquoted path for filename and path matching 2020-09-18 21:11:39 +05:30
Kovid Goyal
de6528b7d8 Dispatch url actions once per event loop tick
This allows subsequent actions to act on the results of the previous
actions
2020-09-18 20:48:40 +05:30
Kovid Goyal
9efdfe0de4 Fix processing of non-string key actions 2020-09-18 20:41:31 +05:30
Kovid Goyal
f936918278 ... 2020-09-18 19:46:49 +05:30
Kovid Goyal
00125577a9 Add a link to rg hyperlink issue 2020-09-18 19:36:43 +05:30
Kovid Goyal
083c47b0f0 A new kitten to easily search files and open the results at the matched line by clicking on them. 2020-09-18 19:33:51 +05:30
Kovid Goyal
8021686154 Merge branch 'fix-typo' of https://github.com/Luflosi/kitty into master 2020-09-18 17:24:00 +05:30
Luflosi
f55ca296da Fix typo 2020-09-18 13:50:56 +02:00
Kovid Goyal
0d6bca3e5d Allow specifying rules to perform arbitrary actions in kitty when opening URLs 2020-09-18 15:01:25 +05:30
Kovid Goyal
be1ff61e4a Improve some utility functions
Make expandvars accept both $VARNAME and ${VARNAME} now matches
the behavior of posixpath.expandvars

Allow overriding the environment variables to_cmdline expands against
2020-09-18 13:25:59 +05:30
Kovid Goyal
e36d41b46f Add a --hold option to icat
Keeps it alive after display images
2020-09-18 11:48:31 +05:30
Kovid Goyal
b4693dc1b3 Fix some lines in the scrollback buffer not being properly rendered after a window resize/font size change
Fixes #2619
2020-09-18 08:26:50 +05:30
Kovid Goyal
ca6010789e Fix a memory leak when changing font sizes
had forgotten to free unused font groups
2020-09-18 07:40:30 +05:30
Kovid Goyal
f8a80ccf5f Move the wcswidth functions out of screen.c
They have nothing to do with screens
2020-09-17 21:24:52 +05:30
Kovid Goyal
6461dccbdc Make wcswidth ignore more escape codes
Needed to ignore OSC 8 hyperlinks since we will
eventually have to send those with SGR formatting to the hints
kitten.
2020-09-17 20:16:17 +05:30
Kovid Goyal
b06f4f2574 Use the correct type for png_row_bytes() result 2020-09-17 16:54:21 +05:30
Kovid Goyal
7695e3a9c8 Cleanup exit flow control for remote hostname not matching 2020-09-17 16:45:02 +05:30
Kovid Goyal
ae475b408f oops 2020-09-17 16:45:02 +05:30
Kovid Goyal
6d9d048287 Verify hostname before acting on remote file URL
Fixes #2971
2020-09-17 16:45:02 +05:30
Kovid Goyal
09e4d101c5 DRYer 2020-09-17 16:45:02 +05:30
Kovid Goyal
36340caf4c Fix URL marking + scrolling
The ranges have to use on screen line numbers not line->ynum
Fixes #2969
2020-09-17 16:44:10 +05:30
Kovid Goyal
9a80ab0700 More robust 2020-09-17 10:42:30 +05:30
Kovid Goyal
339d972d8d Use INT_MAX rather than self->lines + 10
More likely to never co-incide
2020-09-17 10:41:13 +05:30
Kovid Goyal
6d1e1030bd ... 2020-09-17 10:37:14 +05:30
Kovid Goyal
8a3665ce55 Add links to some modern terminal feature specs 2020-09-17 10:25:37 +05:30
Kovid Goyal
8351f0beab Fix #2970 2020-09-17 09:34:22 +05:30
Kovid Goyal
c26808c59e Fix #2967 2020-09-17 09:30:30 +05:30
Kovid Goyal
50b1dfc746 Fix #2968 2020-09-16 22:30:13 +05:30
Kovid Goyal
2ec8ec2327 Add a note about nested SSH sessions not being supported
Fixes #2971
2020-09-16 08:36:08 +05:30
Kovid Goyal
168480b0ee Docs for the remote file kitten 2020-09-15 18:16:45 +05:30
Kovid Goyal
58f78afc2e Assume localhost is always local
Required by the spec
2020-09-15 16:19:27 +05:30
Kovid Goyal
d60020f5ac Allow copying hyperlink URL to clipboard 2020-09-15 13:23:31 +05:30
Kovid Goyal
437efe5473 Allow using the ask kitten to pick a choice 2020-09-15 12:39:07 +05:30
Kovid Goyal
5737904cec Next release will be 0.19.0 2020-09-15 11:53:34 +05:30
Kovid Goyal
431a8af08b Add changelog entry for hyperlinks support
Fixes #68
2020-09-15 10:35:46 +05:30
Kovid Goyal
be29c4a243 Move class definition to utils so can be used from multiple kittens easily 2020-09-15 10:34:37 +05:30
Kovid Goyal
0a027fad9a Allow asking for confirmation before opening hyperlinks 2020-09-15 10:26:05 +05:30
Kovid Goyal
e21a8e3cc2 Implement save as action for remote file kitten 2020-09-15 09:28:59 +05:30
Kovid Goyal
4279d6514d Fix path completion for ~ paths 2020-09-15 09:22:24 +05:30
Kovid Goyal
457db5994b Fix coloring of links using target 2020-09-15 09:14:44 +05:30
Kovid Goyal
71c942d290 Add colors to the path completer 2020-09-14 22:19:56 +05:30
Kovid Goyal
ee5bc8523b Fix input redisplay when cursor not at end 2020-09-14 20:15:36 +05:30
Kovid Goyal
70f4112643 ... 2020-09-14 20:15:30 +05:30
Kovid Goyal
174f6b7ea8 Start work on a path completion implementation 2020-09-14 19:58:36 +05:30
Kovid Goyal
b14fb6f3c4 Implement editing of remote files 2020-09-12 18:43:04 +05:30
Kovid Goyal
d6e27e776b Start work on the remote_file kitten
Easy access to files over SSH
2020-09-12 06:36:41 +05:30
Kovid Goyal
e6839b45e3 typo 2020-09-10 11:57:41 +05:30
Kovid Goyal
229134cf31 Misc. fixes for issues reported by clang's static analyzer
Most of them are false positives. A couple of mem leaks after unlikely
failure conditions.
2020-09-10 10:47:28 +05:30
Kovid Goyal
b552173185 Start work on special handling for remote file hyperlinks 2020-09-10 10:47:28 +05:30
Kovid Goyal
399a1f8fee Add an option to disable OSC 8 2020-09-10 10:47:28 +05:30
Kovid Goyal
9293d9b0ed Add some tests for hyperlink marking 2020-09-10 10:47:28 +05:30
Kovid Goyal
4a75952fc7 Implement marking of hyperlinks 2020-09-10 10:47:27 +05:30
Kovid Goyal
c19906eb87 Fix read overflow reported by ASAN 2020-09-10 10:47:27 +05:30
Kovid Goyal
00ea189074 Infrastructure to support multiple selection ranges
Needed for hyperlinks, that can be discontinuous
2020-09-10 10:47:27 +05:30
Kovid Goyal
c8e2061e2a Move hyperlink to end of hash table when re-referenced 2020-09-10 10:47:27 +05:30
Kovid Goyal
78dc93721d Add tests for hyperlink storage 2020-09-10 10:47:27 +05:30
Kovid Goyal
33beecddda Implement storage of URLs in a hash map 2020-09-10 10:47:27 +05:30
Kovid Goyal
e99d93ca30 Implement parsing of OSC 8
Also start work on storing hyperlinks with cells
2020-09-10 10:47:24 +05:30
Kovid Goyal
0cc54484a4 Add a note about using the scrollback pager history for large scrollbacks 2020-09-09 12:17:19 +05:30
Kovid Goyal
4df7d59de0 ... 2020-09-09 10:18:56 +05:30
Kovid Goyal
9a54da84dc Add a setting tab_activity_symbol
Fixes #2515
2020-09-08 21:54:56 +05:30
Kovid Goyal
83315f7999 Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty into master 2020-09-08 17:45:04 +05:30
Luflosi
eb5ebfd304 GLFW: Fix typo in GLFWwindowmaximizefun documentation
From upstream: 1ed1489831.
2020-09-08 12:57:00 +02:00
Kovid Goyal
77c9124a3d Merge branch 'gwl/fix-move_window_to_top-183' of https://github.com/gregorlarson/kitty into master 2020-09-08 08:38:18 +05:30
Kovid Goyal
317da76ced Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty into master 2020-09-08 07:46:13 +05:30
Luflosi
e8d8ced006 GLFW: Null: Fix out parameters not being set
From upstream: cd0dc76c7c.
2020-09-07 21:26:02 +02:00
Gregor Larson
0321f9bddf tabs.py::move_window_to_top fix 2020-09-07 14:01:56 -04:00
Kovid Goyal
5937892ebc Move focus tracking into the Screen object 2020-09-07 09:31:44 +05:30
Kovid Goyal
e429bea2f4 Move new features to the top of the changelog 2020-09-06 12:44:26 +05:30
Kovid Goyal
d33ad5c616 typo 2020-09-06 12:43:17 +05:30
Kovid Goyal
3e0614a293 Fix a regression that broke kitten_alias
Fixes #2952
2020-09-06 12:15:46 +05:30
Kovid Goyal
80e39212b8 Linux: Fix automatic detection of bold/italic faces for fonts such as IBM Plex Mono and have the regular face with a full name that is the same as the family name
Fixes #2951
2020-09-03 17:14:55 +05:30
Kovid Goyal
d7a6ceb3a6 Add character cell diagonals to box_chars 2020-09-03 13:51:54 +05:30
Kovid Goyal
9d3a9e9d1e Add the half triangle box chars 2020-09-03 11:22:36 +05:30
Kovid Goyal
e95c789d4b Add the block elements to box_chars
Also use a single eight_bar function for all blocks
2020-09-03 10:56:06 +05:30
Kovid Goyal
a508161265 Add smooth mosaic chars to box drawing 2020-09-02 22:00:58 +05:30
Kovid Goyal
a935c80adc Use box drawing for sextants
See the Symbols for Legacy Computing section in the Unicode standard
https://www.unicode.org/charts/PDF/U1FB00.pdf
2020-09-01 19:40:39 +05:30
Kovid Goyal
1d607bf13e Fix #2943 2020-08-30 20:42:01 +05:30
Kovid Goyal
ad57467c93 Avoid exception when generating minimal borders for empty tab in tall/vertical layouts 2020-08-30 20:41:57 +05:30
Kovid Goyal
430eaefc3e If a border width in pts is non-zero have it result in a minimum 1px border
Change the default to 0.5pt instead of 1px
2020-08-30 15:26:37 +05:30
Kovid Goyal
3d60724651 Allow specifying border sizes in either pt or px
Change the default to 1px borders as this works best with the new minimal border style.
2020-08-30 14:17:47 +05:30
Kovid Goyal
31d9f663fc Simplify border drawing code 2020-08-30 14:05:43 +05:30
Kovid Goyal
f324697c7c Implement new minimal border style for tall layout 2020-08-30 13:49:34 +05:30
Kovid Goyal
befae7e677 Make borders code for vertical layout re-usable 2020-08-30 11:32:45 +05:30
Kovid Goyal
7a2c2587ba Use new minimal borders style for vertical layouts 2020-08-30 09:17:14 +05:30
Kovid Goyal
cf3f9d68b3 Use similar border drawing techniques for splits 2020-08-30 08:37:16 +05:30
Kovid Goyal
c99f7c554f Fix unfocused windows in which a bell occurs not changing their border color to red until a relayout 2020-08-29 18:12:17 +05:30
Kovid Goyal
f01c0945da Fix mypy failing 2020-08-29 18:08:32 +05:30
Kovid Goyal
1bcd0c4471 Merge branch 'docs-kitten-example-mouse-buttons' of https://github.com/trygveaa/kitty into master 2020-08-29 18:06:55 +05:30
Trygve Aaberge
07f28e6ec7 Don't use GLFW buttons in custom kitten example
Since send_mouse_event now uses the same numbers as X11 for buttons
instead of the same numbers as GLFW, it is misleading to use the GLFW
constants. I forgot to change this example in commit 8efe08c4.
2020-08-29 13:33:34 +02:00
Kovid Goyal
7074f9f776 Grid layout: Improve rendering of borders when using minimal borders 2020-08-29 15:53:14 +05:30
Kovid Goyal
5029a9ad5a Dont use wheel_scroll_multiplier when sending scroll events in the main screen 2020-08-28 08:14:49 +05:30
Kovid Goyal
0bff30e954 Send wheel events to program when it asks to grab mouse, even in main screen
This matches behavior of other terminals and I dont have a strong
preference either way. Fixes #2939
2020-08-27 21:48:41 +05:30
Kovid Goyal
601d37aa3d Avoid malloc+copy for unknown OSC codes 2020-08-24 20:14:16 +05:30
Kovid Goyal
e2b1ca10c9 Save a line :P 2020-08-24 12:08:09 +05:30
Kovid Goyal
11942ed6dc Fix compilation on OpenBSD
Fixes #2935
2020-08-24 12:05:39 +05:30
Kovid Goyal
f65914599b Remove unreachable code 2020-08-23 10:53:47 +05:30
Kovid Goyal
392c31f5fe Allow specifying watchers in session files and via a command line argument 2020-08-23 10:41:36 +05:30
Kovid Goyal
f4ddaacb3c Allow tracking focus change events in watchers
Fixes #2918
2020-08-23 08:51:04 +05:30
Kovid Goyal
db64aef666 Prevent an infinite recursion if a watcher causes a resize in the on_resize handler 2020-08-23 08:38:25 +05:30
Kovid Goyal
95c8bbd7fb Forgot to increment token 2020-08-22 14:37:51 +05:30
Kovid Goyal
67b4f37d53 ... 2020-08-22 11:38:05 +05:30
Kovid Goyal
427288c16b Add a tmux FAQ
So I can just point to it for all the spurious tmux caused bug reports
2020-08-22 11:36:01 +05:30
Kovid Goyal
4202eee084 Fix launcher on OpenBSD
OpenBSD has apparently refused to move into the 21st century and lacks a
way of determining path to the executable. So we use a kludge, looking
for kitty in PATH and using that. To make it work, simply put a symlink
to kitty somewhere in PATH. Fixes #2675
2020-08-22 09:12:50 +05:30
Kovid Goyal
eca53bfab0 Add a new escape code to allow terminal programs to trigger desktop notifications
Fixes #1474
2020-08-21 20:29:54 +05:30
Kovid Goyal
cd76d109f5 Fix a couple of documentation build warnings 2020-08-21 07:50:35 +05:30
Kovid Goyal
2787f9f19b Drain the notifications queue in FIFO 2020-08-20 14:25:07 +05:30
Kovid Goyal
4e3c6e52aa macOS: Switch to the User Notifications framework
The current notifications framework has been deprecated in Big Sur. The new
framework only allows notifications from signed and notarized applications,
so people using kitty from HomeBrew/source are out of luck. And
notifications can only be displayed once the user grants permission. A
completely brain-dead design. Complain to Apple.
2020-08-20 13:03:00 +05:30
Kovid Goyal
ae5ceedfe9 Allow kitty to build on Big Sur 2020-08-19 20:33:14 +05:30
Kovid Goyal
022e2eb1db Allow updaing on ox with signed/notarized app easily 2020-08-19 20:08:34 +05:30
Kovid Goyal
2fe1ef4774 Mappable function to easily send test notification 2020-08-19 16:45:51 +05:30
Kovid Goyal
70afb2dd4d Fix lib names for OpenSSL 1.1 2020-08-19 12:33:54 +05:30
Kovid Goyal
efa02e3f94 Bump versions of openssl and glib 2020-08-18 21:25:00 +05:30
Kovid Goyal
5eefd41059 Add support for displaying correct colors with PNG files that contain embedded ICC color profiles 2020-08-12 10:34:16 +05:30
Kovid Goyal
6ca1b7c240 Convenience command to upload CI bundles 2020-08-12 10:30:15 +05:30
Kovid Goyal
31191fd48c Log warnings from libpng when debug rendering is on 2020-08-12 06:21:14 +05:30
Kovid Goyal
a6ed54ed48 Image display: Render PNG images with correct colors that have a gamma value different from the sRGB gamma value set in their metadata 2020-08-11 22:52:50 +05:30
Kovid Goyal
95132b4fe1 formatting 2020-08-11 20:19:29 +05:30
Kovid Goyal
df0c5d99e6 Fix #2913 2020-08-11 20:13:50 +05:30
134 changed files with 44181 additions and 29432 deletions

3
.gitattributes vendored
View File

@@ -11,3 +11,6 @@ kitty/parse-graphics-command.h linguist-generated=true
glfw/*.c linguist-vendored=true
glfw/*.h linguist-vendored=true
kittens/unicode_input/names.h linguist-generated=true
*.py text diff=python
*.m text diff=objc

View File

@@ -5,6 +5,7 @@
import io
import os
import shlex
import shutil
import subprocess
import sys
import tarfile
@@ -35,15 +36,18 @@ def install_deps():
run('sudo apt-get update')
run('sudo apt-get install -y libgl1-mesa-dev libxi-dev libxrandr-dev libxinerama-dev'
' libxcursor-dev libxcb-xkb-dev libdbus-1-dev libxkbcommon-dev libharfbuzz-dev'
' libpng-dev libfontconfig-dev libxkbcommon-x11-dev libcanberra-dev')
' libpng-dev liblcms2-dev libfontconfig-dev libxkbcommon-x11-dev libcanberra-dev')
if is_bundle:
install_bundle()
else:
run('pip install Pillow pygments')
if is_macos:
# needed for zlib for pillow, should not be needed after pillow 8.0
os.environ['PKG_CONFIG_PATH'] = '/usr/local/opt/zlib/lib/pkgconfig'
run('pip3 install Pillow pygments')
def build_kitty():
python = 'python3' if is_bundle else sys.executable
python = shutil.which('python3') if is_bundle else sys.executable
cmd = '{} setup.py build --verbose'.format(python)
if os.environ.get('KITTY_SANITIZE') == '1':
cmd += ' --debug --sanitize'
@@ -71,7 +75,7 @@ def replace_in_file(path, src, dest):
def setup_bundle_env():
global SW
os.environ['SW'] = SW = '/Users/Shared/buildbot/sw/sw' if is_macos else os.path.join(os.environ['GITHUB_WORKSPACE'], 'sw')
os.environ['SW'] = SW = '/Users/Shared/kitty-build/sw/sw' if is_macos else os.path.join(os.environ['GITHUB_WORKSPACE'], 'sw')
os.environ['PKG_CONFIG_PATH'] = SW + '/lib/pkgconfig'
if is_macos:
os.environ['PATH'] = '{}:{}'.format('/usr/local/opt/sphinx-doc/bin', os.environ['PATH'])
@@ -85,8 +89,8 @@ def install_bundle():
cwd = os.getcwd()
os.makedirs(SW)
os.chdir(SW)
with urlopen('https://download.calibre-ebook.com/travis/kitty/{}.tar.xz'.format(
'osx' if is_macos else 'linux-64')) as f:
with urlopen('https://download.calibre-ebook.com/ci/kitty/{}-64.tar.xz'.format(
'macos' if is_macos else 'linux')) as f:
data = f.read()
with tarfile.open(fileobj=io.BytesIO(data), mode='r:xz') as tf:
tf.extractall()

View File

@@ -1,4 +1,5 @@
pkg-config
zlib
python
imagemagick
harfbuzz

View File

@@ -65,6 +65,11 @@ def run_kitten(args: List[str]) -> None:
rk(kitten)
def edit_config_file(args: List[str]) -> None:
from kitty.utils import edit_config_file as f
f()
def namespaced(args: List[str]) -> None:
func = namespaced_entry_points[args[1]]
func(args[1:])
@@ -77,6 +82,7 @@ entry_points = {
'runpy': runpy,
'launch': launch,
'kitten': run_kitten,
'edit-config': edit_config_file,
'@': remote_control,
'+': namespaced,

View File

@@ -26,7 +26,7 @@ kitty_constants = iv['kitty_constants']
def binary_includes():
return tuple(map(get_dll_path, (
'expat', 'sqlite3', 'ffi', 'z', 'lzma', 'png16',
'expat', 'sqlite3', 'ffi', 'z', 'lzma', 'png16', 'lcms2',
# dont include freetype because fontconfig is closely coupled to it
# and also distros often patch freetype
@@ -35,7 +35,7 @@ def binary_includes():
'harfbuzz', 'xkbcommon', 'xkbcommon-x11',
'ncursesw', 'readline',
))) + (
get_dll_path('bz2', 2), get_dll_path('ssl', 3), get_dll_path('crypto', 3),
get_dll_path('bz2', 2), get_dll_path('ssl', 2), get_dll_path('crypto', 2),
get_dll_path('python' + py_ver, 2),
)

View File

@@ -262,8 +262,9 @@ class Freeze(object):
'z.1',
'harfbuzz.0',
'png16.16',
'crypto.1.0.0',
'ssl.1.0.0',
'lcms2.2',
'crypto.1.1',
'ssl.1.1',
):
print('\nAdding', x)
x = 'lib%s.dylib' % x

View File

@@ -71,8 +71,8 @@
{
"name": "openssl",
"unix": {
"filename": "openssl-1.0.2u.tar.gz",
"hash": "sha256:ecd0c6ffb493dd06707d38b14bb4d8c2288bb7033735606569d8f90f89669d16",
"filename": "openssl-1.1.1g.tar.gz",
"hash": "sha256:ddb04774f1e32f0c49751e21b67216ac87852ceb056b75209af2443400636d46",
"urls": ["https://www.openssl.org/source/{filename}"]
}
},
@@ -153,6 +153,15 @@
}
},
{
"name": "lcms2",
"unix": {
"filename": "lcms2-2.11.tar.gz",
"hash": "sha256:478c9c3938d7a91b1171de4616f8b04308a8676d73eadc19505b7ace41327f28",
"urls": ["https://github.com/mm2/Little-CMS/archive/2.11/{filename}"]
}
},
{
"name": "graphite",
"os": "linux",
@@ -179,7 +188,7 @@
"unix": {
"filename": "libiconv-1.16.tar.gz",
"hash": "sha256:e6a1b1b589654277ee790cce3734f07876ac4ccfaecbee8afa0b649cf529cc04",
"urls": ["http://ftp.gnu.org/pub/gnu/libiconv/{filename}"]
"urls": ["https://ftp.gnu.org/pub/gnu/libiconv/{filename}"]
}
},
@@ -187,13 +196,12 @@
"name": "glib",
"os": "linux",
"unix": {
"filename": "glib-2.57.1.tar.xz",
"hash": "sha256:d029e7c4536835f1f103472f7510332c28d58b9b7d6cd0e9f45c2653e670d9b4",
"urls": ["http://ftp.gnome.org/pub/GNOME/sources/glib/2.57/{filename}"]
"filename": "glib-2.65.1.tar.xz",
"hash": "sha256:bc63bf6c32713e0ee1dddc28e03f23b4a20c78bcb9a2c5b0f4eea41e46fb9cee",
"urls": ["https://ftp.gnome.org/pub/GNOME/sources/glib/2.65/{filename}"]
}
},
{
"name": "freetype",
"os": "linux",

View File

@@ -18,6 +18,7 @@ Run-time dependencies:
* harfbuzz >= 1.5.0
* zlib
* libpng
* liblcms2
* freetype (not needed on macOS)
* fontconfig (not needed on macOS)
* libcanberra (not needed on macOS)
@@ -63,10 +64,32 @@ This :file:`kitty.app` unlike the released one does not include its own copy of
python and the other dependencies. So if you ever un-install/upgrade those dependencies
you might have to rebuild the app.
Note that the released :file:`kitty.dmg` includes all dependencies, unlike the
:file:`kitty.app` built above and is built automatically by using the :file:`kitty` branch of
`build-calibre <https://github.com/kovidgoyal/build-calibre>`_ however, that
is designed to run on Linux and is not for the faint of heart.
.. note::
The released :file:`kitty.dmg` includes all dependencies, unlike the
:file:`kitty.app` built above and is built automatically by using the
:file:`kitty` branch of `build-calibre
<https://github.com/kovidgoyal/build-calibre>`_ however, that is designed to
run on Linux and is not for the faint of heart.
.. note::
Apple disallows certain functionality, such as notifications for unsigned applications.
If you need this functionality, you can try signing the built kitty.app with
a self signed certificate, see for example, `here
<https://stackoverflow.com/questions/27474751/how-can-i-codesign-an-app-without-being-in-the-mac-developer-program/27474942>`_.
Build and run from source with Nix
-------------------------------------------
On NixOS or any other Linux or macOS system with the Nix package manager
installed, execute `nix-shell
<https://nixos.org/guides/nix-pills/developing-with-nix-shell.html>`_ to create
the correct environment to build kitty or use ``nix-shell --pure`` instead to
eliminate most of the influence of the outside system, e.g. globally installed
packages. ``nix-shell`` will automatically fetch all required dependencies and
make them available in the newly spawned shell.
Then proceed with ``make`` or ``make app`` according to the platform specific instructions above.
Note for Linux/macOS packagers

View File

@@ -4,6 +4,133 @@ Changelog
|kitty| is a feature full, cross-platform, *fast*, GPU based terminal emulator.
To update |kitty|, :doc:`follow the instructions <binary>`.
0.19.2 [2020-11-13]
-------------------
- A new :doc:`kittens/query_terminal` kitten to easily query the running kitty
via escape codes to detect its version, and the values of
configuration options that enable or disable terminal features.
- Options to control mouse pointer shape, :opt:`default_pointer_shape`, and
:opt:`pointer_shape_when_dragging` (:pull:`3041`)
- Font independent rendering for braille characters, which ensures they are properly
aligned at all font sizes.
- Fix a regression in 0.19.0 that caused borders not to be drawn when setting
:opt:`window_margin_width` and keeping :opt:`draw_minimal_borders` on
(:iss:`3017`)
- Fix a regression in 0.19.0 that broke rendering of one-eight bar unicode
characters at very small font sizes (:iss:`3025`)
- Wayland: Fix a crash under GNOME when using multiple OS windows
(:pull:`3066`)
- Fix selections created by dragging upwards not being auto-cleared when
screen contents change (:pull:`3028`)
- macOS: Fix kitty not being added to PATH automatically when using pre-built
binaries (:iss:`3063`)
- Allow adding MIME definitions to kitty by placing a ``mime.types`` file in
the kitty config directory (:iss:`3056`)
- Dont ignore :option:`--title` when using a session file that defines no
windows (:iss:`3055`)
- Fix the send_text action not working in URL handlers (:iss:`3081`)
- Fix last character of URL not being detected if it is the only character on a
new line (:iss:`3088`)
- Don't restrict the ICH,DCH,REP control codes to only the current scroll region (:iss:`3090`, :iss:`3096`)
0.19.1 [2020-10-06]
-------------------
- hints kitten: Add an ``ip`` type for easy selection of IP addresses
(:pull:`3009`)
- Fix a regression that caused a segfault when using
:opt:`scrollback_pager_history_size` and it needs to be expanded (:iss:`3011`)
- Fix update available notifications repeating (:pull:`3006`)
0.19.0 [2020-10-04]
-------------------
- Add support for `hyperlinks from terminal programs
<https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda>`_.
Controlled via :opt:`allow_hyperlinks` (:iss:`68`)
- Add support for easily editing or downloading files over SSH sessions
without the need for any special software, see :doc:`kittens/remote_file`
- A new :doc:`kittens/hyperlinked_grep` kitten to easily search files and open
the results at the matched line by clicking on them.
- Allow customizing the :doc:`actions kitty takes <open_actions>` when clicking on URLs
- Improve rendering of borders when using minimal borders. Use less space and
do not display a box around active windows
- Add a new extensible escape code to allow terminal programs to trigger
desktop notifications. See :ref:`desktop_notifications` (:iss:`1474`)
- Implement special rendering for various characters from the set of "Symbols
for Legacy Computing" from the Unicode 13 standard
- Unicode input kitten: Allow choosing symbols from the NERD font as well.
These are mostly Private Use symbols not in any standard, however are common. (:iss:`2972`)
- Allow specifying border sizes in either pts or pixels. Change the default to
0.5pt borders as this works best with the new minimal border style
- Add support for displaying correct colors with non-sRGB PNG files (Adds a
dependency on liblcms2)
- hints kitten: Add a new :option:`kitty +kitten hints --type` of ``hyperlink`` useful
for activating hyperlinks using just the keyboard
- Allow tracking focus change events in watchers (:iss:`2918`)
- Allow specifying watchers in session files and via a command line argument
(:iss:`2933`)
- Add a setting :opt:`tab_activity_symbol` to show a symbol in the tab title
if one of the windows has some activity after it was last focused
(:iss:`2515`)
- macOS: Switch to using the User Notifications framework for notifications.
The current notifications framework has been deprecated in Big Sur. The new
framework only allows notifications from signed and notarized applications,
so people using kitty from homebrew/source are out of luck. Complain to
Apple.
- When in the main screen and a program grabs the mouse, do not use the scroll
wheel events to scroll the scrollback buffer, instead send them to the
program (:iss:`2939`)
- Fix unfocused windows in which a bell occurs not changing their border color
to red until a relayout
- Linux: Fix automatic detection of bold/italic faces for fonts such as IBM
Plex Mono that have the regular face with a full name that is the same as the
family name (:iss:`2951`)
- Fix a regression that broke :opt:`kitten_alias` (:iss:`2952`)
- Fix a regression that broke the ``move_window_to_top`` action (:pull:`2953`)
- Fix a memory leak when changing font sizes
- Fix some lines in the scrollback buffer not being properly rendered after a
window resize/font size change (:iss:`2619`)
0.18.3 [2020-08-11]
-------------------

View File

@@ -555,7 +555,7 @@ def process_shortcut_link(env: Any, refnode: Any, has_explicit_title: bool, titl
def write_conf_docs(app: Any, all_kitten_names: Iterable[str]) -> None:
app.add_lexer('conf', ConfLexer())
app.add_lexer('conf', ConfLexer)
app.add_object_type(
'opt', 'opt',
indextemplate="pair: %s; Config Setting",
@@ -604,7 +604,7 @@ def setup(app: Any) -> None:
write_cli_docs(kn)
write_remote_control_protocol_docs()
write_conf_docs(app, kn)
app.add_lexer('session', SessionLexer())
app.add_lexer('session', SessionLexer)
app.add_role('link', link_role)
app.add_role('iss', partial(num_role, 'issues'))
app.add_role('pull', partial(num_role, 'pull'))

View File

@@ -165,9 +165,12 @@ 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::
only monospace fonts, since every cell in the grid has to be the same size.
Furthermore, it needs fonts to be freely resizable, so it does not support
bitmapped fonts.
If your font is not listed in ``kitty list-fonts`` it means that it is not
monospace or is a bitmapped font. On Linux you can list all monospace fonts with::
fc-list : family spacing outline scalable | grep -e spacing=100 -e spacing=90 | grep -e outline=True | grep -e scalable=True
@@ -232,3 +235,30 @@ available. The manual way to figure it out is:
This shows the hex value, ``13`` in this case.
3. Use ``\x(hexval)`` in your ``send_text`` command in kitty. So in this example, ``\x13``
I am using tmux and have a problem
--------------------------------------
First, terminal multiplexers are `a bad idea
<https://github.com/kovidgoyal/kitty/issues/391#issuecomment-638320745>`_, do
not use them, if at all possible. kitty contains features that do all of what
tmux does, but better, with the exception of remote persistence (:iss:`391`).
If you still want to use tmux, read on.
Image display will not work, see `tmux issue
<https://github.com/tmux/tmux/issues/1391>`_.
If you are using tmux with multiple terminals or you start it under one
terminal and then switch to another and these terminals have different TERM
variables, tmux will break. You will need to restart it as tmux does not
support multiple terminfo definitions.
Copying to clipboard via OSC 52 will not work, because tmux does not support
the extended version of that protocol, you will need to add ``no-append`` to
:opt:`clipboard_control` in kitty.conf.
If you use any of the advanced features that kitty has innovated, such as
styled underlines, desktop notifications, extended keyboard support, etc.
they may or may not work, depending on the whims of tmux's maintainer, your
version of tmux, etc.

View File

@@ -37,6 +37,7 @@ Some programs that use the kitty graphics protocol:
* `pixcat <https://github.com/mirukana/pixcat>`_ - a third party CLI and python library that wraps the graphics protocol
* `neofetch <https://github.com/dylanaraps/neofetch>`_ - A command line system
information tool
* `viu <https://github.com/atanunq/viu>`_ - a terminal image viewer
.. contents::
@@ -54,27 +55,34 @@ In C:
.. code-block:: c
struct ttysize ts;
ioctl(0, TIOCGWINSZ, &ts);
printf("number of columns: %i, number of rows: %i, screen width: %i, screen height: %i\n", sz.ws_col, sz.ws_row, sz.ws_xpixel, sz.ws_ypixel);
#include <stdio.h>
#include <sys/ioctl.h>
int main(int argc, char **argv) {
struct winsize sz;
ioctl(0, TIOCGWINSZ, &sz);
printf("number of rows: %i, number of columns: %i, screen width: %i, screen height: %i\n", sz.ws_row, sz.ws_col, sz.ws_xpixel, sz.ws_ypixel);
return 0;
}
In Python:
.. code-block:: python
import array, fcntl, termios
import array, fcntl, sys, termios
buf = array.array('H', [0, 0, 0, 0])
fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, buf)
print('number of columns: {}, number of rows: {}, screen width: {}, screen height: {}'.format(*buf))
print('number of rows: {}, number of columns: {}, screen width: {}, screen height: {}'.format(*buf))
Note that some terminals return ``0`` for the width and height values. Such
terminals should be modified to return the correct values. Examples of
terminals that return correct values: ``kitty, xterm``
You can also use the *CSI t* escape code to get the screen size. Send
``<ESC>[14t`` to *stdout* and kitty will reply on *stdin* with
``<ESC>[4;<height>;<width>t`` where *height* and *width* are the window size in
pixels. This escape code is supported in many terminals, not just kitty.
``<ESC>[14t`` to ``STDOUT`` and kitty will reply on ``STDIN`` with
``<ESC>[4;<height>;<width>t`` where ``height`` and ``width`` are the window
size in pixels. This escape code is supported in many terminals, not just
kitty.
A minimal example
------------------
@@ -154,9 +162,10 @@ of transmitting paletted images.
RGB and RGBA data
~~~~~~~~~~~~~~~~~~~
In these formats the pixel data is stored directly as 3 or 4 bytes per pixel, respectively.
When specifying images in this format, the image dimensions **must** be sent in the control data.
For example::
In these formats the pixel data is stored directly as 3 or 4 bytes per pixel,
respectively. The colors in the data **must** be in the *sRGB color space*. When
specifying images in this format, the image dimensions **must** be sent in the
control data. For example::
<ESC>_Gf=24,s=10,v=20;<payload><ESC>\

View File

@@ -10,10 +10,12 @@ kitty - the fast, featureful, GPU based terminal emulator
buttery smooth scrolling. Uses threaded rendering to minimize input latency.
* Supports all modern terminal features: :doc:`graphics (images)
<graphics-protocol>`, unicode, true-color, OpenType ligatures, mouse
protocol, focus tracking, `bracketed paste
<https://cirw.in/blog/bracketed-paste>`_ and several :doc:`new terminal
protocol extensions <protocol-extensions>`.
<graphics-protocol>`, unicode, `true-color
<https://gist.github.com/XVilka/8346728>`_,
OpenType ligatures, mouse protocol, :doc:`hyperlinks <open_actions>`,
focus tracking, `bracketed paste <https://cirw.in/blog/bracketed-paste>`_
and several :doc:`new terminal protocol extensions
<protocol-extensions>`.
* Supports tiling multiple terminal windows side by side in different
:ref:`layouts <layouts>` without needing to use an extra program like tmux
@@ -353,6 +355,15 @@ For example:
focus
launch emacs
# Add a watcher that will be called with various events that occur
# on all subsequent windows. See the documentation of the launch command
# for details on watchers.
watcher /some/python/file.py
launch mpd
launch irssi
# Remove the watcher for further windows
watcher clear
Mouse features
-------------------

View File

@@ -30,6 +30,10 @@ images and other types of documents directly in your terminal, even over SSH.
`koneko <https://github.com/twenty5151/koneko>`_
browse images from the pixiv artist community directly in kitty
`viu <https://github.com/atanunq/viu>`_
view images in the terminal, similar to kitty's icat, however also supports
displaying animated GIFs
System and data visualisation tools
---------------------------------------
@@ -63,6 +67,9 @@ System and data visualisation tools
iplot 'sin(x*3)*exp(x*.2)'
`onefetch <https://github.com/o2sh/onefetch>`_
a tool to fetch information about your git repositories
Editor integration
-----------------------

View File

@@ -156,8 +156,8 @@ from ``kitty.fast_data_types``.
For example, to send a left click at position x: 2, y: 3 to the active window::
from kitty.fast_data_types import send_mouse_event, GLFW_MOUSE_BUTTON_LEFT, PRESS
send_mouse_event(boss.active_window.screen, 2, 3, GLFW_MOUSE_BUTTON_LEFT, PRESS, 0)
from kitty.fast_data_types import send_mouse_event, PRESS
send_mouse_event(boss.active_window.screen, 2, 3, 1, PRESS, 0)
The function will only send the event if the program is receiving events of
that type, and will return ``True`` if it sent the event, and ``False`` if not.

View File

@@ -0,0 +1,68 @@
Hyperlinked grep
=================
This kitten allows you to search your files using `ripgrep
<https://www.google.com/search?q=ripgrep>`_ and open the results
directly in your favorite editor in the terminal, at the line containing
the search result, simply by clicking on the result you want.
To set it up, first create :file:`~/.config/kitty/open-actions.conf` with the
following contents:
.. code:: conf
# Open any file with a fragment in vim, fragments are generated
# by the hyperlink_grep kitten and nothing else so far.
protocol file
fragment_matches [0-9]+
action launch --type=overlay vim +${FRAGMENT} ${FILE_PATH}
# Open text files without fragments in the editor
protocol file
mime text/*
action launch --type=overlay ${EDITOR} ${FILE_PATH}
Now, run a search with::
kitty +kitten hyperlinked_grep something
Hold down the :kbd:`ctrl+shift` keys and click on any of the
result lines, to open the file in vim at the matching line. If
you use some editor other than vim, you should adjust the
:file:`open-actions.conf` file accordingly.
Finally, add an alias to your shell's rc files to invoke the kitten as ``hg``::
alias hg='kitty +kitten hyperlinked_grep'
You can now run searches with::
hg some-search-term
If you want to enable completion, for the kitten, you can delegate completion
to rg. For that, instead of using an alias create a simple wrapper script named
:file:`hg` somewhere in your ``PATH``:
.. code-block:: sh
#!/bin/sh
exec kitty +kitten hyperlinked_grep "$@"
Then, for example, for ZSH, add the following to :file:`.zshrc`::
compdef _rg hg
To learn more about kitty's powerful framework for customizing URL click
actions, :doc:`see here <../open_actions>`.
Hopefully, someday this functionality will make it into some `upstream grep
<https://github.com/BurntSushi/ripgrep/issues/665>`_
program directly removing the need for this kitten.
.. note::
While you can pass any of ripgrep's comand line options to the kitten and
they will be forwarded to rg, do not use options that change the output
formatting as the kitten works by parsing the output from ripgrep.

View File

@@ -0,0 +1,21 @@
Query terminal
=================
Used to query kitty from terminal programs about version, values of various
runtime options controlling its features, etc.
The querying is done using the (*semi*) standard XTGETTCAP escape sequence
pioneered by XTerm, so it works over SSH as well. The downside is that it
is slow, since it requires a roundtrip to the terminal emulator and back.
If you want to do some of the same querying in your terminal program without
depending on the kitten, you can do so, by processing the same escape codes.
Search `this page <https://invisible-island.net/xterm/ctlseqs/ctlseqs.html>`_
for *XTGETTCAP* to see the syntax for the escape code and read the source
of this kitten to find the values of the keys for the various queries.
Command Line Interface
-------------------------
.. include:: ../generated/cli-kitten-query_terminal.rst

View File

@@ -0,0 +1,35 @@
Remote files
==============
|kitty| has the ability to easily *Edit*, *Open* or *Download* files
from a computer into which you are SSHed. In your SSH session run::
ls --hyperlink=auto
Then hold down :kbd:`ctrl+shift` and click the name of the file.
.. figure:: ../screenshots/remote_file.png
:alt: Remote file actions
:align: center
:scale: 100%
Remote file actions
|kitty| will ask you what you want to do with the remote file. You can choose
to *Edit* it in which case kitty will download it and open it locally in your
``EDITOR``. As you make changes to the file, they are automatically transferred
to the remote computer. Note that this happens without needing to install *any*
special software on the server, beyond ``ls`` that supports hyperlinks.
.. note::
Nested SSH sessions are not supported. The kitten will always try to copy
remote files from the first SSH host. This is because there is no way for
|kitty| to detect and follow a nested SSH session robustly.
.. note::
If you have not setup automatic password-less SSH access, then, when
editing starts you will be asked to enter your password just once,
thereafter the SSH connection will be re-used.
Similarly, you can choose to save the file to the local computer or download
and open it in its default file handler.

View File

@@ -74,6 +74,9 @@ functions for the events you are interested in, for example:
def on_resize(boss, window, data):
# Here data will contain old_geometry and new_geometry
def on_focus_change(boss, window, data):
# Here data kill contain focused
def on_close(boss, window, data):
# called when window is closed, typically when the program running in
# it exits.

95
docs/open_actions.rst Normal file
View File

@@ -0,0 +1,95 @@
Customizing the actions taken when clicking on links
======================================================
|kitty| has support for `terminal hyperlinks <https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda>`_. These
are generated by many terminal programs, such as ``ls``, ``gcc``, ``systemd``,
``mdcat``, etc. You can customize exactly what happens when clicking on these hyperlinks
in |kitty|.
You can tell kitty to take arbitrarily many, complex actions
when a link is clicked. Let us illustrate with some examples, first. Create
the file :file:`~/.config/kitty/open-actions.conf` with the following:
.. code:: conf
# Open any image in the full kitty window by clicking on it
protocol file
mime image/*
action launch --type=overlay kitty +kitten icat --hold ${FILE_PATH}
Now, run ``ls --hyperlink=auto`` in kitty and click on the filename of an
image, holding down :kbd:`ctrl+shift`. It will be opened over the current
window. Press any key to close it.
Each entry in :file:`open-actions.conf` consists of one or more
:ref:`matching_criteria`, such as ``protocol`` and ``mime`` and one or more
``action`` entries. In the example above kitty uses the :doc:`launch <launch>`
action which can be used to run external programs. Entries are separated by
blank lines.
Actions are very powerful, anything that you can map to a key combination in
`kitty.conf` can be used as an action. You can specify more than one action per
entry if you like, for example:
.. code:: conf
# Tail a log file (*.log) in a new OS Window and reduce its font size
protocol file
ext log
action launch --title ${FILE} --type=os-window tail -f ${FILE_PATH}
action change_font_size current -2
In the action specification you can expand environment variables, as shown in
the examples above. In addition to regular environment variables, there are
some special variables, documented below:
``URL``
The full URL being opened
``FILE_PATH``
The path portion of the URL (unquoted)
``FILE``
The file portion of the path of the URL (unquoted)
``FRAGMENT``
The fragment (unquoted), if any of the URL or the empty string.
.. _matching_criteria:
Matching criteria
------------------
An entry in :file:`open-actions.conf` must have one or more matching criteria.
URLs that match all criteria for an entry will trigger that entries' actions.
Processing stops at the first matching entry, so put more specific matching
criteria at the start of the list. Entries in the file are separated by blank
lines. The various available criteria are:
``protocol``
A comma separated list of protocols, for example: ``http, https``. If
absent, there is no constraint on protocol
``url``
A regular expression that must match against the entire (unquoted) URL
``fragment_matches``
A regular expression that must match against the fragment (part after #) in
the URL
``mime``
A comma separated list of MIME types, for example: ``text/*, image/*,
application/pdf``. You can add MIME types to kitty by creating the
:file:`mime.types` in the kitty configuration directory. Useful if your
system MIME database does not have definitions you need. This file is
in the standard format of one definition per line, like: ``text/plain rst
md``.
``ext``
A comma separated list of file extensions, for example: ``jpeg, tar.gz``
``file``
A shell glob pattern that must match the filename, for example:
``image-??.png``

View File

@@ -228,3 +228,127 @@ no ill-effects in other terminal emulators.
In case you're using software that can't be easily adapted to this
protocol extension, it can be disabled by specifying ``no-append`` to the
:opt:`clipboard_control` setting.
.. _desktop_notifications:
Desktop notifications
---------------------------------
|kitty| implements an extensible escape code (OSC 99) to show desktop
notifications. It is easy to use from shell scripts and fully extensible to
show title and body. Clicking on the notification can optionally focus the
window it came from, and/or send an escape code back to the application running
in that window.
The design of the escape code is partially based on the discussion in
the defunct
`terminal-wg <https://gitlab.freedesktop.org/terminal-wg/specifications/-/issues/13>`_
The escape code has the form::
<OSC> 99 ; metadata ; payload <terminator>
Here ``<OSC>`` is :code:`<ESC>]` and ``<terminator>`` is
:code:`<ESC><backslash>`. The metadata is a section of colon separated
:code:`key=value` pairs. Every key must be a single character from the set
:code:`a-zA-Z` and every value must be a word consisting of characters from
the set :code:`a-zA-Z0-9-_/\+.,(){}[]*&^%$#@!`~`. The payload must be
interpreted based on the metadata section. The two semi-colons *must* always be
present even when no metadata is present.
Before going into details, lets see how one can display a simple, single line
notification from a shell script::
printf '\x1b]99;;Hello world\x1b\\'
To show a message with a title and a body::
printf '\x1b]99;i=1:d=0;Hello world\x1b\\'
printf '\x1b]99;i=1:d=1:p=body;This is cool\x1b\\'
The most important key in the metadata is the ``p`` key, it controls how the
payload is interpreted. A value of ``title`` means the payload is setting the
title for the notification. A value of ``body`` means it is setting the body,
and so on, see the table below for full details.
The design of the escape code is fundamentally chunked, this is because
different terminal emulators have different limits on how large a single escape
code can be. Chunking is accomplished by the ``i`` and ``d`` keys. The ``i``
key is the *notification id* which can be any string containing the characters
``[a-zA-Z0-9_-+.]``. The ``d`` key stands for *done* and
can only take the values ``0`` and ``1``. A value of ``0`` means the
notification is not yet done and the terminal emulator should hold off
displaying it. A value of ``1`` means the notification is done, and should be
displayed. You can specify the title or body multiple times and the terminal
emulator will concatenate them, thereby allowing arbitrarily long text
(terminal emulators are free to impose a sensible limit to avoid
Denial-of-Service attacks).
Both the ``title`` and ``body`` payloads must be either UTF-8 encoded plain
text with no embedded escape codes, or UTF-8 text that is base64 encoded, in
which case there must be an ``e=1`` key in the metadata to indicate the payload
is base64 encoded.
When the user clicks the notification, a couple of things can happen, the
terminal emulator can focus the window from which the notification came, and/or
it can send back an escape code to the application indicating the notification
was activated. This is controlled by the ``a`` key which takes a comma
separated set of values, ``report`` and ``focus``. The value ``focus`` means
focus the window from which the notification was issued and is the default.
``report`` means send an escape code back to the application. The format of the
returned escape code is::
<OSC> 99 ; i=identifier ; <terminator>
The value of ``identifier`` comes from the ``i`` key in the escape code sent by
the application. If the application sends no identifier, then the terminal
*must* use ``i=0``. Actions can be preceded by a negative sign to turn them
off, so for example if you do not want any action, turn off the default
``focus`` action with::
a=-focus
Complete specification of all the metadata keys is in the table below. If a
terminal emulator encounters a key in the metadata it does not understand,
the key *must* be ignored, to allow for future extensibility of this escape
code. Similarly if values for known keys are unknown, the terminal emulator
*should* either ignore the entire escape code or perform a best guess effort
to display it based on what it does understand.
.. note::
It is possible to extend this escape code to allow specifying an icon for
the notification, however, given that some platforms, such as macOS, dont
allow displaying custom icons on a notification, at all, it was decided to
leave it out of the spec for the time being.
Similarly, features such as scheduled notifications could be added in future
revisions.
======= ==================== ========= =================
Key Value Default Description
======= ==================== ========= =================
``a`` Comma separated list ``focus`` What action to perform when the
of ``report``, notification is clicked
``focus``, with
optional leading
``-``
``d`` ``0`` or ``1`` ``1`` Indicates if the notification is
complete or not.
``e`` ``0`` or ``1`` ``0`` If set to ``1`` means the payload is base64 encoded UTF-8,
otherwise it is plain UTF-8 text with no C0 control codes in it
``i`` ``[a-zA-Z0-9-_+.]`` ``0`` Identifier for the notification
``p`` One of ``title`` or ``title`` Whether the payload is the notification title or body. If a
``body``. notification has no title, the body will be used as title.
======= ==================== ========= =================
.. note::
|kitty| also supports the legacy OSC 9 protocol developed by iTerm2 for
desktop notifications.

View File

@@ -144,7 +144,8 @@ you can map it in :file:`kitty.conf`. For example::
Then pressing the :kbd:`F1` key will set the active window margins to 30.
The syntax for what follows :code:`remote_control` is exactly the same
as the syntax for what follows :code:`kitty @` above.
as the syntax for what follows :code:`kitty @` above. You do not need
to enable remote control to use these mappings.
Documentation for the remote control protocol

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -98,6 +98,18 @@ def parse_ucd() -> None:
elif category.startswith('S'):
all_symbols.add(codepoint)
with open('nerd-fonts-glyphs.txt') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
code, category, name = line.split(' ', 2)
codepoint = int(code, 16)
if name and codepoint not in name_map:
name_map[codepoint] = name.upper()
for word in name.lower().split():
add_word(word, codepoint)
# Some common synonyms
word_search_map['bee'] |= word_search_map['honeybee']
word_search_map['lambda'] |= word_search_map['lamda']
@@ -512,7 +524,7 @@ def gen_wcwidth() -> None:
p('\t}')
p('\treturn 1;\n}')
p('static bool\nis_emoji_presentation_base(uint32_t code) {')
p('static inline bool\nis_emoji_presentation_base(uint32_t code) {')
p('\tswitch(code) {')
for spec in get_ranges(list(emoji_presentation_bases)):
write_case(spec, p)

View File

@@ -529,7 +529,6 @@ void _glfwPlatformGetMonitorWorkarea(_GLFWmonitor* monitor,
*width = (int)frameRect.size.width;
if (height)
*height = (int)frameRect.size.height;
}
GLFWvidmode* _glfwPlatformGetVideoModes(_GLFWmonitor* monitor, int* count)

View File

@@ -1595,10 +1595,10 @@ void _glfwPlatformSetWindowSizeLimits(_GLFWwindow* window,
void _glfwPlatformSetWindowAspectRatio(_GLFWwindow* window, int numer, int denom)
{
if (numer != GLFW_DONT_CARE && denom != GLFW_DONT_CARE)
[window->ns.object setContentAspectRatio:NSMakeSize(numer, denom)];
else
if (numer == GLFW_DONT_CARE || denom == GLFW_DONT_CARE)
[window->ns.object setResizeIncrements:NSMakeSize(1.0, 1.0)];
else
[window->ns.object setContentAspectRatio:NSMakeSize(numer, denom)];
}
void _glfwPlatformSetWindowSizeIncrements(_GLFWwindow* window, int widthincr, int heightincr)
@@ -1994,8 +1994,10 @@ int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
bitmapFormat:NSBitmapFormatAlphaNonpremultiplied
bytesPerRow:src->width * 4
bitsPerPixel:32];
if (rep == nil)
if (rep == nil) {
[native release];
return false;
}
memcpy([rep bitmapData], src->pixels, src->width * src->height * 4);
[native addRepresentation:rep];
@@ -2446,7 +2448,7 @@ START_ALLOW_CASE_RANGE
END_ALLOW_CASE_RANGE
}
if (utf_16_key != 0) {
strncpy(cocoa_key, [[NSString stringWithCharacters:&utf_16_key length:1] UTF8String], key_sz - 1);
strncpy(cocoa_key, [[NSString stringWithCharacters:&utf_16_key length:1] UTF8String], key_sz - 1);
} else {
unsigned str_pos = 0;
for (unsigned i = 0; i < 4 && str_pos < key_sz - 1; i++) {

View File

@@ -10,6 +10,7 @@ from typing import Callable, Dict, List, Optional, Tuple
_plat = sys.platform.lower()
is_linux = 'linux' in _plat
is_openbsd = 'openbsd' in _plat
base = os.path.dirname(os.path.abspath(__file__))
@@ -72,8 +73,9 @@ def init_env(env: Env, pkg_config: Callable, at_least_version: Callable, test_co
if module in ('x11', 'wayland'):
ans.cflags.append('-pthread')
ans.ldpaths.append('-pthread')
ans.ldpaths.extend('-lrt -lm -ldl'.split())
ans.ldpaths.extend('-pthread -lm'.split())
if not is_openbsd:
ans.ldpaths.extend('-lrt -ldl'.split())
at_least_version('xkbcommon', 0, 5)
if module == 'x11':

2
glfw/glfw3.h vendored
View File

@@ -1502,7 +1502,7 @@ typedef void (* GLFWwindowiconifyfun)(GLFWwindow*,int);
* @endcode
*
* @param[in] window The window that was maximized or restored.
* @param[in] iconified `true` if the window was maximized, or
* @param[in] maximized `true` if the window was maximized, or
* `false` if it was restored.
*
* @sa @ref window_maximize

3
glfw/memfd.h vendored
View File

@@ -39,8 +39,7 @@ static inline int memfd_create(const char *name, unsigned int flags) {
#include <unistd.h>
#include <fcntl.h>
static inline int
createTmpfileCloexec(char* tmpname)
static inline int createTmpfileCloexec(char* tmpname)
{
int fd;

6
glfw/null_monitor.c vendored
View File

@@ -70,8 +70,12 @@ void _glfwPlatformFreeMonitor(_GLFWmonitor* monitor)
_glfwFreeGammaArrays(&monitor->null.ramp);
}
void _glfwPlatformGetMonitorPos(_GLFWmonitor* monitor UNUSED, int* xpos UNUSED, int* ypos UNUSED)
void _glfwPlatformGetMonitorPos(_GLFWmonitor* monitor UNUSED, int* xpos, int* ypos)
{
if (xpos)
*xpos = 0;
if (ypos)
*ypos = 0;
}
void _glfwPlatformGetMonitorContentScale(_GLFWmonitor* monitor UNUSED,

23
glfw/null_window.c vendored
View File

@@ -274,10 +274,25 @@ void _glfwPlatformGetWindowFrameSize(_GLFWwindow* window,
{
if (window->null.decorated && !window->monitor)
{
*left = 1;
*top = 10;
*right = 1;
*bottom = 1;
if (left)
*left = 1;
if (top)
*top = 10;
if (right)
*right = 1;
if (bottom)
*bottom = 1;
}
else
{
if (left)
*left = 0;
if (top)
*top = 0;
if (right)
*right = 0;
if (bottom)
*bottom = 0;
}
}

14
glfw/wl_init.c vendored
View File

@@ -717,6 +717,9 @@ glfwWaylandCheckForServerSideDecorations(void) {
int _glfwPlatformInit(void)
{
int i;
_GLFWmonitor* monitor;
_glfw.wl.cursor.handle = _glfw_dlopen("libwayland-cursor.so.0");
if (!_glfw.wl.cursor.handle)
{
@@ -768,6 +771,17 @@ int _glfwPlatformInit(void)
// Sync so we got all initial output events
wl_display_roundtrip(_glfw.wl.display);
for (i = 0; i < _glfw.monitorCount; ++i)
{
monitor = _glfw.monitors[i];
if (monitor->widthMM <= 0 || monitor->heightMM <= 0)
{
// If Wayland does not provide a physical size, assume the default 96 DPI
monitor->widthMM = (int) (monitor->modes[monitor->wl.currentMode].width * 25.4f / 96.f);
monitor->heightMM = (int) (monitor->modes[monitor->wl.currentMode].height * 25.4f / 96.f);
}
}
if (!_glfw.wl.wmBase)
{
_glfwInputError(GLFW_PLATFORM_ERROR,

8
glfw/wl_platform.h vendored
View File

@@ -204,14 +204,14 @@ typedef struct _GLFWlibraryWayland
struct wl_seat* seat;
struct wl_pointer* pointer;
struct wl_keyboard* keyboard;
struct wl_data_device_manager* dataDeviceManager;
struct wl_data_device* dataDevice;
struct xdg_wm_base* wmBase;
struct zxdg_decoration_manager_v1* decorationManager;
struct wp_viewporter* viewporter;
struct zwp_relative_pointer_manager_v1* relativePointerManager;
struct zwp_pointer_constraints_v1* pointerConstraints;
struct zwp_idle_inhibit_manager_v1* idleInhibitManager;
struct wl_data_device_manager* dataDeviceManager;
struct wl_data_device* dataDevice;
struct wl_data_source* dataSourceForClipboard;
struct zwp_primary_selection_device_manager_v1* primarySelectionDeviceManager;
struct zwp_primary_selection_device_v1* primarySelectionDevice;
@@ -226,6 +226,7 @@ typedef struct _GLFWlibraryWayland
int32_t keyboardRepeatRate;
monotonic_t keyboardRepeatDelay;
struct {
uint32_t key;
id_type keyRepeatTimer;
@@ -275,7 +276,8 @@ typedef struct _GLFWmonitorWayland
int x;
int y;
int scale;
} _GLFWmonitorWayland;
} _GLFWmonitorWayland;
// Wayland-specific per-cursor data
//

23
glfw/wl_window.c vendored
View File

@@ -42,8 +42,7 @@
#include <sys/mman.h>
static void
setCursorImage(_GLFWwindow* window)
static void setCursorImage(_GLFWwindow* window)
{
_GLFWcursorWayland defaultCursor = {.shape = GLFW_ARROW_CURSOR};
_GLFWcursorWayland* cursorWayland = window->cursor ? &window->cursor->wl : &defaultCursor;
@@ -247,8 +246,7 @@ static void dispatchChangesAfterConfigure(_GLFWwindow *window, int32_t width, in
* is set to ENOSPC. If posix_fallocate() is not supported, program may
* receive SIGBUS on accessing mmap()'ed file contents instead.
*/
static int
createAnonymousFile(off_t size)
static int createAnonymousFile(off_t size)
{
int ret, fd = -1, shm_anon = 0;
#ifdef HAS_MEMFD_CREATE
@@ -415,10 +413,10 @@ static void createDecorations(_GLFWwindow* window)
static void destroyDecoration(_GLFWdecorationWayland* decoration)
{
if (decoration->surface)
wl_surface_destroy(decoration->surface);
if (decoration->subsurface)
wl_subsurface_destroy(decoration->subsurface);
if (decoration->surface)
wl_surface_destroy(decoration->surface);
if (decoration->viewport)
wp_viewport_destroy(decoration->viewport);
decoration->surface = NULL;
@@ -439,7 +437,9 @@ static void xdgDecorationHandleConfigure(void* data,
uint32_t mode)
{
_GLFWwindow* window = data;
window->wl.decorations.serverSide = (mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
if (!window->wl.decorations.serverSide)
createDecorations(window);
}
@@ -547,8 +547,7 @@ static bool createSurface(_GLFWwindow* window,
return true;
}
static void
setFullscreen(_GLFWwindow* window, _GLFWmonitor* monitor, bool on)
static void setFullscreen(_GLFWwindow* window, _GLFWmonitor* monitor, bool on)
{
if (window->wl.xdg.toplevel)
{
@@ -739,8 +738,7 @@ static bool createXdgSurface(_GLFWwindow* window)
return true;
}
static void
incrementCursorImage(_GLFWwindow* window)
static void incrementCursorImage(_GLFWwindow* window)
{
if (window && window->wl.decorations.focus == mainWindow && window->cursorMode != GLFW_CURSOR_HIDDEN) {
_GLFWcursor* cursor = window->wl.currentCursor;
@@ -788,8 +786,7 @@ wayland_read_events(int poll_result, int events, void *data UNUSED) {
else wl_display_cancel_read(_glfw.wl.display);
}
static void
handleEvents(monotonic_t timeout)
static void handleEvents(monotonic_t timeout)
{
struct wl_display* display = _glfw.wl.display;
errno = 0;
@@ -958,7 +955,6 @@ void _glfwPlatformDestroyWindow(_GLFWwindow* window)
window->context.destroy(window);
destroyDecorations(window);
if (window->wl.xdg.decoration)
zxdg_toplevel_decoration_v1_destroy(window->wl.xdg.decoration);
@@ -1362,6 +1358,7 @@ int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
cursor->wl.buffer = createShmBuffer(image);
if (!cursor->wl.buffer)
return false;
cursor->wl.width = image->width;
cursor->wl.height = image->height;
cursor->wl.xhot = xhot;

22
glfw/x11_window.c vendored
View File

@@ -422,14 +422,18 @@ static char* convertLatin1toUTF8(const char* source)
size_t size = 1;
const char* sp;
for (sp = source; *sp; sp++)
size += (*sp & 0x80) ? 2 : 1;
if (source) {
for (sp = source; *sp; sp++)
size += (*sp & 0x80) ? 2 : 1;
}
char* target = calloc(size, 1);
char* tp = target;
for (sp = source; *sp; sp++)
tp += encodeUTF8(tp, *sp);
if (source) {
for (sp = source; *sp; sp++)
tp += encodeUTF8(tp, *sp);
}
return target;
}
@@ -771,7 +775,7 @@ static Atom writeTargetToProperty(const XSelectionRequestEvent* request)
if (j < formatCount)
{
XChangeProperty(_glfw.x11.display,
if (selectionString) XChangeProperty(_glfw.x11.display,
request->requestor,
targets[i + 1],
targets[i],
@@ -823,7 +827,7 @@ static Atom writeTargetToProperty(const XSelectionRequestEvent* request)
{
// The requested target is one we support
XChangeProperty(_glfw.x11.display,
if (selectionString) XChangeProperty(_glfw.x11.display,
request->requestor,
request->property,
request->target,
@@ -954,8 +958,10 @@ static const char* getSelectionString(Atom selection)
(XPointer) &notification))
{
monotonic_t time = glfwGetTime();
if (time - start > s_to_monotonic_t(2ll))
if (time - start > s_to_monotonic_t(2ll)) {
free(string);
return "";
}
waitForX11Event(s_to_monotonic_t(2ll) - (time - start));
}
@@ -1518,7 +1524,7 @@ static void processEvent(XEvent *event)
count = 3;
formats = (Atom*) event->xclient.data.l + 2;
}
char **atom_names = calloc(count, sizeof(char**));
char **atom_names = calloc(count, sizeof(char*));
if (atom_names) {
get_atom_names(formats, count, atom_names);

View File

@@ -1 +0,0 @@
See https://sw.kovidgoyal.net/kitty/graphics-protocol.html

View File

@@ -1 +0,0 @@
See https://sw.kovidgoyal.net/kitty/key-encoding.html

View File

@@ -13,7 +13,8 @@ from kitty.constants import cache_dir
from kitty.typing import BossType
from ..tui.handler import result_handler
from ..tui.operations import alternate_screen, set_cursor_visible, styled
from ..tui.operations import alternate_screen, styled
from ..tui.utils import get_key_press
if TYPE_CHECKING:
import readline
@@ -70,7 +71,7 @@ class HistoryCompleter:
def option_text() -> str:
return '''\
--type -t
choices=line,yesno
choices=line,yesno,choices
default=line
Type of input. Defaults to asking for a line of text.
@@ -83,6 +84,16 @@ message is shown.
--name -n
The name for this question. Used to store history of previous answers which can
be used for completions and via the browse history readline bindings.
--choice -c
type=list
dest=choices
A choice for the choices type. Every choice has the syntax: letter:text Where
letter is the accelerator key and text is the corresponding text. There can be
an optional color specification after the letter to indicate what color it should
be.
For example: y:Yes and n;red:No
'''
@@ -97,23 +108,34 @@ class Response(TypedDict):
response: Optional[str]
def yesno(cli_opts: AskCLIOptions, items: List[str]) -> Response:
import tty
def choice(cli_opts: AskCLIOptions, items: List[str]) -> Response:
with alternate_screen():
if cli_opts.message:
print(styled(cli_opts.message, bold=True))
print()
print(' ', styled('Y', fg='green') + 'es', ' ', styled('N', fg='red') + 'o', set_cursor_visible(False))
sys.stdout.flush()
tty.setraw(sys.stdin.fileno())
try:
response = sys.stdin.buffer.read(1)
yes = response in (b'y', b'Y', b'\r', b'\n' b' ')
return {'items': items, 'response': 'y' if yes else 'n'}
finally:
sys.stdout.write(set_cursor_visible(True))
tty.setcbreak(sys.stdin.fileno())
sys.stdout.flush()
allowed = ''
for choice in cli_opts.choices:
color = 'green'
letter, text = choice.split(':', maxsplit=1)
if ';' in letter:
letter, color = letter.split(';', maxsplit=1)
letter = letter.lower()
idx = text.lower().index(letter)
allowed += letter
print(text[:idx], styled(text[idx], fg=color), text[idx + 1:], sep='', end=' ')
print()
response = get_key_press(allowed, '')
return {'items': items, 'response': response}
def yesno(cli_opts: AskCLIOptions, items: List[str]) -> Response:
with alternate_screen():
if cli_opts.message:
print(styled(cli_opts.message, bold=True))
print()
print(' ', styled('Y', fg='green') + 'es', ' ', styled('N', fg='red') + 'o')
response = get_key_press('yn', 'n')
return {'items': items, 'response': response}
def main(args: List[str]) -> Response:
@@ -132,6 +154,8 @@ def main(args: List[str]) -> Response:
if cli_opts.type == 'yesno':
return yesno(cli_opts, items)
if cli_opts.type == 'choices':
return choice(cli_opts, items)
import readline as rl
readline = rl

View File

@@ -111,7 +111,7 @@ end:
if (job_data[i] && job_data[i]->started) wait_for_thread(threads, i);
}
}
for (i = 0; i < num_threads; i++) job_data[i] = free_job(job_data[i]);
if (job_data) { for (i = 0; i < num_threads; i++) job_data[i] = free_job(job_data[i]); }
free(job_data);
free_threads(threads);
return ret;
@@ -130,7 +130,7 @@ run_search(Options *opts, GlobalData *global, const char * const *lines, const s
ALLOC_VEC(text_t, chars, 8192 * 20);
if (chars.data == NULL) return 1;
ALLOC_VEC(Candidate, candidates, 8192);
if (candidates.data == NULL) { FREE_VEC(chars) return 1; }
if (candidates.data == NULL) { FREE_VEC(chars); return 1; }
for (size_t i = 0; i < num_lines; i++) {
sz = sizes[i];

View File

@@ -28,8 +28,9 @@
#define ENSURE_SPACE(TYPE, vec, amt) \
if (vec.size + amt >= vec.capacity) { \
vec.capacity = MAX(vec.capacity * 2, vec.size + amt); \
vec.data = (TYPE*)realloc(vec.data, sizeof(TYPE) * vec.capacity); \
if (vec.data == NULL) { REPORT_OOM; ret = 1; break; } \
void *temp = realloc(vec.data, sizeof(TYPE) * vec.capacity); \
if (temp == NULL) { REPORT_OOM; ret = 1; free(vec.data); vec.data = NULL; vec.size = 0; vec.capacity = 0; break; } \
else vec.data = temp; \
}
#define NEXT(vec) (vec.data[vec.size])

View File

@@ -7,7 +7,7 @@ import re
from contextlib import suppress
from functools import lru_cache
from hashlib import md5
from mimetypes import guess_type
from kitty.guess_mime_type import guess_type
from typing import TYPE_CHECKING, Dict, List, Set, Optional, Iterator, Tuple, Union
if TYPE_CHECKING:
@@ -68,7 +68,10 @@ class Collection:
self.removed_count += len(lines_for_path(left_path))
def finalize(self) -> None:
self.all_paths.sort(key=path_name_map.get)
def key(x: str) -> str:
return path_name_map.get(x, '')
self.all_paths.sort(key=key)
def __iter__(self) -> Iterator[Tuple[str, str, Optional[str]]]:
for path in self.all_paths:
@@ -133,7 +136,7 @@ def sanitize(text: str) -> str:
@lru_cache(maxsize=1024)
def mime_type_for_path(path: str) -> str:
return guess_type(path)[0] or 'application/octet-stream'
return guess_type(path) or 'application/octet-stream'
@lru_cache(maxsize=1024)

View File

@@ -13,7 +13,9 @@ from collections import defaultdict
from contextlib import suppress
from functools import partial
from gettext import gettext as _
from typing import DefaultDict, Dict, Iterable, List, Optional, Tuple, Union
from typing import (
Any, DefaultDict, Dict, Iterable, List, Optional, Tuple, Union
)
from kitty.cli import CONFIG_HELP, parse_args
from kitty.cli_stub import DiffCLIOptions
@@ -24,6 +26,11 @@ from kitty.key_encoding import RELEASE, KeyEvent, enter_key, key_defs as K
from kitty.options_stub import DiffOptions
from kitty.utils import ScreenSize
from ..tui.handler import Handler
from ..tui.images import ImageManager, Placement
from ..tui.line_edit import LineEdit
from ..tui.loop import Loop
from ..tui.operations import styled
from . import global_data
from .collect import (
Collection, create_collection, data_for_path, lines_for_path, sanitize,
@@ -35,14 +42,11 @@ from .render import (
ImagePlacement, ImageSupportWarning, Line, LineRef, Reference, render_diff
)
from .search import BadRegex, Search
from ..tui.handler import Handler
from ..tui.images import ImageManager, Placement
from ..tui.line_edit import LineEdit
from ..tui.loop import Loop
from ..tui.operations import styled
try:
from .highlight import initialize_highlighter, highlight_collection, DiffHighlight
from .highlight import (
DiffHighlight, highlight_collection, initialize_highlighter
)
has_highlighter = True
DiffHighlight
except ImportError:
@@ -566,8 +570,8 @@ class ShowWarning:
def __init__(self) -> None:
self.warnings: List[str] = []
def __call__(self, message: str, category: object, filename: str, lineno: int, file: object = None, line: object = None) -> None:
if category is ImageSupportWarning:
def __call__(self, message: Any, category: Any, filename: str, lineno: int, file: object = None, line: object = None) -> None:
if category is ImageSupportWarning and isinstance(message, str):
showwarning.warnings.append(message)

View File

@@ -45,12 +45,21 @@ ESCAPE = K['ESCAPE']
class Mark:
__slots__ = ('index', 'start', 'end', 'text', 'groupdict')
__slots__ = ('index', 'start', 'end', 'text', 'is_hyperlink', 'group_id', 'groupdict')
def __init__(self, index: int, start: int, end: int, text: str, groupdict: Any):
def __init__(
self,
index: int, start: int, end: int,
text: str,
groupdict: Any,
is_hyperlink: bool = False,
group_id: Optional[str] = None
):
self.index, self.start, self.end = index, start, end
self.text = text
self.groupdict = groupdict
self.is_hyperlink = is_hyperlink
self.group_id = group_id
@lru_cache(maxsize=2048)
@@ -227,6 +236,11 @@ def postprocessor(func: PostprocessorFunc) -> PostprocessorFunc:
return func
class InvalidMatch(Exception):
"""Raised when a match turns out to be invalid."""
pass
@postprocessor
def url(text: str, s: int, e: int) -> Tuple[int, int]:
if s > 4 and text[s - 5:s] == 'link:': # asciidoc URLs
@@ -271,11 +285,29 @@ def quotes(text: str, s: int, e: int) -> Tuple[int, int]:
return s, e
@postprocessor
def ip(text: str, s: int, e: int) -> Tuple[int, int]:
from ipaddress import ip_address
# Check validity of IPs (or raise InvalidMatch)
ip = text[s:e]
try:
ip_address(ip)
except Exception:
raise InvalidMatch("Invalid IP")
return s, e
def mark(pattern: str, post_processors: Iterable[PostprocessorFunc], text: str, args: HintsCLIOptions) -> Generator[Mark, None, None]:
pat = re.compile(pattern)
for idx, (s, e, groupdict) in enumerate(regex_finditer(pat, args.minimum_match_length, text)):
for func in post_processors:
s, e = func(text, s, e)
try:
for func in post_processors:
s, e = func(text, s, e)
except InvalidMatch:
continue
mark_text = text[s:e].replace('\n', '').replace('\0', '')
yield Mark(idx, s, e, mark_text, groupdict)
@@ -316,6 +348,15 @@ def functions_for(args: HintsCLIOptions) -> Tuple[str, List[PostprocessorFunc]]:
pattern = '(?m)^\\s*(.+)[\\s\0]*$'
elif args.type == 'hash':
pattern = '[0-9a-f]{7,128}'
elif args.type == 'ip':
pattern = (
# # IPv4 with no validation
r"((?:\d{1,3}\.){3}\d{1,3}"
r"|"
# # IPv6 with no validation
r"(?:[a-fA-F0-9]{0,4}:){2,7}[a-fA-F0-9]{1,4})"
)
post_processors.append(ip)
elif args.type == 'word':
chars = args.word_characters
if chars is None:
@@ -369,13 +410,66 @@ def load_custom_processor(customize_processing: str) -> Any:
return runpy.run_path(custom_path, run_name='__main__')
def remove_sgr(text: str) -> str:
return re.sub(r'\x1b\[.*?m', '', text)
def process_hyperlinks(text: str) -> Tuple[str, Tuple[Mark, ...]]:
hyperlinks: List[Mark] = []
removed_size = idx = 0
active_hyperlink_url: Optional[str] = None
active_hyperlink_id: Optional[str] = None
active_hyperlink_start_offset = 0
def add_hyperlink(end: int) -> None:
nonlocal idx, active_hyperlink_url, active_hyperlink_id, active_hyperlink_start_offset
assert active_hyperlink_url is not None
hyperlinks.append(Mark(
idx, active_hyperlink_start_offset, end,
active_hyperlink_url,
groupdict={},
is_hyperlink=True, group_id=active_hyperlink_id
))
active_hyperlink_url = active_hyperlink_id = None
active_hyperlink_start_offset = 0
idx += 1
def process_hyperlink(m: 're.Match') -> str:
nonlocal removed_size, active_hyperlink_url, active_hyperlink_id, active_hyperlink_start_offset
raw = m.group()
start = m.start() - removed_size
removed_size += len(raw)
if active_hyperlink_url is not None:
add_hyperlink(start)
raw = raw[4:-2]
parts = raw.split(';', 1)
if len(parts) == 2 and parts[1]:
active_hyperlink_url = parts[1]
active_hyperlink_start_offset = start
if parts[0]:
for entry in parts[0].split(':'):
if entry.startswith('id=') and len(entry) > 3:
active_hyperlink_id = entry[3:]
break
return ''
text = re.sub(r'\x1b\]8.+?\x1b\\', process_hyperlink, text)
if active_hyperlink_url is not None:
add_hyperlink(len(text))
return text, tuple(hyperlinks)
def run(args: HintsCLIOptions, text: str, extra_cli_args: Sequence[str] = ()) -> Optional[Dict[str, Any]]:
try:
text = parse_input(text)
text = parse_input(remove_sgr(text))
text, hyperlinks = process_hyperlinks(text)
pattern, post_processors = functions_for(args)
if args.type == 'linenum':
args.customize_processing = '::linenum::'
if args.customize_processing:
if args.type == 'hyperlink':
all_marks = hyperlinks
elif args.customize_processing:
m = load_custom_processor(args.customize_processing)
if 'mark' in m:
all_marks = tuple(m['mark'](text, args, Mark, extra_cli_args))
@@ -384,9 +478,8 @@ def run(args: HintsCLIOptions, text: str, extra_cli_args: Sequence[str] = ()) ->
else:
all_marks = tuple(mark(pattern, post_processors, text, args))
if not all_marks:
input(_('No {} found, press Enter to quit.').format(
'URLs' if args.type == 'url' else 'matches'
))
none_of = {'url': 'URLs', 'hyperlink': 'hyperlinks'}.get(args.type, 'matches')
input(_('No {} found, press Enter to quit.').format(none_of))
return None
largest_index = all_marks[-1].index
@@ -421,7 +514,7 @@ programs.
--type
default=url
choices=url,regex,path,line,hash,word,linenum
choices=url,regex,path,line,hash,word,linenum,hyperlink,ip
The type of text to search for. A value of :code:`linenum` is special, it looks
for error messages using the pattern specified with :option:`--regex`, which
must have the named groups, :code:`path` and :code:`line`. If not specified,
@@ -573,7 +666,12 @@ def main(args: List[str]) -> Optional[Dict[str, Any]]:
if items and not (opts.customize_processing or opts.type == 'linenum'):
print('Extra command line arguments present: {}'.format(' '.join(items)), file=sys.stderr)
input(_('Press Enter to quit'))
return run(opts, text, items)
try:
return run(opts, text, items)
except Exception:
import traceback
traceback.print_exc()
input(_('Press Enter to quit'))
def linenum_handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: BossType, extra_cli_args: Sequence[str], *a: Any) -> None:
@@ -604,7 +702,7 @@ def linenum_handle_result(args: List[str], data: Dict[str, Any], target_window_i
}[action])(*cmd)
@result_handler(type_of_input='screen')
@result_handler(type_of_input='screen-ansi')
def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: BossType) -> None:
if data['customize_processing']:
m = load_custom_processor(data['customize_processing'])
@@ -656,13 +754,20 @@ def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int,
w = boss.window_id_map.get(target_window_id)
if w is not None:
cwd = w.cwd_of_child
if w is None:
w = boss.active_window
program = None if program == 'default' else program
for m, groupdict in zip(matches, groupdicts):
if groupdict:
m = []
for k, v in groupdict.items():
m.append('{}={}'.format(k, v or ''))
boss.open_url(m, program, cwd=cwd)
if text_type == 'hyperlink':
for m in matches:
if w is not None:
w.open_url(m, hyperlink_id=1, cwd=cwd)
else:
for m, groupdict in zip(matches, groupdicts):
if groupdict:
m = []
for k, v in groupdict.items():
m.append('{}={}'.format(k, v or ''))
boss.open_url(m, program, cwd=cwd)
if __name__ == '__main__':

View File

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
import os
import re
import signal
import socket
import subprocess
import sys
from typing import Callable, cast
from urllib.parse import quote_from_bytes
def write_hyperlink(write: Callable[[bytes], None], url: bytes, line: bytes, frag: bytes = b'') -> None:
text = b'\033]8;;' + url
if frag:
text += b'#' + frag
text += b'\033\\' + line + b'\033]8;;\033\\'
write(text)
def main() -> None:
if not sys.stdout.isatty() and '--pretty' not in sys.argv:
os.execlp('rg', 'rg', *sys.argv[1:])
cmdline = ['rg', '--pretty', '--with-filename'] + sys.argv[1:]
p = subprocess.Popen(cmdline, stdout=subprocess.PIPE)
assert p.stdout is not None
write: Callable[[bytes], None] = cast(Callable[[bytes], None], sys.stdout.buffer.write)
sgr_pat = re.compile(br'\x1b\[.*?m')
osc_pat = re.compile(b'\x1b\\].*?\x1b\\\\')
num_pat = re.compile(b'^(\\d+):')
in_result: bytes = b''
hostname = socket.gethostname().encode('utf-8')
try:
for line in p.stdout:
line = osc_pat.sub(b'', line) # remove any existing hyperlinks
clean_line = sgr_pat.sub(b'', line).rstrip() # remove SGR formatting
if not clean_line:
in_result = b''
write(b'\n')
continue
if in_result:
m = num_pat.match(clean_line)
if m is not None:
write_hyperlink(write, in_result, line, frag=m.group(1))
else:
if line.strip():
path = quote_from_bytes(os.path.abspath(clean_line)).encode('utf-8')
in_result = b'file://' + hostname + path
write_hyperlink(write, in_result, line)
else:
write(line)
except KeyboardInterrupt:
p.send_signal(signal.SIGINT)
p.stdout.close()
except EOFError:
p.stdout.close()
raise SystemExit(p.wait())
if __name__ == '__main__':
main()

View File

@@ -3,7 +3,6 @@
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import contextlib
import mimetypes
import os
import re
import socket
@@ -18,6 +17,7 @@ from typing import (
Dict, Generator, List, NamedTuple, Optional, Pattern, Tuple, Union
)
from kitty.guess_mime_type import guess_type
from kitty.cli import parse_args
from kitty.cli_stub import IcatCLIOptions
from kitty.constants import appname
@@ -30,7 +30,7 @@ from ..tui.images import (
ConvertFailed, GraphicsCommand, NoImageMagick, OpenFailed, convert, fsenc,
identify
)
from ..tui.operations import clear_images_on_screen
from ..tui.operations import clear_images_on_screen, raw_mode
OPTIONS = '''\
--align
@@ -110,6 +110,11 @@ default=0
Z-index of the image. When negative, text will be displayed on top of the image. Use
a double minus for values under the threshold for drawing images under cell background
colors. For example, --1 evaluates as -1,073,741,825.
--hold
type=bool-set
Wait for keypress before exiting after displaying the images.
'''
@@ -268,7 +273,7 @@ def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfil
def scan(d: str) -> Generator[Tuple[str, str], None, None]:
for dirpath, dirnames, filenames in os.walk(d):
for f in filenames:
mt = mimetypes.guess_type(f)[0]
mt = guess_type(f)
if mt and mt.startswith('image/'):
yield os.path.join(dirpath, f), mt
@@ -473,11 +478,14 @@ def main(args: List[str] = sys.argv) -> None:
errors.append(e)
if parsed_opts.place:
sys.stdout.buffer.write(b'\0338') # restore cursor
if not errors:
return
for err in errors:
print(err, file=sys.stderr)
raise SystemExit(1)
if errors:
for err in errors:
print(err, file=sys.stderr)
if cli_opts.hold:
with open(os.ctermid()) as tty:
with raw_mode(tty.fileno()):
tty.buffer.read(1)
raise SystemExit(1 if errors else 0)
if __name__ == '__main__':

View File

View File

@@ -0,0 +1,162 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
import re
import sys
from binascii import unhexlify, hexlify
from contextlib import suppress
from typing import Dict, Iterable, List, Type
from kitty.cli import parse_args
from kitty.cli_stub import QueryTerminalCLIOptions
from kitty.constants import appname
from kitty.utils import TTYIO
from kitty.terminfo import names
class Query:
name: str = ''
ans: str = ''
query_name: str = ''
help_text: str = ''
def __init__(self) -> None:
self.encoded_query_name = hexlify(self.query_name.encode('utf-8')).decode('ascii')
self.pat = re.compile('\x1bP([01])\\+r{}(.*?)\x1b\\\\'.format(self.encoded_query_name).encode('ascii'))
def query_code(self) -> str:
return "\x1bP+q{}\x1b\\".format(self.encoded_query_name)
def decode_response(self, res: bytes) -> str:
return unhexlify(res).decode('utf-8')
def more_needed(self, buffer: bytes) -> bool:
m = self.pat.search(buffer)
if m is None:
return True
if m.group(1) == b'1':
q = m.group(2)
if q.startswith(b'='):
with suppress(Exception):
self.ans = self.decode_response(memoryview(q)[1:])
return False
def output_line(self) -> str:
return self.ans
all_queries: Dict[str, Type[Query]] = {}
def query(cls: Type[Query]) -> Type[Query]:
all_queries[cls.name] = cls
return cls
@query
class TerminalName(Query):
name: str = 'name'
query_name: str = 'TN'
help_text: str = f'Terminal name ({names[0]})'
@query
class TerminalVersion(Query):
name: str = 'version'
query_name: str = 'kitty-query-version'
help_text: str = 'Terminal version, for e.g.: 0.19.2'
@query
class AllowHyperlinks(Query):
name: str = 'allow_hyperlinks'
query_name: str = 'kitty-query-allow_hyperlinks'
help_text: str = 'yes, no or ask'
def do_queries(queries: Iterable, cli_opts: QueryTerminalCLIOptions) -> Dict[str, str]:
actions = tuple(all_queries[x]() for x in queries)
qstring = ''.join(a.query_code() for a in actions)
received = b''
def more_needed(data: bytes) -> bool:
nonlocal received
received += data
for a in actions:
if a.more_needed(received):
return True
return False
with TTYIO() as ttyio:
ttyio.send(qstring)
ttyio.recv(more_needed, timeout=cli_opts.wait_for)
return {a.name: a.output_line() for a in actions}
def options_spec() -> str:
return '''\
--wait-for
type=float
default=10
The amount of time (in seconds) to wait for a response from the terminal, after
querying it.
'''
help_text = '''\
Query the terminal this kitten is run in for various
capabilities. This sends escape codes to the terminal
and based on its response prints out data about supported
capabilities. Note that this is a blocking operation, since
it has to wait for a response from the terminal. You can control
the maximum wait time via the ``--wait-for`` option.
The output is lines of the form::
query: data
If a particular query is unsupported by the running kitty version,
the data will be blank.
Note that when calling this from another program, be very
careful not to perform any I/O on the terminal device
until the kitten exits.
Available queries are::
{}
'''.format(' ' + '\n '.join(
f'{name}: {c.help_text}' for name, c in all_queries.items()))
usage = '[query1 query2 ...]'
def main(args: List[str] = sys.argv) -> None:
cli_opts, items_ = parse_args(
args[1:],
options_spec,
usage,
help_text,
'{} +kitten query_terminal'.format(appname),
result_class=QueryTerminalCLIOptions
)
queries: List[str] = list(items_)
if 'all' in queries or not queries:
queries = sorted(all_queries)
else:
extra = frozenset(queries) - frozenset(all_queries)
if extra:
raise SystemExit(f'Unknown queries: {", ".join(extra)}')
for key, val in do_queries(queries, cli_opts).items():
print(key + ':', val)
if __name__ == '__main__':
main()
elif __name__ == '__doc__':
cd = sys.cli_docs # type: ignore
cd['usage'] = usage
cd['options'] = options_spec
cd['help_text'] = help_text

View File

325
kittens/remote_file/main.py Normal file
View File

@@ -0,0 +1,325 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
import json
import os
import shlex
import shutil
import subprocess
import sys
import tempfile
import time
from typing import Any, List, Optional
from kitty.cli import parse_args
from kitty.cli_stub import RemoteFileCLIOptions
from kitty.constants import cache_dir
from kitty.typing import BossType
from kitty.utils import (
SSHConnectionData, command_for_open, get_editor, open_cmd
)
from ..tui.handler import result_handler
from ..tui.operations import (
faint, raw_mode, reset_terminal, styled
)
from ..tui.utils import get_key_press
def key(x: str) -> str:
return styled(x, bold=True, fg='green')
def option_text() -> str:
return '''\
--mode -m
choices=ask,edit
default=ask
Which mode to operate in.
--path -p
Path to the remote file.
--hostname -h
Hostname of the remote host.
--ssh-connection-data
The data used to connect over ssh.
'''
def show_error(msg: str) -> None:
print(styled(msg, fg='red'))
print()
print('Press any key to exit...')
sys.stdout.flush()
with raw_mode():
while True:
try:
q = sys.stdin.buffer.read(1)
if q:
break
except (KeyboardInterrupt, EOFError):
break
def ask_action(opts: RemoteFileCLIOptions) -> str:
print('What would you like to do with the remote file on {}:'.format(styled(opts.hostname or 'unknown', bold=True, fg='magenta')))
print(styled(opts.path or '', fg='yellow', fg_intense=True))
print()
def help_text(x: str) -> str:
return faint(x)
print('{}dit the file'.format(key('E')))
print(help_text('The file will be downloaded and opened in an editor. Any changes you save will'
' be automatically sent back to the remote machine'))
print()
print('{}pen the file'.format(key('O')))
print(help_text('The file will be downloaded and opened by the default open program'))
print()
print('{}ave the file'.format(key('S')))
print(help_text('The file will be downloaded to a destination you select'))
print()
print('{}ancel'.format(key('C')))
print()
sys.stdout.flush()
response = get_key_press('ceos', 'c')
return {'e': 'edit', 'o': 'open', 's': 'save'}.get(response, 'cancel')
def hostname_matches(from_hyperlink: str, actual: str) -> bool:
if from_hyperlink == actual:
return True
if from_hyperlink.partition('.')[0] == actual.partition('.')[0]:
return True
return False
class ControlMaster:
def __init__(self, conn_data: SSHConnectionData, remote_path: str, cli_opts: RemoteFileCLIOptions, dest: str = ''):
self.conn_data = conn_data
self.cli_opts = cli_opts
self.remote_path = remote_path
self.dest = dest
self.tdir = ''
self.cmd_prefix = cmd = [
conn_data.binary, '-o', f'ControlPath=~/.ssh/kitty-master-{os.getpid()}-%r@%h:%p',
'-o', 'TCPKeepAlive=yes', '-o', 'ControlPersist=yes'
]
if conn_data.port:
cmd += ['-p', str(conn_data.port)]
self.batch_cmd_prefix = cmd + ['-o', 'BatchMode=yes']
def __enter__(self) -> 'ControlMaster':
subprocess.check_call(
self.cmd_prefix + ['-o', 'ControlMaster=auto', '-fN', self.conn_data.hostname])
subprocess.check_call(
self.batch_cmd_prefix + ['-O', 'check', self.conn_data.hostname])
if not self.dest:
self.tdir = tempfile.mkdtemp()
self.dest = os.path.join(self.tdir, os.path.basename(self.remote_path))
return self
def __exit__(self, *a: Any) -> bool:
subprocess.Popen(
self.batch_cmd_prefix + ['-O', 'exit', self.conn_data.hostname],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL
).wait()
if self.tdir:
shutil.rmtree(self.tdir)
return True
@property
def is_alive(self) -> bool:
return subprocess.Popen(
self.batch_cmd_prefix + ['-O', 'check', self.conn_data.hostname],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL
).wait() == 0
def check_hostname_matches(self) -> bool:
cp = subprocess.run(self.batch_cmd_prefix + [self.conn_data.hostname, 'hostname', '-f'], stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
if cp.returncode == 0:
q = cp.stdout.decode('utf-8').strip()
if not hostname_matches(self.cli_opts.hostname or '', q):
print(reset_terminal(), end='')
print(f'The remote hostname {styled(q, fg="green")} does not match the')
print(f'hostname in the hyperlink {styled(self.cli_opts.hostname or "", fg="red")}')
print('This indicates that kitty has not connected to the correct remote machine.')
print('This can happen, for example, when using nested SSH sessions.')
print(f'The hostname kitty used to connect was: {styled(self.conn_data.hostname, fg="yellow")}', end='')
if self.conn_data.port is not None:
print(f' with port: {self.conn_data.port}')
print()
print()
print('Do you want to continue anyway?')
print(
f'{styled("Y", fg="green")}es',
f'{styled("N", fg="red")}o', sep='\t'
)
sys.stdout.flush()
response = get_key_press('yn', 'n')
print(reset_terminal(), end='')
return response == 'y'
return True
def download(self) -> bool:
with open(self.dest, 'wb') as f:
return subprocess.run(
self.batch_cmd_prefix + [self.conn_data.hostname, 'cat', self.remote_path],
stdout=f, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL
).returncode == 0
def upload(self, suppress_output: bool = True) -> bool:
cmd_prefix = self.cmd_prefix if suppress_output else self.batch_cmd_prefix
cmd = cmd_prefix + [self.conn_data.hostname, 'cat', '>', self.remote_path]
if not suppress_output:
print(' '.join(map(shlex.quote, cmd)))
redirect = subprocess.DEVNULL if suppress_output else None
with open(self.dest, 'rb') as f:
return subprocess.run(cmd, stdout=redirect, stderr=redirect, stdin=f).returncode == 0
Result = Optional[str]
def main(args: List[str]) -> Result:
msg = 'Ask the user what to do with the remote file'
try:
cli_opts, items = parse_args(args[1:], option_text, '', msg, 'kitty remote_file', result_class=RemoteFileCLIOptions)
except SystemExit as e:
if e.code != 0:
print(e.args[0])
input('Press enter to quit...')
raise SystemExit(e.code)
try:
action = ask_action(cli_opts)
finally:
print(reset_terminal(), end='', flush=True)
try:
return handle_action(action, cli_opts)
except Exception:
print(reset_terminal(), end='', flush=True)
import traceback
traceback.print_exc()
show_error('Failed with unhandled exception')
def save_as(conn_data: SSHConnectionData, remote_path: str, cli_opts: RemoteFileCLIOptions) -> None:
ddir = cache_dir()
os.makedirs(ddir, exist_ok=True)
last_used_store_path = os.path.join(ddir, 'remote-file-last-used.txt')
try:
with open(last_used_store_path) as f:
last_used_path = f.read()
except FileNotFoundError:
last_used_path = tempfile.gettempdir()
last_used_file = os.path.join(last_used_path, os.path.basename(remote_path))
print(
'Where do you wish to save the file? Leaving it blank will save it as:',
styled(last_used_file, fg='yellow')
)
print('Relative paths will be resolved from:', styled(os.getcwd(), fg_intense=True, bold=True))
print()
from ..tui.path_completer import PathCompleter
try:
dest = PathCompleter().input()
except (KeyboardInterrupt, EOFError):
return
if dest:
dest = os.path.expandvars(os.path.expanduser(dest))
if os.path.isdir(dest):
dest = os.path.join(dest, os.path.basename(remote_path))
with open(last_used_store_path, 'w') as f:
f.write(os.path.dirname(os.path.abspath(dest)))
else:
dest = last_used_file
if os.path.exists(dest):
print(reset_terminal(), end='')
print(f'The file {styled(dest, fg="yellow")} already exists. What would you like to do?')
print(f'{key("O")}verwrite {key("A")}bort Auto {key("R")}ename {key("N")}ew name')
response = get_key_press('anor', 'a')
if response == 'a':
return
if response == 'n':
print(reset_terminal(), end='')
return save_as(conn_data, remote_path, cli_opts)
if response == 'r':
q = dest
c = 0
while os.path.exists(q):
c += 1
b, ext = os.path.splitext(dest)
q = f'{b}-{c}{ext}'
dest = q
if os.path.dirname(dest):
os.makedirs(os.path.dirname(dest), exist_ok=True)
with ControlMaster(conn_data, remote_path, cli_opts, dest=dest) as master:
if master.check_hostname_matches():
if not master.download():
show_error('Failed to copy file from remote machine')
def handle_action(action: str, cli_opts: RemoteFileCLIOptions) -> Result:
conn_data = SSHConnectionData(*json.loads(cli_opts.ssh_connection_data or ''))
remote_path = cli_opts.path or ''
if action == 'open':
print('Opening', cli_opts.path, 'from', cli_opts.hostname)
dest = os.path.join(tempfile.mkdtemp(), os.path.basename(remote_path))
with ControlMaster(conn_data, remote_path, cli_opts, dest=dest) as master:
if master.check_hostname_matches():
if master.download():
return dest
show_error('Failed to copy file from remote machine')
elif action == 'edit':
print('Editing', cli_opts.path, 'from', cli_opts.hostname)
with ControlMaster(conn_data, remote_path, cli_opts) as master:
if not master.check_hostname_matches():
return None
if not master.download():
show_error(f'Failed to download {remote_path}')
return None
mtime = os.path.getmtime(master.dest)
print(reset_terminal(), end='', flush=True)
editor = get_editor()
editor_process = subprocess.Popen(editor + [master.dest])
while editor_process.poll() is None:
time.sleep(0.1)
newmtime = os.path.getmtime(master.dest)
if newmtime > mtime:
mtime = newmtime
if master.is_alive:
master.upload()
print(reset_terminal(), end='', flush=True)
if master.is_alive:
if not master.upload(suppress_output=False):
show_error(f'Failed to upload {remote_path}')
else:
show_error(f'Failed to upload {remote_path}, SSH master process died')
elif action == 'save':
print('Saving', cli_opts.path, 'from', cli_opts.hostname)
save_as(conn_data, remote_path, cli_opts)
@result_handler()
def handle_result(args: List[str], data: Result, target_window_id: int, boss: BossType) -> None:
if data:
cmd = command_for_open(boss.opts.open_url_with)
open_cmd(cmd, data)
if __name__ == '__main__':
main(sys.argv)

View File

@@ -7,7 +7,10 @@ import re
import shlex
import subprocess
import sys
from typing import List, NoReturn, Set, Tuple
from contextlib import suppress
from typing import List, NoReturn, Optional, Set, Tuple
from kitty.utils import SSHConnectionData
SHELL_SCRIPT = '''\
#!/bin/sh
@@ -60,6 +63,41 @@ def get_ssh_cli() -> Tuple[Set[str], Set[str]]:
return set('-' + x for x in boolean_ssh_args), set('-' + x for x in other_ssh_args)
def get_connection_data(args: List[str]) -> Optional[SSHConnectionData]:
boolean_ssh_args, other_ssh_args = get_ssh_cli()
found_ssh = ''
port: Optional[int] = None
expecting_port = False
expecting_option_val = False
for i, arg in enumerate(args):
if not found_ssh:
if os.path.basename(arg).lower() in ('ssh', 'ssh.exe'):
found_ssh = arg
continue
if arg.startswith('-') and not expecting_option_val:
if arg in boolean_ssh_args:
continue
if arg.startswith('-p'):
if arg[2:].isdigit():
with suppress(Exception):
port = int(arg[2:])
elif arg == '-p':
expecting_port = True
expecting_option_val = True
continue
if expecting_option_val:
if expecting_port:
with suppress(Exception):
port = int(arg)
expecting_port = False
expecting_option_val = False
continue
return SSHConnectionData(found_ssh, arg, port)
def parse_ssh_args(args: List[str]) -> Tuple[List[str], List[str], bool]:
boolean_ssh_args, other_ssh_args = get_ssh_cli()
passthrough_args = {'-' + x for x in 'Nnf'}
@@ -80,15 +118,14 @@ def parse_ssh_args(args: List[str]) -> Tuple[List[str], List[str], bool]:
if arg in boolean_ssh_args:
ssh_args.append(arg)
continue
if arg.startswith('-p') and arg[2:].isdigit():
ssh_args.append(arg)
continue
if arg in other_ssh_args:
if i != len(all_args) - 1:
raise SystemExit('Option {} cannot occur in the middle'.format(arg))
ssh_args.append(arg)
expecting_option_val = True
continue
rest = all_args[i+1:]
if rest:
ssh_args.append(rest)
else:
expecting_option_val = True
break
raise SystemExit('Unknown option: {}'.format(arg))
continue
if expecting_option_val:

385
kittens/tui/dircolors.py Normal file
View File

@@ -0,0 +1,385 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
import os
import stat
from contextlib import suppress
from typing import Dict, Generator, Optional, Tuple, Union
DEFAULT_DIRCOLORS = r"""# {{{
# Configuration file for dircolors, a utility to help you set the
# LS_COLORS environment variable used by GNU ls with the --color option.
# Copyright (C) 1996-2019 Free Software Foundation, Inc.
# Copying and distribution of this file, with or without modification,
# are permitted provided the copyright notice and this notice are preserved.
# The keywords COLOR, OPTIONS, and EIGHTBIT (honored by the
# slackware version of dircolors) are recognized but ignored.
# Below are TERM entries, which can be a glob patterns, to match
# against the TERM environment variable to determine if it is colorizable.
TERM Eterm
TERM ansi
TERM *color*
TERM con[0-9]*x[0-9]*
TERM cons25
TERM console
TERM cygwin
TERM dtterm
TERM gnome
TERM hurd
TERM jfbterm
TERM konsole
TERM kterm
TERM linux
TERM linux-c
TERM mlterm
TERM putty
TERM rxvt*
TERM screen*
TERM st
TERM terminator
TERM tmux*
TERM vt100
TERM xterm*
# Below are the color init strings for the basic file types.
# One can use codes for 256 or more colors supported by modern terminals.
# The default color codes use the capabilities of an 8 color terminal
# with some additional attributes as per the following codes:
# Attribute codes:
# 00=none 01=bold 04=underscore 05=blink 07=reverse 08=concealed
# Text color codes:
# 30=black 31=red 32=green 33=yellow 34=blue 35=magenta 36=cyan 37=white
# Background color codes:
# 40=black 41=red 42=green 43=yellow 44=blue 45=magenta 46=cyan 47=white
#NORMAL 00 # no color code at all
#FILE 00 # regular file: use no color at all
RESET 0 # reset to "normal" color
DIR 01;34 # directory
LINK 01;36 # symbolic link. (If you set this to 'target' instead of a
# numerical value, the color is as for the file pointed to.)
MULTIHARDLINK 00 # regular file with more than one link
FIFO 40;33 # pipe
SOCK 01;35 # socket
DOOR 01;35 # door
BLK 40;33;01 # block device driver
CHR 40;33;01 # character device driver
ORPHAN 40;31;01 # symlink to nonexistent file, or non-stat'able file ...
MISSING 00 # ... and the files they point to
SETUID 37;41 # file that is setuid (u+s)
SETGID 30;43 # file that is setgid (g+s)
CAPABILITY 30;41 # file with capability
STICKY_OTHER_WRITABLE 30;42 # dir that is sticky and other-writable (+t,o+w)
OTHER_WRITABLE 34;42 # dir that is other-writable (o+w) and not sticky
STICKY 37;44 # dir with the sticky bit set (+t) and not other-writable
# This is for files with execute permission:
EXEC 01;32
# List any file extensions like '.gz' or '.tar' that you would like ls
# to colorize below. Put the extension, a space, and the color init string.
# (and any comments you want to add after a '#')
# If you use DOS-style suffixes, you may want to uncomment the following:
#.cmd 01;32 # executables (bright green)
#.exe 01;32
#.com 01;32
#.btm 01;32
#.bat 01;32
# Or if you want to colorize scripts even if they do not have the
# executable bit actually set.
#.sh 01;32
#.csh 01;32
# archives or compressed (bright red)
.tar 01;31
.tgz 01;31
.arc 01;31
.arj 01;31
.taz 01;31
.lha 01;31
.lz4 01;31
.lzh 01;31
.lzma 01;31
.tlz 01;31
.txz 01;31
.tzo 01;31
.t7z 01;31
.zip 01;31
.z 01;31
.dz 01;31
.gz 01;31
.lrz 01;31
.lz 01;31
.lzo 01;31
.xz 01;31
.zst 01;31
.tzst 01;31
.bz2 01;31
.bz 01;31
.tbz 01;31
.tbz2 01;31
.tz 01;31
.deb 01;31
.rpm 01;31
.jar 01;31
.war 01;31
.ear 01;31
.sar 01;31
.rar 01;31
.alz 01;31
.ace 01;31
.zoo 01;31
.cpio 01;31
.7z 01;31
.rz 01;31
.cab 01;31
.wim 01;31
.swm 01;31
.dwm 01;31
.esd 01;31
# image formats
.jpg 01;35
.jpeg 01;35
.mjpg 01;35
.mjpeg 01;35
.gif 01;35
.bmp 01;35
.pbm 01;35
.pgm 01;35
.ppm 01;35
.tga 01;35
.xbm 01;35
.xpm 01;35
.tif 01;35
.tiff 01;35
.png 01;35
.svg 01;35
.svgz 01;35
.mng 01;35
.pcx 01;35
.mov 01;35
.mpg 01;35
.mpeg 01;35
.m2v 01;35
.mkv 01;35
.webm 01;35
.ogm 01;35
.mp4 01;35
.m4v 01;35
.mp4v 01;35
.vob 01;35
.qt 01;35
.nuv 01;35
.wmv 01;35
.asf 01;35
.rm 01;35
.rmvb 01;35
.flc 01;35
.avi 01;35
.fli 01;35
.flv 01;35
.gl 01;35
.dl 01;35
.xcf 01;35
.xwd 01;35
.yuv 01;35
.cgm 01;35
.emf 01;35
# https://wiki.xiph.org/MIME_Types_and_File_Extensions
.ogv 01;35
.ogx 01;35
# audio formats
.aac 00;36
.au 00;36
.flac 00;36
.m4a 00;36
.mid 00;36
.midi 00;36
.mka 00;36
.mp3 00;36
.mpc 00;36
.ogg 00;36
.ra 00;36
.wav 00;36
# https://wiki.xiph.org/MIME_Types_and_File_Extensions
.oga 00;36
.opus 00;36
.spx 00;36
.xspf 00;36
""" # }}}
# special file?
special_types = (
(stat.S_IFLNK, 'ln'), # symlink
(stat.S_IFIFO, 'pi'), # pipe (FIFO)
(stat.S_IFSOCK, 'so'), # socket
(stat.S_IFBLK, 'bd'), # block device
(stat.S_IFCHR, 'cd'), # character device
(stat.S_ISUID, 'su'), # setuid
(stat.S_ISGID, 'sg'), # setgid
)
CODE_MAP = {
'RESET': 'rs',
'DIR': 'di',
'LINK': 'ln',
'MULTIHARDLINK': 'mh',
'FIFO': 'pi',
'SOCK': 'so',
'DOOR': 'do',
'BLK': 'bd',
'CHR': 'cd',
'ORPHAN': 'or',
'MISSING': 'mi',
'SETUID': 'su',
'SETGID': 'sg',
'CAPABILITY': 'ca',
'STICKY_OTHER_WRITABLE': 'tw',
'OTHER_WRITABLE': 'ow',
'STICKY': 'st',
'EXEC': 'ex',
}
def stat_at(file: str, cwd: Optional[Union[int, str]] = None, follow_symlinks: bool = False) -> os.stat_result:
dirfd: Optional[int] = None
need_to_close = False
if isinstance(cwd, str):
dirfd = os.open(cwd, os.O_RDONLY)
need_to_close = True
elif isinstance(cwd, int):
dirfd = cwd
try:
return os.stat(file, dir_fd=dirfd, follow_symlinks=follow_symlinks)
finally:
if need_to_close and dirfd is not None:
os.close(dirfd)
class Dircolors:
def __init__(self) -> None:
self.codes: Dict[str, str] = {}
self.extensions: Dict[str, str] = {}
if not self.load_from_environ() and not self.load_from_file():
self.load_defaults()
def clear(self) -> None:
self.codes.clear()
self.extensions.clear()
def load_from_file(self) -> bool:
for candidate in (os.path.expanduser('~/.dir_colors'), '/etc/DIR_COLORS'):
with suppress(Exception):
with open(candidate) as f:
return self.load_from_dircolors(f.read())
return False
def load_from_lscolors(self, lscolors: str) -> bool:
self.clear()
if not lscolors:
return False
for item in lscolors.split(':'):
try:
code, color = item.split('=', 1)
except ValueError:
continue
if code.startswith('*.'):
self.extensions[code[1:]] = color
else:
self.codes[code] = color
return bool(self.codes or self.extensions)
def load_from_environ(self, envvar: str = 'LS_COLORS') -> bool:
return self.load_from_lscolors(os.environ.get(envvar) or '')
def load_from_dircolors(self, database: str, strict: bool = False) -> bool:
self.clear()
for line in database.splitlines():
line = line.split('#')[0].strip()
if not line:
continue
split = line.split()
if len(split) != 2:
if strict:
raise ValueError(f'Warning: unable to parse dircolors line "{line}"')
continue
key, val = split
if key == 'TERM':
continue
if key in CODE_MAP:
self.codes[CODE_MAP[key]] = val
elif key.startswith('.'):
self.extensions[key] = val
elif strict:
raise ValueError(f'Warning: unable to parse dircolors line "{line}"')
return bool(self.codes or self.extensions)
def load_defaults(self) -> bool:
self.clear()
return self.load_from_dircolors(DEFAULT_DIRCOLORS, True)
def generate_lscolors(self) -> str:
""" Output the database in the format used by the LS_COLORS environment variable. """
def gen_pairs() -> Generator[Tuple[str, str], None, None]:
for pair in self.codes.items():
yield pair
for pair in self.extensions.items():
# change .xyz to *.xyz
yield '*' + pair[0], pair[1]
return ':'.join('%s=%s' % pair for pair in gen_pairs())
def _format_code(self, text: str, code: str) -> str:
val = self.codes.get(code)
return '\033[%sm%s\033[%sm' % (val, text, self.codes.get('rs', '0')) if val else text
def _format_ext(self, text: str, ext: str) -> str:
val = self.extensions.get(ext, '0')
return '\033[%sm%s\033[%sm' % (val, text, self.codes.get('rs', '0')) if val else text
def format_mode(self, text: str, sr: os.stat_result) -> str:
mode = sr.st_mode
if stat.S_ISDIR(mode):
if (mode & (stat.S_ISVTX | stat.S_IWOTH)) == (stat.S_ISVTX | stat.S_IWOTH):
# sticky and world-writable
return self._format_code(text, 'tw')
if mode & stat.S_ISVTX:
# sticky but not world-writable
return self._format_code(text, 'st')
if mode & stat.S_IWOTH:
# world-writable but not sticky
return self._format_code(text, 'ow')
# normal directory
return self._format_code(text, 'di')
for mask, code in special_types:
if (mode & mask) == mask:
return self._format_code(text, code)
# executable file?
if mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH):
return self._format_code(text, 'ex')
# regular file, format according to its extension
ext = os.path.splitext(text)[1]
if ext:
return self._format_ext(text, ext)
return text
def __call__(self, path: str, text: str, cwd: Optional[Union[int, str]] = None) -> str:
follow_symlinks = self.codes.get('ln') == 'target'
try:
sr = stat_at(path, cwd, follow_symlinks)
except OSError:
return text
return self.format_mode(text, sr)
def develop() -> None:
import sys
print(Dircolors()(sys.argv[-1], sys.argv[-1]))

View File

@@ -10,6 +10,7 @@ import re
import selectors
import signal
import sys
import termios
from contextlib import contextmanager
from functools import partial
from typing import Any, Callable, Dict, Generator, List, NamedTuple, Optional
@@ -61,8 +62,9 @@ debug = Debug()
class TermManager:
def __init__(self) -> None:
def __init__(self, optional_actions: int = termios.TCSANOW) -> None:
self.extra_finalize: Optional[str] = None
self.optional_actions = optional_actions
def set_state_for_loop(self, set_raw: bool = True) -> None:
if set_raw:
@@ -82,7 +84,7 @@ class TermManager:
self.set_state_for_loop()
def __enter__(self) -> 'TermManager':
self.tty_fd, self.original_termios = open_tty()
self.tty_fd, self.original_termios = open_tty(False, self.optional_actions)
self.set_state_for_loop(set_raw=False)
return self
@@ -183,7 +185,8 @@ class Loop:
def __init__(
self,
sanitize_bracketed_paste: str = '[\x03\x04\x0e\x0f\r\x07\x7f\x8d\x8e\x8f\x90\x9b\x9d\x9e\x9f]'
sanitize_bracketed_paste: str = '[\x03\x04\x0e\x0f\r\x07\x7f\x8d\x8e\x8f\x90\x9b\x9d\x9e\x9f]',
optional_actions: int = termios.TCSADRAIN
):
if is_macos:
# On macOS PTY devices are not supported by the KqueueSelector and
@@ -193,6 +196,7 @@ class Loop:
else:
self.asycio_loop = asyncio.get_event_loop()
self.return_code = 0
self.optional_actions = optional_actions
self.read_buf = ''
self.decoder = codecs.getincrementaldecoder('utf-8')('ignore')
try:
@@ -388,7 +392,7 @@ class Loop:
handler.on_resize(handler.screen_size)
signal_manager = SignalManager(self.asycio_loop, _on_sigwinch, handler.on_interrupt, handler.on_term)
with TermManager() as term_manager, signal_manager:
with TermManager(self.optional_actions) as term_manager, signal_manager:
self._get_screen_size: ScreenSizeGetter = screen_size_function(term_manager.tty_fd)
image_manager = None
if handler.image_manager_class is not None:

View File

@@ -73,6 +73,11 @@ def clear_to_eol() -> str:
return '\033[K'
@cmd
def reset_terminal() -> str:
return '\033]\033\\\033c'
@cmd
def bell() -> str:
return '\a'
@@ -288,6 +293,20 @@ def alternate_screen(f: Optional[IO[str]] = None) -> Generator[None, None, None]
print(reset_mode('ALTERNATE_SCREEN'), end='', file=f)
@contextmanager
def raw_mode(fd: Optional[int] = None) -> Generator[None, None, None]:
import tty
import termios
if fd is None:
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
yield
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
@cmd
def set_default_colors(
fg: Optional[Union[Color, str]] = None,

View File

@@ -0,0 +1,150 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
import os
from typing import Any, Callable, Dict, Generator, Optional, Sequence, Tuple
from kitty.fast_data_types import wcswidth
from kitty.utils import ScreenSize, screen_size_function
from .operations import styled
def directory_completions(path: str, qpath: str, prefix: str = '') -> Generator[str, None, None]:
try:
entries = os.scandir(qpath)
except OSError:
return
for x in entries:
try:
is_dir = x.is_dir()
except OSError:
is_dir = False
name = x.name + (os.sep if is_dir else '')
if not prefix or name.startswith(prefix):
if path:
yield os.path.join(path, name)
else:
yield name
def expand_path(path: str) -> str:
return os.path.abspath(os.path.expandvars(os.path.expanduser(path)))
def find_completions(path: str) -> Generator[str, None, None]:
if path and path[0] == '~':
if path == '~':
yield '~' + os.sep
return
if os.sep not in path:
qpath = os.path.expanduser(path)
if qpath != path:
yield path + os.sep
return
qpath = expand_path(path)
if not path or path.endswith(os.sep):
yield from directory_completions(path, qpath)
else:
yield from directory_completions(os.path.dirname(path), os.path.dirname(qpath), os.path.basename(qpath))
def print_table(items: Sequence[str], screen_size: ScreenSize, dir_colors: Callable[[str, str], str]) -> None:
max_width = 0
item_widths = {}
for item in items:
item_widths[item] = w = wcswidth(item)
max_width = max(w, max_width)
col_width = max_width + 2
num_of_cols = max(1, screen_size.cols // col_width)
cr = 0
at_start = False
for item in items:
w = item_widths[item]
left = col_width - w
print(dir_colors(expand_path(item), item), ' ' * left, sep='', end='')
at_start = False
cr = (cr + 1) % num_of_cols
if not cr:
print()
at_start = True
if not at_start:
print()
class PathCompleter:
def __init__(self, prompt: str = '> '):
self.prompt = prompt
self.prompt_len = wcswidth(self.prompt)
def __enter__(self) -> 'PathCompleter':
import readline
from .dircolors import Dircolors
if 'libedit' in readline.__doc__:
readline.parse_and_bind("bind -e")
readline.parse_and_bind("bind '\t' rl_complete")
else:
readline.parse_and_bind('tab: complete')
readline.parse_and_bind('set colored-stats on')
readline.set_completer_delims(' \t\n`!@#$%^&*()-=+[{]}\\|;:\'",<>?')
readline.set_completion_display_matches_hook(self.format_completions)
self.original_completer = readline.get_completer()
readline.set_completer(self)
self.cache: Dict[str, Tuple[str, ...]] = {}
self.dircolors = Dircolors()
return self
def format_completions(self, substitution: str, matches: Sequence[str], longest_match_length: int) -> None:
import readline
print()
files, dirs = [], []
for m in matches:
if m.endswith('/'):
if len(m) > 1:
m = m[:-1]
dirs.append(m)
else:
files.append(m)
ss = screen_size_function()()
if dirs:
print(styled('Directories', bold=True, fg_intense=True))
print_table(dirs, ss, self.dircolors)
if files:
print(styled('Files', bold=True, fg_intense=True))
print_table(files, ss, self.dircolors)
buf = readline.get_line_buffer()
x = readline.get_endidx()
buflen = wcswidth(buf)
print(self.prompt, buf, sep='', end='')
if x < buflen:
pos = x + self.prompt_len
print(f"\r\033[{pos}C", end='')
print(sep='', end='', flush=True)
def __call__(self, text: str, state: int) -> Optional[str]:
options = self.cache.get(text)
if options is None:
options = self.cache[text] = tuple(find_completions(text))
if options and state < len(options):
return options[state]
def __exit__(self, *a: Any) -> bool:
import readline
del self.cache
readline.set_completer(self.original_completer)
readline.set_completion_display_matches_hook()
return True
def input(self) -> str:
with self:
return input(self.prompt)
def develop() -> None:
PathCompleter().input()

29
kittens/tui/utils.py Normal file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
import sys
from contextlib import suppress
from .operations import raw_mode, set_cursor_visible
def get_key_press(allowed: str, default: str) -> str:
response = default
with raw_mode():
print(set_cursor_visible(False), end='', flush=True)
try:
while True:
q = sys.stdin.buffer.read(1)
if q:
if q in b'\x1b\x03':
break
with suppress(Exception):
response = q.decode('utf-8').lower()
if response in allowed:
break
except (KeyboardInterrupt, EOFError):
pass
finally:
print(set_cursor_visible(True), end='', flush=True)
return response

File diff suppressed because one or more lines are too long

View File

@@ -69,6 +69,7 @@ class Borders:
self.os_window_id = os_window_id
self.tab_id = tab_id
self.draw_active_borders = opts.active_border_color is not None
self.draw_minimal_borders = opts.draw_minimal_borders and max(opts.window_margin_width) < 1
def __call__(
self,
@@ -88,29 +89,24 @@ class Borders:
if groups:
bw = groups[0].effective_border()
draw_borders = bw > 0 and draw_window_borders
if draw_borders:
border_data = current_layout.resolve_borders(all_windows)
active_group = all_windows.active_group
for i, wg in enumerate(groups):
window_bg = wg.default_bg
window_bg = (window_bg << 8) | BorderColor.window_bg
if draw_borders:
if draw_borders and not self.draw_minimal_borders:
# Draw the border rectangles
if wg is active_group and self.draw_active_borders:
color = BorderColor.active
else:
color = BorderColor.bell if wg.needs_attention else BorderColor.inactive
try:
colors = tuple(color if needed else window_bg for needed in next(border_data))
draw_edges(self.os_window_id, self.tab_id, colors, wg, borders=True)
except StopIteration:
pass
draw_edges(self.os_window_id, self.tab_id, (color, color, color, color), wg, borders=True)
if not has_background_image:
# Draw the background rectangles over the padding region
colors = window_bg, window_bg, window_bg, window_bg
draw_edges(self.os_window_id, self.tab_id, colors, wg)
color = BorderColor.inactive
for (left, top, right, bottom) in current_layout.window_independent_borders(all_windows):
add_borders_rect(self.os_window_id, self.tab_id, left, top, right, bottom, color)
if self.draw_minimal_borders:
for border_line in current_layout.minimal_borders(all_windows):
left, top, right, bottom = border_line.edges
add_borders_rect(self.os_window_id, self.tab_id, left, top, right, bottom, border_line.color)

View File

@@ -28,7 +28,7 @@ from .constants import (
)
from .fast_data_types import (
CLOSE_BEING_CONFIRMED, IMPERATIVE_CLOSE_REQUESTED, NO_CLOSE_REQUESTED,
ChildMonitor, background_opacity_of, change_background_opacity,
ChildMonitor, add_timer, background_opacity_of, change_background_opacity,
change_os_window_state, cocoa_set_menubar_title, create_os_window,
current_application_quit_request, current_os_window, destroy_global_data,
focus_os_window, get_clipboard_string, global_font_size,
@@ -39,6 +39,7 @@ from .fast_data_types import (
)
from .keys import get_shortcut, shortcut_matches
from .layout.base import set_layout_options
from .notify import notification_activated
from .options_stub import Options
from .os_window_size import initial_window_size_func
from .rgb import Color, color_from_int
@@ -63,12 +64,6 @@ class OSWindowDict(TypedDict):
tabs: List[TabDict]
def notification_activated(identifier: str) -> None:
if identifier == 'new-version':
from .update_check import notification_activated as do
do()
def listen_on(spec: str) -> int:
import socket
family, address, socket_path = parse_address_spec(spec)
@@ -172,12 +167,18 @@ class Boss:
if new_os_window_trigger is not None:
self.keymap.pop(new_os_window_trigger, None)
if is_macos:
from .fast_data_types import cocoa_set_notification_activated_callback
from .fast_data_types import (
cocoa_set_notification_activated_callback
)
cocoa_set_notification_activated_callback(notification_activated)
def startup_first_child(self, os_window_id: Optional[int]) -> None:
from kitty.launch import load_watch_modules
startup_sessions = create_sessions(self.opts, self.args, default_session=self.opts.startup_session)
watchers = load_watch_modules(self.args.watcher)
for startup_session in startup_sessions:
if watchers is not None and watchers.has_watchers:
startup_session.add_watchers_to_all_windows(watchers)
self.add_os_window(startup_session, os_window_id=os_window_id)
os_window_id = None
if self.args.start_as != 'normal':
@@ -350,8 +351,10 @@ class Boss:
return response
def remote_control(self, *args: str) -> None:
from .rc.base import (
PayloadGetter, command_for_name, parse_subcommand_cli
)
from .remote_control import parse_rc_args
from .rc.base import command_for_name, parse_subcommand_cli, PayloadGetter
try:
global_opts, items = parse_rc_args(['@'] + list(args))
if not items:
@@ -931,6 +934,9 @@ class Boss:
kargs = shlex.split(cmdline) if cmdline else []
self._run_kitten(kitten, kargs)
def run_kitten(self, kitten: str, *args: str) -> None:
self._run_kitten(kitten, args)
def on_kitten_finish(self, target_window_id: str, end_kitten: Callable, source_window: Window) -> None:
output = self.get_output(source_window, num_lines=None)
from kittens.runner import deserialize
@@ -1014,13 +1020,29 @@ class Boss:
tab.set_active_window(window_id)
def open_url(self, url: str, program: Optional[Union[str, List[str]]] = None, cwd: Optional[str] = None) -> None:
if url:
if isinstance(program, str):
program = to_cmdline(program)
if not url:
return
if isinstance(program, str):
program = to_cmdline(program)
found_action = False
if program is None:
from .open_actions import actions_for_url
actions = list(actions_for_url(url))
if actions:
found_action = True
self.dispatch_action(actions.pop(0))
if actions:
self.drain_actions(actions)
if not found_action:
open_url(url, program or self.opts.open_url_with, cwd=cwd)
def open_url_lines(self, lines: Iterable[str], program: Optional[Union[str, List[str]]] = None) -> None:
self.open_url(''.join(lines), program)
def drain_actions(self, actions: List) -> None:
def callback(timer_id: Optional[int]) -> None:
self.dispatch_action(actions.pop(0))
if actions:
self.drain_actions(actions)
add_timer(callback, 0, False)
def destroy(self) -> None:
self.shutting_down = True
@@ -1284,7 +1306,7 @@ class Boss:
self._new_window(list(args), cwd_from=cwd_from)
def launch(self, *args: str) -> None:
from kitty.launch import parse_launch_args, launch
from kitty.launch import launch, parse_launch_args
opts, args_ = parse_launch_args(args)
launch(self, opts, args_)
@@ -1357,7 +1379,9 @@ class Boss:
log_error('Failed to process update check data {!r}, with error: {}'.format(raw, e))
def dbus_notification_callback(self, activated: bool, a: int, b: Union[int, str]) -> None:
from .notify import dbus_notification_created, dbus_notification_activated
from .notify import (
dbus_notification_activated, dbus_notification_created
)
if activated:
assert isinstance(b, str)
dbus_notification_activated(a, b)
@@ -1374,7 +1398,9 @@ class Boss:
self.show_error(_('Errors in kitty.conf'), msg)
def set_colors(self, *args: str) -> None:
from kitty.rc.base import parse_subcommand_cli, command_for_name, PayloadGetter
from kitty.rc.base import (
PayloadGetter, command_for_name, parse_subcommand_cli
)
from kitty.remote_control import parse_rc_args
c = command_for_name('set_colors')
opts, items = parse_subcommand_cli(c, ['set-colors'] + list(args))
@@ -1536,3 +1562,21 @@ class Boss:
set_background_image(path, os_windows, configured)
for os_window_id in os_windows:
self.default_bg_changed_for(os_window_id)
# Can be called with kitty -o "map f1 send_test_notification"
def send_test_notification(self) -> None:
from time import monotonic
from .notify import notify
now = monotonic()
ident = f'test-notify-{now}'
notify(f'Test {now}', f'At: {now}', identifier=ident, subtitle=f'Test subtitle {now}')
def notification_activated(self, identifier: str, window_id: int, focus: bool, report: bool) -> None:
w = self.window_id_map.get(window_id)
if w is None:
return
if focus:
self.set_active_window(w, switch_os_window_if_needed=True)
if report:
w.report_notification_activated(identifier)

View File

@@ -236,7 +236,7 @@ schedule_write_to_child(unsigned long id, unsigned int num, ...) {
va_list ap;
va_start(ap, num);
for (unsigned int i = 0; i < num; i++) {
data = va_arg(ap, const char*);
va_arg(ap, const char*);
sz += va_arg(ap, size_t);
}
va_end(ap);
@@ -1539,7 +1539,7 @@ send_response(id_type peer_id, const char *msg, size_t msg_sz) {
peer->write.capacity += msg_sz;
} else fatal("Out of memory");
}
memcpy(peer->write.data + peer->write.used, msg, msg_sz);
if (msg) memcpy(peer->write.data + peer->write.used, msg, msg_sz);
peer->write.used += msg_sz;
}
wakeup = true;

View File

@@ -544,6 +544,15 @@ def parse_cmdline(oc: Options, disabled: OptionSpecSeq, ans: Any, args: Optional
return leftover_args
WATCHER_DEFINITION = '''
--watcher -w
type=list
Path to a python file. Appropriately named functions in this file will be called
for various events, such as when the window is resized, focused or closed. See the section
on watchers in the launch command documentation :doc:`launch`. Relative paths are
resolved relative to the kitty config directory.'''
def options_spec() -> str:
if not hasattr(options_spec, 'ans'):
OPTIONS = '''
@@ -593,6 +602,11 @@ Path to a file containing the startup :italic:`session` (tabs, windows, layout,
Use - to read from STDIN. See the README file for details and an example.
{watcher}
Note that this watcher will be added only to all initially created windows, not new windows
created after startup.
--hold
type=bool-set
Remain open after child process exits. Note that this only affects the first
@@ -695,8 +709,8 @@ type=bool-set
!
'''
setattr(options_spec, 'ans', OPTIONS.format(
appname=appname, config_help=CONFIG_HELP.format(appname=appname, conf_name=appname)
appname=appname, config_help=CONFIG_HELP.format(appname=appname, conf_name=appname),
watcher=WATCHER_DEFINITION
))
ans: str = getattr(options_spec, 'ans')
return ans

View File

@@ -12,7 +12,8 @@ class CLIOptions:
LaunchCLIOptions = AskCLIOptions = ClipboardCLIOptions = DiffCLIOptions = CLIOptions
HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions
ErrorCLIOptions = UnicodeCLIOptions = RCOptions = CLIOptions
ErrorCLIOptions = UnicodeCLIOptions = RCOptions = RemoteFileCLIOptions = CLIOptions
QueryTerminalCLIOptions = CLIOptions
def generate_stub() -> None:
@@ -35,6 +36,9 @@ def generate_stub() -> None:
from kittens.ask.main import option_text
do(option_text(), 'AskCLIOptions')
from kittens.remote_file.main import option_text
do(option_text(), 'RemoteFileCLIOptions')
from kittens.clipboard.main import OPTIONS
do(OPTIONS(), 'ClipboardCLIOptions')
@@ -47,6 +51,9 @@ def generate_stub() -> None:
from kittens.icat.main import options_spec
do(options_spec(), 'IcatCLIOptions')
from kittens.query_terminal.main import options_spec
do(options_spec(), 'QueryTerminalCLIOptions')
from kittens.panel.main import OPTIONS
do(OPTIONS(), 'PanelCLIOptions')

View File

@@ -9,6 +9,9 @@
#include "state.h"
#include "monotonic.h"
#include <Cocoa/Cocoa.h>
#ifndef KITTY_USE_DEPRECATED_MACOS_NOTIFICATION_API
#include <UserNotifications/UserNotifications.h>
#endif
#include <AvailabilityMacros.h>
// Needed for _NSGetProgname
@@ -133,6 +136,7 @@ get_dock_menu(id self UNUSED, SEL _cmd UNUSED, NSApplication *sender UNUSED) {
}
static PyObject *notification_activated_callback = NULL;
static PyObject*
set_notification_activated_callback(PyObject *self UNUSED, PyObject *callback) {
if (notification_activated_callback) Py_DECREF(notification_activated_callback);
@@ -141,6 +145,8 @@ set_notification_activated_callback(PyObject *self UNUSED, PyObject *callback) {
Py_RETURN_NONE;
}
#ifdef KITTY_USE_DEPRECATED_MACOS_NOTIFICATION_API
@interface NotificationDelegate : NSObject <NSUserNotificationCenterDelegate>
@end
@@ -170,32 +176,15 @@ set_notification_activated_callback(PyObject *self UNUSED, PyObject *callback) {
static PyObject*
cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
char *identifier = NULL, *title = NULL, *subtitle = NULL, *informativeText = NULL, *path_to_image = NULL;
if (!PyArg_ParseTuple(args, "zssz|z", &identifier, &title, &informativeText, &path_to_image, &subtitle)) return NULL;
char *identifier = NULL, *title = NULL, *informativeText = NULL, *subtitle = NULL;
if (!PyArg_ParseTuple(args, "zsz|z", &identifier, &title, &informativeText, &subtitle)) return NULL;
NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
if (!center) {PyErr_SetString(PyExc_RuntimeError, "Failed to get the user notification center"); return NULL; }
if (!center.delegate) center.delegate = [[NotificationDelegate alloc] init];
NSUserNotification *n = [NSUserNotification new];
NSImage *img = nil;
if (path_to_image) {
NSString *p = @(path_to_image);
NSURL *url = [NSURL fileURLWithPath:p];
img = [[NSImage alloc] initWithContentsOfURL:url];
[url release]; [p release];
if (img) {
[n setValue:img forKey:@"_identityImage"];
[n setValue:@(false) forKey:@"_identityImageHasBorder"];
}
[img release];
}
#define SET(x) { \
if (x) { \
NSString *t = @(x); \
n.x = t; \
[t release]; \
}}
SET(title); SET(subtitle); SET(informativeText);
#undef SET
if (title) n.title = @(title);
if (subtitle) n.subtitle = @(subtitle);
if (informativeText) n.informativeText = @(informativeText);
if (identifier) {
n.userInfo = @{@"user_id": @(identifier)};
}
@@ -203,22 +192,127 @@ cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
Py_RETURN_NONE;
}
#else
@interface NotificationDelegate : NSObject <UNUserNotificationCenterDelegate>
@end
@implementation NotificationDelegate
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void (^)(void))completionHandler {
(void)(center);
if (notification_activated_callback) {
NSString *identifier = [[[response notification] request] identifier];
PyObject *ret = PyObject_CallFunction(notification_activated_callback, "z",
identifier ? [identifier UTF8String] : NULL);
if (ret == NULL) PyErr_Print();
else Py_DECREF(ret);
}
completionHandler();
}
@end
static void
schedule_notification(const char *identifier, const char *title, const char *body, const char *subtitle) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
if (!center) return;
// Configure the notification's payload.
UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
if (title) content.title = @(title);
if (body) content.body = @(body);
if (subtitle) content.subtitle = @(subtitle);
// Deliver the notification
static unsigned long counter = 1;
UNNotificationRequest* request = [
UNNotificationRequest requestWithIdentifier:(identifier ? @(identifier) : [NSString stringWithFormat:@"Id_%lu", counter++])
content:content trigger:nil];
[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
if (error != nil) {
log_error("Failed to show notification: %s", [[error localizedDescription] UTF8String]);
}
}];
[content release];
}
typedef struct {
char *identifier, *title, *body, *subtitle;
} QueuedNotification;
typedef struct {
QueuedNotification *notifications;
size_t count, capacity;
} NotificationQueue;
static NotificationQueue notification_queue = {0};
static void
queue_notification(const char *identifier, const char *title, const char* body, const char* subtitle) {
ensure_space_for((&notification_queue), notifications, QueuedNotification, notification_queue.count + 16, capacity, 16, true);
QueuedNotification *n = notification_queue.notifications + notification_queue.count++;
n->identifier = identifier ? strdup(identifier) : NULL;
n->title = title ? strdup(title) : NULL;
n->body = body ? strdup(body) : NULL;
n->subtitle = subtitle ? strdup(subtitle) : NULL;
}
static void
drain_pending_notifications(BOOL granted) {
if (granted) {
for (size_t i = 0; i < notification_queue.count; i++) {
QueuedNotification *n = notification_queue.notifications + i;
schedule_notification(n->identifier, n->title, n->body, n->subtitle);
}
}
while(notification_queue.count) {
QueuedNotification *n = notification_queue.notifications + --notification_queue.count;
free(n->identifier); free(n->title); free(n->body); free(n->subtitle);
n->identifier = NULL; n->title = NULL; n->body = NULL; n->subtitle = NULL;
}
}
static PyObject*
cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
char *identifier = NULL, *title = NULL, *body = NULL, *subtitle = NULL;
if (!PyArg_ParseTuple(args, "zsz|z", &identifier, &title, &body, &subtitle)) return NULL;
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
if (!center) Py_RETURN_NONE;
if (!center.delegate) center.delegate = [[NotificationDelegate alloc] init];
queue_notification(identifier, title, body, subtitle);
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert)
completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (error != nil) {
log_error("Failed to request permission for showing notification: %s", [[error localizedDescription] UTF8String]);
}
dispatch_async(dispatch_get_main_queue(), ^{
drain_pending_notifications(granted);
});
}
];
Py_RETURN_NONE;
}
#endif
@interface ServiceProvider : NSObject
@end
@implementation ServiceProvider
- (void)openTab:(NSPasteboard*)pasteboard
- (BOOL)openTab:(NSPasteboard*)pasteboard
userData:(NSString *) UNUSED userData error:(NSError **) UNUSED error {
[self openFilesFromPasteboard:pasteboard type:NEW_TAB_WITH_WD];
return [self openFilesFromPasteboard:pasteboard type:NEW_TAB_WITH_WD];
}
- (void)openOSWindow:(NSPasteboard*)pasteboard
- (BOOL)openOSWindow:(NSPasteboard*)pasteboard
userData:(NSString *) UNUSED userData error:(NSError **) UNUSED error {
[self openFilesFromPasteboard:pasteboard type:NEW_OS_WINDOW_WITH_WD];
return [self openFilesFromPasteboard:pasteboard type:NEW_OS_WINDOW_WITH_WD];
}
- (void)openFilesFromPasteboard:(NSPasteboard *)pasteboard type:(int)type {
- (BOOL)openFilesFromPasteboard:(NSPasteboard *)pasteboard type:(int)type {
NSDictionary *options = @{ NSPasteboardURLReadingFileURLsOnlyKey: @YES };
NSArray *filePathArray = [pasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]] options:options];
for (NSURL *url in filePathArray) {
@@ -231,6 +325,7 @@ cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
set_cocoa_pending_action(type, [path UTF8String]);
}
}
return YES;
}
@end
@@ -497,8 +592,15 @@ cleanup() {
if (dockMenu) [dockMenu release];
dockMenu = nil;
if (notification_activated_callback) Py_DECREF(notification_activated_callback);
notification_activated_callback = NULL;
Py_CLEAR(notification_activated_callback);
#ifndef KITTY_USE_DEPRECATED_MACOS_NOTIFICATION_API
drain_pending_notifications(NO);
free(notification_queue.notifications);
notification_queue.notifications = NULL;
notification_queue.capacity = 0;
#endif
} // autoreleasepool
}

View File

@@ -254,6 +254,8 @@ def complete_kitty_cli_arg(ans: Completions, opt: Optional[OptionDict], prefix:
complete_files_and_dirs(ans, prefix, files_group_name='Config files', predicate=is_conf_file)
elif dest == 'session':
complete_files_and_dirs(ans, prefix, files_group_name='Session files')
elif dest == 'watcher':
complete_files_and_dirs(ans, prefix, files_group_name='Watcher files')
elif dest == 'directory':
complete_files_and_dirs(ans, prefix, files_group_name='Directories', predicate=os.path.isdir)
elif dest == 'start_as':
@@ -392,10 +394,10 @@ def complete_files_and_dirs(
def complete_icat_args(ans: Completions, opt: Optional[OptionDict], prefix: str, unknown_args: Delegate) -> None:
from mimetypes import guess_type
from .guess_mime_type import guess_type
def icat_file_predicate(filename: str) -> bool:
mt = guess_type(filename)[0]
mt = guess_type(filename)
if mt and mt.startswith('image/'):
return True
return False

View File

@@ -11,7 +11,7 @@ from typing import (
)
from ..rgb import Color, to_color as as_color
from ..utils import log_error
from ..utils import log_error, expandvars
key_pat = re.compile(r'([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$')
T = TypeVar('T')
@@ -53,13 +53,36 @@ def to_bool(x: str) -> bool:
return x.lower() in ('y', 'yes', 'true')
def to_cmdline(x: str) -> List[str]:
return list(
map(
lambda y: os.path.expandvars(os.path.expanduser(y)),
shlex.split(x)
class ToCmdline:
def __init__(self) -> None:
self.override_env: Optional[Dict[str, str]] = None
def __enter__(self) -> 'ToCmdline':
return self
def __exit__(self, *a: Any) -> None:
self.override_env = None
def filter_env_vars(self, *a: str) -> 'ToCmdline':
remove = frozenset(a)
self.override_env = {k: v for k, v in os.environ.items() if k not in remove}
return self
def __call__(self, x: str) -> List[str]:
return list(
map(
lambda y: expandvars(
os.path.expanduser(y),
os.environ if self.override_env is None else self.override_env,
fallback_to_os_env=False
),
shlex.split(x)
)
)
)
to_cmdline = ToCmdline()
def python_string(text: str) -> str:

View File

@@ -393,7 +393,7 @@ class KeyDefinition:
if len(expanded) > 1:
rest = expanded[1] + ' ' + rest
if changed:
self.action = self.action._replace(args=[kitten + (' ' + rest).rstrip()])
self.action = self.action._replace(args=[kitten, rest.rstrip()])
def parse_key(val: str, key_definitions: List[KeyDefinition]) -> None:

View File

@@ -13,7 +13,7 @@ from typing import (
from . import fast_data_types as defines
from .conf.definition import Option, Shortcut, option_func
from .conf.utils import (
choices, positive_float, positive_int, to_bool, to_cmdline, to_color,
choices, positive_float, positive_int, to_bool, to_cmdline as tc, to_color,
to_color_or_none, unit_float
)
from .constants import FloatEdges, config_dir, is_macos
@@ -29,6 +29,10 @@ mod_map = {'CTRL': 'CONTROL', 'CMD': 'SUPER', '⌘': 'SUPER',
'': 'ALT', 'OPTION': 'ALT', 'KITTY_MOD': 'KITTY'}
def to_cmdline(x: str) -> List[str]:
return tc(x)
def parse_mods(parts: Iterable[str], sc: str) -> Optional[int]:
def map_mod(m: str) -> str:
@@ -321,7 +325,9 @@ Note that this code is indexed by PostScript name, and not the font
family. This allows you to define very precise feature settings; e.g. you can
disable a feature in the italic font but not in the regular font.
To get the PostScript name for a font, use :code:`kitty + list-fonts --psnames`::
To get the PostScript name for a font, use :code:`kitty + list-fonts --psnames`:
.. code-block:: sh
$ kitty + list-fonts --psnames | grep Fira
Fira Code
@@ -440,8 +446,8 @@ def scrollback_pager_history_size(x: str) -> int:
o('scrollback_lines', 2000, option_type=scrollback_lines, long_text=_('''
Number of lines of history to keep in memory for scrolling back. Memory is allocated
on demand. Negative numbers are (effectively) infinite scrollback. Note that using
very large scrollback is not recommended as it can slow down resizing of the terminal
and also use large amounts of RAM.'''))
very large scrollback is not recommended as it can slow down performance of the terminal
and also use large amounts of RAM. Instead, consider using :opt:`scrollback_pager_history_size`.'''))
o('scrollback_pager', 'less --chop-long-lines --RAW-CONTROL-CHARS +INPUT_LINE_NUMBER', option_type=to_cmdline, long_text=_('''
Program with which to view scrollback in a new window. The scrollback buffer is
@@ -455,9 +461,9 @@ o('scrollback_pager_history_size', 0, option_type=scrollback_pager_history_size,
Separate scrollback history size, used only for browsing the scrollback buffer (in MB).
This separate buffer is not available for interactive scrolling but will be
piped to the pager program when viewing scrollback buffer in a separate window.
The current implementation stores one character in 4 bytes, so approximatively
2500 lines per megabyte at 100 chars per line. A value of zero or less disables
this feature. The maximum allowed size is 4GB.'''))
The current implementation stores the data in UTF-8, so approximatively
10000 lines per megabyte at 100 chars per line, for pure ASCII text, unformatted text.
A value of zero or less disables this feature. The maximum allowed size is 4GB.'''))
o('wheel_scroll_multiplier', 5.0, long_text=_('''
Modify the amount scrolled by the mouse wheel. Note this is only used for low
@@ -570,6 +576,16 @@ The shape of the mouse pointer when the program running in the terminal grabs th
Valid values are: :code:`arrow`, :code:`beam` and :code:`hand`
'''))
o('default_pointer_shape', 'beam', option_type=choices('arrow', 'beam', 'hand'), long_text=('''
The default shape of the mouse pointer.
Valid values are: :code:`arrow`, :code:`beam` and :code:`hand`
'''))
o('pointer_shape_when_dragging', 'beam', option_type=choices('arrow', 'beam', 'hand'), long_text=('''
The default shape of the mouse pointer when dragging across text.
Valid values are: :code:`arrow`, :code:`beam` and :code:`hand`
'''))
# }}}
g('performance') # {{{
@@ -668,9 +684,27 @@ for vertical resizing.
'''))
o('window_resize_step_lines', 2, option_type=positive_int)
o('window_border_width', 1.0, option_type=positive_float, long_text=_('''
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.'''))
def window_border_width(x: Union[str, int, float]) -> Tuple[float, str]:
unit = 'pt'
if isinstance(x, str):
trailer = x[-2:]
if trailer in ('px', 'pt'):
unit = trailer
val = float(x[:-2])
else:
val = float(x)
else:
val = float(x)
return max(0, val), unit
o('window_border_width', '0.5pt', option_type=window_border_width, long_text=_('''
The width of window borders. Can be either in pixels (px) or pts (pt). Values
in pts will be rounded to the nearest number of pixels based on screen
resolution. If not specified the unit is assumed to be pts.
Note that borders are displayed only when more than one window
is visible. They are meant to separate multiple windows.'''))
o('draw_minimal_borders', True, long_text=_('''
Draw only the minimum borders needed. This means that only the minimum
@@ -863,6 +897,17 @@ o('tab_separator', '"{}"'.format(default_tab_separator), option_type=tab_separat
The separator between tabs in the tab bar when using :code:`separator` as the :opt:`tab_bar_style`.'''))
def tab_activity_symbol(x: str) -> Optional[str]:
if x == 'none':
return None
return x or None
o('tab_activity_symbol', 'none', option_type=tab_activity_symbol, long_text=_('''
Some text or a unicode symbol to show on the tab if a window in the tab that does
not have focus has some activity.'''))
def tab_title_template(x: str) -> str:
if x:
for q in '\'"':
@@ -1113,6 +1158,20 @@ 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.
'''))
def allow_hyperlinks(x: str) -> int:
if x == 'ask':
return 0b11
return 1 if to_bool(x) else 0
o('allow_hyperlinks', 'yes', option_type=allow_hyperlinks, long_text=_('''
Process hyperlink (OSC 8) escape sequences. If disabled OSC 8 escape
sequences are ignored. Otherwise they become clickable links, that you
can click by holding down ctrl+shift and clicking with the mouse. The special
value of ``ask`` means that kitty will ask before opening the link.'''))
o('term', 'xterm-kitty', long_text=_('''
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
@@ -1438,6 +1497,9 @@ k('goto_file_line', 'kitty_mod+p>n', 'kitten hints --type linenum', _('Open the
Select something that looks like :code:`filename:linenum` and open it in vim at
the specified line number.'''))
k('open_selected_hyperlink', 'kitty_mod+p>y', 'kitten hints --type hyperlink', _('Open the selected hyperlink'), long_text=_('''
Select a hyperlink (i.e. a URL that has been marked as such by the terminal program, for example, by ls --hyperlink=auto).
'''))
# }}}

View File

@@ -20,7 +20,7 @@ class Version(NamedTuple):
appname: str = 'kitty'
version: Version = Version(0, 18, 3)
version: Version = Version(0, 19, 2)
str_version: str = '.'.join(map(str, version))
_plat = sys.platform.lower()
is_macos: bool = 'darwin' in _plat

View File

@@ -161,12 +161,13 @@ coretext_all_fonts(PyObject UNUSED *_self) {
CFArrayRef matches = CTFontCollectionCreateMatchingFontDescriptors(all_fonts_collection());
const CFIndex count = CFArrayGetCount(matches);
PyObject *ans = PyTuple_New(count), *temp;
if (ans == NULL) return PyErr_NoMemory();
if (ans == NULL) { CFRelease(matches); return PyErr_NoMemory(); }
for (CFIndex i = 0; i < count; i++) {
temp = font_descriptor_to_python((CTFontDescriptorRef) CFArrayGetValueAtIndex(matches, i));
if (temp == NULL) { Py_DECREF(ans); return NULL; }
if (temp == NULL) { CFRelease(matches); Py_DECREF(ans); return NULL; }
PyTuple_SET_ITEM(ans, i, temp); temp = NULL;
}
CFRelease(matches);
return ans;
}
@@ -212,6 +213,7 @@ manually_search_fallback_fonts(CTFontRef current_font, CPUCell *cell) {
CFRelease(new_font);
}
}
CFRelease(fonts);
return ans;
}

View File

@@ -13,6 +13,8 @@
#endif
#include "data-types.h"
#include "control-codes.h"
#include "wcwidth-std.h"
#include "wcswidth.h"
#include "modes.h"
#include <stddef.h>
#include <termios.h>
@@ -89,7 +91,7 @@ stop_profiler(PyObject UNUSED *self, PyObject *args UNUSED) {
#endif
static inline bool
put_tty_in_raw_mode(int fd, const struct termios* termios_p, bool read_with_timeout) {
put_tty_in_raw_mode(int fd, const struct termios* termios_p, bool read_with_timeout, int optional_actions) {
struct termios raw_termios = *termios_p;
cfmakeraw(&raw_termios);
if (read_with_timeout) {
@@ -97,14 +99,14 @@ put_tty_in_raw_mode(int fd, const struct termios* termios_p, bool read_with_time
} else {
raw_termios.c_cc[VMIN] = 1; raw_termios.c_cc[VTIME] = 0;
}
if (tcsetattr(fd, TCSAFLUSH, &raw_termios) != 0) { PyErr_SetFromErrno(PyExc_OSError); return false; }
if (tcsetattr(fd, optional_actions, &raw_termios) != 0) { PyErr_SetFromErrno(PyExc_OSError); return false; }
return true;
}
static PyObject*
open_tty(PyObject *self UNUSED, PyObject *args) {
int read_with_timeout = 0;
if (!PyArg_ParseTuple(args, "|p", &read_with_timeout)) return NULL;
int read_with_timeout = 0, optional_actions = TCSAFLUSH;
if (!PyArg_ParseTuple(args, "|pi", &read_with_timeout, &optional_actions)) return NULL;
int flags = O_RDWR | O_CLOEXEC | O_NOCTTY;
if (!read_with_timeout) flags |= O_NONBLOCK;
static char ctty[L_ctermid+1];
@@ -113,26 +115,26 @@ open_tty(PyObject *self UNUSED, PyObject *args) {
struct termios *termios_p = calloc(1, sizeof(struct termios));
if (!termios_p) return PyErr_NoMemory();
if (tcgetattr(fd, termios_p) != 0) { free(termios_p); PyErr_SetFromErrno(PyExc_OSError); return NULL; }
if (!put_tty_in_raw_mode(fd, termios_p, read_with_timeout != 0)) { free(termios_p); return NULL; }
if (!put_tty_in_raw_mode(fd, termios_p, read_with_timeout != 0, optional_actions)) { free(termios_p); return NULL; }
return Py_BuildValue("iN", fd, PyLong_FromVoidPtr(termios_p));
}
#define TTY_ARGS \
PyObject *tp; int fd; \
if (!PyArg_ParseTuple(args, "iO!", &fd, &PyLong_Type, &tp)) return NULL; \
PyObject *tp; int fd; int optional_actions = TCSAFLUSH; \
if (!PyArg_ParseTuple(args, "iO!|i", &fd, &PyLong_Type, &tp, &optional_actions)) return NULL; \
struct termios *termios_p = PyLong_AsVoidPtr(tp);
static PyObject*
normal_tty(PyObject *self UNUSED, PyObject *args) {
TTY_ARGS
if (tcsetattr(fd, TCSAFLUSH, termios_p) != 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; }
if (tcsetattr(fd, optional_actions, termios_p) != 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; }
Py_RETURN_NONE;
}
static PyObject*
raw_tty(PyObject *self UNUSED, PyObject *args) {
TTY_ARGS
if (!put_tty_in_raw_mode(fd, termios_p, false)) return NULL;
if (!put_tty_in_raw_mode(fd, termios_p, false, optional_actions)) return NULL;
Py_RETURN_NONE;
}
@@ -140,7 +142,7 @@ raw_tty(PyObject *self UNUSED, PyObject *args) {
static PyObject*
close_tty(PyObject *self UNUSED, PyObject *args) {
TTY_ARGS
tcsetattr(fd, TCSAFLUSH, termios_p); // deliberately ignore failure
tcsetattr(fd, optional_actions, termios_p); // deliberately ignore failure
free(termios_p);
safe_close(fd, __FILE__, __LINE__);
Py_RETURN_NONE;
@@ -148,7 +150,15 @@ close_tty(PyObject *self UNUSED, PyObject *args) {
#undef TTY_ARGS
static PyObject*
wcwidth_wrap(PyObject UNUSED *self, PyObject *chr) {
return PyLong_FromLong(wcwidth_std(PyLong_AsLong(chr)));
}
static PyMethodDef module_methods[] = {
{"wcwidth", (PyCFunction)wcwidth_wrap, METH_O, ""},
{"wcswidth", (PyCFunction)wcswidth_std, METH_O, ""},
{"open_tty", open_tty, METH_VARARGS, ""},
{"normal_tty", normal_tty, METH_VARARGS, ""},
{"raw_tty", raw_tty, METH_VARARGS, ""},

View File

@@ -41,6 +41,8 @@ void log_error(const char *fmt, ...) __attribute__ ((format (printf, 1, 2)));
typedef unsigned long long id_type;
typedef uint32_t char_type;
typedef uint32_t color_type;
typedef uint16_t hyperlink_id_type;
#define HYPERLINK_MAX_NUMBER UINT16_MAX
typedef uint16_t combining_type;
typedef uint32_t pixel;
typedef unsigned int index_type;
@@ -160,6 +162,7 @@ typedef struct {
typedef struct {
char_type ch;
combining_type cc_idx[2];
hyperlink_id_type hyperlink_id;
} CPUCell;
@@ -190,13 +193,20 @@ typedef struct {
} HistoryBufSegment;
typedef struct {
index_type bufsize, maxsz;
Py_UCS4 *buffer;
index_type start, end;
index_type bufend;
uint8_t *buffer;
size_t buffer_size, max_sz;
size_t start, length;
bool rewrap_needed;
} PagerHistoryBuf;
typedef struct {int x;} *HYPERLINK_POOL_HANDLE;
typedef struct {
Py_UCS4 *buf;
size_t len, capacity;
HYPERLINK_POOL_HANDLE hyperlink_pool;
hyperlink_id_type active_hyperlink_id;
} ANSIBuf;
typedef struct {
PyObject_HEAD
@@ -311,6 +321,7 @@ void play_canberra_sound(const char *which_sound, const char *event_id);
#endif
SPRITE_MAP_HANDLE alloc_sprite_map(unsigned int, unsigned int);
SPRITE_MAP_HANDLE free_sprite_map(SPRITE_MAP_HANDLE);
const char* get_hyperlink_for_id(const HYPERLINK_POOL_HANDLE, hyperlink_id_type id, bool only_url);
static inline void safe_close(int fd, const char* file UNUSED, const int line UNUSED) {
#if 0

2
kitty/emoji.h generated
View File

@@ -1,4 +1,4 @@
// unicode data, built from the unicode standard on: 2020-08-06
// unicode data, built from the unicode standard on: 2020-09-22
// see gen-wcwidth.py
#pragma once
#include "data-types.h"

View File

@@ -4,6 +4,7 @@ from typing import (
Union
)
import termios
from kitty.boss import Boss
from kitty.fonts.render import FontObject
from kitty.options_stub import Options
@@ -461,7 +462,7 @@ def coretext_all_fonts() -> Tuple[CoreTextFont, ...]:
def add_timer(
callback: Callable[[Optional[int]], bool],
callback: Callable[[Optional[int]], None],
interval: float,
repeats: bool = True
) -> int:
@@ -519,9 +520,8 @@ def dbus_send_notification(
def cocoa_send_notification(
identifier: Optional[str],
title: str,
informative_text: str,
path_to_img: Optional[str],
subtitle: Optional[str] = None
body: Optional[str],
subtitle: Optional[str],
) -> None:
pass
@@ -866,19 +866,19 @@ def viewport_for_window(
TermiosPtr = NewType('TermiosPtr', int)
def raw_tty(fd: int, termios_ptr: TermiosPtr) -> None:
def raw_tty(fd: int, termios_ptr: TermiosPtr, optional_actions: int = termios.TCSAFLUSH) -> None:
pass
def close_tty(fd: int, termios_ptr: TermiosPtr) -> None:
def close_tty(fd: int, termios_ptr: TermiosPtr, optional_actions: int = termios.TCSAFLUSH) -> None:
pass
def normal_tty(fd: int, termios_ptr: TermiosPtr) -> None:
def normal_tty(fd: int, termios_ptr: TermiosPtr, optional_actions: int = termios.TCSAFLUSH) -> None:
pass
def open_tty(read_with_timeout: bool = False) -> Tuple[int, TermiosPtr]:
def open_tty(read_with_timeout: bool = False, optional_actions: int = termios.TCSAFLUSH) -> Tuple[int, TermiosPtr]:
pass
@@ -945,7 +945,10 @@ class HistoryBuf:
def as_text(self, callback: Callable[[str], None], as_ansi: bool, insert_wrap_markers: bool) -> None:
pass
def pagerhist_as_text(self, callback: Callable[[str], None]) -> None:
def pagerhist_as_text(self) -> str:
pass
def pagerhist_as_bytes(self) -> bytes:
pass
@@ -1065,6 +1068,15 @@ class Screen:
def erase_in_display(self, how: int = 0, private: bool = False) -> None:
pass
def focus_changed(self, focused: bool) -> bool:
pass
def has_focus(self) -> bool:
pass
def has_activity_since_last_focus(self) -> bool:
pass
def set_tab_bar_render_data(
os_window_id: int, xstart: float, ystart: float, dx: float, dy: float,

View File

@@ -130,12 +130,48 @@ font_group_is_unused(FontGroup *fg) {
return true;
}
void
free_maps(Font *font) {
#define free_a_map(type, attr) {\
type *s, *t; \
for (size_t i = 0; i < sizeof(font->attr)/sizeof(font->attr[0]); i++) { \
s = font->attr[i].next; \
while (s) { \
t = s; \
s = s->next; \
free(t); \
} \
}\
memset(font->attr, 0, sizeof(font->attr)); \
}
free_a_map(SpritePosition, sprite_map);
free_a_map(SpecialGlyphCache, special_glyph_cache);
#undef free_a_map
}
static inline void
del_font(Font *f) {
Py_CLEAR(f->face);
free(f->ffs_hb_features); f->ffs_hb_features = NULL;
free_maps(f);
f->bold = false; f->italic = false;
}
static inline void
del_font_group(FontGroup *fg) {
free(fg->canvas); fg->canvas = NULL;
fg->sprite_map = free_sprite_map(fg->sprite_map);
for (size_t i = 0; i < fg->fonts_count; i++) del_font(fg->fonts + i);
free(fg->fonts); fg->fonts = NULL;
}
static inline void
trim_unused_font_groups(void) {
save_window_font_groups();
size_t i = 0;
while (i < num_font_groups) {
if (font_group_is_unused(font_groups + i)) {
del_font_group(font_groups + i);
size_t num_to_right = (--num_font_groups) - i;
if (!num_to_right) break;
memmove(font_groups + i, font_groups + 1 + i, num_to_right * sizeof(FontGroup));
@@ -282,25 +318,6 @@ sprite_tracker_current_layout(FONTS_DATA_HANDLE data, unsigned int *x, unsigned
*x = fg->sprite_tracker.xnum; *y = fg->sprite_tracker.ynum; *z = fg->sprite_tracker.z;
}
void
free_maps(Font *font) {
#define free_a_map(type, attr) {\
type *s, *t; \
for (size_t i = 0; i < sizeof(font->attr)/sizeof(font->attr[0]); i++) { \
s = font->attr[i].next; \
while (s) { \
t = s; \
s = s->next; \
free(t); \
} \
}\
memset(font->attr, 0, sizeof(font->attr)); \
}
free_a_map(SpritePosition, sprite_map);
free_a_map(SpecialGlyphCache, special_glyph_cache);
#undef free_a_map
}
void
clear_sprite_map(Font *font) {
#define CLEAR(s) s->filled = false; s->rendered = false; s->colored = false; s->glyph = 0; zero_at_ptr(&s->extra_glyphs); s->x = 0; s->y = 0; s->z = 0; s->ligature_index = 0;
@@ -384,22 +401,6 @@ init_font(Font *f, PyObject *face, bool bold, bool italic, bool emoji_presentati
return true;
}
static inline void
del_font(Font *f) {
Py_CLEAR(f->face);
free(f->ffs_hb_features); f->ffs_hb_features = NULL;
free_maps(f);
f->bold = false; f->italic = false;
}
static inline void
del_font_group(FontGroup *fg) {
free(fg->canvas); fg->canvas = NULL;
fg->sprite_map = free_sprite_map(fg->sprite_map);
for (size_t i = 0; i < fg->fonts_count; i++) del_font(fg->fonts + i);
free(fg->fonts); fg->fonts = NULL;
}
static inline void
free_font_groups(void) {
if (font_groups) {
@@ -595,11 +596,14 @@ START_ALLOW_CASE_RANGE
case 0x2500 ... 0x2573:
case 0x2574 ... 0x259f:
case 0xe0b0 ... 0xe0b4:
case 0x2800 ... 0x28ff:
case 0xe0b6:
case 0xe0b8: // 
case 0xe0ba: // 
case 0xe0bc: // 
case 0xe0be: // 
case 0x1fb00 ... 0x1fb8b: // symbols for legacy computing
case 0x1fba0 ... 0x1fbae:
return BOX_FONT;
default:
ans = in_symbol_maps(fg, cpu_cell->ch);
@@ -637,8 +641,14 @@ START_ALLOW_CASE_RANGE
return ch - 0x2500; // IDs from 0x00 to 0x9f
case 0xe0b0 ... 0xe0d4:
return 0xa0 + ch - 0xe0b0; // IDs from 0xa0 to 0xc4
case 0x1fb00 ... 0x1fb8b:
return 0xc5 + ch - 0x1fb00; // IDs from 0xc5 to 0x150
case 0x1fba0 ... 0x1fbae: // IDs from 0x151 to 0x15f
return 0x151 + ch - 0x1fba0;
case 0x2800 ... 0x28ff:
return 0x160 + ch - 0x2800;
default:
return 0xff;
return 0xffff;
}
END_ALLOW_CASE_RANGE
}

View File

@@ -184,11 +184,12 @@ def supersampled(supersample_factor: int = 4) -> Callable:
return create_wrapper
def fill_region(buf: BufType, width: int, height: int, xlimits: Iterable[Iterable[float]]) -> None:
def fill_region(buf: BufType, width: int, height: int, xlimits: Iterable[Iterable[float]], inverted: bool = False) -> None:
full, empty = (0, 255) if inverted else (255, 0)
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
buf[x + offset] = full if upper <= y <= lower else empty
def line_equation(x1: int, y1: int, x2: int, y2: int) -> Callable[[int], float]:
@@ -231,6 +232,32 @@ def corner_triangle(buf: BufType, width: int, height: int, corner: str) -> None:
fill_region(buf, width, height, xlimits)
@supersampled()
def half_triangle(buf: BufType, width: int, height: int, which: str = 'left', inverted: bool = False) -> None:
mid_x, mid_y = width // 2, height // 2
if which == 'left':
upper_y = line_equation(0, 0, mid_x, mid_y)
lower_y = line_equation(0, height - 1, mid_x, mid_y)
limits = tuple((upper_y(x), lower_y(x)) for x in range(width))
elif which == 'top':
first_y = line_equation(0, 0, mid_x, mid_y)
first = tuple((0, first_y(x)) for x in range(mid_x))
second_y = line_equation(mid_x, mid_y, width - 1, 0)
second = tuple((0, second_y(x)) for x in range(mid_x, width))
limits = first + second
elif which == 'right':
upper_y = line_equation(mid_x, mid_y, width - 1, 0)
lower_y = line_equation(mid_x, mid_y, width - 1, height - 1)
limits = tuple((upper_y(x), lower_y(x)) for x in range(width))
elif which == 'bottom':
first_y = line_equation(0, height - 1, mid_x, mid_y)
first_ = tuple((first_y(x), height - 1) for x in range(mid_x))
second_y = line_equation(mid_x, mid_y, width - 1, height - 1)
second_ = tuple((second_y(x), height - 1) for x in range(mid_x, width))
limits = first_ + second_
fill_region(buf, width, height, limits, inverted)
def thick_line(buf: BufType, width: int, height: int, thickness_in_pixels: int, p1: Tuple[int, int], p2: Tuple[int, int]) -> None:
if p1[0] > p2[0]:
p1, p2 = p2, p1
@@ -276,6 +303,26 @@ def half_cross_line(buf: BufType, width: int, height: int, which: str = 'tl', le
thick_line(buf, width, height, thickness_in_pixels, p1, p2)
@supersampled()
def mid_lines(buf: BufType, width: int, height: int, level: int = 1, pts: Iterable[str] = ('lt',)) -> None:
mid_x, mid_y = width // 2, height // 2
supersample_factor = getattr(buf, 'supersample_factor')
def pt_to_coords(p: str) -> Tuple[int, int]:
if p == 'l':
return 0, mid_y
if p == 't':
return mid_x, 0
if p == 'r':
return width - 1, mid_y
if p == 'b':
return mid_x, height - 1
for x in pts:
p1, p2 = map(pt_to_coords, x)
thick_line(buf, width, height, supersample_factor * thickness(level), p1, p2)
BezierFunc = Callable[[float], float]
@@ -513,24 +560,6 @@ def inner_corner(buf: BufType, width: int, height: int, which: str = 'tl', level
draw_vline(buf, width, y1, y2, width // 2 + (xd * hgap), level)
def vblock(buf: BufType, width: int, height: int, frac: float = 1., gravity: str = 'top') -> None:
num_rows = min(height, round(frac * height))
start = 0 if gravity == 'top' else height - num_rows
for r in range(start, start + num_rows):
off = r * width
for c in range(off, off + width):
buf[c] = 255
def hblock(buf: BufType, width: int, height: int, frac: float = 1., gravity: str = 'left') -> None:
num_cols = min(width, round(frac * width))
start = 0 if gravity == 'left' else width - num_cols
for r in range(height):
off = r * width + start
for c in range(off, off + num_cols):
buf[c] = 255
def shade(buf: BufType, width: int, height: int, light: bool = False, invert: bool = False) -> None:
square_sz = max(1, width // 12)
number_of_rows = height // square_sz
@@ -582,6 +611,106 @@ def quad(buf: BufType, width: int, height: int, x: int = 0, y: int = 0) -> None:
buf[off + c] = 255
def sextant(buf: BufType, width: int, height: int, level: int = 1, which: int = 0) -> None:
def draw_sextant(row: int = 0, col: int = 0) -> None:
if row == 0:
y_start, y_end = 0, height // 3
elif row == 1:
y_start, y_end = height // 3, 2 * height // 3
else:
y_start, y_end = 2 * height // 3, height
if col == 0:
x_start, x_end = 0, width // 2
else:
x_start, x_end = width // 2, width
for r in range(y_start, y_end):
off = r * width
for c in range(x_start, x_end):
buf[c + off] = 255
def add_row(q: int, r: int) -> None:
if q & 1:
draw_sextant(r)
if q & 2:
draw_sextant(r, col=1)
add_row(which % 4, 0)
add_row(which // 4, 1)
add_row(which // 16, 2)
@supersampled()
def smooth_mosaic(
buf: BufType, width: int, height: int, level: int = 1,
lower: bool = True, a: Tuple[float, float] = (0, 0), b: Tuple[float, float] = (0, 0)
) -> None:
ax, ay = int(a[0] * (width - 1)), int(a[1] * (height - 1))
bx, by = int(b[0] * (width - 1)), int(b[1] * (height - 1))
line = line_equation(ax, ay, bx, by)
def lower_condition(x: int, y: int) -> bool:
return y >= line(x)
def upper_condition(x: int, y: int) -> bool:
return y <= line(x)
condition = lower_condition if lower else upper_condition
for y in range(height):
offset = width * y
for x in range(width):
if condition(x, y):
buf[offset + x] = 255
def eight_bar(buf: BufType, width: int, height: int, level: int = 1, which: int = 0, horizontal: bool = False) -> None:
if horizontal:
x_range = range(0, width)
thickness = max(1, height // 8)
y_start = min(which * thickness, height - 2)
y_range = range(y_start, height if which == 7 else min(y_start + thickness, height))
else:
y_range = range(0, height)
thickness = max(1, width // 8)
x_start = min(which * thickness, width - 2)
x_range = range(x_start, width if which == 7 else min(x_start + thickness, width))
for y in y_range:
offset = y * width
for x in x_range:
buf[offset + x] = 255
def eight_block(buf: BufType, width: int, height: int, level: int = 1, which: Tuple[int, ...] = (0,), horizontal: bool = False) -> None:
for x in which:
eight_bar(buf, width, height, level, x, horizontal)
def braille_dot(buf: BufType, width: int, height: int, col: int, row: int) -> None:
dot_height = max(1, height // 8)
dot_width = max(1, width // 4)
top_margin = (height - 7 * dot_height) // 2
left_margin = (width - 3 * dot_width) // 2
x_start = left_margin + (col * 2 * dot_width)
y_start = top_margin + (row * 2 * dot_height)
if y_start < height:
for y in range(y_start, min(height, y_start + dot_height)):
if x_start < width:
offset = y * width
for x in range(x_start, min(width, x_start + dot_width)):
buf[offset + x] = 255
def braille(buf: BufType, width: int, height: int, which: int = 0) -> None:
if not which:
return
for i, x in enumerate(reversed(bin(which)[2:])):
if x == '1':
q = i + 1
col = 0 if q in (1, 2, 3, 7) else 1
row = 0 if q in (1, 4) else 1 if q in (2, 5) else 2 if q in (3, 6) else 3
braille_dot(buf, width, height, col, row)
box_chars: Dict[str, List[Callable]] = {
'': [hline],
'': [p(hline, level=3)],
@@ -649,28 +778,28 @@ box_chars: Dict[str, List[Callable]] = {
'': [p(cross_line, left=False)],
'': [cross_line],
'': [cross_line, p(cross_line, left=False)],
'': [p(vblock, frac=1/2)],
'': [p(vblock, frac=1/8, gravity='bottom')],
'': [p(vblock, frac=1/4, gravity='bottom')],
'': [p(vblock, frac=3/8, gravity='bottom')],
'': [p(vblock, frac=1/2, gravity='bottom')],
'': [p(vblock, frac=5/8, gravity='bottom')],
'': [p(vblock, frac=3/4, gravity='bottom')],
'': [p(vblock, frac=7/8, gravity='bottom')],
'': [p(vblock, frac=1, gravity='bottom')],
'': [p(hblock, frac=7/8)],
'': [p(hblock, frac=3/4)],
'': [p(hblock, frac=5/8)],
'': [p(hblock, frac=1/2)],
'': [p(hblock, frac=3/8)],
'': [p(hblock, frac=1/4)],
'': [p(hblock, frac=1/8)],
'': [p(hblock, frac=1/2, gravity='right')],
'': [p(eight_block, horizontal=True, which=(0, 1, 2, 3))],
'': [p(eight_bar, which=7, horizontal=True)],
'': [p(eight_block, horizontal=True, which=(6, 7))],
'': [p(eight_block, horizontal=True, which=(5, 6, 7))],
'': [p(eight_block, horizontal=True, which=(4, 5, 6, 7))],
'': [p(eight_block, horizontal=True, which=(3, 4, 5, 6, 7))],
'': [p(eight_block, horizontal=True, which=(2, 3, 4, 5, 6, 7))],
'': [p(eight_block, horizontal=True, which=(1, 2, 3, 4, 5, 6, 7))],
'': [p(eight_block, horizontal=True, which=(0, 1, 2, 3, 4, 5, 6, 7))],
'': [p(eight_block, which=(0, 1, 2, 3, 4, 5, 6))],
'': [p(eight_block, which=(0, 1, 2, 3, 4, 5))],
'': [p(eight_block, which=(0, 1, 2, 3, 4))],
'': [p(eight_block, which=(0, 1, 2, 3))],
'': [p(eight_block, which=(0, 1, 2))],
'': [p(eight_block, which=(0, 1))],
'': [p(eight_bar)],
'': [p(eight_block, which=(4, 5, 6, 7))],
'': [p(shade, light=True)],
'': [shade],
'': [p(shade, invert=True)],
'': [p(vblock, frac=1/8)],
'': [p(hblock, frac=1/8, gravity='right')],
'': [p(eight_bar, horizontal=True)],
'': [p(eight_bar, which=7)],
'': [p(quad, y=1)],
'': [p(quad, x=1, y=1)],
'': [quad],
@@ -681,6 +810,102 @@ box_chars: Dict[str, List[Callable]] = {
'': [p(quad, x=1)],
'': [p(quad, x=1), p(quad, y=1)],
'': [p(quad, x=1), p(quad, y=1), p(quad, x=1, y=1)],
'🬼': [p(smooth_mosaic, a=(0, 0.75), b=(0.5, 1))],
'🬽': [p(smooth_mosaic, a=(0, 0.75), b=(1, 1))],
'🬾': [p(smooth_mosaic, a=(0, 0.5), b=(0.5, 1))],
'🬿': [p(smooth_mosaic, a=(0, 0.5), b=(1, 1))],
'🭀': [p(smooth_mosaic, a=(0, 0), b=(0.5, 1))],
'🭁': [p(smooth_mosaic, a=(0, 0.25), b=(0.5, 0))],
'🭂': [p(smooth_mosaic, a=(0, 0.25), b=(1, 0))],
'🭃': [p(smooth_mosaic, a=(0, 0.75), b=(0.5, 0))],
'🭄': [p(smooth_mosaic, a=(0, 0.75), b=(1, 0))],
'🭅': [p(smooth_mosaic, a=(0, 1), b=(0.5, 0))],
'🭆': [p(smooth_mosaic, a=(0, 0.75), b=(1, 0.25))],
'🭇': [p(smooth_mosaic, a=(0.5, 1), b=(1, 0.75))],
'🭈': [p(smooth_mosaic, a=(0, 1), b=(1, 0.75))],
'🭉': [p(smooth_mosaic, a=(0.5, 1), b=(1, 0.25))],
'🭊': [p(smooth_mosaic, a=(0, 1), b=(1, 0.25))],
'🭋': [p(smooth_mosaic, a=(0.5, 1), b=(1, 0))],
'🭌': [p(smooth_mosaic, a=(0.5, 0), b=(1, 0.25))],
'🭍': [p(smooth_mosaic, a=(0, 0), b=(1, 0.25))],
'🭎': [p(smooth_mosaic, a=(0.5, 0), b=(1, 0.75))],
'🭏': [p(smooth_mosaic, a=(0, 0), b=(1, 0.75))],
'🭐': [p(smooth_mosaic, a=(0.5, 0), b=(1, 1))],
'🭑': [p(smooth_mosaic, a=(0, 0.25), b=(1, 0.75))],
'🭒': [p(smooth_mosaic, lower=False, a=(0, 0.75), b=(0.5, 1))],
'🭓': [p(smooth_mosaic, lower=False, a=(0, 0.75), b=(1, 1))],
'🭔': [p(smooth_mosaic, lower=False, a=(0, 0.25), b=(0.5, 1))],
'🭕': [p(smooth_mosaic, lower=False, a=(0, 0.25), b=(1, 1))],
'🭖': [p(smooth_mosaic, lower=False, a=(0, 0), b=(0.5, 1))],
'🭗': [p(smooth_mosaic, lower=False, a=(0, 0.25), b=(0.5, 0))],
'🭘': [p(smooth_mosaic, lower=False, a=(0, 0.25), b=(1, 0))],
'🭙': [p(smooth_mosaic, lower=False, a=(0, 0.75), b=(0.5, 0))],
'🭚': [p(smooth_mosaic, lower=False, a=(0, 0.75), b=(1, 0))],
'🭛': [p(smooth_mosaic, lower=False, a=(0, 1), b=(0.5, 0))],
'🭜': [p(smooth_mosaic, lower=False, a=(0, 0.75), b=(1, 0.25))],
'🭝': [p(smooth_mosaic, lower=False, a=(0.5, 1), b=(1, 0.75))],
'🭞': [p(smooth_mosaic, lower=False, a=(0, 1), b=(1, 0.75))],
'🭟': [p(smooth_mosaic, lower=False, a=(0.5, 1), b=(1, 0.25))],
'🭠': [p(smooth_mosaic, lower=False, a=(0, 1), b=(1, 0.25))],
'🭡': [p(smooth_mosaic, lower=False, a=(0.5, 1), b=(1, 0))],
'🭢': [p(smooth_mosaic, lower=False, a=(0.5, 0), b=(1, 0.25))],
'🭣': [p(smooth_mosaic, lower=False, a=(0, 0), b=(1, 0.25))],
'🭤': [p(smooth_mosaic, lower=False, a=(0.5, 0), b=(1, 0.75))],
'🭥': [p(smooth_mosaic, lower=False, a=(0, 0), b=(1, 0.75))],
'🭦': [p(smooth_mosaic, lower=False, a=(0.5, 0), b=(1, 1))],
'🭧': [p(smooth_mosaic, lower=False, a=(0, 0.25), b=(1, 0.75))],
'🭨': [p(half_triangle, inverted=True)],
'🭩': [p(half_triangle, which='top', inverted=True)],
'🭪': [p(half_triangle, which='right', inverted=True)],
'🭫': [p(half_triangle, which='bottom', inverted=True)],
'🭬': [half_triangle],
'🭭': [p(half_triangle, which='top')],
'🭮': [p(half_triangle, which='right')],
'🭯': [p(half_triangle, which='bottom')],
'🭼': [eight_bar, p(eight_bar, which=7, horizontal=True)],
'🭽': [eight_bar, p(eight_bar, horizontal=True)],
'🭾': [p(eight_bar, which=7), p(eight_bar, horizontal=True)],
'🭿': [p(eight_bar, which=7), p(eight_bar, which=7, horizontal=True)],
'🮀': [p(eight_bar, horizontal=True), p(eight_bar, which=7, horizontal=True)],
'🮁': [
p(eight_bar, horizontal=True), p(eight_bar, which=2, horizontal=True),
p(eight_bar, which=4, horizontal=True), p(eight_bar, which=7, horizontal=True)],
'🮂': [p(eight_block, horizontal=True, which=(0, 1))],
'🮃': [p(eight_block, horizontal=True, which=(0, 1, 2))],
'🮄': [p(eight_block, horizontal=True, which=(0, 1, 2, 3, 4))],
'🮅': [p(eight_block, horizontal=True, which=(0, 1, 2, 3, 4, 5))],
'🮆': [p(eight_block, horizontal=True, which=(0, 1, 2, 3, 4, 5, 6))],
'🮇': [p(eight_block, which=(6, 7))],
'🮈': [p(eight_block, which=(5, 6, 7))],
'🮉': [p(eight_block, which=(3, 4, 5, 6, 7))],
'🮊': [p(eight_block, which=(2, 3, 4, 5, 6, 7))],
'🮋': [p(eight_block, which=(1, 2, 3, 4, 5, 6, 7))],
'🮠': [mid_lines],
'🮡': [p(mid_lines, pts=('tr',))],
'🮢': [p(mid_lines, pts=('lb',))],
'🮣': [p(mid_lines, pts=('br',))],
'🮤': [p(mid_lines, pts=('lt', 'lb'))],
'🮥': [p(mid_lines, pts=('rt', 'rb'))],
'🮦': [p(mid_lines, pts=('rb', 'lb'))],
'🮧': [p(mid_lines, pts=('rt', 'lt'))],
'🮨': [p(mid_lines, pts=('rb', 'lt'))],
'🮩': [p(mid_lines, pts=('lb', 'rt'))],
'🮪': [p(mid_lines, pts=('lb', 'rt', 'rb'))],
'🮫': [p(mid_lines, pts=('lb', 'lt', 'rb'))],
'🮬': [p(mid_lines, pts=('rt', 'lt', 'rb'))],
'🮭': [p(mid_lines, pts=('rt', 'lt', 'lb'))],
'🮮': [p(mid_lines, pts=('rt', 'rb', 'lt', 'lb'))],
}
t, f = 1, 3
@@ -709,6 +934,21 @@ for chars, func_ in (('╒╕╘╛', dvcorner), ('╓╖╙╜', dhcorner), ('
for ch in chars:
box_chars[ch] = [p(cast(Callable, func_), which=ch)]
for i in range(256):
box_chars[chr(0x2800 + i)] = [p(braille, which=i)]
c = 0x1fb00
for i in range(1, 63):
if i in (20, 40):
continue
box_chars[chr(c)] = [p(sextant, which=i)]
c += 1
for i in range(1, 7):
box_chars[chr(0x1fb6f + i)] = [p(eight_bar, which=i)]
box_chars[chr(0x1fb75 + i)] = [p(eight_bar, which=i, horizontal=True)]
def render_box_char(ch: str, buf: BufType, width: int, height: int, dpi: float = 96.0) -> BufType:
global _dpi

View File

@@ -84,9 +84,14 @@ def find_best_match(family: str, bold: bool = False, italic: bool = False, monos
# First look for an exact match
for selector in ('ps_map', 'full_map', 'family_map'):
candidates = font_map[selector].get(q)
if candidates:
candidates.sort(key=score)
return candidates[0]
if not candidates:
continue
if len(candidates) == 1 and (bold or italic) and candidates[0].get('family') == candidates[0].get('full_name'):
# IBM Plex Mono does this, where the full name of the regular font
# face is the same as its family name
continue
candidates.sort(key=score)
return candidates[0]
# Use fc-match to see if we can find a monospaced font that matches family
for spacing in (FC_MONO, FC_DUAL):

View File

@@ -589,10 +589,16 @@ render_glyphs_in_cells(PyObject *f, bool bold, bool italic, hb_glyph_info_t *inf
if (!render_color_bitmap(self, info[i].codepoint, &bm, cell_width, cell_height, num_cells, baseline)) {
if (PyErr_Occurred()) PyErr_Print();
*was_colored = false;
if (!render_bitmap(self, info[i].codepoint, &bm, cell_width, cell_height, num_cells, bold, italic, true, fg)) return false;
if (!render_bitmap(self, info[i].codepoint, &bm, cell_width, cell_height, num_cells, bold, italic, true, fg)) {
free_processed_bitmap(&bm);
return false;
}
}
} else {
if (!render_bitmap(self, info[i].codepoint, &bm, cell_width, cell_height, num_cells, bold, italic, true, fg)) return false;
if (!render_bitmap(self, info[i].codepoint, &bm, cell_width, cell_height, num_cells, bold, italic, true, fg)) {
free_processed_bitmap(&bm);
return false;
}
}
x_offset = x + (float)positions[i].x_offset / 64.0f;
y = (float)positions[i].y_offset / 64.0f;

2
kitty/glfw-wrapper.h generated
View File

@@ -1240,7 +1240,7 @@ typedef void (* GLFWwindowiconifyfun)(GLFWwindow*,int);
* @endcode
*
* @param[in] window The window that was maximized or restored.
* @param[in] iconified `true` if the window was maximized, or
* @param[in] maximized `true` if the window was maximized, or
* `false` if it was restored.
*
* @sa @ref window_maximize

View File

@@ -421,6 +421,7 @@ make_os_window_context_current(OSWindow *w) {
static inline void
get_window_content_scale(GLFWwindow *w, float *xscale, float *yscale, double *xdpi, double *ydpi) {
*xscale = 1; *yscale = 1;
if (w) glfwGetWindowContentScale(w, xscale, yscale);
else {
GLFWmonitor *monitor = glfwGetPrimaryMonitor();
@@ -440,7 +441,7 @@ get_window_content_scale(GLFWwindow *w, float *xscale, float *yscale, double *xd
static inline void
get_window_dpi(GLFWwindow *w, double *x, double *y) {
float xscale = 1, yscale = 1;
float xscale, yscale;
get_window_content_scale(w, &xscale, &yscale, x, y);
}

57
kitty/guess_mime_type.py Normal file
View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
import os
from contextlib import suppress
from typing import Optional
known_extensions = {
'asciidoc': 'text/asciidoctor',
'conf': 'text/config',
'md': 'text/markdown',
'pyj': 'text/rapydscript-ng',
'recipe': 'text/python',
'rst': 'text/restructured-text',
'toml': 'text/toml',
'vim': 'text/vim',
'yaml': 'text/yaml',
'js': 'text/javascript',
'json': 'text/json',
}
text_mimes = ('application/javascript', 'application/x-sh', 'application/json')
def is_rc_file(path: str) -> bool:
name = os.path.basename(path)
return '.' not in name and name.endswith('rc')
def initialize_mime_database() -> None:
if hasattr(initialize_mime_database, 'inited'):
return
setattr(initialize_mime_database, 'inited', True)
from mimetypes import init
init(None)
from kitty.constants import config_dir
local_defs = os.path.join(config_dir, 'mime.types')
if os.path.exists(local_defs):
init((local_defs,))
def guess_type(path: str) -> Optional[str]:
from mimetypes import guess_type as stdlib_guess_type
initialize_mime_database()
mt = None
with suppress(Exception):
mt = stdlib_guess_type(path)[0]
if not mt:
ext = path.rpartition('.')[-1].lower()
mt = known_extensions.get(ext)
if mt in text_mimes:
mt = 'text/' + mt.split('/', 1)[-1]
if not mt and is_rc_file(path):
mt = 'text/plain'
return mt

View File

@@ -5,8 +5,9 @@
* Distributed under terms of the GPL3 license.
*/
#include "data-types.h"
#include "wcswidth.h"
#include "lineops.h"
#include "charsets.h"
#include <structmember.h>
extern PyTypeObject Line_Type;
@@ -55,14 +56,14 @@ attrptr(HistoryBuf *self, index_type y) {
}
static inline PagerHistoryBuf*
alloc_pagerhist(unsigned int pagerhist_sz) {
alloc_pagerhist(size_t pagerhist_sz) {
PagerHistoryBuf *ph;
if (!pagerhist_sz) return NULL;
ph = PyMem_Calloc(1, sizeof(PagerHistoryBuf));
if (!ph) return NULL;
ph->maxsz = pagerhist_sz / sizeof(Py_UCS4);
ph->bufsize = 1024*1024 / sizeof(Py_UCS4);
ph->buffer = PyMem_RawMalloc(1024*1024);
ph->max_sz = pagerhist_sz;
ph->buffer_size = MIN(1024u*1024u, ph->max_sz);
ph->buffer = PyMem_Malloc(ph->buffer_size);
if (!ph->buffer) { PyMem_Free(ph); return NULL; }
return ph;
}
@@ -76,36 +77,35 @@ free_pagerhist(HistoryBuf *self) {
static inline bool
pagerhist_extend(PagerHistoryBuf *ph, size_t minsz) {
if (ph->bufsize >= ph->maxsz) return false;
size_t newsz = ph->bufsize + MAX(1024u * 1024u, minsz);
void *newbuf = PyMem_Realloc(ph->buffer, newsz * sizeof(Py_UCS4));
if (ph->buffer_size >= ph->max_sz) return false;
size_t newsz = MIN(ph->max_sz, ph->buffer_size + MAX(1024u * 1024u, minsz));
uint8_t *newbuf = PyMem_Malloc(newsz);
if (!newbuf) return false;
size_t copied = MIN(ph->length, ph->buffer_size - ph->start);
if (copied) memcpy(newbuf, ph->buffer + ph->start, copied);
if (copied < ph->length) memcpy(newbuf + copied, ph->buffer, (ph->length - copied));
PyMem_Free(ph->buffer);
ph->start = 0;
ph->buffer = newbuf;
ph->bufsize = newsz;
ph->buffer_size = newsz;
return true;
}
static inline void
pagerhist_clear(HistoryBuf *self) {
if (!self->pagerhist || !self->pagerhist->maxsz) return;
index_type pagerhist_sz = self->pagerhist->maxsz * sizeof(Py_UCS4);
if (!self->pagerhist || !self->pagerhist->max_sz) return;
index_type pagerhist_sz = self->pagerhist->max_sz;
free_pagerhist(self);
self->pagerhist = alloc_pagerhist(pagerhist_sz);
}
static PyObject *
new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
HistoryBuf *self;
unsigned int xnum = 1, ynum = 1, pagerhist_sz = 0;
if (!PyArg_ParseTuple(args, "II|I", &ynum, &xnum, &pagerhist_sz)) return NULL;
static HistoryBuf*
create_historybuf(PyTypeObject *type, unsigned int xnum, unsigned int ynum, unsigned int pagerhist_sz) {
if (xnum == 0 || ynum == 0) {
PyErr_SetString(PyExc_ValueError, "Cannot create an empty history buffer");
return NULL;
}
self = (HistoryBuf *)type->tp_alloc(type, 0);
HistoryBuf *self = (HistoryBuf *)type->tp_alloc(type, 0);
if (self != NULL) {
self->xnum = xnum;
self->ynum = ynum;
@@ -115,8 +115,15 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
self->line->xnum = xnum;
self->pagerhist = alloc_pagerhist(pagerhist_sz);
}
return self;
}
return (PyObject*)self;
static PyObject *
new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
unsigned int xnum = 1, ynum = 1, pagerhist_sz = 0;
if (!PyArg_ParseTuple(args, "II|I", &ynum, &xnum, &pagerhist_sz)) return NULL;
HistoryBuf *ans = create_historybuf(type, xnum, ynum, pagerhist_sz);
return (PyObject*)ans;
}
static void
@@ -155,6 +162,11 @@ historybuf_init_line(HistoryBuf *self, index_type lnum, Line *l) {
init_line(self, index_of(self, lnum), l);
}
CPUCell*
historybuf_cpu_cells(HistoryBuf *self, index_type lnum) {
return cpu_lineptr(self, index_of(self, lnum));
}
void
historybuf_mark_line_clean(HistoryBuf *self, index_type y) {
line_attrs_type *p = attrptr(self, index_of(self, y));
@@ -174,57 +186,80 @@ historybuf_clear(HistoryBuf *self) {
self->start_of_data = 0;
}
static inline bool
pagerhist_write_bytes(PagerHistoryBuf *ph, const uint8_t *buf, size_t sz) {
if (sz > ph->max_sz) return false;
if (!sz) return true;
if (sz > ph->buffer_size - ph->length) pagerhist_extend(ph, sz);
if (sz > ph->buffer_size) return false;
size_t start_writing_at = (ph->start + ph->length) % ph->buffer_size;
size_t available_space = ph->buffer_size - ph->length;
size_t overlap = available_space < sz ? sz - available_space : 0;
size_t copied = MIN(sz, ph->buffer_size - start_writing_at);
ph->length += sz - overlap;
ph->start = (ph->start + overlap) % ph->buffer_size;
if (copied) memcpy(ph->buffer + start_writing_at, buf, copied);
if (copied < sz) memcpy(ph->buffer, buf + copied, (sz - copied));
return true;
}
static inline bool
pagerhist_ensure_start_is_valid_utf8(PagerHistoryBuf *ph) {
uint32_t state = UTF8_ACCEPT, codep;
size_t pos = ph->start, count = 0;
size_t last_reject_at = 0;
while (count < ph->length) {
decode_utf8(&state, &codep, ph->buffer[pos]);
count++;
if (state == UTF8_ACCEPT) break;
if (state == UTF8_REJECT) { state = UTF8_ACCEPT; last_reject_at = count; }
pos = pos == ph->buffer_size - 1 ? 0: pos + 1;
}
if (last_reject_at) {
ph->start = (ph->start + last_reject_at) % ph->buffer_size;
ph->length -= last_reject_at;
return true;
}
return false;
}
static inline bool
pagerhist_write_ucs4(PagerHistoryBuf *ph, const Py_UCS4 *buf, size_t sz) {
uint8_t scratch[4];
for (size_t i = 0; i < sz; i++) {
unsigned int num = encode_utf8(buf[i], (char*)scratch);
if (!pagerhist_write_bytes(ph, scratch, num)) return false;
}
return true;
}
static inline void
pagerhist_push(HistoryBuf *self) {
pagerhist_push(HistoryBuf *self, ANSIBuf *as_ansi_buf) {
PagerHistoryBuf *ph = self->pagerhist;
if (!ph) return;
bool truncated;
const GPUCell *prev_cell = NULL;
Line l = {.xnum=self->xnum};
init_line(self, self->start_of_data, &l);
#define EXPAND_IF_FULL(sz) { \
if (ph->bufsize - ph->end < sz && !pagerhist_extend(ph, sz)) { \
ph->bufend = ph->end; ph->end = 0; \
} \
}
size_t sz = MAX(1024u, ph->bufsize - ph->end);
sz = MAX(sz, self->xnum + self->xnum);
EXPAND_IF_FULL(sz);
if (ph->start != ph->end && !l.continued) {
ph->buffer[ph->end++] = '\n';
}
while(sz < ph->maxsz - 2) {
size_t num = line_as_ansi(&l, ph->buffer + ph->end, ph->bufsize - ph->end - 2, &truncated, &prev_cell);
if (!truncated) {
ph->end += num;
ph->buffer[ph->end++] = '\r';
if (ph->bufend) {
ph->start = ph->end + 1 < ph->bufend ? ph->end + 1 : 0;
}
break;
}
// check if sz is too large too fit in buffer
if (ph->bufsize > ph->maxsz && !ph->end) break;
sz *= 2;
EXPAND_IF_FULL(sz);
}
#undef EXPAND_IF_FULL
line_as_ansi(&l, as_ansi_buf, &prev_cell);
if (ph->length != 0 && !l.continued) pagerhist_write_bytes(ph, (const uint8_t*)"\n", 1);
pagerhist_write_bytes(ph, (const uint8_t*)"\x1b[m", 3);
if (pagerhist_write_ucs4(ph, as_ansi_buf->buf, as_ansi_buf->len)) pagerhist_write_bytes(ph, (const uint8_t*)"\r", 1);
}
static inline index_type
historybuf_push(HistoryBuf *self) {
historybuf_push(HistoryBuf *self, ANSIBuf *as_ansi_buf) {
index_type idx = (self->start_of_data + self->count) % self->ynum;
init_line(self, idx, self->line);
if (self->count == self->ynum) {
pagerhist_push(self);
pagerhist_push(self, as_ansi_buf);
self->start_of_data = (self->start_of_data + 1) % self->ynum;
} else self->count++;
return idx;
}
void
historybuf_add_line(HistoryBuf *self, const Line *line) {
index_type idx = historybuf_push(self);
historybuf_add_line(HistoryBuf *self, const Line *line, ANSIBuf *as_ansi_buf) {
index_type idx = historybuf_push(self, as_ansi_buf);
copy_line(line, self->line);
*attrptr(self, idx) = (line->continued & CONTINUED_MASK) | (line->has_dirty_text ? TEXT_DIRTY_MASK : 0);
}
@@ -261,122 +296,154 @@ push(HistoryBuf *self, PyObject *args) {
#define push_doc "Push a line into this buffer, removing the oldest line, if necessary"
Line *line;
if (!PyArg_ParseTuple(args, "O!", &Line_Type, &line)) return NULL;
historybuf_add_line(self, line);
ANSIBuf as_ansi_buf = {0};
historybuf_add_line(self, line, &as_ansi_buf);
free(as_ansi_buf.buf);
Py_RETURN_NONE;
}
static PyObject*
as_ansi(HistoryBuf *self, PyObject *callback) {
#define as_ansi_doc "as_ansi(callback) -> The contents of this buffer as ANSI escaped text. callback is called with each successive line."
static Py_UCS4 t[5120];
Line l = {.xnum=self->xnum};
bool truncated;
const GPUCell *prev_cell = NULL;
ANSIBuf output = {0};
for(unsigned int i = 0; i < self->count; i++) {
init_line(self, i, &l);
if (i < self->count - 1) {
l.continued = *attrptr(self, index_of(self, i + 1)) & CONTINUED_MASK;
} else l.continued = false;
index_type num = line_as_ansi(&l, t, 5120, &truncated, &prev_cell);
if (!(l.continued) && num < 5119) t[num++] = 10; // 10 = \n
PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, t, num);
if (ans == NULL) return PyErr_NoMemory();
line_as_ansi(&l, &output, &prev_cell);
if (!l.continued) {
ensure_space_for(&output, buf, Py_UCS4, output.len + 1, capacity, 2048, false);
output.buf[output.len++] = 10; // 10 = \n
}
PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len);
if (ans == NULL) { PyErr_NoMemory(); goto end; }
PyObject *ret = PyObject_CallFunctionObjArgs(callback, ans, NULL);
Py_CLEAR(ans);
if (ret == NULL) return NULL;
if (ret == NULL) goto end;
Py_CLEAR(ret);
}
end:
free(output.buf);
if (PyErr_Occurred()) return NULL;
Py_RETURN_NONE;
}
static inline Line*
get_line(HistoryBuf *self, index_type y, Line *l) { init_line(self, index_of(self, self->count - y - 1), l); return l; }
static inline char_type
pagerhist_read_char(PagerHistoryBuf *ph, size_t pos, unsigned *count, uint8_t record[8]) {
uint32_t codep, state = UTF8_ACCEPT;
*count = 0;
while (true) {
decode_utf8(&state, &codep, ph->buffer[pos]);
record[(*count)++] = ph->buffer[pos];
if (state == UTF8_REJECT) { codep = 0; break; }
if (state == UTF8_ACCEPT) break;
pos = pos == ph->buffer_size - 1 ? 0 : (pos + 1);
}
return codep;
}
static void
pagerhist_rewrap(PagerHistoryBuf *ph, index_type xnum) {
Py_UCS4 *buf = PyMem_RawMalloc(ph->bufsize * sizeof(Py_UCS4));
if (!buf) return;
index_type s = ph->start, i = s, dest = 0, dest_bufend = 0, x = 0;
index_type end = ph->bufend ? ph->bufend : ph->end;
index_type lastmod_s = 0, lastmod_len = 0;
#define CPY(_s, _l) { if (dest + (_l) >= ph->bufsize - 1) { dest_bufend = dest; dest = 0; } \
memcpy(buf + dest, ph->buffer + (_s), (_l) * sizeof(Py_UCS4)); dest += (_l); }
while (i < end) {
switch (ph->buffer[i]) {
case '\n':
CPY(s, i - s + 1);
x = 0; s = i + 1; lastmod_len = 0;
break;
case '\r':
CPY(s, i - s);
if (!memcmp(ph->buffer + lastmod_s, ph->buffer + i + 1, lastmod_len * sizeof(Py_UCS4)))
i += lastmod_len;
s = i + 1;
break;
case '\x1b':
if (ph->buffer[i+1] != '[') break;
lastmod_s = i;
while (ph->buffer[++i] != 'm');
lastmod_len = i - lastmod_s + 1;
break;
default:
x++; break;
}
i++;
if (ph->bufend && i == ph->bufend) {
if (s != i) CPY(s, i - s);
end = ph->end; i = s = 0;
}
if (x == xnum) {
CPY(s, i - s); buf[dest++] = '\r'; s = i; x = 0;
if (!(ph->buffer[i] == '\x1b' && ph->buffer[i+1] == '[') && lastmod_len)
CPY(lastmod_s, lastmod_len);
pagerhist_rewrap_to(HistoryBuf *self, index_type cells_in_line) {
PagerHistoryBuf *ph = self->pagerhist;
if (!ph->length) return;
PagerHistoryBuf *nph = PyMem_Calloc(sizeof(PagerHistoryBuf), 1);
if (!nph) return;
nph->buffer_size = ph->buffer_size;
nph->max_sz = ph->max_sz;
nph->buffer = PyMem_Malloc(nph->buffer_size);
if (!nph->buffer) { PyMem_Free(nph); return ; }
size_t i = 0, pos;
ssize_t ch_width = 0;
unsigned count;
uint8_t record[8];
index_type num_in_current_line = 0;
char_type ch;
WCSState wcs_state;
initialize_wcs_state(&wcs_state);
#define READ_CHAR(ch) { \
ch = pagerhist_read_char(ph, pos, &count, record); \
i += count; pos += count; \
if (pos >= ph->buffer_size) pos = pos - ph->buffer_size; \
}
#define WRITE_CHAR() { \
if (num_in_current_line + ch_width > cells_in_line) { \
pagerhist_write_bytes(nph, (const uint8_t*)"\r", 1); \
num_in_current_line = 0; \
}\
if (ch_width >= 0 || (int)num_in_current_line >= -ch_width) num_in_current_line += ch_width; \
pagerhist_write_bytes(nph, record, count); \
}
for (i = 0; i < ph->length;) {
pos = ph->start + i;
if (pos >= ph->buffer_size) pos = pos - ph->buffer_size;
READ_CHAR(ch);
if (ch == '\n') {
initialize_wcs_state(&wcs_state);
ch_width = 1;
WRITE_CHAR();
num_in_current_line = 0;
} else if (ch != '\r') {
ch_width = wcswidth_step(&wcs_state, ch);
WRITE_CHAR();
}
}
#undef CPY
PyMem_Free(ph->buffer);
ph->buffer = buf;
ph->end = dest; ph->bufend = dest_bufend;
ph->start = dest_bufend ? dest + 1 : 0;
ph->rewrap_needed = false;
free_pagerhist(self);
self->pagerhist = nph;
#undef READ_CHAR
}
static PyObject*
pagerhist_write(HistoryBuf *self, PyObject *what) {
if (self->pagerhist && self->pagerhist->max_sz) {
if (PyBytes_Check(what)) pagerhist_write_bytes(self->pagerhist, (const uint8_t*)PyBytes_AS_STRING(what), PyBytes_GET_SIZE(what));
else if (PyUnicode_Check(what) && PyUnicode_READY(what) == 0) {
Py_UCS4 *buf = PyUnicode_AsUCS4Copy(what);
if (buf) {
pagerhist_write_ucs4(self->pagerhist, buf, PyUnicode_GET_LENGTH(what));
PyMem_Free(buf);
}
}
}
Py_RETURN_NONE;
}
static PyObject*
pagerhist_as_bytes(HistoryBuf *self, PyObject *args UNUSED) {
PagerHistoryBuf *ph = self->pagerhist;
if (!ph || !ph->length) return PyBytes_FromStringAndSize("", 0);
pagerhist_ensure_start_is_valid_utf8(ph);
if (ph->rewrap_needed) pagerhist_rewrap_to(self, self->xnum);
Line l = {.xnum=self->xnum}; get_line(self, 0, &l);
size_t sz = ph->length;
if (!l.continued) sz += 1;
PyObject *ans = PyBytes_FromStringAndSize(NULL, sz);
if (!ans) return NULL;
uint8_t *buf = (uint8_t*)PyBytes_AS_STRING(ans);
size_t copied = MIN(ph->length, ph->buffer_size - ph->start);
if (copied) memcpy(buf, ph->buffer + ph->start, copied);
if (copied < ph->length) memcpy(buf + copied, ph->buffer, (ph->length - copied));
if (!l.continued) buf[sz-1] = '\n';
return ans;
}
static PyObject *
pagerhist_as_text(HistoryBuf *self, PyObject *callback) {
PagerHistoryBuf *ph = self->pagerhist;
PyObject *ret = NULL, *t = NULL;
Py_UCS4 *buf = NULL;
index_type num;
if (!ph) Py_RETURN_NONE;
if (ph->rewrap_needed) pagerhist_rewrap(ph, self->xnum);
#define CALLBACK { \
if (t == NULL) goto end; \
ret = PyObject_CallFunctionObjArgs(callback, t, NULL); \
Py_DECREF(t); \
if (ret == NULL) goto end; \
Py_DECREF(ret); \
}
num = (ph->bufend ? ph->bufend : ph->end) - ph->start;
buf = ph->buffer + ph->start;
t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, buf, num);
CALLBACK;
if (ph->bufend) {
num = ph->end; buf = ph->buffer;
t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, buf, num);
CALLBACK;
}
Line l = {.xnum=self->xnum}; get_line(self, 0, &l);
if (!l.continued) {
t = PyUnicode_FromString("\n");
CALLBACK;
}
#undef CALLBACK
end:
if (PyErr_Occurred()) return NULL;
Py_RETURN_NONE;
pagerhist_as_text(HistoryBuf *self, PyObject *args) {
PyObject *ans = NULL;
PyObject *bytes = pagerhist_as_bytes(self, args);
if (bytes) {
ans = PyUnicode_DecodeUTF8(PyBytes_AS_STRING(bytes), PyBytes_GET_SIZE(bytes), "ignore");
Py_DECREF(bytes);
}
return ans;
}
typedef struct {
@@ -395,7 +462,10 @@ static PyObject*
as_text(HistoryBuf *self, PyObject *args) {
GetLineWrapper glw = {.self=self};
glw.line.xnum = self->xnum;
return as_text_generic(args, &glw, get_line_wrapper, self->count, self->xnum);
ANSIBuf output = {0};
PyObject *ans = as_text_generic(args, &glw, get_line_wrapper, self->count, &output);
free(output.buf);
return ans;
}
@@ -411,6 +481,14 @@ dirty_lines(HistoryBuf *self, PyObject *a UNUSED) {
return ans;
}
static PyObject*
pagerhist_rewrap(HistoryBuf *self, PyObject *xnum) {
if (self->pagerhist) {
pagerhist_rewrap_to(self, PyLong_AsUnsignedLong(xnum));
}
Py_RETURN_NONE;
}
// Boilerplate {{{
static PyObject* rewrap(HistoryBuf *self, PyObject *args);
@@ -419,7 +497,10 @@ static PyObject* rewrap(HistoryBuf *self, PyObject *args);
static PyMethodDef methods[] = {
METHOD(line, METH_O)
METHOD(as_ansi, METH_O)
METHODB(pagerhist_as_text, METH_O),
METHODB(pagerhist_write, METH_O),
METHODB(pagerhist_rewrap, METH_O),
METHODB(pagerhist_as_text, METH_NOARGS),
METHODB(pagerhist_as_bytes, METH_NOARGS),
METHODB(as_text, METH_VARARGS),
METHOD(dirty_lines, METH_NOARGS)
METHOD(push, METH_VARARGS)
@@ -450,7 +531,7 @@ PyTypeObject HistoryBuf_Type = {
INIT_TYPE(HistoryBuf)
HistoryBuf *alloc_historybuf(unsigned int lines, unsigned int columns, unsigned int pagerhist_sz) {
return (HistoryBuf*)new(&HistoryBuf_Type, Py_BuildValue("III", lines, columns, pagerhist_sz), NULL);
return create_historybuf(&HistoryBuf_Type, columns, lines, pagerhist_sz);
}
// }}}
@@ -462,13 +543,13 @@ HistoryBuf *alloc_historybuf(unsigned int lines, unsigned int columns, unsigned
#define is_src_line_continued(src_y) (map_src_index(src_y) < src->ynum - 1 ? (*attrptr(src, map_src_index(src_y + 1)) & CONTINUED_MASK) : false)
#define next_dest_line(cont) *attrptr(dest, historybuf_push(dest)) = cont & CONTINUED_MASK; dest->line->continued = cont;
#define next_dest_line(cont) *attrptr(dest, historybuf_push(dest, as_ansi_buf)) = cont & CONTINUED_MASK; dest->line->continued = cont;
#define first_dest_line next_dest_line(false);
#include "rewrap.h"
void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other) {
void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other, ANSIBuf *as_ansi_buf) {
while(other->num_segments < self->num_segments) add_segment(other);
if (other->xnum == self->xnum && other->ynum == self->ynum) {
// Fast path
@@ -480,12 +561,12 @@ void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other) {
other->count = self->count; other->start_of_data = self->start_of_data;
return;
}
if (other->pagerhist && other->xnum != self->xnum && other->pagerhist->end != other->pagerhist->start)
if (other->pagerhist && other->xnum != self->xnum && other->pagerhist->length)
other->pagerhist->rewrap_needed = true;
other->count = 0; other->start_of_data = 0;
index_type x = 0, y = 0;
if (self->count > 0) {
rewrap_inner(self, other, self->count, NULL, &x, &y);
rewrap_inner(self, other, self->count, NULL, &x, &y, as_ansi_buf);
for (index_type i = 0; i < other->count; i++) *attrptr(other, (other->start_of_data + i) % other->ynum) |= TEXT_DIRTY_MASK;
}
}
@@ -494,6 +575,8 @@ static PyObject*
rewrap(HistoryBuf *self, PyObject *args) {
HistoryBuf *other;
if (!PyArg_ParseTuple(args, "O!", &HistoryBuf_Type, &other)) return NULL;
historybuf_rewrap(self, other);
ANSIBuf as_ansi_buf = {0};
historybuf_rewrap(self, other, &as_ansi_buf);
free(as_ansi_buf.buf);
Py_RETURN_NONE;
}

161
kitty/hyperlink.c Normal file
View File

@@ -0,0 +1,161 @@
/*
* hyperlink.c
* Copyright (C) 2020 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "hyperlink.h"
#include "uthash.h"
#include <string.h>
#define MAX_KEY_LEN 2048
#define MAX_ID_LEN 256
#define MAX_ADDS_BEFORE_GC 256
#undef uthash_fatal
#define uthash_fatal(msg) fatal(msg)
typedef struct {
const char *key;
hyperlink_id_type id;
UT_hash_handle hh;
} HyperLinkEntry;
typedef struct {
HyperLinkEntry *hyperlinks;
unsigned int max_link_id, num_of_adds_since_garbage_collection;
} HyperLinkPool;
static void
free_hyperlink_entry(HyperLinkEntry *s) {
free((void*)s->key);
free(s);
}
static void
clear_pool(HyperLinkPool *pool) {
if (pool->hyperlinks) {
HyperLinkEntry *tmp, *s;
HASH_ITER(hh, pool->hyperlinks, s, tmp) {
HASH_DEL(pool->hyperlinks, s);
free_hyperlink_entry(s); s = NULL;
}
pool->max_link_id = 0;
}
}
HYPERLINK_POOL_HANDLE
alloc_hyperlink_pool(void) {
return calloc(1, sizeof(HyperLinkPool));
}
void
clear_hyperlink_pool(HYPERLINK_POOL_HANDLE h) {
if (h) {
HyperLinkPool *pool = (HyperLinkPool*)h;
clear_pool(pool);
}
}
void
free_hyperlink_pool(HYPERLINK_POOL_HANDLE h) {
if (h) {
HyperLinkPool *pool = (HyperLinkPool*)h;
clear_pool(pool);
free(pool);
}
}
void
screen_garbage_collect_hyperlink_pool(Screen *screen) {
HyperLinkPool *pool = (HyperLinkPool*)screen->hyperlink_pool;
pool->num_of_adds_since_garbage_collection = 0;
if (!pool->max_link_id) return;
hyperlink_id_type *map = calloc(HYPERLINK_MAX_NUMBER + 4, sizeof(hyperlink_id_type));
if (!map) fatal("Out of memory");
hyperlink_id_type num = remap_hyperlink_ids(screen, map);
if (num) {
HyperLinkEntry *s, *tmp;
pool->max_link_id = 0;
HASH_ITER(hh, pool->hyperlinks, s, tmp) {
if (map[s->id]) {
s->id = map[s->id];
pool->max_link_id = MAX(pool->max_link_id, s->id);
} else {
HASH_DEL(pool->hyperlinks, s);
free_hyperlink_entry(s); s = NULL;
}
}
} else clear_pool(pool);
free(map);
}
hyperlink_id_type
get_id_for_hyperlink(Screen *screen, const char *id, const char *url) {
if (!url) return 0;
HyperLinkPool *pool = (HyperLinkPool*)screen->hyperlink_pool;
static char key[MAX_KEY_LEN] = {0};
int keylen = snprintf(key, MAX_KEY_LEN-1, "%.*s:%s", MAX_ID_LEN, id ? id : "", url);
if (keylen < 0) keylen = strlen(key);
else keylen = MIN(keylen, MAX_KEY_LEN - 2); // snprintf returns how many chars it would have written in case of truncation
key[keylen] = 0;
HyperLinkEntry *s = NULL;
if (pool->hyperlinks) {
HASH_FIND_STR(pool->hyperlinks, key, s);
if (s) {
// Remove and re-add s so that it is the last entry in the hash table and
// The first entry is discarded when hash table is full.
HASH_DEL(pool->hyperlinks, s);
HASH_ADD_KEYPTR(hh, pool->hyperlinks, s->key, strlen(s->key), s);
return s->id;
}
}
hyperlink_id_type new_id = 0;
if (pool->num_of_adds_since_garbage_collection >= MAX_ADDS_BEFORE_GC) screen_garbage_collect_hyperlink_pool(screen);
if (pool->max_link_id >= HYPERLINK_MAX_NUMBER && pool->hyperlinks) {
log_error("Too many hyperlinks, discarding oldest, this means some hyperlinks might be incorrect");
new_id = pool->hyperlinks->id;
HyperLinkEntry *s = pool->hyperlinks;
HASH_DEL(pool->hyperlinks, s);
free_hyperlink_entry(s); s = NULL;
}
s = malloc(sizeof(HyperLinkEntry));
if (!s) fatal("Out of memory");
s->key = malloc(keylen + 1);
if (!s->key) fatal("Out of memory");
memcpy((void*)s->key, key, keylen + 1);
s->id = new_id ? new_id : ++pool->max_link_id;
HASH_ADD_KEYPTR(hh, pool->hyperlinks, s->key, keylen, s);
pool->num_of_adds_since_garbage_collection++;
return s->id;
}
const char*
get_hyperlink_for_id(const HYPERLINK_POOL_HANDLE handle, hyperlink_id_type id, bool only_url) {
const HyperLinkPool *pool = (HyperLinkPool*)handle;
HyperLinkEntry *s, *tmp;
HASH_ITER(hh, pool->hyperlinks, s, tmp) {
if (s->id == id) return only_url ? strstr(s->key, ":") + 1 : s->key;
}
return NULL;
}
PyObject*
screen_hyperlinks_as_list(Screen *screen) {
HyperLinkPool *pool = (HyperLinkPool*)screen->hyperlink_pool;
PyObject *ans = PyList_New(0);
HyperLinkEntry *s, *tmp;
HASH_ITER(hh, pool->hyperlinks, s, tmp) {
PyObject *e = Py_BuildValue("sH", s->key, s->id);
PyList_Append(ans, e);
Py_DECREF(e);
}
return ans;
}

17
kitty/hyperlink.h Normal file
View File

@@ -0,0 +1,17 @@
/*
* Copyright (C) 2020 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#pragma once
#include "screen.h"
HYPERLINK_POOL_HANDLE alloc_hyperlink_pool(void);
void free_hyperlink_pool(HYPERLINK_POOL_HANDLE);
void clear_hyperlink_pool(HYPERLINK_POOL_HANDLE);
hyperlink_id_type get_id_for_hyperlink(Screen*, const char*, const char*);
hyperlink_id_type remap_hyperlink_ids(Screen *self, hyperlink_id_type *map);
PyObject* screen_hyperlinks_as_list(Screen *screen);
void screen_garbage_collect_hyperlink_pool(Screen *screen);

2
kitty/key_encoding.py generated
View File

@@ -424,7 +424,7 @@ def update_encoding() -> None:
ans = ENCODING
key_map = {}
i = len(ans)
for k in sorted(keys, key=lambda k: getattr(defines, k)):
for k in sorted(keys, key=lambda k: int(getattr(defines, k))):
if k in ('GLFW_KEY_LAST', 'GLFW_KEY_LAST_PRINTABLE'):
continue
val = getattr(defines, k)

View File

@@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple
from .boss import Boss
from .child import Child
from .cli import parse_args
from .cli import parse_args, WATCHER_DEFINITION
from .cli_stub import LaunchCLIOptions
from .constants import resolve_custom_file
from .fast_data_types import set_clipboard_string
@@ -81,7 +81,11 @@ currently active window.
--copy-env
type=bool-set
Copy the environment variables from the currently active window into the
newly launched child process.
newly launched child process. Note that most shells only set environment
variables for child processes, so this will only copy the environment
variables that the shell process itself has not the environment variables
child processes inside the shell see. To copy that enviroment, use the
kitty remote control feature with :code:`kitty @launch --copy-env`.
--location
@@ -112,11 +116,11 @@ computers (for example, over ssh) or as other users.
type=choices
default=none
choices=none,@selection,@screen,@screen_scrollback,@alternate,@alternate_scrollback
Pass the screen contents as :code:`STDIN` to the child process. @selection is
the currently selected text. @screen is the contents of the currently active
window. @screen_scrollback is the same as @screen, but includes the scrollback
buffer as well. @alternate is the secondary screen of the current active
window. For example if you run a full screen terminal application, the
Pass the screen contents as :code:`STDIN` to the child process. :code:`@selection` is
the currently selected text. :code:`@screen` is the contents of the currently active
window. :code:`@screen_scrollback` is the same as :code:`@screen`, but includes the
scrollback buffer as well. :code:`@alternate` is the secondary screen of the current
active window. For example if you run a full screen terminal application, the
secondary screen will be the screen you return to when quitting the
application.
@@ -152,13 +156,7 @@ Set the WM_NAME property on X11 for the newly created OS Window when using
:option:`launch --type`=os-window. Defaults to :option:`launch --os-window-class`.
--watcher -w
type=list
Path to a python file. Appropriately named functions in this file will be called
for various events, such as when the window is resized or closed. See the section
on watchers in the launch command documentation :doc:`launch`. Relative paths are
resolved relative to the kitty config directory.
'''
''' + WATCHER_DEFINITION
def parse_launch_args(args: Optional[Sequence[str]] = None) -> Tuple[LaunchCLIOptions, List[str]]:
@@ -202,12 +200,12 @@ def tab_for_window(boss: Boss, opts: LaunchCLIOptions, target_tab: Optional[Tab]
return tab
def load_watch_modules(opts: LaunchCLIOptions) -> Optional[Watchers]:
if not opts.watcher:
def load_watch_modules(watchers: Sequence[str]) -> Optional[Watchers]:
if not watchers:
return None
import runpy
ans = Watchers()
for path in opts.watcher:
for path in watchers:
path = resolve_custom_file(path)
m = runpy.run_path(path, run_name='__kitty_watcher__')
w = m.get('on_close')
@@ -216,6 +214,9 @@ def load_watch_modules(opts: LaunchCLIOptions) -> Optional[Watchers]:
w = m.get('on_resize')
if callable(w):
ans.on_resize.append(w)
w = m.get('on_focus_change')
if callable(w):
ans.on_focus_change.append(w)
return ans
@@ -304,7 +305,7 @@ def launch(boss: Boss, opts: LaunchCLIOptions, args: List[str], target_tab: Opti
else:
tab = tab_for_window(boss, opts, target_tab)
if tab is not None:
watchers = load_watch_modules(opts)
watchers = load_watch_modules(opts.watcher)
new_window: Window = tab.new_window(env=env or None, watchers=watchers or None, **kw)
if opts.keep_focus and active:
boss.set_active_window(active, switch_os_window_if_needed=True)

View File

@@ -16,13 +16,12 @@ from kitty.fast_data_types import (
from kitty.options_stub import Options
from kitty.typing import TypedDict, WindowType
from kitty.window_list import WindowGroup, WindowList
from kitty.borders import BorderColor
class Borders(NamedTuple):
left: bool
top: bool
right: bool
bottom: bool
class BorderLine(NamedTuple):
edges: Edges = Edges()
color: BorderColor = BorderColor.inactive
class LayoutOpts:
@@ -39,8 +38,6 @@ class LayoutData(NamedTuple):
content_size: int = 0
all_borders = Borders(True, True, True, True)
no_borders = Borders(False, False, False, False)
DecorationPairs = Sequence[Tuple[int, int]]
LayoutDimension = Generator[LayoutData, None, None]
ListOfWindows = List[WindowType]
@@ -330,10 +327,11 @@ class Layout:
bias: Optional[Sequence[float]] = None,
start: Optional[int] = None,
size: Optional[int] = None,
offset: int = 0
offset: int = 0,
border_mult: int = 1
) -> LayoutDimension:
decoration_pairs = tuple(
(g.decoration('left'), g.decoration('right')) for i, g in
(g.decoration('left', border_mult=border_mult), g.decoration('right', border_mult=border_mult)) for i, g in
enumerate(groups) if i >= offset
)
if start is None:
@@ -348,10 +346,11 @@ class Layout:
bias: Optional[Sequence[float]] = None,
start: Optional[int] = None,
size: Optional[int] = None,
offset: int = 0
offset: int = 0,
border_mult: int = 1
) -> LayoutDimension:
decoration_pairs = tuple(
(g.decoration('top'), g.decoration('bottom')) for i, g in
(g.decoration('top', border_mult=border_mult), g.decoration('bottom', border_mult=border_mult)) for i, g in
enumerate(groups) if i >= offset
)
if start is None:
@@ -375,21 +374,9 @@ class Layout:
def compute_needs_borders_map(self, all_windows: WindowList) -> Dict[int, bool]:
return all_windows.compute_needs_borders_map(lgd.draw_active_borders)
def resolve_borders(self, all_windows: WindowList) -> Generator[Borders, None, None]:
if lgd.draw_minimal_borders:
needs_borders_map = self.compute_needs_borders_map(all_windows)
yield from self.minimal_borders(all_windows, needs_borders_map)
else:
for i in range(all_windows.num_groups):
yield all_borders
def window_independent_borders(self, windows: WindowList) -> Generator[Edges, None, None]:
def minimal_borders(self, windows: WindowList) -> Generator[BorderLine, None, None]:
return
yield Edges() # type: ignore
def minimal_borders(self, windows: WindowList, needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]:
for needs_border in needs_borders_map.values():
yield all_borders if needs_border else no_borders
yield BorderLine() # type: ignore
def layout_action(self, action_name: str, args: Sequence[str], all_windows: WindowList) -> Optional[bool]:
pass

View File

@@ -5,15 +5,18 @@
from functools import lru_cache
from itertools import repeat
from math import ceil, floor
from typing import Callable, Dict, Generator, List, Optional, Sequence, Tuple
from typing import (
Callable, Dict, Generator, List, Optional, Sequence, Set, Tuple
)
from kitty.borders import BorderColor
from kitty.constants import Edges
from kitty.typing import WindowType
from kitty.window_list import WindowGroup, WindowList
from .base import (
Layout, LayoutData, LayoutDimension, ListOfWindows, NeighborsMap,
layout_dimension, lgd, variable_bias
BorderLine, Layout, LayoutData, LayoutDimension, ListOfWindows,
NeighborsMap, layout_dimension, lgd, variable_bias
)
from .tall import neighbors_for_tall_window
@@ -35,6 +38,7 @@ def calc_grid_size(n: int) -> Tuple[int, int, int, int]:
class Grid(Layout):
name = 'grid'
no_minimal_window_borders = True
def remove_all_biases(self) -> bool:
self.biased_rows: Dict[int, float] = {}
@@ -165,9 +169,7 @@ class Grid(Layout):
def position_window_in_grid_cell(window_idx: int, xl: LayoutData, yl: LayoutData) -> None:
wg = groups[window_idx]
edges = Edges(
wg.decoration('left'), wg.decoration('top'), wg.decoration('right'), wg.decoration('bottom')
)
edges = Edges(wg.decoration('left'), wg.decoration('top'), wg.decoration('right'), wg.decoration('bottom'))
xl = layout(xl, lgd.cell_width, edges.left, edges.right)
yl = layout(yl, lgd.cell_height, edges.top, edges.bottom)
self.set_window_group_geometry(wg, xl, yl)
@@ -176,34 +178,72 @@ class Grid(Layout):
n, nrows, ncols, special_rows, special_col, on_col_done):
position_window_in_grid_cell(window_idx, xl, yl)
def window_independent_borders(self, all_windows: WindowList) -> Generator[Edges, None, None]:
def minimal_borders(self, all_windows: WindowList) -> Generator[BorderLine, None, None]:
n = all_windows.num_groups
if not lgd.draw_minimal_borders or n < 2:
return
needs_borders_map = all_windows.compute_needs_borders_map(lgd.draw_active_borders)
ncols, nrows, special_rows, special_col = calc_grid_size(n)
row_borders: List[List[Edges]] = [[]]
col_borders: List[Edges] = []
is_first_row: Set[int] = set()
is_last_row: Set[int] = set()
is_first_column: Set[int] = set()
is_last_column: Set[int] = set()
groups = tuple(all_windows.iter_all_layoutable_groups())
bw = groups[0].effective_border()
if not bw:
return
xl: LayoutData = LayoutData()
yl: LayoutData = LayoutData()
prev_col_windows: List[int] = []
layout_data_map: Dict[int, Tuple[LayoutData, LayoutData]] = {}
def on_col_done(col_windows: List[int]) -> None:
left = xl.content_pos + xl.content_size + xl.space_after - bw // 2
col_borders.append(Edges(left, lgd.central.top, left + bw, lgd.central.bottom))
row_borders.append([])
nonlocal prev_col_windows, is_first_column
if col_windows:
is_first_row.add(groups[col_windows[0]].id)
is_last_row.add(groups[col_windows[-1]].id)
if not prev_col_windows:
is_first_column = {groups[x].id for x in col_windows}
prev_col_windows = col_windows
all_groups_in_order: List[WindowGroup] = []
for window_idx, xl, yl in self.layout_windows(n, nrows, ncols, special_rows, special_col, on_col_done):
top = yl.content_pos + yl.content_size + yl.space_after - bw // 2
right = xl.content_pos + xl.content_size + xl.space_after
row_borders[-1].append(Edges(xl.content_pos - xl.space_before, top, right, top + bw))
wg = groups[window_idx]
all_groups_in_order.append(wg)
layout_data_map[wg.id] = xl, yl
is_last_column = {groups[x].id for x in prev_col_windows}
active_group = all_windows.active_group
for border in col_borders[:-1]:
yield border
def ends(yl: LayoutData) -> Tuple[int, int]:
return yl.content_pos - yl.space_before, yl.content_pos + yl.content_size + yl.space_after
for rows in row_borders:
for border in rows[:-1]:
yield border
def borders_for_window(gid: int) -> Generator[Edges, None, None]:
xl, yl = layout_data_map[gid]
left, right = ends(xl)
top, bottom = ends(yl)
first_row, last_row = gid in is_first_row, gid in is_last_row
first_column, last_column = gid in is_first_column, gid in is_last_column
# Horizontal
if not first_row:
yield Edges(left, top, right, top + bw)
if not last_row:
yield Edges(left, bottom - bw, right, bottom)
# Vertical
if not first_column:
yield Edges(left, top, left + bw, bottom)
if not last_column:
yield Edges(right - bw, top, right, bottom)
for wg in all_groups_in_order:
for edges in borders_for_window(wg.id):
yield BorderLine(edges)
for wg in all_groups_in_order:
if needs_borders_map.get(wg.id):
color = BorderColor.active if wg is active_group else BorderColor.bell
for edges in borders_for_window(wg.id):
yield BorderLine(edges, color)
def neighbors_for_window(self, window: WindowType, all_windows: WindowList) -> NeighborsMap:
n = all_windows.num_groups

View File

@@ -3,19 +3,26 @@
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import (
Collection, Dict, Generator, Optional, Sequence, Tuple, Union
Collection, Dict, Generator, List, NamedTuple, Optional, Sequence, Tuple,
Union
)
from kitty.borders import BorderColor
from kitty.constants import Edges, WindowGeometry
from kitty.typing import EdgeLiteral, WindowType
from kitty.window_list import WindowGroup, WindowList
from .base import (
Layout, LayoutOpts, NeighborsMap, blank_rects_for_window, lgd,
BorderLine, Layout, LayoutOpts, NeighborsMap, blank_rects_for_window, lgd,
window_geometry_from_layouts
)
class Extent(NamedTuple):
start: int = 0
end: int = 0
class Pair:
def __init__(self, horizontal: bool = True):
@@ -23,11 +30,13 @@ class Pair:
self.one: Optional[Union[Pair, int]] = None
self.two: Optional[Union[Pair, int]] = None
self.bias = 0.5
self.between_border: Optional[Edges] = None
self.top = self.left = self.width = self.height = 0
self.between_borders: List[Edges] = []
self.first_extent = self.second_extent = Extent()
def __repr__(self) -> str:
return 'Pair(horizontal={}, bias={:.2f}, one={}, two={}, between_border={})'.format(
self.horizontal, self.bias, self.one, self.two, self.between_border)
return 'Pair(horizontal={}, bias={:.2f}, one={}, two={}, between_borders={})'.format(
self.horizontal, self.bias, self.one, self.two, self.between_borders)
def all_window_ids(self) -> Generator[int, None, None]:
if self.one is not None:
@@ -168,7 +177,12 @@ class Pair:
id_window_map: Dict[int, WindowGroup],
layout_object: Layout
) -> None:
self.between_border = None
self.between_borders = []
self.left, self.top, self.width, self.height = left, top, width, height
bw = self.effective_border(id_window_map) if lgd.draw_minimal_borders else 0
border_mult = 0 if lgd.draw_minimal_borders else 1
bw2 = bw * 2
self.first_extent = self.second_extent = Extent()
if self.one is None or self.two is None:
q = self.one or self.two
if isinstance(q, Pair):
@@ -176,56 +190,62 @@ class Pair:
if q is None:
return
wg = id_window_map[q]
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=width))
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=height))
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=width, border_mult=border_mult))
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=height, border_mult=border_mult))
geom = window_geometry_from_layouts(xl, yl)
self.first_extent = Extent(left, left + width)
self.apply_window_geometry(q, geom, id_window_map, layout_object)
return
bw = self.effective_border(id_window_map) if lgd.draw_minimal_borders else 0
b1 = bw // 2
b2 = bw - b1
if self.horizontal:
w1 = max(2*lgd.cell_width + 1, int(self.bias * width) - b1)
w2 = max(2*lgd.cell_width + 1, width - w1 - b1 - b2)
w1 = max(2*lgd.cell_width + 1, int(self.bias * width) - bw)
w2 = max(2*lgd.cell_width + 1, width - w1 - bw2)
self.first_extent = Extent(max(0, left - bw), left + w1 + bw)
self.second_extent = Extent(left + w1 + bw, left + width + bw)
if isinstance(self.one, Pair):
self.one.layout_pair(left, top, w1, height, id_window_map, layout_object)
else:
wg = id_window_map[self.one]
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=height))
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=w1))
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=height, border_mult=border_mult))
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=w1, border_mult=border_mult))
geom = window_geometry_from_layouts(xl, yl)
self.apply_window_geometry(self.one, geom, id_window_map, layout_object)
if b1 + b2:
self.between_border = Edges(left + w1, top, left + w1 + b1 + b2, top + height)
left += b1 + b2
self.between_borders = [
Edges(left + w1, top, left + w1 + bw, top + height),
Edges(left + w1 + bw, top, left + w1 + bw2, top + height),
]
left += bw2
if isinstance(self.two, Pair):
self.two.layout_pair(left + w1, top, w2, height, id_window_map, layout_object)
else:
wg = id_window_map[self.two]
xl = next(layout_object.xlayout(iter((wg,)), start=left + w1, size=w2))
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=height))
xl = next(layout_object.xlayout(iter((wg,)), start=left + w1, size=w2, border_mult=border_mult))
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=height, border_mult=border_mult))
geom = window_geometry_from_layouts(xl, yl)
self.apply_window_geometry(self.two, geom, id_window_map, layout_object)
else:
h1 = max(2*lgd.cell_height + 1, int(self.bias * height) - b1)
h2 = max(2*lgd.cell_height + 1, height - h1 - b1 - b2)
h1 = max(2*lgd.cell_height + 1, int(self.bias * height) - bw)
h2 = max(2*lgd.cell_height + 1, height - h1 - bw2)
self.first_extent = Extent(max(0, top - bw), top + h1 + bw)
self.second_extent = Extent(top + h1 + bw, top + height + bw)
if isinstance(self.one, Pair):
self.one.layout_pair(left, top, width, h1, id_window_map, layout_object)
else:
wg = id_window_map[self.one]
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=width))
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=h1))
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=width, border_mult=border_mult))
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=h1, border_mult=border_mult))
geom = window_geometry_from_layouts(xl, yl)
self.apply_window_geometry(self.one, geom, id_window_map, layout_object)
if b1 + b2:
self.between_border = Edges(left, top + h1, left + width, top + h1 + b1 + b2)
top += b1 + b2
self.between_borders = [
Edges(left, top + h1, left + width, top + h1 + bw),
Edges(left, top + h1 + bw, left + width, top + h1 + bw2),
]
top += bw2
if isinstance(self.two, Pair):
self.two.layout_pair(left, top + h1, width, h2, id_window_map, layout_object)
else:
wg = id_window_map[self.two]
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=width))
yl = next(layout_object.ylayout(iter((wg,)), start=top + h1, size=h2))
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=width, border_mult=border_mult))
yl = next(layout_object.ylayout(iter((wg,)), start=top + h1, size=h2, border_mult=border_mult))
geom = window_geometry_from_layouts(xl, yl)
self.apply_window_geometry(self.two, geom, id_window_map, layout_object)
@@ -244,6 +264,55 @@ class Pair:
return parent.modify_size_of_child(which, increment, is_horizontal, layout_object)
return False
def borders_for_window(self, layout_object: 'Splits', window_id: int) -> Generator[Edges, None, None]:
is_first = self.one == window_id
if self.between_borders:
yield self.between_borders[0 if is_first else 1]
q = self
found_same_direction = found_transverse1 = found_transverse2 = False
while not (found_same_direction and found_transverse1 and found_transverse2):
parent = q.parent(layout_object.pairs_root)
if parent is None:
break
q = parent
if not q.between_borders:
continue
if q.horizontal == self.horizontal:
if not found_same_direction:
if self.horizontal:
is_before = q.between_borders[0].left <= self.left
else:
is_before = q.between_borders[0].top <= self.top
if is_before == is_first:
found_same_direction = True
edges = q.between_borders[1 if is_before else 0]
if self.horizontal:
yield edges._replace(top=self.top, bottom=self.top + self.height)
else:
yield edges._replace(left=self.left, right=self.left + self.width)
else:
if self.horizontal:
is_before = q.between_borders[0].top <= self.top
else:
is_before = q.between_borders[0].left <= self.left
extent = self.first_extent if is_first else self.second_extent
if is_before:
if not found_transverse1:
found_transverse1 = True
edges = q.between_borders[1]
if self.horizontal:
yield edges._replace(left=extent.start, right=extent.end)
else:
yield edges._replace(top=extent.start, bottom=extent.end)
else:
if not found_transverse2:
found_transverse2 = True
edges = q.between_borders[0]
if self.horizontal:
yield edges._replace(left=extent.start, right=extent.end)
else:
yield edges._replace(top=extent.start, bottom=extent.end)
def neighbors_for_window(self, window_id: int, ans: NeighborsMap, layout_object: 'Splits', all_windows: WindowList) -> None:
def quadrant(is_horizontal: bool, is_first: bool) -> Tuple[EdgeLiteral, EdgeLiteral]:
@@ -325,6 +394,7 @@ class Splits(Layout):
name = 'splits'
needs_all_windows = True
layout_opts = SplitsLayoutOpts({})
no_minimal_window_borders = True
@property
def default_axis_is_horizontal(self) -> bool:
@@ -417,14 +487,24 @@ class Splits(Layout):
pair.bias = 0.5
return True
def window_independent_borders(self, all_windows: WindowList) -> Generator[Edges, None, None]:
def minimal_borders(self, all_windows: WindowList) -> Generator[BorderLine, None, None]:
groups = tuple(all_windows.iter_all_layoutable_groups())
window_count = len(groups)
if not lgd.draw_minimal_borders or window_count < 2:
return
for pair in self.pairs_root.self_and_descendants():
if pair.between_border is not None:
yield pair.between_border
for edges in pair.between_borders:
yield BorderLine(edges)
needs_borders_map = all_windows.compute_needs_borders_map(lgd.draw_active_borders)
ag = all_windows.active_group
active_group_id = -1 if ag is None else ag.id
for grp_id, needs_borders in needs_borders_map.items():
if needs_borders:
qpair = self.pairs_root.pair_for_window(grp_id)
if qpair is not None:
color = BorderColor.active if grp_id is active_group_id else BorderColor.bell
for edges in qpair.borders_for_window(self, grp_id):
yield BorderLine(edges, color)
def neighbors_for_window(self, window: WindowType, all_windows: WindowList) -> NeighborsMap:
wg = all_windows.group_for_window(window)

View File

@@ -5,14 +5,17 @@
from itertools import islice, repeat
from typing import Dict, Generator, List, Optional, Sequence, Tuple
from kitty.borders import BorderColor
from kitty.conf.utils import to_bool
from kitty.constants import Edges
from kitty.typing import EdgeLiteral, WindowType
from kitty.window_list import WindowList
from kitty.window_list import WindowGroup, WindowList
from .base import (
Borders, Layout, LayoutDimension, LayoutOpts, NeighborsMap, all_borders,
lgd, no_borders, normalize_biases, safe_increment_bias, variable_bias
BorderLine, Layout, LayoutData, LayoutDimension, LayoutOpts, NeighborsMap,
lgd, normalize_biases, safe_increment_bias, variable_bias
)
from .vertical import borders
def neighbors_for_tall_window(
@@ -75,9 +78,7 @@ class Tall(Layout):
name = 'tall'
main_is_horizontal = True
only_between_border = Borders(False, False, False, True)
only_main_border = Borders(False, False, True, False)
only_main_border_mirrored = Borders(True, False, False, False)
no_minimal_window_borders = True
layout_opts = TallLayoutOpts({})
main_axis_layout = Layout.xlayout
perp_axis_layout = Layout.ylayout
@@ -121,27 +122,27 @@ class Tall(Layout):
self.biased_map = candidate
return before != after
def do_layout(self, all_windows: WindowList) -> None:
def simple_layout(self, all_windows: WindowList) -> Generator[Tuple[WindowGroup, LayoutData, LayoutData, bool], None, None]:
num = all_windows.num_groups
if num == 1:
self.layout_single_window_group(next(all_windows.iter_all_layoutable_groups()))
return
is_fat = not self.main_is_horizontal
mirrored = self.layout_opts.mirrored
groups = tuple(all_windows.iter_all_layoutable_groups())
main_bias = self.main_bias[::-1] if mirrored else self.main_bias
if num <= self.num_full_size_windows + 1:
if mirrored:
groups = tuple(reversed(groups))
if num < self.num_full_size_windows + 1:
main_bias = normalize_biases(main_bias[:num])
xlayout = self.main_axis_layout(iter(groups), bias=main_bias)
for wg, xl in zip(groups, xlayout):
yl = next(self.perp_axis_layout(iter((wg,))))
if is_fat:
xl, yl = yl, xl
self.set_window_group_geometry(wg, xl, yl)
return
if mirrored:
groups = tuple(reversed(groups))
main_bias = normalize_biases(main_bias[:num])
xlayout = self.main_axis_layout(iter(groups), bias=main_bias)
for wg, xl in zip(groups, xlayout):
yl = next(self.perp_axis_layout(iter((wg,))))
if is_fat:
xl, yl = yl, xl
yield wg, xl, yl, True
def full_layout(self, all_windows: WindowList) -> Generator[Tuple[WindowGroup, LayoutData, LayoutData, bool], None, None]:
is_fat = not self.main_is_horizontal
mirrored = self.layout_opts.mirrored
groups = tuple(all_windows.iter_all_layoutable_groups())
main_bias = self.main_bias[::-1] if mirrored else self.main_bias
start = lgd.central.top if is_fat else lgd.central.left
size = 0
@@ -156,19 +157,18 @@ class Tall(Layout):
yl = next(self.perp_axis_layout(iter((wg,))))
if is_fat:
xl, yl = yl, xl
self.set_window_group_geometry(wg, xl, yl)
yield wg, xl, yl, True
else:
xlayout = self.main_axis_layout(islice(groups, self.num_full_size_windows + 1), bias=main_bias)
attr: EdgeLiteral = 'bottom' if is_fat else 'right'
for i, wg in enumerate(groups):
if i >= self.num_full_size_windows:
break
xl = next(xlayout)
yl = next(self.perp_axis_layout(iter((wg,))))
start = xl.content_pos + xl.content_size + xl.space_after
if is_fat:
xl, yl = yl, xl
geom = self.set_window_group_geometry(wg, xl, yl)
start = getattr(geom, attr) + wg.decoration(attr)
yield wg, xl, yl, True
size = (lgd.central.height if is_fat else lgd.central.width) - start
ylayout = self.variable_layout(all_windows, self.biased_map)
@@ -179,36 +179,20 @@ class Tall(Layout):
xl = next(self.main_axis_layout(iter((wg,)), start=start, size=size))
if is_fat:
xl, yl = yl, xl
yield wg, xl, yl, False
def do_layout(self, all_windows: WindowList) -> None:
num = all_windows.num_groups
if num == 1:
self.layout_single_window_group(next(all_windows.iter_all_layoutable_groups()))
return
layouts = (self.simple_layout if num <= self.num_full_size_windows + 1 else self.full_layout)(all_windows)
for wg, xl, yl, is_full_size in layouts:
self.set_window_group_geometry(wg, xl, yl)
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> NeighborsMap:
return neighbors_for_tall_window(self.num_full_size_windows, window, windows, self.layout_opts.mirrored, self.main_is_horizontal)
def minimal_borders(self, all_windows: WindowList, needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]:
mirrored = self.layout_opts.mirrored
only_main_border = self.only_main_border_mirrored if mirrored else self.only_main_border
num = all_windows.num_groups
last_i = num - 1
groups = tuple(all_windows.iter_all_layoutable_groups())
for i, wg in enumerate(groups):
if needs_borders_map[wg.id]:
yield all_borders
continue
if i < self.num_full_size_windows:
next_window_is_full_sized = last_i == i+1 or i+1 < self.num_full_size_windows
if next_window_is_full_sized and needs_borders_map[groups[i+1].id]:
yield no_borders
else:
yield no_borders if i == last_i else only_main_border
continue
if i == last_i:
yield no_borders
break
if needs_borders_map[groups[i+1].id]:
yield no_borders
else:
yield self.only_between_border
def layout_action(self, action_name: str, args: Sequence[str], all_windows: WindowList) -> Optional[bool]:
if action_name == 'increase_num_full_size_windows':
self.layout_opts.full_size += 1
@@ -218,13 +202,86 @@ class Tall(Layout):
self.layout_opts.full_size -= 1
return True
def minimal_borders(self, all_windows: WindowList) -> Generator[BorderLine, None, None]:
num = all_windows.num_groups
if num < 2 or not lgd.draw_minimal_borders:
return
try:
bw = next(all_windows.iter_all_layoutable_groups()).effective_border()
except StopIteration:
bw = 0
if not bw:
return
if num <= self.num_full_size_windows + 1:
layout = (x[:3] for x in self.simple_layout(all_windows))
yield from borders(layout, self.main_is_horizontal, all_windows)
return
main_layouts: List[Tuple[WindowGroup, LayoutData, LayoutData]] = []
perp_borders: List[BorderLine] = []
layouts = (self.simple_layout if num <= self.num_full_size_windows else self.full_layout)(all_windows)
needs_borders_map = all_windows.compute_needs_borders_map(lgd.draw_active_borders)
active_group = all_windows.active_group
mirrored = self.layout_opts.mirrored
for wg, xl, yl, is_full_size in layouts:
if is_full_size:
main_layouts.append((wg, xl, yl))
else:
color = BorderColor.inactive
if needs_borders_map.get(wg.id):
color = BorderColor.active if wg is active_group else BorderColor.bell
if self.main_is_horizontal:
e1 = Edges(
xl.content_pos - xl.space_before,
yl.content_pos - yl.space_before,
xl.content_pos + xl.content_size + xl.space_after,
yl.content_pos - yl.space_before + bw
)
e3 = Edges(
xl.content_pos - xl.space_before,
yl.content_pos + yl.content_size + yl.space_after - bw,
xl.content_pos + xl.content_size + xl.space_after,
yl.content_pos + yl.content_size + yl.space_after,
)
e2 = Edges(
xl.content_pos + ((xl.content_size + xl.space_after - bw) if mirrored else -xl.space_before),
yl.content_pos - yl.space_before,
xl.content_pos + ((xl.content_size + xl.space_after) if mirrored else (bw - xl.space_before)),
yl.content_pos + yl.content_size + yl.space_after,
)
else:
e1 = Edges(
xl.content_pos - xl.space_before,
yl.content_pos - yl.space_before,
xl.content_pos - xl.space_before + bw,
yl.content_pos + yl.content_size + yl.space_after,
)
e3 = Edges(
xl.content_pos + xl.content_size + xl.space_after - bw,
yl.content_pos - yl.space_before,
xl.content_pos + xl.content_size + xl.space_after,
yl.content_pos + yl.content_size + yl.space_after,
)
e2 = Edges(
xl.content_pos - xl.space_before,
yl.content_pos + ((yl.content_size + yl.space_after - bw) if mirrored else -yl.space_before),
xl.content_pos + xl.content_size + xl.space_after,
yl.content_pos + ((yl.content_size + yl.space_after) if mirrored else (bw - yl.space_before)),
)
perp_borders.append(BorderLine(e1, color))
perp_borders.append(BorderLine(e2, color))
perp_borders.append(BorderLine(e3, color))
mirrored = self.layout_opts.mirrored
yield from borders(
main_layouts, self.main_is_horizontal, all_windows,
start_offset=int(not mirrored), end_offset=int(mirrored)
)
yield from perp_borders[1:-1]
class Fat(Tall):
name = 'fat'
main_is_horizontal = False
only_between_border = Borders(False, False, True, False)
only_main_border = Borders(False, False, False, True)
only_main_border_mirrored = Borders(False, True, False, False)
main_axis_layout = Layout.ylayout
perp_axis_layout = Layout.xlayout

View File

@@ -2,22 +2,71 @@
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import Dict, Generator
from typing import Dict, Generator, Iterable, List, Tuple
from kitty.borders import BorderColor
from kitty.constants import Edges
from kitty.typing import WindowType
from kitty.window_list import WindowList
from kitty.window_list import WindowGroup, WindowList
from .base import (
Borders, Layout, LayoutDimension, NeighborsMap, all_borders, no_borders,
variable_bias
BorderLine, Layout, LayoutData, LayoutDimension, NeighborsMap,
lgd, variable_bias
)
def borders(
data: Iterable[Tuple[WindowGroup, LayoutData, LayoutData]],
is_horizontal: bool,
all_windows: WindowList,
start_offset: int = 1, end_offset: int = 1
) -> Generator[BorderLine, None, None]:
borders: List[BorderLine] = []
active_group = all_windows.active_group
needs_borders_map = all_windows.compute_needs_borders_map(lgd.draw_active_borders)
try:
bw = next(all_windows.iter_all_layoutable_groups()).effective_border()
except StopIteration:
bw = 0
if not bw:
return
for wg, xl, yl in data:
if is_horizontal:
e1 = Edges(
xl.content_pos - xl.space_before, yl.content_pos - yl.space_before,
xl.content_pos - xl.space_before + bw, yl.content_pos + yl.content_size + yl.space_after
)
e2 = Edges(
xl.content_pos + xl.content_size + xl.space_after - bw, yl.content_pos - yl.space_before,
xl.content_pos + xl.content_size + xl.space_after, yl.content_pos + yl.content_size + yl.space_after
)
else:
e1 = Edges(
xl.content_pos - xl.space_before, yl.content_pos - yl.space_before,
xl.content_pos + xl.content_size + xl.space_after, yl.content_pos - yl.space_before + bw
)
e2 = Edges(
xl.content_pos - xl.space_before, yl.content_pos + yl.content_size + yl.space_after - bw,
xl.content_pos + xl.content_size + xl.space_after, yl.content_pos + yl.content_size + yl.space_after
)
color = BorderColor.inactive
if needs_borders_map.get(wg.id):
color = BorderColor.active if wg is active_group else BorderColor.bell
borders.append(BorderLine(e1, color))
borders.append(BorderLine(e2, color))
last_idx = len(borders) - 1 - end_offset
for i, x in enumerate(borders):
if start_offset <= i <= last_idx:
yield x
class Vertical(Layout):
name = 'vertical'
main_is_horizontal = False
only_between_border = Borders(False, False, False, True)
no_minimal_window_borders = True
main_axis_layout = Layout.ylayout
perp_axis_layout = Layout.xlayout
@@ -26,6 +75,9 @@ class Vertical(Layout):
bias = variable_bias(num_windows, biased_map) if num_windows else None
return self.main_axis_layout(all_windows.iter_all_layoutable_groups(), bias=bias)
def fixed_layout(self, wg: WindowGroup) -> LayoutDimension:
return self.perp_axis_layout(iter((wg,)), border_mult=0 if lgd.draw_minimal_borders else 1)
def remove_all_biases(self) -> bool:
self.biased_map: Dict[int, float] = {}
return True
@@ -45,33 +97,27 @@ class Vertical(Layout):
self.biased_map = candidate
return True
def generate_layout_data(self, all_windows: WindowList) -> Generator[Tuple[WindowGroup, LayoutData, LayoutData], None, None]:
ylayout = self.variable_layout(all_windows, self.biased_map)
for wg, yl in zip(all_windows.iter_all_layoutable_groups(), ylayout):
xl = next(self.fixed_layout(wg))
if self.main_is_horizontal:
xl, yl = yl, xl
yield wg, xl, yl
def do_layout(self, all_windows: WindowList) -> None:
window_count = all_windows.num_groups
if window_count == 1:
self.layout_single_window_group(next(all_windows.iter_all_layoutable_groups()))
return
ylayout = self.variable_layout(all_windows, self.biased_map)
for i, (wg, yl) in enumerate(zip(all_windows.iter_all_layoutable_groups(), ylayout)):
xl = next(self.perp_axis_layout(iter((wg,))))
if self.main_is_horizontal:
xl, yl = yl, xl
for wg, xl, yl in self.generate_layout_data(all_windows):
self.set_window_group_geometry(wg, xl, yl)
def minimal_borders(self, all_windows: WindowList, needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]:
last_i = all_windows.num_groups - 1
groups = tuple(all_windows.iter_all_layoutable_groups())
for i, wg in enumerate(groups):
if needs_borders_map[wg.id]:
yield all_borders
continue
if i == last_i:
yield no_borders
break
if needs_borders_map[groups[i+1].id]:
yield no_borders
else:
yield self.only_between_border
def minimal_borders(self, all_windows: WindowList) -> Generator[BorderLine, None, None]:
window_count = all_windows.num_groups
if window_count < 2 or not lgd.draw_minimal_borders:
return
yield from borders(self.generate_layout_data(all_windows), self.main_is_horizontal, all_windows)
def neighbors_for_window(self, window: WindowType, all_windows: WindowList) -> NeighborsMap:
wg = all_windows.group_for_window(window)
@@ -89,6 +135,5 @@ class Horizontal(Vertical):
name = 'horizontal'
main_is_horizontal = True
only_between_border = Borders(False, False, True, False)
main_axis_layout = Layout.xlayout
perp_axis_layout = Layout.ylayout

View File

@@ -399,30 +399,36 @@ delete_lines(LineBuf *self, PyObject *args) {
static PyObject*
as_ansi(LineBuf *self, PyObject *callback) {
#define as_ansi_doc "as_ansi(callback) -> The contents of this buffer as ANSI escaped text. callback is called with each successive line."
static Py_UCS4 t[5120];
Line l = {.xnum=self->xnum};
// remove trailing empty lines
index_type ylimit = self->ynum - 1;
bool truncated;
const GPUCell *prev_cell = NULL;
ANSIBuf output = {0};
do {
init_line(self, (&l), self->line_map[ylimit]);
if (line_as_ansi(&l, t, 5120, &truncated, &prev_cell) != 0) break;
line_as_ansi(&l, &output, &prev_cell);
if (output.len) break;
ylimit--;
} while(ylimit > 0);
for(index_type i = 0; i <= ylimit; i++) {
l.continued = ((i < self->ynum - 1) ? self->line_attrs[i+1] : self->line_attrs[i]) & CONTINUED_MASK;
init_line(self, (&l), self->line_map[i]);
index_type num = line_as_ansi(&l, t, 5120, &truncated, &prev_cell);
if (!(l.continued) && num < 5119) t[num++] = 10; // 10 = \n
PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, t, num);
if (ans == NULL) return PyErr_NoMemory();
line_as_ansi(&l, &output, &prev_cell);
if (!l.continued) {
ensure_space_for(&output, buf, Py_UCS4, output.len + 1, capacity, 2048, false);
output.buf[output.len++] = 10; // 10 = \n
}
PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len);
if (ans == NULL) { PyErr_NoMemory(); goto end; }
PyObject *ret = PyObject_CallFunctionObjArgs(callback, ans, NULL);
Py_CLEAR(ans);
if (ret == NULL) return NULL;
if (ret == NULL) goto end;
Py_CLEAR(ret);
}
end:
free(output.buf);
if (PyErr_Occurred()) return NULL;
Py_RETURN_NONE;
}
@@ -435,7 +441,10 @@ get_line(void *x, int y) {
static PyObject*
as_text(LineBuf *self, PyObject *args) {
return as_text_generic(args, self, get_line, self->ynum, self->xnum);
ANSIBuf output = {0};
PyObject* ans = as_text_generic(args, self, get_line, self->ynum, &output);
free(output.buf);
return ans;
}
@@ -528,7 +537,7 @@ copy_old(LineBuf *self, PyObject *y) {
#include "rewrap.h"
void
linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *num_content_lines_before, index_type *num_content_lines_after, HistoryBuf *historybuf, index_type *track_x, index_type *track_y) {
linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *num_content_lines_before, index_type *num_content_lines_after, HistoryBuf *historybuf, index_type *track_x, index_type *track_y, ANSIBuf *as_ansi_buf) {
index_type first, i;
bool is_empty = true;
@@ -558,7 +567,7 @@ linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *num_content_lines_befo
return;
}
rewrap_inner(self, other, first + 1, historybuf, track_x, track_y);
rewrap_inner(self, other, first + 1, historybuf, track_x, track_y, as_ansi_buf);
*num_content_lines_after = other->line->ynum + 1;
for (i = 0; i < *num_content_lines_after; i++) other->line_attrs[i] |= TEXT_DIRTY_MASK;
*num_content_lines_before = first + 1;
@@ -572,7 +581,9 @@ rewrap(LineBuf *self, PyObject *args) {
if (!PyArg_ParseTuple(args, "O!O!", &LineBuf_Type, &other, &HistoryBuf_Type, &historybuf)) return NULL;
index_type x = 0, y = 0;
linebuf_rewrap(self, other, &nclb, &ncla, historybuf, &x, &y);
ANSIBuf as_ansi_buf = {0};
linebuf_rewrap(self, other, &nclb, &ncla, historybuf, &x, &y, &as_ansi_buf);
free(as_ansi_buf.buf);
return Py_BuildValue("II", nclb, ncla);
}

View File

@@ -268,23 +268,48 @@ sprite_at(Line* self, PyObject *x) {
return Py_BuildValue("HHH", c->sprite_x, c->sprite_y, c->sprite_z);
}
static inline bool
write_sgr(const char *val, Py_UCS4 *buf, index_type buflen, index_type *i) {
static char s[128];
unsigned int num = snprintf(s, sizeof(s), "\x1b[%sm", val);
if (buflen - (*i) < num + 3) return false;
for(unsigned int si=0; si < num; si++) buf[(*i)++] = s[si];
return true;
static inline void
write_sgr(const char *val, ANSIBuf *output) {
#define W(c) output->buf[output->len++] = c
W(0x1b); W('[');
for (size_t i = 0; val[i] != 0 && i < 122; i++) W(val[i]);
W('m');
#undef W
}
index_type
line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen, bool *truncated, const GPUCell** prev_cell) {
#define WRITE_SGR(val) { if (!write_sgr(val, buf, buflen, &i)) { *truncated = true; return i; } }
#define WRITE_CH(val) if (i > buflen - 1) { *truncated = true; return i; } buf[i++] = val;
static inline void
write_hyperlink(hyperlink_id_type hid, ANSIBuf *output) {
#define W(c) output->buf[output->len++] = c
const char *key = hid ? get_hyperlink_for_id(output->hyperlink_pool, hid, false) : NULL;
if (!key) hid = 0;
output->active_hyperlink_id = hid;
W(0x1b); W(']'); W('8');
if (!hid) {
W(';'); W(';');
} else {
const char* partition = strstr(key, ":");
W(';');
if (partition != key) {
W('i'); W('d'); W('=');
while (key != partition) W(*(key++));
}
W(';');
while(*(++partition)) W(*partition);
}
W(0x1b); W('\\');
#undef W
}
index_type limit = xlimit_for_line(self), i=0;
*truncated = false;
if (limit == 0) return 0;
void
line_as_ansi(Line *self, ANSIBuf *output, const GPUCell** prev_cell) {
#define ENSURE_SPACE(extra) ensure_space_for(output, buf, Py_UCS4, output->len + extra, capacity, 2048, false);
#define WRITE_SGR(val) { ENSURE_SPACE(128); write_sgr(val, output); }
#define WRITE_CH(val) { ENSURE_SPACE(1); output->buf[output->len++] = val; }
#define WRITE_HYPERLINK(val) { ENSURE_SPACE(2256); write_hyperlink(val, output); }
output->len = 0;
index_type limit = xlimit_for_line(self);
if (limit == 0) return;
char_type previous_width = 0;
static const GPUCell blank_cell = { 0 };
@@ -297,6 +322,12 @@ line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen, bool *truncated, const
if (previous_width == 2) { previous_width = 0; continue; }
ch = ' ';
}
if (output->hyperlink_pool) {
hyperlink_id_type hid = self->cpu_cells[pos].hyperlink_id;
if (hid != output->active_hyperlink_id) {
WRITE_HYPERLINK(hid);
}
}
cell = &self->gpu_cells[pos];
@@ -320,21 +351,22 @@ line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen, bool *truncated, const
}
previous_width = cell->attrs & WIDTH_MASK;
}
return i;
#undef CMP_ATTRS
#undef CMP
#undef WRITE_SGR
#undef WRITE_CH
#undef ENSURE_SPACE
#undef WRITE_HYPERLINK
}
static PyObject*
as_ansi(Line* self, PyObject *a UNUSED) {
#define as_ansi_doc "Return the line's contents with ANSI (SGR) escape codes for formatting"
static Py_UCS4 t[5120] = {0};
bool truncated;
const GPUCell *prev_cell = NULL;
index_type num = line_as_ansi(self, t, 5120, &truncated, &prev_cell);
PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, t, num);
ANSIBuf output = {0};
line_as_ansi(self, &output, &prev_cell);
PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len);
free(output.buf);
return ans;
}
@@ -420,6 +452,7 @@ set_text(Line* self, PyObject *args) {
for (index_type i = cursor->x; offset < limit && i < self->xnum; i++, offset++) {
self->cpu_cells[i].ch = (PyUnicode_READ(kind, buf, offset));
self->cpu_cells[i].hyperlink_id = 0;
self->gpu_cells[i].attrs = attrs;
self->gpu_cells[i].fg = fg;
self->gpu_cells[i].bg = bg;
@@ -454,15 +487,10 @@ cursor_from(Line* self, PyObject *args) {
void
line_clear_text(Line *self, unsigned int at, unsigned int num, char_type ch) {
attrs_type width = ch ? 1 : 0;
#define PREFIX \
for (index_type i = at; i < MIN(self->xnum, at + num); i++) { \
self->cpu_cells[i].ch = ch; memset(self->cpu_cells[i].cc_idx, 0, sizeof(self->cpu_cells[i].cc_idx)); \
self->gpu_cells[i].attrs = (self->gpu_cells[i].attrs & ATTRS_MASK_WITHOUT_WIDTH) | width; \
}
if (CHAR_IS_BLANK(ch)) {
PREFIX
} else {
PREFIX
for (index_type i = at; i < MIN(self->xnum, at + num); i++) {
self->cpu_cells[i].ch = ch; memset(self->cpu_cells[i].cc_idx, 0, sizeof(self->cpu_cells[i].cc_idx));
self->cpu_cells[i].hyperlink_id = 0;
self->gpu_cells[i].attrs = (self->gpu_cells[i].attrs & ATTRS_MASK_WITHOUT_WIDTH) | width;
}
}
@@ -486,6 +514,7 @@ line_apply_cursor(Line *self, Cursor *cursor, unsigned int at, unsigned int num,
for (index_type i = at; i < self->xnum && i < at + num; i++) {
if (clear_char) {
self->cpu_cells[i].ch = BLANK_CHAR;
self->cpu_cells[i].hyperlink_id = 0;
memset(self->cpu_cells[i].cc_idx, 0, sizeof(self->cpu_cells[i].cc_idx));
self->gpu_cells[i].attrs = attrs;
clear_sprite_position(self->gpu_cells[i]);
@@ -517,6 +546,7 @@ void line_right_shift(Line *self, unsigned int at, unsigned int num) {
char_type w = (self->gpu_cells[self->xnum - 1].attrs) & WIDTH_MASK;
if (w != 1) {
self->cpu_cells[self->xnum - 1].ch = BLANK_CHAR;
self->cpu_cells[self->xnum - 1].hyperlink_id = 0;
self->gpu_cells[self->xnum - 1].attrs = BLANK_CHAR ? 1 : 0;
clear_sprite_position(self->gpu_cells[self->xnum - 1]);
}
@@ -558,7 +588,7 @@ line_get_char(Line *self, index_type at) {
}
void
line_set_char(Line *self, unsigned int at, uint32_t ch, unsigned int width, Cursor *cursor, bool UNUSED is_second) {
line_set_char(Line *self, unsigned int at, uint32_t ch, unsigned int width, Cursor *cursor, hyperlink_id_type hyperlink_id) {
GPUCell *g = self->gpu_cells + at;
if (cursor == NULL) {
g->attrs = (self->gpu_cells[at].attrs & ATTRS_MASK_WITHOUT_WIDTH) | width;
@@ -569,22 +599,24 @@ line_set_char(Line *self, unsigned int at, uint32_t ch, unsigned int width, Curs
g->decoration_fg = cursor->decoration_fg & COL_MASK;
}
self->cpu_cells[at].ch = ch;
self->cpu_cells[at].hyperlink_id = hyperlink_id;
memset(self->cpu_cells[at].cc_idx, 0, sizeof(self->cpu_cells[at].cc_idx));
}
static PyObject*
set_char(Line *self, PyObject *args) {
#define set_char_doc "set_char(at, ch, width=1, cursor=None) -> Set the character at the specified cell. If cursor is not None, also set attributes from that cursor."
#define set_char_doc "set_char(at, ch, width=1, cursor=None, hyperlink_id=0) -> Set the character at the specified cell. If cursor is not None, also set attributes from that cursor."
unsigned int at, width=1;
int ch;
Cursor *cursor = NULL;
unsigned int hyperlink_id = 0;
if (!PyArg_ParseTuple(args, "IC|IO!", &at, &ch, &width, &Cursor_Type, &cursor)) return NULL;
if (!PyArg_ParseTuple(args, "IC|IO!I", &at, &ch, &width, &Cursor_Type, &cursor, &hyperlink_id)) return NULL;
if (at >= self->xnum) {
PyErr_SetString(PyExc_ValueError, "Out of bounds");
return NULL;
}
line_set_char(self, at, ch, width, cursor, false);
line_set_char(self, at, ch, width, cursor, hyperlink_id);
Py_RETURN_NONE;
}
@@ -762,21 +794,17 @@ mark_text_in_line(PyObject *marker, Line *line) {
}
PyObject*
as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, index_type columns) {
as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, ANSIBuf *ansibuf) {
PyObject *callback;
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");
PyObject *cr = PyUnicode_FromString("\r");
PyObject *sgr_reset = PyUnicode_FromString("\x1b[m");
if (nl == NULL || cr == NULL || sgr_reset == NULL) goto end;
const GPUCell *prev_cell = NULL;
if (nl == NULL || cr == NULL) goto end;
if (as_ansi) {
buf = malloc(sizeof(Py_UCS4) * columns * 100);
if (buf == NULL) { PyErr_NoMemory(); goto end; }
}
ansibuf->active_hyperlink_id = 0;
for (index_type y = 0; y < lines; y++) {
Line *line = get_line(container, y);
if (!line->continued && y > 0) {
@@ -785,16 +813,15 @@ as_text_generic(PyObject *args, void *container, get_line_func get_line, index_t
Py_CLEAR(ret);
}
if (as_ansi) {
bool truncated;
// less has a bug where it resets colors when it sees a \r, so work
// around it by resetting SGR at the start of every line. This is
// pretty sad performance wise, but I guess it will remain till I
// get around to writing a nice pager kitten.
// see https://github.com/kovidgoyal/kitty/issues/2381
prev_cell = NULL;
index_type num = line_as_ansi(line, buf, columns * 100 - 2, &truncated, &prev_cell);
t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, buf, num);
if (t && num > 0) {
line_as_ansi(line, ansibuf, &prev_cell);
t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, ansibuf->buf, ansibuf->len);
if (t && ansibuf->len > 0) {
ret = PyObject_CallFunctionObjArgs(callback, sgr_reset, NULL);
if (ret == NULL) goto end;
Py_CLEAR(ret);
@@ -811,18 +838,37 @@ as_text_generic(PyObject *args, void *container, get_line_func get_line, index_t
Py_CLEAR(ret);
}
}
if (ansibuf->active_hyperlink_id) {
ansibuf->active_hyperlink_id = 0;
t = PyUnicode_FromString("\x1b]8;;\x1b\\");
if (t) {
ret = PyObject_CallFunctionObjArgs(callback, t, NULL);
Py_CLEAR(t);
Py_CLEAR(ret);
}
}
end:
Py_CLEAR(nl); Py_CLEAR(cr); Py_CLEAR(sgr_reset); free(buf);
Py_CLEAR(nl); Py_CLEAR(cr); Py_CLEAR(sgr_reset);
if (PyErr_Occurred()) return NULL;
Py_RETURN_NONE;
}
// Boilerplate {{{
static PyObject*
copy_char(Line* self, PyObject *args);
#define copy_char_doc "copy_char(src, to, dest) -> Copy the character at src to to the character dest in the line `to`"
#define hyperlink_ids_doc "hyperlink_ids() -> Tuple of hyper link ids at every cell"
static PyObject*
hyperlink_ids(Line *self, PyObject *args UNUSED) {
PyObject *ans = PyTuple_New(self->xnum);
for (index_type x = 0; x < self->xnum; x++) {
PyTuple_SET_ITEM(ans, x, PyLong_FromUnsignedLong(self->cpu_cells[x].hyperlink_id));
}
return ans;
}
static PyObject *
richcmp(PyObject *obj1, PyObject *obj2, int op);
@@ -845,6 +891,7 @@ static PyMethodDef methods[] = {
METHOD(set_attribute, METH_VARARGS)
METHOD(as_ansi, METH_NOARGS)
METHOD(is_continued, METH_NOARGS)
METHOD(hyperlink_ids, METH_NOARGS)
METHOD(width, METH_O)
METHOD(url_start_at, METH_O)
METHOD(url_end_at, METH_VARARGS)

View File

@@ -28,7 +28,7 @@ static inline void
clear_chars_in_line(CPUCell *cpu_cells, GPUCell *gpu_cells, index_type xnum, char_type ch) {
// Clear only the char part of each cell, the rest must have been cleared by a memset or similar
if (ch) {
for (index_type i = 0; i < xnum; i++) { cpu_cells[i].ch = ch; gpu_cells[i].attrs = 1; }
for (index_type i = 0; i < xnum; i++) { cpu_cells[i].ch = ch; cpu_cells[i].hyperlink_id = 0; gpu_cells[i].attrs = 1; }
}
}
@@ -61,6 +61,7 @@ left_shift_line(Line *line, index_type at, index_type num) {
}
if (at < line->xnum && ((line->gpu_cells[at].attrs) & WIDTH_MASK) != 1) {
line->cpu_cells[at].ch = BLANK_CHAR;
line->cpu_cells[at].hyperlink_id = 0;
line->gpu_cells[at].attrs = BLANK_CHAR ? 1 : 0;
clear_sprite_position(line->gpu_cells[at]);
}
@@ -70,13 +71,13 @@ typedef Line*(get_line_func)(void *, int);
void line_clear_text(Line *self, unsigned int at, unsigned int num, char_type ch);
void line_apply_cursor(Line *self, Cursor *cursor, unsigned int at, unsigned int num, bool clear_char);
char_type line_get_char(Line *self, index_type at);
void line_set_char(Line *, unsigned int , uint32_t , unsigned int , Cursor *, bool);
void line_set_char(Line *, unsigned int , uint32_t , unsigned int , Cursor *, hyperlink_id_type);
void line_right_shift(Line *, unsigned int , unsigned int );
void line_add_combining_char(Line *, uint32_t , unsigned int );
index_type line_url_start_at(Line *self, index_type x);
index_type line_url_end_at(Line *self, index_type x, bool, char_type, bool);
bool line_startswith_url_chars(Line*);
index_type line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen, bool*, const GPUCell**) __attribute__((nonnull));
void line_as_ansi(Line *self, ANSIBuf *output, const GPUCell**) __attribute__((nonnull));
unsigned int line_length(Line *self);
size_t cell_as_unicode(CPUCell *cell, bool include_cc, Py_UCS4 *buf, char_type);
size_t cell_as_unicode_for_fallback(CPUCell *cell, Py_UCS4 *buf);
@@ -93,19 +94,20 @@ void linebuf_clear_line(LineBuf *self, index_type y);
void linebuf_insert_lines(LineBuf *self, unsigned int num, unsigned int y, unsigned int bottom);
void linebuf_delete_lines(LineBuf *self, index_type num, index_type y, index_type bottom);
void linebuf_set_attribute(LineBuf *, unsigned int , unsigned int );
void linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *, index_type *, HistoryBuf *, index_type *, index_type *);
void linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *, index_type *, HistoryBuf *, index_type *, index_type *, ANSIBuf*);
void linebuf_mark_line_dirty(LineBuf *self, index_type y);
void linebuf_mark_line_clean(LineBuf *self, index_type y);
void linebuf_mark_line_as_not_continued(LineBuf *self, index_type y);
unsigned int linebuf_char_width_at(LineBuf *self, index_type x, index_type y);
void linebuf_refresh_sprite_positions(LineBuf *self);
void historybuf_add_line(HistoryBuf *self, const Line *line);
void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other);
void historybuf_add_line(HistoryBuf *self, const Line *line, ANSIBuf*);
void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other, ANSIBuf*);
void historybuf_init_line(HistoryBuf *self, index_type num, Line *l);
CPUCell* historybuf_cpu_cells(HistoryBuf *self, index_type num);
void historybuf_mark_line_clean(HistoryBuf *self, index_type y);
void historybuf_mark_line_dirty(HistoryBuf *self, index_type y);
void historybuf_refresh_sprite_positions(HistoryBuf *self);
void historybuf_clear(HistoryBuf *self);
void mark_text_in_line(PyObject *marker, Line *line);
bool line_has_mark(Line *, attrs_type mark);
PyObject* as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, index_type columns);
PyObject* as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, ANSIBuf *ansibuf);

View File

@@ -245,10 +245,8 @@ empty:
error:
Py_XDECREF(py_ret);
if (procargs != NULL)
free(procargs);
if (procenv != NULL)
free(procargs);
free(procargs);
free(procenv);
return NULL;
}

View File

@@ -195,6 +195,14 @@ cell_for_pos(Window *w, unsigned int *x, unsigned int *y, bool *in_left_half_of_
#define HANDLER(name) static inline void name(Window UNUSED *w, int UNUSED button, int UNUSED modifiers, unsigned int UNUSED window_idx)
static inline void
set_mouse_cursor_when_dragging(void) {
if (mouse_cursor_shape != OPT(pointer_shape_when_dragging)) {
mouse_cursor_shape = OPT(pointer_shape_when_dragging);
set_mouse_cursor(mouse_cursor_shape);
}
}
static inline void
update_drag(bool from_button, Window *w, bool is_release, int modifiers) {
Screen *screen = w->render_data.screen;
@@ -202,16 +210,18 @@ update_drag(bool from_button, Window *w, bool is_release, int modifiers) {
if (is_release) {
global_state.active_drag_in_window = 0;
w->last_drag_scroll_at = 0;
if (screen->selection.in_progress)
if (screen->selections.in_progress) {
screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, true, false);
}
}
else {
global_state.active_drag_in_window = w->id;
screen_start_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, modifiers == (int)OPT(rectangle_select_modifiers) || modifiers == ((int)OPT(rectangle_select_modifiers) | (int)OPT(terminal_select_modifiers)), EXTEND_CELL);
}
} else if (screen->selection.in_progress) {
} else if (screen->selections.in_progress) {
screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, false, false);
}
set_mouse_cursor_when_dragging();
}
static inline bool
@@ -249,6 +259,7 @@ extend_selection(Window *w, bool ended) {
if (screen_has_selection(screen)) {
screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, ended, false);
}
set_mouse_cursor_when_dragging();
}
static inline void
@@ -264,7 +275,7 @@ extend_url(Screen *screen, Line *line, index_type *x, index_type *y, char_type s
// we deliberately allow non-continued lines as some programs, like
// mutt split URLs with newlines at line boundaries
index_type new_x = line_url_end_at(line, 0, false, sentinel, next_line_starts_with_url_chars);
if (!new_x) break;
if (!new_x && !line_startswith_url_chars(line)) break;
*y += 1; *x = new_x;
}
}
@@ -294,7 +305,7 @@ get_url_sentinel(Line *line, index_type url_start) {
static inline void
set_mouse_cursor_for_screen(Screen *screen) {
mouse_cursor_shape = screen->modes.mouse_tracking_mode == NO_TRACKING ? BEAM : OPT(pointer_shape_when_grabbed);
mouse_cursor_shape = screen->modes.mouse_tracking_mode == NO_TRACKING ? OPT(default_pointer_shape): OPT(pointer_shape_when_grabbed);
}
static inline void
@@ -302,6 +313,11 @@ detect_url(Screen *screen, unsigned int x, unsigned int y) {
bool has_url = false;
index_type url_start, url_end = 0;
Line *line = screen_visual_line(screen, y);
if (line->cpu_cells[x].hyperlink_id) {
mouse_cursor_shape = HAND;
screen_mark_hyperlink(screen, x, y);
return;
}
char_type sentinel;
if (line) {
url_start = line_url_start_at(line, x);
@@ -331,7 +347,7 @@ detect_url(Screen *screen, unsigned int x, unsigned int y) {
static inline void
handle_mouse_movement_in_kitty(Window *w, int button, bool mouse_cell_changed) {
Screen *screen = w->render_data.screen;
if (screen->selection.in_progress && (button == GLFW_MOUSE_BUTTON_LEFT || button == GLFW_MOUSE_BUTTON_RIGHT)) {
if (screen->selections.in_progress && (button == GLFW_MOUSE_BUTTON_LEFT || button == GLFW_MOUSE_BUTTON_RIGHT)) {
monotonic_t now = monotonic();
if ((now - w->last_drag_scroll_at) >= ms_to_monotonic_t(20ll) || mouse_cell_changed) {
update_drag(false, w, false, 0);
@@ -391,6 +407,7 @@ multi_click(Window *w, unsigned int count) {
screen_start_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, false, mode);
screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, false, true);
}
set_mouse_cursor_when_dragging();
}
static inline double
@@ -685,9 +702,8 @@ scroll_event(double UNUSED xoffset, double yoffset, int flags, int modifiers) {
s = (int)round(pixels) / (int)global_state.callback_os_window->fonts_data->cell_height;
screen->pending_scroll_pixels = pixels - s * (int) global_state.callback_os_window->fonts_data->cell_height;
} else {
if (screen->linebuf == screen->main_linebuf || !screen->modes.mouse_tracking_mode) {
// Only use wheel_scroll_multiplier if we are scrolling kitty scrollback or in mouse
// tracking mode, where the application is responsible for interpreting scroll events
if (!screen->modes.mouse_tracking_mode) {
// Dont use multiplier if we are sending events to the application
yoffset *= OPT(wheel_scroll_multiplier);
} else if (OPT(wheel_scroll_multiplier) < 0) {
// ensure that changing scroll direction still works, even though
@@ -702,20 +718,17 @@ scroll_event(double UNUSED xoffset, double yoffset, int flags, int modifiers) {
}
if (s == 0) return;
bool upwards = s > 0;
if (screen->linebuf == screen->main_linebuf) {
screen_history_scroll(screen, abs(s), upwards);
} else {
if (screen->modes.mouse_tracking_mode) {
int sz = encode_mouse_scroll(w, upwards, modifiers);
if (sz > 0) {
mouse_event_buf[sz] = 0;
for (s = abs(s); s > 0; s--) {
write_escape_code_to_child(screen, CSI, mouse_event_buf);
}
if (screen->modes.mouse_tracking_mode) {
int sz = encode_mouse_scroll(w, upwards, modifiers);
if (sz > 0) {
mouse_event_buf[sz] = 0;
for (s = abs(s); s > 0; s--) {
write_escape_code_to_child(screen, CSI, mouse_event_buf);
}
} else {
fake_scroll(w, abs(s), upwards);
}
} else {
if (screen->linebuf == screen->main_linebuf) screen_history_scroll(screen, abs(s), upwards);
else fake_scroll(w, abs(s), upwards);
}
}

View File

@@ -2,9 +2,14 @@
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
from typing import Dict, Optional
from base64 import standard_b64decode
from collections import OrderedDict
from itertools import count
from typing import Dict, Optional, Callable
from .constants import is_macos, logo_png_file
from .fast_data_types import get_boss
from .utils import log_error
if is_macos:
from .fast_data_types import cocoa_send_notification
@@ -15,9 +20,10 @@ if is_macos:
timeout: int = 5000,
application: str = 'kitty',
icon: bool = True,
identifier: Optional[str] = None
identifier: Optional[str] = None,
subtitle: Optional[str] = None,
) -> None:
cocoa_send_notification(identifier, title, body, None)
cocoa_send_notification(identifier, title, body, subtitle)
else:
@@ -35,7 +41,6 @@ else:
rmap = {v: k for k, v in identifier_map.items()}
identifier = rmap.get(notification_id)
if identifier is not None:
from .boss import notification_activated
notification_activated(identifier)
def notify(
@@ -44,7 +49,8 @@ else:
timeout: int = -1,
application: str = 'kitty',
icon: bool = True,
identifier: Optional[str] = None
identifier: Optional[str] = None,
subtitle: Optional[str] = None,
) -> None:
icf = ''
if icon is True:
@@ -52,3 +58,147 @@ else:
alloc_id = dbus_send_notification(application, icf, title, body, 'Click to see changes', timeout)
if alloc_id and identifier is not None:
alloc_map[alloc_id] = identifier
class NotificationCommand:
done: bool = True
identifier: str = '0'
title: str = ''
body: str = ''
actions: str = ''
def parse_osc_9(raw: str) -> NotificationCommand:
ans = NotificationCommand()
ans.title = raw
return ans
def parse_osc_99(raw: str) -> NotificationCommand:
cmd = NotificationCommand()
metadata, payload = raw.partition(';')[::2]
payload_is_encoded = False
payload_type = 'title'
if metadata:
for part in metadata.split(':'):
try:
k, v = part.split('=', 1)
except Exception:
log_error('Malformed OSC 99: metadata is not key=value pairs')
return cmd
if k == 'p':
payload_type = v
elif k == 'i':
cmd.identifier = v
elif k == 'e':
payload_is_encoded = v == '1'
elif k == 'd':
cmd.done = v != '0'
elif k == 'a':
cmd.actions += ',' + v
if payload_type not in ('body', 'title'):
log_error(f'Malformed OSC 99: unknown payload type: {payload_type}')
return NotificationCommand()
if payload_is_encoded:
try:
payload = standard_b64decode(payload).decode('utf-8')
except Exception:
log_error('Malformed OSC 99: payload is not base64 encoded UTF-8 text')
return NotificationCommand()
if payload_type == 'title':
cmd.title = payload
else:
cmd.body = payload
return cmd
def limit_size(x: str) -> str:
if len(x) > 1024:
x = x[:1024]
return x
def merge_osc_99(prev: NotificationCommand, cmd: NotificationCommand) -> NotificationCommand:
if prev.done or prev.identifier != cmd.identifier:
return cmd
cmd.actions = limit_size(prev.actions + ',' + cmd.actions)
cmd.title = limit_size(prev.title + cmd.title)
cmd.body = limit_size(prev.body + cmd.body)
return cmd
identifier_registry: "OrderedDict[str, RegisteredNotification]" = OrderedDict()
id_counter = count()
class RegisteredNotification:
identifier: str
window_id: int
focus: bool = True
report: bool = False
def __init__(self, cmd: NotificationCommand, window_id: int):
self.window_id = window_id
for x in cmd.actions.strip(',').split(','):
val = not x.startswith('-')
x = x.lstrip('+-')
if x == 'focus':
self.focus = val
elif x == 'report':
self.report = val
self.identifier = cmd.identifier
def register_identifier(identifier: str, cmd: NotificationCommand, window_id: int) -> None:
identifier_registry[identifier] = RegisteredNotification(cmd, window_id)
if len(identifier_registry) > 100:
identifier_registry.popitem(False)
def notification_activated(identifier: str, activated_implementation: Optional[Callable] = None) -> None:
if identifier == 'new-version':
from .update_check import notification_activated as do
do()
elif identifier.startswith('test-notify-'):
log_error(f'Test notification {identifier} activated')
else:
r = identifier_registry.pop(identifier, None)
if r is not None and (r.focus or r.report):
if activated_implementation is None:
get_boss().notification_activated(r.identifier, r.window_id, r.focus, r.report)
else:
activated_implementation(r.identifier, r.window_id, r.focus, r.report)
def reset_registry() -> None:
global id_counter
identifier_registry.clear()
id_counter = count()
def notify_with_command(cmd: NotificationCommand, window_id: int, notify: Callable = notify) -> None:
title = cmd.title or cmd.body
body = cmd.body if cmd.title else ''
if title:
identifier = 'i' + str(next(id_counter))
notify(title, body, identifier=identifier)
register_identifier(identifier, cmd, window_id)
def handle_notification_cmd(
osc_code: int,
raw_data: str,
window_id: int,
prev_cmd: NotificationCommand,
notify_implementation: Callable = notify
) -> Optional[NotificationCommand]:
if osc_code == 99:
cmd = merge_osc_99(prev_cmd, parse_osc_99(raw_data))
if cmd.done:
notify_with_command(cmd, window_id, notify_implementation)
cmd = NotificationCommand()
return cmd
if osc_code == 9:
cmd = parse_osc_9(raw_data)
notify_with_command(cmd, window_id, notify_implementation)

193
kitty/open_actions.py Normal file
View File

@@ -0,0 +1,193 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
import os
import posixpath
from contextlib import suppress
from functools import lru_cache
from typing import (
Any, Generator, Iterable, List, NamedTuple, Optional, Tuple, cast
)
from urllib.parse import ParseResult, unquote, urlparse
from .conf.utils import to_cmdline
from .config import KeyAction, parse_key_action
from .constants import config_dir
from .typing import MatchType
from .utils import expandvars, log_error
from .guess_mime_type import guess_type
class MatchCriteria(NamedTuple):
type: MatchType
value: str
class OpenAction(NamedTuple):
match_criteria: Tuple[MatchCriteria, ...]
actions: Tuple[KeyAction, ...]
def parse(lines: Iterable[str]) -> Generator[OpenAction, None, None]:
match_criteria: List[MatchCriteria] = []
actions: List[KeyAction] = []
for line in lines:
line = line.strip()
if line.startswith('#'):
continue
if not line:
if match_criteria and actions:
yield OpenAction(tuple(match_criteria), tuple(actions))
match_criteria = []
actions = []
continue
parts = line.split(maxsplit=1)
if len(parts) != 2:
continue
key, rest = parts
key = key.lower()
if key == 'action':
with to_cmdline.filter_env_vars('URL', 'FILE_PATH', 'FILE', 'FRAGMENT'):
x = parse_key_action(rest)
if x is not None:
actions.append(x)
elif key in ('mime', 'ext', 'protocol', 'file', 'path', 'url', 'fragment_matches'):
if key != 'url':
rest = rest.lower()
match_criteria.append(MatchCriteria(cast(MatchType, key), rest))
else:
log_error(f'Ignoring malformed open actions line: {line}')
if match_criteria and actions:
yield OpenAction(tuple(match_criteria), tuple(actions))
def url_matches_criterion(purl: 'ParseResult', url: str, unquoted_path: str, mc: MatchCriteria) -> bool:
if mc.type == 'url':
import re
try:
pat = re.compile(mc.value)
except re.error:
return False
return pat.search(unquote(url)) is not None
if mc.type == 'mime':
import fnmatch
mt = guess_type(unquoted_path)
if not mt:
return False
mt = mt.lower()
for mpat in mc.value.split(','):
mpat = mpat.strip()
with suppress(Exception):
if fnmatch.fnmatchcase(mt, mpat):
return True
return False
if mc.type == 'ext':
if not purl.path:
return False
path = unquoted_path.lower()
for ext in mc.value.split(','):
ext = ext.strip()
if path.endswith('.' + ext):
return True
return False
if mc.type == 'protocol':
protocol = (purl.scheme or 'file').lower()
for key in mc.value.split(','):
if key.strip() == protocol:
return True
return False
if mc.type == 'fragment_matches':
import re
try:
pat = re.compile(mc.value)
except re.error:
return False
return pat.search(unquote(purl.fragment)) is not None
if mc.type == 'path':
import fnmatch
try:
return fnmatch.fnmatchcase(unquoted_path.lower(), mc.value)
except Exception:
return False
if mc.type == 'file':
import fnmatch
import posixpath
try:
fname = posixpath.basename(unquoted_path)
except Exception:
return False
try:
return fnmatch.fnmatchcase(fname.lower(), mc.value)
except Exception:
return False
def url_matches_criteria(purl: 'ParseResult', url: str, unquoted_path: str, criteria: Iterable[MatchCriteria]) -> bool:
for x in criteria:
try:
if not url_matches_criterion(purl, url, unquoted_path, x):
return False
except Exception:
return False
return True
def actions_for_url_from_list(url: str, actions: Iterable[OpenAction]) -> Generator[KeyAction, None, None]:
try:
purl = urlparse(url)
except Exception:
return
path = unquote(purl.path)
env = {
'URL': url,
'FILE_PATH': path,
'FILE': posixpath.basename(path),
'FRAGMENT': unquote(purl.fragment)
}
def expand(x: Any) -> Any:
as_bytes = isinstance(x, bytes)
if as_bytes:
x = x.decode('utf-8')
if isinstance(x, str):
ans = expandvars(x, env, fallback_to_os_env=False)
if as_bytes:
return ans.encode('utf-8')
return ans
return x
for action in actions:
if url_matches_criteria(purl, url, path, action.match_criteria):
for ac in action.actions:
yield ac._replace(args=tuple(map(expand, ac.args)))
return
@lru_cache(maxsize=2)
def load_open_actions() -> Tuple[OpenAction, ...]:
try:
f = open(os.path.join(config_dir, 'open-actions.conf'))
except FileNotFoundError:
return ()
with f:
return tuple(parse(f))
def actions_for_url(url: str, actions_spec: Optional[str] = None) -> Generator[KeyAction, None, None]:
if actions_spec is None:
actions = load_open_actions()
else:
actions = tuple(parse(actions_spec.splitlines()))
yield from actions_for_url_from_list(url, actions)

View File

@@ -4,6 +4,9 @@
* Distributed under terms of the GPL3 license.
*/
// Need _POSIX_C_SOURCE for strtok_r
#define _POSIX_C_SOURCE 200809L
#include "data-types.h"
#include "control-codes.h"
#include "screen.h"
@@ -121,6 +124,9 @@ _report_params(PyObject *dump_callback, const char *name, unsigned int *params,
#define REPORT_OSC2(name, code, string) \
Py_XDECREF(PyObject_CallFunction(dump_callback, "sIO", #name, code, string)); PyErr_Clear();
#define REPORT_HYPERLINK(id, url) \
Py_XDECREF(PyObject_CallFunction(dump_callback, "szz", "set_active_hyperlink", id, url)); PyErr_Clear();
#else
#define DUMP_UNUSED UNUSED
@@ -134,6 +140,7 @@ _report_params(PyObject *dump_callback, const char *name, unsigned int *params,
#define FLUSH_DRAW
#define REPORT_OSC(name, string)
#define REPORT_OSC2(name, code, string)
#define REPORT_HYPERLINK(id, url)
#endif
@@ -301,10 +308,58 @@ handle_esc_mode_char(Screen *screen, uint32_t ch, PyObject DUMP_UNUSED *dump_cal
} // }}}
// OSC mode {{{
static inline bool
parse_osc_8(char *buf, char **id, char **url) {
char *boundary = strstr(buf, ";");
if (boundary == NULL) return false;
*boundary = 0;
if (*(boundary + 1)) *url = boundary + 1;
char *save, *token = strtok_r(buf, ":", &save);
while (token != NULL) {
size_t len = strlen(token);
if (len > 3 && token[0] == 'i' && token[1] == 'd' && token[2] == '=' && token[3]) {
*id = token + 3;
break;
}
token = strtok_r(NULL, ":", &save);
}
return true;
}
static inline void
dispatch_hyperlink(Screen *screen, size_t pos, size_t size, PyObject DUMP_UNUSED *dump_callback) {
// since the spec says only ASCII printable chars are allowed in OSC 8, we
// can just convert to char* directly
if (!size) return; // ignore empty OSC 8 since it must have two semi-colons to be valid, which means one semi-colon here
char *id = NULL, *url = NULL;
char *data = malloc(size + 1);
if (!data) fatal("Out of memory");
for (size_t i = 0; i < size; i++) {
data[i] = screen->parser_buf[i + pos] & 0x7f;
if (data[i] < 32 || data[i] > 126) data[i] = '_';
}
data[size] = 0;
if (parse_osc_8(data, &id, &url)) {
REPORT_HYPERLINK(id, url);
set_active_hyperlink(screen, id, url);
} else {
REPORT_ERROR("Ignoring malformed OSC 8 code");
}
free(data);
}
static inline void
dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
#define DISPATCH_OSC_WITH_CODE(name) REPORT_OSC2(name, code, string); name(screen, code, string);
#define DISPATCH_OSC(name) REPORT_OSC(name, string); name(screen, string);
#define SET_COLOR(name) REPORT_OSC2(name, code, string); name(screen, code, string);
#define START_DISPATCH {\
PyObject *string = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, screen->parser_buf + i, limit - i); \
if (string) {
#define END_DISPATCH Py_CLEAR(string); } PyErr_Clear(); break; }
const unsigned int limit = screen->parser_buf_pos;
unsigned int code=0, i;
for (i = 0; i < MIN(limit, 5u); i++) {
@@ -314,54 +369,66 @@ dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
code = utoi(screen->parser_buf, i);
if (i < limit && screen->parser_buf[i] == ';') i++;
}
PyObject *string = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, screen->parser_buf + i, limit - i);
if (string != NULL) {
switch(code) {
case 0:
DISPATCH_OSC(set_title);
DISPATCH_OSC(set_icon);
break;
case 1:
DISPATCH_OSC(set_icon);
break;
case 2:
DISPATCH_OSC(set_title);
break;
case 4:
case 104:
SET_COLOR(set_color_table_color);
break;
case 10:
case 11:
case 12:
case 17:
case 19:
case 110:
case 111:
case 112:
case 117:
case 119:
SET_COLOR(set_dynamic_color);
break;
case 52:
DISPATCH_OSC(clipboard_control);
break;
case 30001:
REPORT_COMMAND(screen_push_dynamic_colors);
screen_push_dynamic_colors(screen);
break;
case 30101:
REPORT_COMMAND(screen_pop_dynamic_colors);
screen_pop_dynamic_colors(screen);
break;
default:
REPORT_ERROR("Unknown OSC code: %u", code);
break;
}
Py_CLEAR(string);
switch(code) {
case 0:
START_DISPATCH
DISPATCH_OSC(set_title);
DISPATCH_OSC(set_icon);
END_DISPATCH
case 1:
START_DISPATCH
DISPATCH_OSC(set_icon);
END_DISPATCH
case 2:
START_DISPATCH
DISPATCH_OSC(set_title);
END_DISPATCH
case 4:
case 104:
START_DISPATCH
DISPATCH_OSC_WITH_CODE(set_color_table_color);
END_DISPATCH
case 8:
dispatch_hyperlink(screen, i, limit-i, dump_callback);
break;
case 9:
case 99:
START_DISPATCH
DISPATCH_OSC_WITH_CODE(desktop_notify)
END_DISPATCH
case 10:
case 11:
case 12:
case 17:
case 19:
case 110:
case 111:
case 112:
case 117:
case 119:
START_DISPATCH
DISPATCH_OSC_WITH_CODE(set_dynamic_color);
END_DISPATCH
case 52:
START_DISPATCH
DISPATCH_OSC(clipboard_control);
END_DISPATCH
case 30001:
REPORT_COMMAND(screen_push_dynamic_colors);
screen_push_dynamic_colors(screen);
break;
case 30101:
REPORT_COMMAND(screen_pop_dynamic_colors);
screen_pop_dynamic_colors(screen);
break;
default:
REPORT_ERROR("Unknown OSC code: %u", code);
break;
}
#undef DISPATCH_OSC
#undef SET_COLOR
#undef DISPATCH_OSC_WITH_CODE
#undef START_DISPATCH
#undef END_DISPATCH
}
// }}}

View File

@@ -6,8 +6,11 @@
*/
#include "png-reader.h"
#include "state.h"
#include <lcms2.h>
static cmsHPROFILE srgb_profile = NULL;
struct fake_file { const uint8_t *buf; size_t sz, cur; };
static void
@@ -35,8 +38,8 @@ read_png_error_handler(png_structp png_ptr, png_const_charp msg) {
}
static void
read_png_warn_handler(png_structp UNUSED png_ptr, png_const_charp UNUSED msg) {
// ignore warnings
read_png_warn_handler(png_structp UNUSED png_ptr, png_const_charp msg) {
if (global_state.debug_rendering) log_error("libpng WARNING: %s", msg);
}
#define ABRT(code, msg) { if(d->err_handler) d->err_handler(#code, msg); goto err; }
@@ -61,6 +64,33 @@ inflate_png_inner(png_read_data *d, const uint8_t *buf, size_t bufsz) {
d->height = png_get_image_height(png, info);
color_type = png_get_color_type(png, info);
bit_depth = png_get_bit_depth(png, info);
double image_gamma;
int intent;
cmsHPROFILE input_profile = NULL;
cmsHTRANSFORM colorspace_transform = NULL;
if (png_get_sRGB(png, info, &intent)) {
// do nothing since we output sRGB
} else if (png_get_gAMA(png, info, &image_gamma)) {
if (image_gamma != 0 && fabs(image_gamma - 1.0/2.2) > 0.0001) png_set_gamma(png, 2.2, image_gamma);
} else {
// Look for an embedded color profile
png_charp name;
int compression_type;
png_bytep profdata;
png_uint_32 proflen;
if (png_get_iCCP(png, info, &name, &compression_type, &profdata, &proflen) & PNG_INFO_iCCP) {
input_profile = cmsOpenProfileFromMem(profdata, proflen);
if (input_profile) {
if (!srgb_profile) {
srgb_profile = cmsCreate_sRGBProfile();
if (!srgb_profile) ABRT(ENOMEM, "Out of memory allocating sRGB colorspace profile");
}
colorspace_transform = cmsCreateTransform(
input_profile, TYPE_RGBA_8, srgb_profile, TYPE_RGBA_8, INTENT_PERCEPTUAL, 0);
}
}
}
// Ensure we get RGBA data out of libpng
if (bit_depth == 16) png_set_strip_16(png);
@@ -76,15 +106,23 @@ inflate_png_inner(png_read_data *d, const uint8_t *buf, size_t bufsz) {
if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) png_set_gray_to_rgb(png);
png_read_update_info(png, info);
int rowbytes = png_get_rowbytes(png, info);
png_uint_32 rowbytes = png_get_rowbytes(png, info);
d->sz = sizeof(png_byte) * rowbytes * d->height;
d->decompressed = malloc(d->sz + 16);
if (d->decompressed == NULL) ABRT(ENOMEM, "Out of memory allocating decompression buffer for PNG");
d->row_pointers = malloc(d->height * sizeof(png_bytep));
if (d->row_pointers == NULL) ABRT(ENOMEM, "Out of memory allocating row_pointers buffer for PNG");
for (int i = 0; i < d->height; i++) d->row_pointers[i] = d->decompressed + i * rowbytes;
for (size_t i = 0; i < (size_t)d->height; i++) d->row_pointers[i] = d->decompressed + i * rowbytes * sizeof(png_byte);
png_read_image(png, d->row_pointers);
if (colorspace_transform) {
for (int i = 0; i < d->height; i++) {
cmsDoTransform(colorspace_transform, d->row_pointers[i], d->row_pointers[i], d->width);
}
cmsDeleteTransform(colorspace_transform);
}
if (input_profile) cmsCloseProfile(input_profile);
d->ok = true;
err:
if (png) png_destroy_read_struct(&png, info ? &info : NULL, NULL);
@@ -119,8 +157,20 @@ static PyMethodDef module_methods[] = {
{NULL, NULL, 0, NULL} /* Sentinel */
};
static void
unload(void) {
if (srgb_profile) cmsCloseProfile(srgb_profile);
srgb_profile = NULL;
}
bool
init_png_reader(PyObject *module) {
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
if (Py_AtExit(unload) != 0) {
PyErr_SetString(PyExc_RuntimeError, "Failed to register the PNG library at exit handler");
return false;
}
return true;
}

View File

@@ -29,7 +29,8 @@
linebuf_index(dest, 0, dest->ynum - 1); \
if (historybuf != NULL) { \
init_dest_line(dest->ynum - 1); \
historybuf_add_line(historybuf, dest->line); \
dest->line->has_dirty_text = true; \
historybuf_add_line(historybuf, dest->line, as_ansi_buf); \
}\
linebuf_clear_line(dest, dest->ynum - 1); \
} else dest_y++; \
@@ -49,7 +50,7 @@ copy_range(Line *src, index_type src_at, Line* dest, index_type dest_at, index_t
static void
rewrap_inner(BufType *src, BufType *dest, const index_type src_limit, HistoryBuf UNUSED *historybuf, index_type *track_x, index_type *track_y) {
rewrap_inner(BufType *src, BufType *dest, const index_type src_limit, HistoryBuf UNUSED *historybuf, index_type *track_x, index_type *track_y, ANSIBuf *as_ansi_buf) {
bool src_line_is_continued = false;
index_type src_y = 0, src_x = 0, dest_x = 0, dest_y = 0, num = 0, src_x_limit = 0;

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