Compare commits

...

188 Commits

Author SHA1 Message Date
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
111 changed files with 43544 additions and 29301 deletions

View File

@@ -5,6 +5,7 @@
import io
import os
import shlex
import shutil
import subprocess
import sys
import tarfile
@@ -35,15 +36,15 @@ 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')
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 +72,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 +86,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

@@ -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,19 @@ 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>`_.
Note for Linux/macOS packagers

View File

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

@@ -232,3 +232,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

@@ -54,27 +54,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 +161,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

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

91
docs/open_actions.rst Normal file
View File

@@ -0,0 +1,91 @@
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``
``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.

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)

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

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;
}
}

20
glfw/x11_window.c vendored
View File

@@ -422,14 +422,16 @@ 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 +773,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 +825,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 +956,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 +1522,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

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

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

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'}

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

@@ -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
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,7 @@ class CLIOptions:
LaunchCLIOptions = AskCLIOptions = ClipboardCLIOptions = DiffCLIOptions = CLIOptions
HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions
ErrorCLIOptions = UnicodeCLIOptions = RCOptions = CLIOptions
ErrorCLIOptions = UnicodeCLIOptions = RCOptions = RemoteFileCLIOptions = CLIOptions
def generate_stub() -> None:
@@ -35,6 +35,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')

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,24 +176,12 @@ 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); \
@@ -203,6 +197,111 @@ 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
@@ -497,8 +596,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':

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
@@ -668,9 +674,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 +887,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 +1148,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 +1487,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, 1)
str_version: str = '.'.join(map(str, version))
_plat = sys.platform.lower()
is_macos: bool = 'darwin' in _plat

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

@@ -461,7 +461,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 +519,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
@@ -945,7 +944,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 +1067,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) {
@@ -600,6 +601,8 @@ START_ALLOW_CASE_RANGE
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 +640,12 @@ 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:
return 0x151 + ch - 0x1fba0;
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,80 @@ 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 = 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 = 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)
box_chars: Dict[str, List[Callable]] = {
'': [hline],
'': [p(hline, level=3)],
@@ -649,28 +752,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 +784,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
@@ -710,6 +909,18 @@ for chars, func_ in (('╒╕╘╛', dvcorner), ('╓╖╙╜', dhcorner), ('
box_chars[ch] = [p(cast(Callable, func_), which=ch)]
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
_dpi = 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

@@ -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_RawMalloc(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);

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

@@ -202,14 +202,15 @@ 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);
}
}
@@ -302,6 +303,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 +337,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);
@@ -685,9 +691,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 +707,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)

190
kitty/open_actions.py Normal file
View File

@@ -0,0 +1,190 @@
#!/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
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
from mimetypes import guess_type
try:
mt = guess_type(unquoted_path)[0]
except Exception:
return False
if mt is None:
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:
if isinstance(x, str):
return expandvars(x, env, fallback_to_os_env=False)
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;

View File

@@ -13,7 +13,7 @@
#include "state.h"
#include "fonts.h"
#include "lineops.h"
#include "screen.h"
#include "hyperlink.h"
#include <structmember.h>
#include <limits.h>
#include <sys/types.h>
@@ -26,12 +26,18 @@
#include "charsets.h"
static const ScreenModes empty_modes = {0, .mDECAWM=true, .mDECTCEM=true, .mDECARM=true};
static Selection EMPTY_SELECTION = {{0}};
#define CSI_REP_MAX_REPETITIONS 65535u
// Constructor/destructor {{{
static inline void
clear_selection(Selections *selections) {
selections->in_progress = false;
selections->extend_mode = EXTEND_CELL;
selections->count = 0;
}
static inline void
init_tabstops(bool *tabstops, index_type count) {
// In terminfo we specify the number of initial tabstops (it) as 8
@@ -111,6 +117,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
self->historybuf = alloc_historybuf(MAX(scrollback, lines), columns, OPT(scrollback_pager_history_size));
self->main_grman = grman_alloc();
self->alt_grman = grman_alloc();
self->active_hyperlink_id = 0;
self->grman = self->main_grman;
self->pending_mode.wait_time = s_double_to_monotonic_t(2.0);
@@ -128,6 +135,9 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
init_tabstops(self->main_tabstops, self->columns);
init_tabstops(self->alt_tabstops, self->columns);
if (!init_overlay_line(self, self->columns)) { Py_CLEAR(self); return NULL; }
self->hyperlink_pool = alloc_hyperlink_pool();
if (!self->hyperlink_pool) { Py_CLEAR(self); return PyErr_NoMemory(); }
self->as_ansi_buf.hyperlink_pool = self->hyperlink_pool;
}
return (PyObject*) self;
}
@@ -141,8 +151,10 @@ screen_reset(Screen *self) {
if (self->overlay_line.is_active) deactivate_overlay_line(self);
linebuf_clear(self->linebuf, BLANK_CHAR);
historybuf_clear(self->historybuf);
clear_hyperlink_pool(self->hyperlink_pool);
grman_clear(self->grman, false, self->cell_size);
self->modes = empty_modes;
self->active_hyperlink_id = 0;
#define R(name) self->color_profile->overridden.name = 0
R(default_fg); R(default_bg); R(cursor_color); R(highlight_fg); R(highlight_bg);
#undef R
@@ -153,7 +165,8 @@ screen_reset(Screen *self) {
init_tabstops(self->alt_tabstops, self->columns);
cursor_reset(self->cursor);
self->is_dirty = true;
self->selection = EMPTY_SELECTION;
clear_selection(&self->selections);
clear_selection(&self->url_ranges);
screen_cursor_position(self, 1, 1);
set_dynamic_color(self, 110, NULL);
set_dynamic_color(self, 111, NULL);
@@ -175,19 +188,19 @@ screen_dirty_sprite_positions(Screen *self) {
}
static inline HistoryBuf*
realloc_hb(HistoryBuf *old, unsigned int lines, unsigned int columns) {
realloc_hb(HistoryBuf *old, unsigned int lines, unsigned int columns, ANSIBuf *as_ansi_buf) {
HistoryBuf *ans = alloc_historybuf(lines, columns, 0);
if (ans == NULL) { PyErr_NoMemory(); return NULL; }
ans->pagerhist = old->pagerhist; old->pagerhist = NULL;
historybuf_rewrap(old, ans);
historybuf_rewrap(old, ans, as_ansi_buf);
return ans;
}
static inline LineBuf*
realloc_lb(LineBuf *old, unsigned int lines, unsigned int columns, index_type *nclb, index_type *ncla, HistoryBuf *hb, index_type *x, index_type *y) {
realloc_lb(LineBuf *old, unsigned int lines, unsigned int columns, index_type *nclb, index_type *ncla, HistoryBuf *hb, index_type *x, index_type *y, ANSIBuf *as_ansi_buf) {
LineBuf *ans = alloc_linebuf(lines, columns);
if (ans == NULL) { PyErr_NoMemory(); return NULL; }
linebuf_rewrap(old, ans, nclb, ncla, hb, x, y);
linebuf_rewrap(old, ans, nclb, ncla, hb, x, y, as_ansi_buf);
return ans;
}
@@ -209,11 +222,11 @@ screen_resize(Screen *self, unsigned int lines, unsigned int columns) {
if (!init_overlay_line(self, columns)) return false;
// Resize main linebuf
HistoryBuf *nh = realloc_hb(self->historybuf, self->historybuf->ynum, columns);
HistoryBuf *nh = realloc_hb(self->historybuf, self->historybuf->ynum, columns, &self->as_ansi_buf);
if (nh == NULL) return false;
Py_CLEAR(self->historybuf); self->historybuf = nh;
index_type x = self->cursor->x, y = self->cursor->y;
LineBuf *n = realloc_lb(self->main_linebuf, lines, columns, &num_content_lines_before, &num_content_lines_after, self->historybuf, &x, &y);
LineBuf *n = realloc_lb(self->main_linebuf, lines, columns, &num_content_lines_before, &num_content_lines_after, self->historybuf, &x, &y, &self->as_ansi_buf);
if (n == NULL) return false;
Py_CLEAR(self->main_linebuf); self->main_linebuf = n;
if (is_main) setup_cursor();
@@ -221,7 +234,7 @@ screen_resize(Screen *self, unsigned int lines, unsigned int columns) {
// Resize alt linebuf
x = self->cursor->x, y = self->cursor->y;
n = realloc_lb(self->alt_linebuf, lines, columns, &num_content_lines_before, &num_content_lines_after, NULL, &x, &y);
n = realloc_lb(self->alt_linebuf, lines, columns, &num_content_lines_before, &num_content_lines_after, NULL, &x, &y, &self->as_ansi_buf);
if (n == NULL) return false;
Py_CLEAR(self->alt_linebuf); self->alt_linebuf = n;
if (!is_main) setup_cursor();
@@ -241,8 +254,8 @@ screen_resize(Screen *self, unsigned int lines, unsigned int columns) {
init_tabstops(self->main_tabstops, self->columns);
init_tabstops(self->alt_tabstops, self->columns);
self->is_dirty = true;
self->selection = EMPTY_SELECTION;
self->url_range = EMPTY_SELECTION;
clear_selection(&self->selections);
clear_selection(&self->url_ranges);
/* printf("old_cursor: (%u, %u) new_cursor: (%u, %u) beyond_content: %d\n", self->cursor->x, self->cursor->y, cursor_x, cursor_y, cursor_is_beyond_content); */
self->cursor->x = MIN(cursor_x, self->columns - 1);
self->cursor->y = MIN(cursor_y, self->lines - 1);
@@ -287,6 +300,10 @@ dealloc(Screen* self) {
PyMem_Free(self->overlay_line.gpu_cells);
PyMem_Free(self->main_tabstops);
free(self->pending_mode.buf);
free(self->selections.items);
free(self->url_ranges.items);
free_hyperlink_pool(self->hyperlink_pool);
free(self->as_ansi_buf.buf);
Py_TYPE(self)->tp_free((PyObject*)self);
} // }}}
@@ -346,19 +363,59 @@ move_widened_char(Screen *self, CPUCell* cpu_cell, GPUCell *gpu_cell, index_type
}
static inline bool
is_selection_empty(Selection *s) {
is_selection_empty(const Selection *s) {
int start_y = (int)s->start.y - (int)s->start_scrolled_by, end_y = (int)s->end.y - (int)s->end_scrolled_by;
return s->start.x == s->end.x && s->start.in_left_half_of_cell == s->end.in_left_half_of_cell && start_y == end_y;
}
static inline bool
selection_has_screen_line(Selection *s, int y) {
if (is_selection_empty(s)) return false;
int top = (int)s->start.y - s->start_scrolled_by;
int bottom = (int)s->end.y - s->end_scrolled_by;
return top <= y && y <= bottom;
selection_has_screen_line(const Selections *selections, const int y) {
for (size_t i = 0; i < selections->count; i++) {
const Selection *s = selections->items + i;
if (!is_selection_empty(s)) {
int top = (int)s->start.y - s->start_scrolled_by;
int bottom = (int)s->end.y - s->end_scrolled_by;
if (top <= y && y <= bottom) return true;
}
}
return false;
}
void
set_active_hyperlink(Screen *self, char *id, char *url) {
if (OPT(allow_hyperlinks)) {
if (!url || !url[0]) {
self->active_hyperlink_id = 0;
return;
}
self->active_hyperlink_id = get_id_for_hyperlink(self, id, url);
}
}
hyperlink_id_type
remap_hyperlink_ids(Screen *self, hyperlink_id_type *map) {
#define PROCESS_CELL(cell) { hid = (cell).hyperlink_id; if (hid) { if (!map[hid]) map[hid] = ++num; (cell).hyperlink_id = map[hid]; }}
hyperlink_id_type num = 0, hid;
if (self->historybuf->count) {
for (index_type y = self->historybuf->count; y-- > 0;) {
CPUCell *cells = historybuf_cpu_cells(self->historybuf, y);
for (index_type x = 0; x < self->historybuf->xnum; x++) {
PROCESS_CELL(cells[x]);
}
}
}
LineBuf *second = self->linebuf, *first = second == self->main_linebuf ? self->alt_linebuf : self->main_linebuf;
for (index_type i = 0; i < self->lines * self->columns; i++) {
PROCESS_CELL(first->cpu_cell_buf[i]);
}
for (index_type i = 0; i < self->lines * self->columns; i++) {
PROCESS_CELL(second->cpu_cell_buf[i]);
}
return num;
#undef PROCESS_CELL
}
static inline bool is_flag_pair(char_type a, char_type b) {
return is_flag_codepoint(a) && is_flag_codepoint(b);
}
@@ -379,7 +436,7 @@ draw_second_flag_codepoint(Screen *self, char_type ch) {
if (!is_flag_pair(cell->ch, ch) || cell->cc_idx[0]) return false;
line_add_combining_char(self->linebuf->line, ch, xpos);
self->is_dirty = true;
if (selection_has_screen_line(&self->selection, ypos)) self->selection = EMPTY_SELECTION;
if (selection_has_screen_line(&self->selections, ypos)) clear_selection(&self->selections);
linebuf_mark_line_dirty(self->linebuf, ypos);
return true;
}
@@ -413,13 +470,13 @@ draw_combining_char(Screen *self, char_type ch) {
if (has_prev_char) {
line_add_combining_char(self->linebuf->line, ch, xpos);
self->is_dirty = true;
if (selection_has_screen_line(&self->selection, ypos)) self->selection = EMPTY_SELECTION;
if (selection_has_screen_line(&self->selections, ypos)) clear_selection(&self->selections);
linebuf_mark_line_dirty(self->linebuf, ypos);
if (ch == 0xfe0f) { // emoji presentation variation marker makes default text presentation emoji (narrow emoji) into wide emoji
CPUCell *cpu_cell = self->linebuf->line->cpu_cells + xpos;
GPUCell *gpu_cell = self->linebuf->line->gpu_cells + xpos;
if ((gpu_cell->attrs & WIDTH_MASK) != 2 && cpu_cell->cc_idx[0] == VS16 && is_emoji_presentation_base(cpu_cell->ch)) {
if (self->cursor->x <= self->columns - 1) line_set_char(self->linebuf->line, self->cursor->x, 0, 0, self->cursor, true);
if (self->cursor->x <= self->columns - 1) line_set_char(self->linebuf->line, self->cursor->x, 0, 0, self->cursor, self->active_hyperlink_id);
gpu_cell->attrs = (gpu_cell->attrs & !WIDTH_MASK) | 2;
if (xpos == self->columns - 1) move_widened_char(self, cpu_cell, gpu_cell, xpos, ypos);
else self->cursor->x++;
@@ -444,6 +501,9 @@ draw_combining_char(Screen *self, char_type ch) {
void
screen_draw(Screen *self, uint32_t och) {
if (is_ignored_char(och)) return;
if (!self->has_activity_since_last_focus && !self->has_focus) {
self->has_activity_since_last_focus = true;
}
uint32_t ch = och < 256 ? self->g_charset[och] : och;
bool is_cc = is_combining_char(ch);
if (UNLIKELY(is_cc)) {
@@ -473,14 +533,14 @@ screen_draw(Screen *self, uint32_t och) {
if (self->modes.mIRM) {
line_right_shift(self->linebuf->line, self->cursor->x, char_width);
}
line_set_char(self->linebuf->line, self->cursor->x, ch, char_width, self->cursor, false);
line_set_char(self->linebuf->line, self->cursor->x, ch, char_width, self->cursor, self->active_hyperlink_id);
self->cursor->x++;
if (char_width == 2) {
line_set_char(self->linebuf->line, self->cursor->x, 0, 0, self->cursor, true);
line_set_char(self->linebuf->line, self->cursor->x, 0, 0, self->cursor, self->active_hyperlink_id);
self->cursor->x++;
}
self->is_dirty = true;
if (selection_has_screen_line(&self->selection, self->cursor->y)) self->selection = EMPTY_SELECTION;
if (selection_has_screen_line(&self->selections, self->cursor->y)) clear_selection(&self->selections);
linebuf_mark_line_dirty(self->linebuf, self->cursor->y);
}
@@ -643,6 +703,7 @@ screen_handle_graphics_command(Screen *self, const GraphicsCommand *cmd, const u
void
screen_toggle_screen_buffer(Screen *self, bool save_cursor, bool clear_alt_screen) {
bool to_alt = self->linebuf == self->main_linebuf;
self->active_hyperlink_id = 0;
grman_clear(self->alt_grman, true, self->cell_size); // always clear the alt buffer graphics to free up resources, since it has to be cleared when switching back to it anyway
if (to_alt) {
if (clear_alt_screen) linebuf_clear(self->alt_linebuf, BLANK_CHAR);
@@ -660,7 +721,7 @@ screen_toggle_screen_buffer(Screen *self, bool save_cursor, bool clear_alt_scree
}
screen_history_scroll(self, SCROLL_FULL, false);
self->is_dirty = true;
self->selection = EMPTY_SELECTION;
clear_selection(&self->selections);
}
void screen_normal_keypad_mode(Screen UNUSED *self) {} // Not implemented as this is handled by the GUI
@@ -904,18 +965,22 @@ screen_cursor_to_column(Screen *self, unsigned int column) {
}
static inline void
index_selection(Screen *self, Selection *s, bool up) {
if (is_selection_empty(s)) return;
if (up) {
if (s->start.y == 0) s->start_scrolled_by += 1;
else s->start.y--;
if (s->end.y == 0) s->end_scrolled_by += 1;
else s->end.y--;
} else {
if (s->start.y >= self->lines - 1) s->start_scrolled_by -= 1;
else s->start.y++;
if (s->end.y >= self->lines - 1) s->end_scrolled_by -= 1;
else s->end.y++;
index_selection(const Screen *self, Selections *selections, bool up) {
for (size_t i = 0; i < selections->count; i++) {
Selection *s = selections->items + i;
if (!is_selection_empty(s)) {
if (up) {
if (s->start.y == 0) s->start_scrolled_by += 1;
else s->start.y--;
if (s->end.y == 0) s->end_scrolled_by += 1;
else s->end.y--;
} else {
if (s->start.y >= self->lines - 1) s->start_scrolled_by -= 1;
else s->start.y++;
if (s->end.y >= self->lines - 1) s->end_scrolled_by -= 1;
else s->end.y++;
}
}
}
}
@@ -935,12 +1000,12 @@ index_selection(Screen *self, Selection *s, bool up) {
if (self->linebuf == self->main_linebuf && bottom == self->lines - 1) { \
/* Only add to history when no page margins have been set */ \
linebuf_init_line(self->linebuf, bottom); \
historybuf_add_line(self->historybuf, self->linebuf->line); \
historybuf_add_line(self->historybuf, self->linebuf->line, &self->as_ansi_buf); \
self->history_line_added_count++; \
} \
linebuf_clear_line(self->linebuf, bottom); \
self->is_dirty = true; \
index_selection(self, &self->selection, true);
index_selection(self, &self->selections, true);
void
screen_index(Screen *self) {
@@ -967,7 +1032,7 @@ screen_scroll(Screen *self, unsigned int count) {
linebuf_clear_line(self->linebuf, top); \
INDEX_GRAPHICS(1) \
self->is_dirty = true; \
index_selection(self, &self->selection, false);
index_selection(self, &self->selections, false);
void
screen_reverse_index(Screen *self) {
@@ -1151,7 +1216,7 @@ screen_erase_in_line(Screen *self, unsigned int how, bool private) {
line_apply_cursor(self->linebuf->line, self->cursor, s, n, true);
}
self->is_dirty = true;
if (selection_has_screen_line(&self->selection, self->cursor->y)) self->selection = EMPTY_SELECTION;
if (selection_has_screen_line(&self->selections, self->cursor->y)) clear_selection(&self->selections);
linebuf_mark_line_dirty(self->linebuf, self->cursor->y);
}
}
@@ -1196,7 +1261,7 @@ screen_erase_in_display(Screen *self, unsigned int how, bool private) {
linebuf_mark_line_as_not_continued(self->linebuf, i);
}
self->is_dirty = true;
self->selection = EMPTY_SELECTION;
clear_selection(&self->selections);
}
if (how != 2) {
screen_erase_in_line(self, how, private);
@@ -1218,7 +1283,7 @@ screen_insert_lines(Screen *self, unsigned int count) {
if (top <= self->cursor->y && self->cursor->y <= bottom) {
linebuf_insert_lines(self->linebuf, count, self->cursor->y, bottom);
self->is_dirty = true;
self->selection = EMPTY_SELECTION;
clear_selection(&self->selections);
screen_carriage_return(self);
}
}
@@ -1239,7 +1304,7 @@ screen_delete_lines(Screen *self, unsigned int count) {
if (top <= self->cursor->y && self->cursor->y <= bottom) {
linebuf_delete_lines(self->linebuf, count, self->cursor->y, bottom);
self->is_dirty = true;
self->selection = EMPTY_SELECTION;
clear_selection(&self->selections);
screen_carriage_return(self);
}
}
@@ -1256,7 +1321,7 @@ screen_insert_characters(Screen *self, unsigned int count) {
line_apply_cursor(self->linebuf->line, self->cursor, x, num, true);
linebuf_mark_line_dirty(self->linebuf, self->cursor->y);
self->is_dirty = true;
if (selection_has_screen_line(&self->selection, self->cursor->y)) self->selection = EMPTY_SELECTION;
if (selection_has_screen_line(&self->selections, self->cursor->y)) clear_selection(&self->selections);
}
}
@@ -1294,7 +1359,7 @@ screen_delete_characters(Screen *self, unsigned int count) {
line_apply_cursor(self->linebuf->line, self->cursor, self->columns - num, num, true);
linebuf_mark_line_dirty(self->linebuf, self->cursor->y);
self->is_dirty = true;
if (selection_has_screen_line(&self->selection, self->cursor->y)) self->selection = EMPTY_SELECTION;
if (selection_has_screen_line(&self->selections, self->cursor->y)) clear_selection(&self->selections);
}
}
@@ -1308,7 +1373,7 @@ screen_erase_characters(Screen *self, unsigned int count) {
line_apply_cursor(self->linebuf->line, self->cursor, x, num, true);
linebuf_mark_line_dirty(self->linebuf, self->cursor->y);
self->is_dirty = true;
if (selection_has_screen_line(&self->selection, self->cursor->y)) self->selection = EMPTY_SELECTION;
if (selection_has_screen_line(&self->selections, self->cursor->y)) clear_selection(&self->selections);
}
// }}}
@@ -1495,6 +1560,11 @@ set_title(Screen *self, PyObject *title) {
CALLBACK("title_changed", "O", title);
}
void
desktop_notify(Screen *self, unsigned int osc_code, PyObject *data) {
CALLBACK("desktop_notify", "IO", osc_code, data);
}
void
set_icon(Screen *self, PyObject *icon) {
CALLBACK("icon_changed", "O", icon);
@@ -1628,9 +1698,7 @@ screen_update_cell_data(Screen *self, void *address, FONTS_DATA_HANDLE fonts_dat
}
update_line_data(self->linebuf->line, y, address);
}
if (was_dirty) {
self->url_range = EMPTY_SELECTION;
}
if (was_dirty) clear_selection(&self->url_ranges);
}
@@ -1772,41 +1840,52 @@ iteration_data_is_empty(const Screen *self, const IterationData *idata) {
}
static inline void
apply_selection(Screen *self, uint8_t *data, const Selection *s, IterationData *last_rendered, uint8_t set_mask) {
iteration_data(self, s, last_rendered, -self->historybuf->count, true);
apply_selection(Screen *self, uint8_t *data, Selection *s, uint8_t set_mask) {
iteration_data(self, s, &s->last_rendered, -self->historybuf->count, true);
for (int y = MAX(0, last_rendered->y); y < last_rendered->y_limit && y < (int)self->lines; y++) {
for (int y = MAX(0, s->last_rendered.y); y < s->last_rendered.y_limit && y < (int)self->lines; y++) {
Line *line = visual_line_(self, y);
uint8_t *line_start = data + self->columns * y;
XRange xr = xrange_for_iteration(last_rendered, y, line);
XRange xr = xrange_for_iteration(&s->last_rendered, y, line);
for (index_type x = xr.x; x < xr.x_limit; x++) line_start[x] |= set_mask;
}
last_rendered->y = MAX(0, last_rendered->y);
s->last_rendered.y = MAX(0, s->last_rendered.y);
}
bool
screen_has_selection(Screen *self) {
if (is_selection_empty(&self->selection)) return false;
IterationData idata;
iteration_data(self, &self->selection, &idata, -self->historybuf->count, true);
if (iteration_data_is_empty(self, &idata)) return false;
return true;
for (size_t i = 0; i < self->selections.count; i++) {
Selection *s = self->selections.items + i;
if (!is_selection_empty(s)) {
iteration_data(self, s, &idata, -self->historybuf->count, true);
if (!iteration_data_is_empty(self, &idata)) return true;
}
}
return false;
}
void
screen_apply_selection(Screen *self, void *address, size_t size) {
memset(address, 0, size);
apply_selection(self, address, &self->selection, &self->last_rendered.selection, 1);
apply_selection(self, address, &self->url_range, &self->last_rendered.url, 2);
for (size_t i = 0; i < self->selections.count; i++) {
apply_selection(self, address, self->selections.items + i, 1);
}
self->selections.last_rendered_count = self->selections.count;
for (size_t i = 0; i < self->url_ranges.count; i++) {
apply_selection(self, address, self->url_ranges.items + i, 2);
}
self->url_ranges.last_rendered_count = self->url_ranges.count;
}
static inline PyObject*
text_for_range(Screen *self, const Selection *sel, bool insert_newlines) {
IterationData idata;
iteration_data(self, sel, &idata, -self->historybuf->count, false);
PyObject *ans = PyTuple_New(idata.y_limit - idata.y);
int limit = MIN((int)self->lines, idata.y_limit);
PyObject *ans = PyTuple_New(limit - idata.y);
if (!ans) return NULL;
for (int i = 0, y = idata.y; y < idata.y_limit && y < (int)self->lines; y++, i++) {
for (int i = 0, y = idata.y; y < limit; y++, i++) {
Line *line = range_line_(self, y);
XRange xr = xrange_for_iteration(&idata, y, line);
char leading_char = (i > 0 && insert_newlines && !line->continued) ? '\n' : 0;
@@ -1817,22 +1896,86 @@ text_for_range(Screen *self, const Selection *sel, bool insert_newlines) {
return ans;
}
static inline hyperlink_id_type
hyperlink_id_for_range(Screen *self, const Selection *sel) {
IterationData idata;
iteration_data(self, sel, &idata, -self->historybuf->count, false);
for (int i = 0, y = idata.y; y < idata.y_limit && y < (int)self->lines; y++, i++) {
Line *line = range_line_(self, y);
XRange xr = xrange_for_iteration(&idata, y, line);
for (index_type x = xr.x; x < xr.x_limit; x++) {
if (line->cpu_cells[x].hyperlink_id) return line->cpu_cells[x].hyperlink_id;
}
}
return 0;
}
static inline PyObject*
extend_tuple(PyObject *a, PyObject *b) {
Py_ssize_t bs = PyBytes_GET_SIZE(b);
if (bs < 1) return a;
Py_ssize_t off = PyTuple_GET_SIZE(a);
if (_PyTuple_Resize(&a, off + bs) != 0) return NULL;
for (Py_ssize_t y = 0; y < bs; y++) {
PyObject *t = PyTuple_GET_ITEM(b, y);
Py_INCREF(t);
PyTuple_SET_ITEM(a, off + y, t);
}
return a;
}
static PyObject*
current_url_text(Screen *self, PyObject *args UNUSED) {
PyObject *empty_string = PyUnicode_FromString(""), *ans = NULL;
if (!empty_string) return NULL;
for (size_t i = 0; i < self->url_ranges.count; i++) {
Selection *s = self->url_ranges.items + i;
if (!is_selection_empty(s)) {
PyObject *temp = text_for_range(self, s, false);
if (!temp) goto error;
PyObject *text = PyUnicode_Join(empty_string, temp);
Py_CLEAR(temp);
if (!text) goto error;
if (ans) {
PyObject *t = ans;
ans = PyUnicode_Concat(ans, text);
Py_CLEAR(text); Py_CLEAR(t);
if (!ans) goto error;
} else ans = text;
}
}
Py_CLEAR(empty_string);
if (!ans) Py_RETURN_NONE;
return ans;
error:
Py_CLEAR(empty_string); Py_CLEAR(ans);
return NULL;
}
bool
screen_open_url(Screen *self) {
if (is_selection_empty(&self->url_range)) return false;
PyObject *lines = text_for_range(self, &self->url_range, false);
bool ret = false;
if (lines) {
PyObject *joiner = PyUnicode_FromString("");
if (joiner) {
PyObject *url = PyUnicode_Join(joiner, lines);
if (url) { call_boss(open_url_lines, "(O)", url); Py_CLEAR(url); ret = true; }
Py_CLEAR(joiner);
if (!self->url_ranges.count) return false;
hyperlink_id_type hid = hyperlink_id_for_range(self, self->url_ranges.items);
if (hid) {
const char *url = get_hyperlink_for_id(self->hyperlink_pool, hid, true);
if (url) {
CALLBACK("open_url", "sH", url, hid);
return true;
}
Py_CLEAR(lines);
}
if (PyErr_Occurred()) PyErr_Print();
return ret;
PyObject *text = current_url_text(self, NULL);
if (!text) {
if (PyErr_Occurred()) PyErr_Print();
return false;
}
bool found = false;
if (PyUnicode_Check(text)) {
CALLBACK("open_url", "OH", text, 0);
found = true;
}
Py_CLEAR(text);
return found;
}
static void
@@ -1862,6 +2005,20 @@ deactivate_overlay_line(Screen *self) {
#define WRAP2(name, defval1, defval2) static PyObject* name(Screen *self, PyObject *args) { unsigned int a=defval1, b=defval2; if(!PyArg_ParseTuple(args, "|II", &a, &b)) return NULL; screen_##name(self, a, b); Py_RETURN_NONE; }
#define WRAP2B(name) static PyObject* name(Screen *self, PyObject *args) { unsigned int a, b; int p; if(!PyArg_ParseTuple(args, "IIp", &a, &b, &p)) return NULL; screen_##name(self, a, b, (bool)p); Py_RETURN_NONE; }
WRAP0(garbage_collect_hyperlink_pool)
static PyObject*
hyperlinks_as_list(Screen *self, PyObject *args UNUSED) {
return screen_hyperlinks_as_list(self);
}
static PyObject*
hyperlink_for_id(Screen *self, PyObject *val) {
unsigned long id = PyLong_AsUnsignedLong(val);
if (id > HYPERLINK_MAX_NUMBER) { PyErr_SetString(PyExc_IndexError, "Out of bounds"); return NULL; }
return Py_BuildValue("s", get_hyperlink_for_id(self->hyperlink_pool, id, true));
}
static PyObject*
set_pending_timeout(Screen *self, PyObject *val) {
if (!PyFloat_Check(val)) { PyErr_SetString(PyExc_TypeError, "timeout must be a float"); return NULL; }
@@ -1875,17 +2032,17 @@ static Line* get_range_line(void *x, int y) { return range_line_(x, y); }
static PyObject*
as_text(Screen *self, PyObject *args) {
return as_text_generic(args, self, get_visual_line, self->lines, self->columns);
return as_text_generic(args, self, get_visual_line, self->lines, &self->as_ansi_buf);
}
static PyObject*
as_text_non_visual(Screen *self, PyObject *args) {
return as_text_generic(args, self, get_range_line, self->lines, self->columns);
return as_text_generic(args, self, get_range_line, self->lines, &self->as_ansi_buf);
}
static inline PyObject*
as_text_generic_wrapper(Screen *self, PyObject *args, get_line_func get_line) {
return as_text_generic(args, self, get_line, self->lines, self->columns);
return as_text_generic(args, self, get_line, self->lines, &self->as_ansi_buf);
}
static PyObject*
@@ -1898,68 +2055,6 @@ as_text_alternate(Screen *self, PyObject *args) {
}
static PyObject*
screen_wcswidth(PyObject UNUSED *self, PyObject *str) {
if (PyUnicode_READY(str) != 0) return NULL;
int kind = PyUnicode_KIND(str);
void *data = PyUnicode_DATA(str);
Py_ssize_t len = PyUnicode_GET_LENGTH(str), i;
unsigned long ans = 0;
char_type prev_ch = 0;
int prev_width = 0;
typedef enum {NORMAL, IN_SGR, FLAG_PAIR_STARTED} WCSState;
WCSState state = NORMAL;
for (i = 0; i < len; i++) {
char_type ch = PyUnicode_READ(kind, data, i);
switch(state) {
case IN_SGR: {
if (ch == 'm') state = NORMAL;
} continue;
case FLAG_PAIR_STARTED: {
state = NORMAL;
if (is_flag_pair(prev_ch, ch)) break;
} /* fallthrough */
case NORMAL: {
if (ch == 0x1b && i + 1 < len && PyUnicode_READ(kind, data, i + 1) == '[') { state = IN_SGR; continue; }
switch(ch) {
case 0xfe0f: {
if (is_emoji_presentation_base(prev_ch) && prev_width == 1) {
ans += 1;
prev_width = 2;
} else prev_width = 0;
} break;
case 0xfe0e: {
if (is_emoji_presentation_base(prev_ch) && prev_width == 2) {
ans -= 1;
prev_width = 1;
} else prev_width = 0;
} break;
default: {
if (is_flag_codepoint(ch)) state = FLAG_PAIR_STARTED;
int w = wcwidth_std(ch);
switch(w) {
case -1:
case 0:
prev_width = 0; break;
case 2:
prev_width = 2; break;
default:
prev_width = 1; break;
}
ans += prev_width;
} break;
} break; // switch(ch)
} break; // case NORMAL
} // switch(state)
prev_ch = ch;
}
return PyLong_FromUnsignedLong(ans);
}
static PyObject*
screen_truncate_point_for_length(PyObject UNUSED *self, PyObject *args) {
@@ -2147,8 +2242,8 @@ update_selection(Screen *self, PyObject *args) {
}
static PyObject*
clear_selection(Screen *self, PyObject *args UNUSED) {
self->selection = EMPTY_SELECTION;
clear_selection_(Screen *s, PyObject *args UNUSED) {
clear_selection(&s->selections);
Py_RETURN_NONE;
}
@@ -2176,9 +2271,8 @@ start_selection(Screen *self, PyObject *args) {
static PyObject*
is_rectangle_select(Screen *self, PyObject *a UNUSED) {
PyObject *ans = self->selection.rectangle_select ? Py_True : Py_False;
Py_INCREF(ans);
return ans;
if (self->selections.count && self->selections.items[0].rectangle_select) Py_RETURN_TRUE;
Py_RETURN_FALSE;
}
static PyObject*
@@ -2189,7 +2283,19 @@ copy_colors_from(Screen *self, Screen *other) {
static PyObject*
text_for_selection(Screen *self, PyObject *a UNUSED) {
return text_for_range(self, &self->selection, true);
PyObject *lines = NULL;
for (size_t i = 0; i < self->selections.count; i++) {
PyObject *temp = text_for_range(self, self->selections.items + i, true);
if (temp) {
if (lines) {
lines = extend_tuple(lines, temp);
Py_DECREF(temp);
} else lines = temp;
} else break;
}
if (PyErr_Occurred()) { Py_CLEAR(lines); return NULL; }
if (!lines) lines = PyTuple_New(0);
return lines;
}
bool
@@ -2296,44 +2402,135 @@ bool
screen_is_selection_dirty(Screen *self) {
IterationData q;
if (self->scrolled_by != self->last_rendered.scrolled_by) return true;
iteration_data(self, &self->selection, &q, 0, true);
if (memcmp(&q, &self->last_rendered.selection, sizeof(IterationData)) != 0) return true;
iteration_data(self, &self->url_range, &q, 0, true);
if (memcmp(&q, &self->last_rendered.url, sizeof(IterationData)) != 0) return true;
if (self->selections.last_rendered_count != self->selections.count || self->url_ranges.last_rendered_count != self->url_ranges.count) return true;
for (size_t i = 0; i < self->selections.count; i++) {
iteration_data(self, self->selections.items + i, &q, 0, true);
if (memcmp(&q, &self->selections.items[i].last_rendered, sizeof(IterationData)) != 0) return true;
}
for (size_t i = 0; i < self->url_ranges.count; i++) {
iteration_data(self, self->url_ranges.items + i, &q, 0, true);
if (memcmp(&q, &self->url_ranges.items[i].last_rendered, sizeof(IterationData)) != 0) return true;
}
return false;
}
void
screen_start_selection(Screen *self, index_type x, index_type y, bool in_left_half_of_cell, bool rectangle_select, SelectionExtendMode extend_mode) {
#define A(attr, val) self->selection.attr = val;
#define A(attr, val) self->selections.items->attr = val;
ensure_space_for(&self->selections, items, Selection, self->selections.count + 1, capacity, 1, false);
memset(self->selections.items, 0, sizeof(Selection));
self->selections.count = 1;
self->selections.in_progress = true;
self->selections.extend_mode = extend_mode;
self->selections.items[0].last_rendered.y = INT_MAX;
A(start.x, x); A(end.x, x); A(start.y, y); A(end.y, y); A(start_scrolled_by, self->scrolled_by); A(end_scrolled_by, self->scrolled_by);
A(in_progress, true); A(rectangle_select, rectangle_select); A(extend_mode, extend_mode); A(start.in_left_half_of_cell, in_left_half_of_cell); A(end.in_left_half_of_cell, in_left_half_of_cell);
A(rectangle_select, rectangle_select); A(start.in_left_half_of_cell, in_left_half_of_cell); A(end.in_left_half_of_cell, in_left_half_of_cell);
A(input_start.x, x); A(input_start.y, y); A(input_start.in_left_half_of_cell, in_left_half_of_cell);
A(input_current.x, x); A(input_current.y, y); A(input_current.in_left_half_of_cell, in_left_half_of_cell);
#undef A
}
void
screen_mark_url(Screen *self, index_type start_x, index_type start_y, index_type end_x, index_type end_y) {
#define A(attr, val) self->url_range.attr = val;
A(start.x, start_x); A(end.x, end_x); A(start.y, start_y); A(end.y, end_y); A(start_scrolled_by, self->scrolled_by); A(end_scrolled_by, self->scrolled_by);
A(start.in_left_half_of_cell, true); A(end.in_left_half_of_cell, start_x == end_x && start_y == end_y);
static inline void
add_url_range(Screen *self, index_type start_x, index_type start_y, index_type end_x, index_type end_y) {
#define A(attr, val) r->attr = val;
ensure_space_for(&self->url_ranges, items, Selection, self->url_ranges.count + 8, capacity, 8, false);
Selection *r = self->url_ranges.items + self->url_ranges.count++;
memset(r, 0, sizeof(Selection));
r->last_rendered.y = INT_MAX;
A(start.x, start_x); A(end.x, end_x); A(start.y, start_y); A(end.y, end_y);
A(start_scrolled_by, self->scrolled_by); A(end_scrolled_by, self->scrolled_by);
A(start.in_left_half_of_cell, true);
#undef A
}
void
screen_update_selection(Screen *self, index_type x, index_type y, bool in_left_half_of_cell, bool ended, bool start_extended_selection) {
self->selection.in_progress = !ended;
self->selection.input_current.x = x; self->selection.input_current.y = y;
self->selection.input_current.in_left_half_of_cell = in_left_half_of_cell;
self->selection.end_scrolled_by = self->scrolled_by;
SelectionBoundary start, end, *a, *b;
a = &self->selection.start, b = &self->selection.end;
screen_mark_url(Screen *self, index_type start_x, index_type start_y, index_type end_x, index_type end_y) {
self->url_ranges.count = 0;
if (start_x || start_y || end_x || end_y) add_url_range(self, start_x, start_y, end_x, end_y);
}
switch(self->selection.extend_mode) {
static bool
mark_hyperlinks_in_line(Screen *self, Line *line, hyperlink_id_type id, index_type y) {
index_type start = 0;
bool found = false;
bool in_range = false;
for (index_type x = 0; x < line->xnum; x++) {
bool has_hyperlink = line->cpu_cells[x].hyperlink_id == id;
if (in_range) {
if (!has_hyperlink) {
add_url_range(self, start, y, x - 1, y);
in_range = false;
start = 0;
}
} else {
if (has_hyperlink) {
start = x; in_range = true;
found = true;
}
}
}
if (in_range) add_url_range(self, start, y, self->columns - 1, y);
return found;
}
static int
compare_ranges(const void *a_, const void* b_) {
const Selection *a = a_, *b = b_;
int ans = a->sort_y - b->sort_y;
if (!ans) ans = a->sort_x - b->sort_x;
return ans;
}
static void
sort_ranges(const Screen *self, Selections *s) {
IterationData a;
for (size_t i = 0; i < s->count; i++) {
iteration_data(self, s->items + i, &a, 0, false);
s->items[i].sort_x = a.first.x;
s->items[i].sort_y = a.y;
}
qsort(s->items, s->count, sizeof(Selection), compare_ranges);
}
hyperlink_id_type
screen_mark_hyperlink(Screen *self, index_type x, index_type y) {
self->url_ranges.count = 0;
Line *line = screen_visual_line(self, y);
hyperlink_id_type id = line->cpu_cells[x].hyperlink_id;
if (!id) return 0;
index_type ypos = y, last_marked_line = y;
do {
if (mark_hyperlinks_in_line(self, line, id, ypos)) last_marked_line = ypos;
if (ypos == 0) break;
ypos--;
line = screen_visual_line(self, ypos);
} while (last_marked_line - ypos < 5);
ypos = y + 1; last_marked_line = y;
while (ypos < self->lines - 1 && ypos - last_marked_line < 5) {
line = screen_visual_line(self, ypos);
if (mark_hyperlinks_in_line(self, line, id, ypos)) last_marked_line = ypos;
ypos++;
}
if (self->url_ranges.count > 1) sort_ranges(self, &self->url_ranges);
return id;
}
void
screen_update_selection(Screen *self, index_type x, index_type y, bool in_left_half_of_cell, bool ended, bool start_extended_selection) {
if (!self->selections.count) return;
self->selections.in_progress = !ended;
Selection *s = self->selections.items;
s->input_current.x = x; s->input_current.y = y;
s->input_current.in_left_half_of_cell = in_left_half_of_cell;
s->end_scrolled_by = self->scrolled_by;
SelectionBoundary start, end, *a, *b;
a = &s->start, b = &s->end;
switch(self->selections.extend_mode) {
case EXTEND_WORD: {
SelectionBoundary *before = &self->selection.input_start, *after = &self->selection.input_current;
if (selection_boundary_less_than(after, before)) { before = after; after = &self->selection.input_start; }
SelectionBoundary *before = &s->input_start, *after = &s->input_current;
if (selection_boundary_less_than(after, before)) { before = after; after = &s->input_start; }
bool found_at_start = screen_selection_range_for_word(self, before->x, before->y, &start.y, &end.y, &start.x, &end.x, true);
if (found_at_start) {
a->x = start.x; a->y = start.y; a->in_left_half_of_cell = true;
@@ -2348,13 +2545,13 @@ screen_update_selection(Screen *self, index_type x, index_type y, bool in_left_h
}
case EXTEND_LINE: {
index_type top_line, bottom_line;
if (start_extended_selection || y == self->selection.start.y) {
if (start_extended_selection || y == s->start.y) {
top_line = y; bottom_line = y;
} else if (y < self->selection.start.y) {
top_line = y; bottom_line = self->selection.start.y;
a = &self->selection.end; b = &self->selection.start;
} else if (y > self->selection.start.y) {
bottom_line = y; top_line = self->selection.start.y;
} else if (y < s->start.y) {
top_line = y; bottom_line = s->start.y;
a = &s->end; b = &s->start;
} else if (y > s->start.y) {
bottom_line = y; top_line = s->start.y;
} else break;
while (top_line > 0 && visual_line_(self, top_line)->continued) {
if (!screen_selection_range_for_line(self, top_line - 1, &start.x, &end.x)) break;
@@ -2372,10 +2569,10 @@ screen_update_selection(Screen *self, index_type x, index_type y, bool in_left_h
break;
}
case EXTEND_CELL:
self->selection.end.x = x; self->selection.end.y = y; self->selection.end.in_left_half_of_cell = in_left_half_of_cell;
s->end.x = x; s->end.y = y; s->end.in_left_half_of_cell = in_left_half_of_cell;
break;
}
if (!self->selection.in_progress) call_boss(set_primary_selection, NULL);
if (!self->selections.in_progress) call_boss(set_primary_selection, NULL);
}
static PyObject*
@@ -2520,6 +2717,30 @@ paste_bytes(Screen *self, PyObject *bytes) {
Py_RETURN_NONE;
}
static PyObject*
focus_changed(Screen *self, PyObject *has_focus_) {
bool previous = self->has_focus;
bool has_focus = PyObject_IsTrue(has_focus_) ? true : false;
if (has_focus != previous) {
self->has_focus = has_focus;
if (has_focus) self->has_activity_since_last_focus = false;
if (self->modes.mFOCUS_TRACKING) write_escape_code_to_child(self, CSI, has_focus ? "I" : "O");
Py_RETURN_TRUE;
}
Py_RETURN_FALSE;
}
static PyObject*
has_focus(Screen *self, PyObject *args UNUSED) {
if (self->has_focus) Py_RETURN_TRUE;
Py_RETURN_FALSE;
}
static PyObject*
has_activity_since_last_focus(Screen *self, PyObject *args UNUSED) {
if (self->has_activity_since_last_focus) Py_RETURN_TRUE;
Py_RETURN_FALSE;
}
WRAP2(cursor_position, 1, 1)
@@ -2534,11 +2755,6 @@ COUNT_WRAP(cursor_down)
COUNT_WRAP(cursor_down1)
COUNT_WRAP(cursor_forward)
static PyObject*
wcwidth_wrap(PyObject UNUSED *self, PyObject *chr) {
return PyLong_FromLong(wcwidth_std(PyLong_AsLong(chr)));
}
static PyObject*
screen_is_emoji_presentation_base(PyObject UNUSED *self, PyObject *code_) {
unsigned long code = PyLong_AsUnsignedLong(code_);
@@ -2546,6 +2762,17 @@ screen_is_emoji_presentation_base(PyObject UNUSED *self, PyObject *code_) {
Py_RETURN_FALSE;
}
static PyObject*
hyperlink_at(Screen *self, PyObject *args) {
unsigned int x, y;
if (!PyArg_ParseTuple(args, "II", &x, &y)) return NULL;
screen_mark_hyperlink(self, x, y);
if (!self->url_ranges.count) Py_RETURN_NONE;
hyperlink_id_type hid = hyperlink_id_for_range(self, self->url_ranges.items);
if (!hid) Py_RETURN_NONE;
const char *url = get_hyperlink_for_id(self->hyperlink_pool, hid, true);
return Py_BuildValue("s", url);
}
#define MND(name, args) {#name, (PyCFunction)name, args, #name},
#define MODEFUNC(name) MND(name, METH_NOARGS) MND(set_##name, METH_O)
@@ -2553,6 +2780,7 @@ screen_is_emoji_presentation_base(PyObject UNUSED *self, PyObject *code_) {
static PyMethodDef methods[] = {
MND(line, METH_O)
MND(visual_line, METH_VARARGS)
MND(current_url_text, METH_NOARGS)
MND(draw, METH_O)
MND(cursor_position, METH_VARARGS)
MND(set_mode, METH_VARARGS)
@@ -2565,6 +2793,9 @@ static PyMethodDef methods[] = {
MND(erase_in_line, METH_VARARGS)
MND(erase_in_display, METH_VARARGS)
MND(scroll_until_cursor, METH_NOARGS)
MND(hyperlinks_as_list, METH_NOARGS)
MND(garbage_collect_hyperlink_pool, METH_NOARGS)
MND(hyperlink_for_id, METH_O)
METHOD(current_char_width, METH_NOARGS)
MND(insert_lines, METH_VARARGS)
MND(delete_lines, METH_VARARGS)
@@ -2589,7 +2820,7 @@ static PyMethodDef methods[] = {
MND(clear_tab_stop, METH_VARARGS)
MND(start_selection, METH_VARARGS)
MND(update_selection, METH_VARARGS)
MND(clear_selection, METH_NOARGS)
{"clear_selection", (PyCFunction)clear_selection_, METH_NOARGS, ""},
MND(reverse_index, METH_NOARGS)
MND(mark_as_dirty, METH_NOARGS)
MND(resize, METH_VARARGS)
@@ -2599,10 +2830,14 @@ static PyMethodDef methods[] = {
MND(is_rectangle_select, METH_NOARGS)
MND(scroll, METH_VARARGS)
MND(send_escape_code_to_child, METH_VARARGS)
MND(hyperlink_at, METH_VARARGS)
MND(toggle_alt_screen, METH_NOARGS)
MND(reset_callbacks, METH_NOARGS)
MND(paste, METH_O)
MND(paste_bytes, METH_O)
MND(focus_changed, METH_O)
MND(has_focus, METH_NOARGS)
MND(has_activity_since_last_focus, METH_NOARGS)
MND(copy_colors_from, METH_O)
MND(set_marker, METH_VARARGS)
MND(marked_cells, METH_NOARGS)
@@ -2662,8 +2897,6 @@ PyTypeObject Screen_Type = {
};
static PyMethodDef module_methods[] = {
{"wcwidth", (PyCFunction)wcwidth_wrap, METH_O, ""},
{"wcswidth", (PyCFunction)screen_wcswidth, METH_O, ""},
{"is_emoji_presentation_base", (PyCFunction)screen_is_emoji_presentation_base, METH_O, ""},
{"truncate_point_for_length", (PyCFunction)screen_truncate_point_for_length, METH_VARARGS, ""},
{NULL} /* Sentinel */

View File

@@ -26,13 +26,30 @@ typedef struct {
typedef enum SelectionExtendModes { EXTEND_CELL, EXTEND_WORD, EXTEND_LINE } SelectionExtendMode;
typedef struct {
index_type x, x_limit;
} XRange;
typedef struct {
int y, y_limit;
XRange first, body, last;
} IterationData;
typedef struct {
SelectionBoundary start, end, input_start, input_current;
unsigned int start_scrolled_by, end_scrolled_by;
bool in_progress, rectangle_select;
SelectionExtendMode extend_mode;
bool rectangle_select;
IterationData last_rendered;
int sort_y, sort_x;
} Selection;
typedef struct {
Selection *items;
size_t count, capacity, last_rendered_count;
bool in_progress;
SelectionExtendMode extend_mode;
} Selections;
#define SAVEPOINTS_SZ 256
typedef struct {
@@ -63,15 +80,6 @@ typedef struct {
index_type xstart, ynum, xnum;
} OverlayLine;
typedef struct {
index_type x, x_limit;
} XRange;
typedef struct {
int y, y_limit;
XRange first, body, last;
} IterationData;
typedef struct {
PyObject_HEAD
@@ -82,9 +90,8 @@ typedef struct {
id_type window_id;
uint32_t utf8_state, utf8_codepoint, *g0_charset, *g1_charset, *g_charset;
unsigned int current_charset;
Selection selection, url_range;
Selections selections, url_ranges;
struct {
IterationData selection, url;
unsigned int cursor_x, cursor_y, scrolled_by;
index_type lines, columns;
} last_rendered;
@@ -121,6 +128,11 @@ typedef struct {
} pending_mode;
DisableLigature disable_ligatures;
PyObject *marker;
bool has_focus;
bool has_activity_since_last_focus;
hyperlink_id_type active_hyperlink_id;
HYPERLINK_POOL_HANDLE hyperlink_pool;
ANSIBuf as_ansi_buf;
} Screen;
@@ -178,6 +190,7 @@ void screen_handle_print(Screen *, PyObject *cmd);
void screen_designate_charset(Screen *, uint32_t which, uint32_t as);
void screen_use_latin1(Screen *, bool);
void set_title(Screen *self, PyObject*);
void desktop_notify(Screen *self, unsigned int, PyObject*);
void set_icon(Screen *self, PyObject*);
void set_dynamic_color(Screen *self, unsigned int code, PyObject*);
void clipboard_control(Screen *self, PyObject*);
@@ -203,6 +216,8 @@ bool screen_history_scroll(Screen *self, int amt, bool upwards);
Line* screen_visual_line(Screen *self, index_type y);
unsigned long screen_current_char_width(Screen *self);
void screen_mark_url(Screen *self, index_type start_x, index_type start_y, index_type end_x, index_type end_y);
void set_active_hyperlink(Screen*, char*, char*);
hyperlink_id_type screen_mark_hyperlink(Screen*, index_type, index_type);
void screen_handle_graphics_command(Screen *self, const GraphicsCommand *cmd, const uint8_t *payload);
bool screen_open_url(Screen*);
void screen_dirty_sprite_positions(Screen *self);

View File

@@ -14,6 +14,7 @@ from .options_stub import Options
from .os_window_size import WindowSize, WindowSizeData, WindowSizes
from .typing import SpecialWindowInstance
from .utils import log_error, resolved_shell
from .window import Watchers
def get_os_window_sizing_data(opts: Options, session: Optional['Session'] = None) -> WindowSizeData:
@@ -26,14 +27,15 @@ def get_os_window_sizing_data(opts: Options, session: Optional['Session'] = None
class Tab:
def __init__(self, opts: Options, name: str):
self.windows: List[Union[List[str], 'SpecialWindowInstance']] = []
def __init__(self, opts: Options, name: str, watchers: Watchers):
self.windows: List['SpecialWindowInstance'] = []
self.name = name.strip()
self.active_window_idx = 0
self.enabled_layouts = opts.enabled_layouts
self.layout = (self.enabled_layouts or ['tall'])[0]
self.cwd: Optional[str] = None
self.next_title: Optional[str] = None
self.watchers: Watchers = watchers.copy()
class Session:
@@ -44,11 +46,25 @@ class Session:
self.default_title = default_title
self.os_window_size: Optional[WindowSizes] = None
self.os_window_class: Optional[str] = None
self.watchers = Watchers()
def add_watchers_to_all_windows(self, watchers: Watchers) -> None:
def add(w: 'SpecialWindowInstance') -> 'SpecialWindowInstance':
if w.watchers is None:
return w._replace(watchers=watchers)
wt = w.watchers.copy()
wt.add(watchers)
return w._replace(watchers=wt)
for tab in self.tabs:
tab.windows = [add(w) for w in tab.windows]
def add_tab(self, opts: Options, name: str = '') -> None:
if self.tabs and not self.tabs[-1].windows:
del self.tabs[-1]
self.tabs.append(Tab(opts, name))
if self.tabs:
self.tabs[-1].watchers = self.watchers.copy()
self.tabs.append(Tab(opts, name, self.watchers))
def set_next_title(self, title: str) -> None:
self.tabs[-1].next_title = title.strip()
@@ -65,10 +81,16 @@ class Session:
cmd = None
from .tabs import SpecialWindow
t = self.tabs[-1]
t.windows.append(SpecialWindow(cmd, cwd=t.cwd, override_title=t.next_title or self.default_title))
watchers: Optional[Watchers] = None
if self.watchers.has_watchers:
watchers = self.watchers.copy()
sw = SpecialWindow(cmd, cwd=t.cwd, override_title=t.next_title or self.default_title, watchers=watchers)
t.windows.append(sw)
t.next_title = None
def add_special_window(self, sw: 'SpecialWindowInstance') -> None:
if self.watchers.has_watchers:
sw = sw._replace(watchers=self.watchers.copy())
self.tabs[-1].windows.append(sw)
def focus(self) -> None:
@@ -87,9 +109,13 @@ class Session:
def parse_session(raw: str, opts: Options, default_title: Optional[str] = None) -> Generator[Session, None, None]:
def finalize_session(ans: Session) -> Session:
from .tabs import SpecialWindow
for t in ans.tabs:
if not t.windows:
t.windows.append(resolved_shell(opts))
w: Optional[Watchers] = None
if t.watchers.has_watchers:
w = t.watchers.copy()
t.windows.append(SpecialWindow(cmd=resolved_shell(opts), watchers=w))
return ans
ans = Session(default_title)
@@ -107,7 +133,9 @@ def parse_session(raw: str, opts: Options, default_title: Optional[str] = None)
ans.add_tab(opts, rest)
elif cmd == 'new_os_window':
yield finalize_session(ans)
wt = ans.watchers
ans = Session(default_title)
ans.watchers = wt.copy()
ans.add_tab(opts, rest)
elif cmd == 'layout':
ans.set_layout(rest)
@@ -127,6 +155,14 @@ def parse_session(raw: str, opts: Options, default_title: Optional[str] = None)
ans.os_window_size = WindowSizes(WindowSize(*w), WindowSize(*h))
elif cmd == 'os_window_class':
ans.os_window_class = rest
elif cmd == 'watcher':
from .launch import load_watch_modules
if rest == 'clear':
ans.watchers = Watchers()
else:
watchers = load_watch_modules((rest,))
if watchers is not None:
ans.watchers.add(watchers)
else:
raise ValueError('Unknown command in session file: {}'.format(cmd))
yield finalize_session(ans)

View File

@@ -78,8 +78,7 @@ class Completer:
def __init__(self) -> None:
self.matches: List[str] = []
ddir = cache_dir()
with suppress(FileExistsError):
os.makedirs(ddir)
os.makedirs(ddir, exist_ok=True)
self.history_path = os.path.join(ddir, 'shell.history')
def complete(self, text: str, state: int) -> Optional[str]:

View File

@@ -701,6 +701,7 @@ PYWRAP1(set_options) {
S(force_ltr, PyObject_IsTrue);
S(resize_draw_strategy, PyLong_AsLong);
S(resize_in_steps, PyObject_IsTrue);
S(allow_hyperlinks, PyObject_IsTrue);
S(pointer_shape_when_grabbed, pointer_shape);
GA(tab_bar_style);

View File

@@ -63,6 +63,7 @@ typedef struct {
bool close_on_child_death;
bool window_alert_on_bell;
bool debug_keyboard;
bool allow_hyperlinks;
monotonic_t resize_debounce_time;
MouseShape pointer_shape_when_grabbed;
struct {

View File

@@ -24,6 +24,7 @@ class TabBarData(NamedTuple):
needs_attention: bool
num_windows: int
layout_name: str
has_activity_since_last_focus: bool
class DrawData(NamedTuple):
@@ -40,6 +41,7 @@ class DrawData(NamedTuple):
default_bg: Color
title_template: str
active_title_template: Optional[str]
tab_activity_symbol: Optional[str]
def as_rgb(x: int) -> int:
@@ -65,6 +67,12 @@ def draw_title(draw_data: DrawData, screen: Screen, tab: TabBarData, index: int)
screen.cursor.fg = draw_data.bell_fg
screen.draw('🔔 ')
screen.cursor.fg = fg
if tab.has_activity_since_last_focus and draw_data.tab_activity_symbol:
fg = screen.cursor.fg
screen.cursor.fg = draw_data.bell_fg
screen.draw(draw_data.tab_activity_symbol)
screen.cursor.fg = fg
template = draw_data.title_template
if tab.is_active and draw_data.active_title_template is not None:
template = draw_data.active_title_template
@@ -226,7 +234,8 @@ class TabBar:
self.opts.tab_fade, self.opts.active_tab_foreground, self.opts.active_tab_background,
self.opts.inactive_tab_foreground, self.opts.inactive_tab_background,
self.opts.tab_bar_background or self.opts.background, self.opts.tab_title_template,
self.opts.active_tab_title_template
self.opts.active_tab_title_template,
self.opts.tab_activity_symbol
)
if self.opts.tab_bar_style == 'separator':
self.draw_func = draw_tab_with_separator

View File

@@ -48,6 +48,7 @@ class SpecialWindowInstance(NamedTuple):
cwd: Optional[str]
overlay_for: Optional[int]
env: Optional[Dict[str, str]]
watchers: Optional[Watchers]
def SpecialWindow(
@@ -57,9 +58,10 @@ def SpecialWindow(
cwd_from: Optional[int] = None,
cwd: Optional[str] = None,
overlay_for: Optional[int] = None,
env: Optional[Dict[str, str]] = None
env: Optional[Dict[str, str]] = None,
watchers: Optional[Watchers] = None
) -> SpecialWindowInstance:
return SpecialWindowInstance(cmd, stdin, override_title, cwd_from, cwd, overlay_for, env)
return SpecialWindowInstance(cmd, stdin, override_title, cwd_from, cwd, overlay_for, env, watchers)
def add_active_id_to_history(items: Deque[int], item_id: int, maxlen: int = 64) -> None:
@@ -135,10 +137,7 @@ class Tab: # {{{
def startup(self, session_tab: 'SessionTab') -> None:
for cmd in session_tab.windows:
if isinstance(cmd, (SpecialWindowInstance,)):
self.new_special_window(cmd)
else:
self.new_window(cmd=cmd)
self.new_special_window(cmd)
self.windows.set_active_window_group_for(self.windows.all_windows[session_tab.active_window_idx])
def serialize_state(self) -> Dict[str, Any]:
@@ -333,14 +332,14 @@ class Tab: # {{{
special_window: SpecialWindowInstance,
location: Optional[str] = None,
copy_colors_from: Optional[Window] = None,
allow_remote_control: bool = False
allow_remote_control: bool = False,
) -> Window:
return self.new_window(
use_shell=False, cmd=special_window.cmd, stdin=special_window.stdin,
override_title=special_window.override_title,
cwd_from=special_window.cwd_from, cwd=special_window.cwd, overlay_for=special_window.overlay_for,
env=special_window.env, location=location, copy_colors_from=copy_colors_from,
allow_remote_control=allow_remote_control
allow_remote_control=allow_remote_control, watchers=special_window.watchers
)
def close_window(self) -> None:
@@ -440,9 +439,9 @@ class Tab: # {{{
self.relayout()
def move_window_to_top(self) -> None:
n = self.windows.num_groups
if n > 1:
self.move_window(1 - n)
n = self.windows.active_group_idx
if n > 0:
self.move_window(-n)
def move_window_forward(self) -> None:
self.move_window()
@@ -737,13 +736,16 @@ class TabManager: # {{{
for t in self.tabs:
title = (t.name or t.title or appname).strip()
needs_attention = False
has_activity_since_last_focus = False
for w in t:
if w.needs_attention:
needs_attention = True
break
if w.has_activity_since_last_focus:
has_activity_since_last_focus = True
ans.append(TabBarData(
title, t is at, needs_attention,
len(t), t.current_layout.name or ''
len(t), t.current_layout.name or '',
has_activity_since_last_focus
))
return ans

View File

@@ -18,4 +18,5 @@ TermManagerType = LoopType = Debug = GraphicsCommandType = None
CompletedProcess = Tuple
TypedDict = dict
EdgeLiteral = str
MatchType = str
Protocol = object

View File

@@ -43,6 +43,7 @@ from .config import ( # noqa; noqa
)
EdgeLiteral = Literal['left', 'top', 'right', 'bottom']
MatchType = Literal['mime', 'ext', 'protocol', 'file', 'path', 'url', 'fragment_matches']
GRT_a = Literal['t', 'T', 'q', 'p', 'd']
GRT_f = Literal[24, 32, 100]
GRT_t = Literal['d', 'f', 't', 's']

2
kitty/unicode-data.c 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
#include "data-types.h"

View File

@@ -90,7 +90,9 @@ def save_notification(version: Version) -> None:
for version in sorted(notified_versions):
n = notified_versions[version]
lines.append('{},{},{}'.format(
'.'.join(map(str, n.version)), n.time_of_last_notification, n.count))
'.'.join(map(str, n.version)),
n.time_of_last_notification,
n.notification_count))
atomic_save('\n'.join(lines).encode('utf-8'), version_notification_log())
@@ -109,7 +111,7 @@ def run_worker() -> None:
print(get_released_version())
def update_check(timer_id: Optional[int] = None) -> bool:
def update_check() -> bool:
try:
p = subprocess.Popen([
kitty_exe(), '+runpy',
@@ -123,6 +125,10 @@ def update_check(timer_id: Optional[int] = None) -> bool:
return True
def update_check_callback(timer_id: Optional[int]) -> None:
update_check()
def run_update_check(interval: float = CHECK_INTERVAL) -> None:
if update_check():
add_timer(update_check, interval)
add_timer(update_check_callback, interval)

1227
kitty/uthash.h Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@ from contextlib import suppress
from functools import lru_cache
from time import monotonic
from typing import (
Any, Callable, Dict, Generator, Iterable, List, Match, NamedTuple,
Any, Callable, Dict, Generator, Iterable, List, Mapping, Match, NamedTuple,
Optional, Tuple, Union, cast
)
@@ -28,18 +28,21 @@ from .typing import AddressFamily, PopenType, Socket, StartupCtx
BASE = os.path.dirname(os.path.abspath(__file__))
def expandvars(val: str, env: Dict[str, str] = {}) -> str:
def expandvars(val: str, env: Mapping[str, str] = {}, fallback_to_os_env: bool = True) -> str:
def sub(m: Match) -> str:
key = m.group(1)
key = m.group(1) or m.group(2)
result = env.get(key)
if result is None:
if result is None and fallback_to_os_env:
result = os.environ.get(key)
if result is None:
result = m.group()
return result
return re.sub(r'\$\{(\S+?)\}', sub, val)
if '$' not in val:
return val
return re.sub(r'\$(?:(\w+)|\{([^}]+)\})', sub, val)
def platform_window_id(os_window_id: int) -> Optional[int]:
@@ -561,7 +564,7 @@ def read_shell_environment(opts: Optional[Options] = None) -> Dict[str, str]:
def parse_uri_list(text: str) -> Generator[str, None, None]:
' Get paths from file:// URLs '
from urllib.parse import urlparse, unquote
from urllib.parse import unquote, urlparse
for line in text.splitlines():
if not line or line.startswith('#'):
continue
@@ -575,3 +578,9 @@ def parse_uri_list(text: str) -> Generator[str, None, None]:
continue
if purl.path:
yield unquote(purl.path)
class SSHConnectionData(NamedTuple):
binary: str
hostname: str
port: Optional[int] = None

135
kitty/wcswidth.c Normal file
View File

@@ -0,0 +1,135 @@
/*
* wcswidth.c
* Copyright (C) 2020 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "wcwidth-std.h"
#include "wcswidth.h"
#include "unicode-data.h"
void
initialize_wcs_state(WCSState *state) {
zero_at_ptr(state);
}
static inline bool
is_flag_pair(char_type a, char_type b) {
return is_flag_codepoint(a) && is_flag_codepoint(b);
}
int
wcswidth_step(WCSState *state, const char_type ch) {
int ans = 0;
switch (state->parser_state) {
case IN_CSI: {
state->prev_width = 0;
if (0x40 <= ch && ch <= 0x7e) state->parser_state = NORMAL;
} break;
case IN_ST_TERMINATED: {
state->prev_width = 0;
if (ch == 0x9c || (ch == '\\' && state->prev_ch == 0x1b)) state->parser_state = NORMAL;
} break;
case FLAG_PAIR_STARTED: {
state->parser_state = NORMAL;
if (is_flag_pair(state->prev_ch, ch)) break;
} /* fallthrough */
case NORMAL: {
switch(ch) {
case 0x1b: {
state->prev_width = 0;
state->parser_state = IN_ESC;
} break;
case 0xfe0f: {
if (is_emoji_presentation_base(state->prev_ch) && state->prev_width == 1) {
ans += 1;
state->prev_width = 2;
} else state->prev_width = 0;
} break;
case 0xfe0e: {
if (is_emoji_presentation_base(state->prev_ch) && state->prev_width == 2) {
ans -= 1;
state->prev_width = 1;
} else state->prev_width = 0;
} break;
default: {
if (is_flag_codepoint(ch)) state->parser_state = FLAG_PAIR_STARTED;
int w = wcwidth_std(ch);
switch(w) {
case -1:
case 0:
state->prev_width = 0; break;
case 2:
state->prev_width = 2; break;
default:
state->prev_width = 1; break;
}
ans += state->prev_width;
} break;
} break; // switch(ch)
} break; // case NORMAL
case IN_ESC:
switch (ch) {
case '[':
state->parser_state = IN_CSI; break;
case 'P':
case ']':
case 'X':
case '^':
case '_':
state->parser_state = IN_ST_TERMINATED; break;
case 'D':
case 'E':
case 'H':
case 'M':
case 'N':
case 'O':
case 'Z':
case '6':
case '7':
case '8':
case '9':
case '=':
case '>':
case 'F':
case 'c':
case 'l':
case 'm':
case 'n':
case 'o':
case '|':
case '}':
case '~':
break;
default:
state->prev_ch = 0x1b;
state->prev_width = 0;
state->parser_state = NORMAL;
return wcswidth_step(state, ch);
} break;
}
state->prev_ch = ch;
return ans;
}
PyObject *
wcswidth_std(PyObject UNUSED *self, PyObject *str) {
if (PyUnicode_READY(str) != 0) return NULL;
int kind = PyUnicode_KIND(str);
void *data = PyUnicode_DATA(str);
Py_ssize_t len = PyUnicode_GET_LENGTH(str), i;
WCSState state;
initialize_wcs_state(&state);
size_t ans = 0;
for (i = 0; i < len; i++) {
char_type ch = PyUnicode_READ(kind, data, i);
ans += wcswidth_step(&state, ch);
}
return PyLong_FromSize_t(ans);
}

22
kitty/wcswidth.h Normal file
View File

@@ -0,0 +1,22 @@
/*
* Copyright (C) 2020 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#pragma once
#include "data-types.h"
typedef enum {NORMAL, IN_ESC, IN_CSI, FLAG_PAIR_STARTED, IN_ST_TERMINATED} WCSParserState;
typedef struct {
char_type prev_ch;
int prev_width;
WCSParserState parser_state;
} WCSState;
void initialize_wcs_state(WCSState *state);
int wcswidth_step(WCSState *state, const char_type ch);
PyObject * wcswidth_std(PyObject UNUSED *self, PyObject *str);

4
kitty/wcwidth-std.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"
@@ -2848,7 +2848,7 @@ wcwidth_std(int32_t code) {
}
return 1;
}
static bool
static inline bool
is_emoji_presentation_base(uint32_t code) {
switch(code) {
case 0x23:

View File

@@ -8,6 +8,8 @@ import sys
import weakref
from collections import deque
from enum import IntEnum
from functools import partial
from gettext import gettext as _
from itertools import chain
from typing import (
Any, Callable, Deque, Dict, Iterable, List, Optional, Pattern, Sequence,
@@ -20,16 +22,17 @@ from .config import build_ansi_color_table
from .constants import ScreenGeometry, WindowGeometry, appname, wakeup
from .fast_data_types import (
BGIMAGE_PROGRAM, BLIT_PROGRAM, CELL_BG_PROGRAM, CELL_FG_PROGRAM,
CELL_PROGRAM, CELL_SPECIAL_PROGRAM, CSI, DCS, DECORATION, DIM,
CELL_PROGRAM, CELL_SPECIAL_PROGRAM, DCS, DECORATION, DIM,
GRAPHICS_ALPHA_MASK_PROGRAM, GRAPHICS_PREMULT_PROGRAM, GRAPHICS_PROGRAM,
MARK, MARK_MASK, OSC, REVERSE, SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE,
STRIKETHROUGH, TINT_PROGRAM, Screen, add_window, cell_size_for_window,
compile_program, get_boss, get_clipboard_string, init_cell_program,
pt_to_px, set_clipboard_string, set_titlebar_color, set_window_padding,
set_window_render_data, update_window_title, update_window_visibility,
viewport_for_window
STRIKETHROUGH, TINT_PROGRAM, Screen, add_timer, add_window,
cell_size_for_window, compile_program, get_boss, get_clipboard_string,
init_cell_program, pt_to_px, set_clipboard_string, set_titlebar_color,
set_window_padding, set_window_render_data, update_window_title,
update_window_visibility, viewport_for_window
)
from .keys import defines, extended_key_event, keyboard_mode_name
from .notify import NotificationCommand, handle_notification_cmd
from .options_stub import Options
from .rgb import to_color
from .terminfo import get_capabilities
@@ -86,9 +89,48 @@ class Watcher:
class Watchers:
on_resize: List[Watcher]
on_close: List[Watcher]
on_focus_change: List[Watcher]
def __init__(self) -> None:
self.on_resize: List[Watcher] = []
self.on_close: List[Watcher] = []
self.on_resize = []
self.on_close = []
self.on_focus_change = []
def add(self, others: 'Watchers') -> None:
def merge(base: List[Watcher], other: List[Watcher]) -> None:
for x in other:
if x not in base:
base.append(x)
merge(self.on_resize, others.on_resize)
merge(self.on_close, others.on_close)
merge(self.on_focus_change, others.on_focus_change)
def clear(self) -> None:
del self.on_close[:], self.on_resize[:], self.on_focus_change[:]
def copy(self) -> 'Watchers':
ans = Watchers()
ans.on_close = self.on_close[:]
ans.on_resize = self.on_resize[:]
ans.on_focus_change = self.on_focus_change
return ans
@property
def has_watchers(self) -> bool:
return bool(self.on_close or self.on_resize or self.on_focus_change)
def call_watchers(windowref: Callable[[], Optional['Window']], which: str, data: Dict[str, Any]) -> None:
def callback(timer_id: Optional[int]) -> None:
w = windowref()
if w is not None:
watchers: List[Watcher] = getattr(w.watchers, which)
w.call_watchers(watchers, data)
add_timer(callback, 0, False)
def calculate_gl_geometry(window_geometry: WindowGeometry, viewport_width: int, viewport_height: int, cell_width: int, cell_height: int) -> ScreenGeometry:
@@ -100,6 +142,38 @@ def calculate_gl_geometry(window_geometry: WindowGeometry, viewport_width: int,
return ScreenGeometry(xstart, ystart, window_geometry.xnum, window_geometry.ynum, dx, dy)
def as_text(
screen: Screen,
as_ansi: bool = False,
add_history: bool = False,
add_wrap_markers: bool = False,
alternate_screen: bool = False
) -> str:
lines: List[str] = []
add_history = add_history and not (screen.is_using_alternate_linebuf() ^ alternate_screen)
if alternate_screen:
f = screen.as_text_alternate
else:
f = screen.as_text_non_visual if add_history else screen.as_text
f(lines.append, as_ansi, add_wrap_markers)
if add_history:
h: List[str] = []
pht = screen.historybuf.pagerhist_as_text()
if pht:
h.append(pht)
if h and (not as_ansi or not add_wrap_markers):
sanitizer = text_sanitizer(as_ansi, add_wrap_markers)
h = list(map(sanitizer, h))
screen.historybuf.as_text(h.append, as_ansi, add_wrap_markers)
if h:
if not screen.linebuf.is_continued(0):
h[-1] += '\n'
if as_ansi:
h[-1] += '\x1b[m'
return ''.join(chain(h, lines))
return ''.join(lines)
class LoadShaderPrograms:
use_selection_fg = True
@@ -218,6 +292,7 @@ class Window:
watchers: Optional[Watchers] = None
):
self.watchers = watchers or Watchers()
self.prev_osc99_cmd = NotificationCommand()
self.action_on_close: Optional[Callable] = None
self.action_on_removal: Optional[Callable] = None
self.current_marker_spec: Optional[Tuple[str, Union[str, Tuple[Tuple[int, str], ...]]]] = None
@@ -289,7 +364,12 @@ class Window:
self.update_effective_padding()
def effective_border(self) -> int:
return pt_to_px(self.opts.window_border_width, self.os_window_id)
val, unit = self.opts.window_border_width
if unit == 'pt':
val = max(1 if val > 0 else 0, pt_to_px(val, self.os_window_id))
else:
val = round(val)
return int(val)
@property
def title(self) -> str:
@@ -383,7 +463,7 @@ class Window:
self.screen.resize(new_geometry.ynum, new_geometry.xnum)
sg = self.update_position(new_geometry)
self.needs_layout = False
self.call_watchers(self.watchers.on_resize, {'old_geometry': self.geometry, 'new_geometry': new_geometry})
call_watchers(weakref.ref(self), 'on_resize', {'old_geometry': self.geometry, 'new_geometry': new_geometry})
else:
sg = self.update_position(new_geometry)
current_pty_size = (
@@ -434,20 +514,80 @@ class Window:
self.override_title = title or None
self.title_updated()
def desktop_notify(self, osc_code: int, raw_data: str) -> None:
cmd = handle_notification_cmd(osc_code, raw_data, self.id, self.prev_osc99_cmd)
if cmd is not None and osc_code == 99:
self.prev_osc99_cmd = cmd
# screen callbacks {{{
def use_utf8(self, on: bool) -> None:
get_boss().child_monitor.set_iutf8_winid(self.id, on)
def open_url(self, url: str, hyperlink_id: int, cwd: Optional[str] = None) -> None:
if hyperlink_id:
if not self.opts.allow_hyperlinks:
return
from urllib.parse import unquote, urlparse, urlunparse
try:
purl = urlparse(url)
except Exception:
return
if (not purl.scheme or purl.scheme == 'file'):
if purl.netloc:
from socket import gethostname
try:
hostname = gethostname()
except Exception:
hostname = ''
remote_hostname = purl.netloc.partition(':')[0]
if remote_hostname and remote_hostname != hostname and remote_hostname != 'localhost':
self.handle_remote_file(purl.netloc, unquote(purl.path))
return
url = urlunparse(purl._replace(netloc=''))
if self.opts.allow_hyperlinks & 0b10:
from kittens.tui.operations import styled
get_boss()._run_kitten('ask', ['--type=choices', '--message', _(
'What would you like to do with this URL:\n') +
styled(unquote(url), fg='yellow'),
'--choice=o:Open', '--choice=c:Copy to clipboard', '--choice=n;red:Nothing'
],
window=self,
custom_callback=partial(self.hyperlink_open_confirmed, url, cwd)
)
return
get_boss().open_url(url, cwd=cwd)
def hyperlink_open_confirmed(self, url: str, cwd: Optional[str], data: Dict[str, Any], *a: Any) -> None:
q = data['response']
if q == 'o':
get_boss().open_url(url, cwd=cwd)
elif q == 'c':
set_clipboard_string(url)
def handle_remote_file(self, netloc: str, remote_path: str) -> None:
from kittens.ssh.main import get_connection_data
args = self.child.foreground_cmdline
conn_data = get_connection_data(args)
if conn_data is None:
get_boss().show_error('Could not handle remote file', 'No SSH connection data found in: {args}')
return
get_boss().run_kitten(
'remote_file', '--hostname', netloc.partition(':')[0], '--path', remote_path,
'--ssh-connection-data', json.dumps(conn_data)
)
def focus_changed(self, focused: bool) -> None:
if self.destroyed:
return
call_watchers(weakref.ref(self), 'on_focus_change', {'focused': focused})
self.screen.focus_changed(focused)
if focused:
changed = self.needs_attention
self.needs_attention = False
if self.screen.focus_tracking_enabled:
self.screen.send_escape_code_to_child(CSI, 'I')
else:
if self.screen.focus_tracking_enabled:
self.screen.send_escape_code_to_child(CSI, 'O')
if changed:
tab = self.tabref()
if tab is not None:
tab.relayout_borders()
def title_changed(self, new_title: Optional[str]) -> None:
self.child_title = sanitize_title(new_title or self.default_title)
@@ -461,17 +601,24 @@ class Window:
def is_active(self) -> bool:
return get_boss().active_window is self
@property
def has_activity_since_last_focus(self) -> bool:
return self.screen.has_activity_since_last_focus()
def on_bell(self) -> None:
if self.opts.command_on_bell and self.opts.command_on_bell != ['none']:
import subprocess
import shlex
import subprocess
env = self.child.final_env
env['KITTY_CHILD_CMDLINE'] = ' '.join(map(shlex.quote, self.child.cmdline))
subprocess.Popen(self.opts.command_on_bell, env=env, cwd=self.child.foreground_cwd)
if not self.is_active:
changed = not self.needs_attention
self.needs_attention = True
tab = self.tabref()
if tab is not None:
if changed:
tab.relayout_borders()
tab.on_bell(self)
def change_titlebar_color(self) -> None:
@@ -511,6 +658,9 @@ class Window:
b |= b << 8
self.screen.send_escape_code_to_child(OSC, '{};rgb:{:04x}/{:04x}/{:04x}'.format(code, r, g, b))
def report_notification_activated(self, identifier: str) -> None:
self.screen.send_escape_code_to_child(OSC, f'99;i={identifier};')
def set_dynamic_color(self, code: int, value: Union[str, bytes]) -> None:
if isinstance(value, bytes):
value = value.decode('utf-8')
@@ -654,27 +804,7 @@ class Window:
add_wrap_markers: bool = False,
alternate_screen: bool = False
) -> str:
lines: List[str] = []
add_history = add_history and not (self.screen.is_using_alternate_linebuf() ^ alternate_screen)
if alternate_screen:
f = self.screen.as_text_alternate
else:
f = self.screen.as_text_non_visual if add_history else self.screen.as_text
f(lines.append, as_ansi, add_wrap_markers)
if add_history:
h: List[str] = []
self.screen.historybuf.pagerhist_as_text(h.append)
if h and (not as_ansi or not add_wrap_markers):
sanitizer = text_sanitizer(as_ansi, add_wrap_markers)
h = list(map(sanitizer, h))
self.screen.historybuf.as_text(h.append, as_ansi, add_wrap_markers)
if h:
if not self.screen.linebuf.is_continued(0):
h[-1] += '\n'
if as_ansi:
h[-1] += '\x1b[m'
return ''.join(chain(h, lines))
return ''.join(lines)
return as_text(self.screen, as_ansi, add_history, add_wrap_markers, alternate_screen)
@property
def cwd_of_child(self) -> Optional[str]:
@@ -806,7 +936,7 @@ class Window:
self.current_marker_spec = key
def set_marker(self, spec: Union[str, Sequence[str]]) -> None:
from .config import toggle_marker, parse_marker_spec
from .config import parse_marker_spec, toggle_marker
from .marks import marker_from_spec
if isinstance(spec, str):
func, (ftype, spec_, flags) = toggle_marker('toggle_marker', spec)

View File

@@ -37,10 +37,18 @@ class Callbacks:
def use_utf8(self, on):
self.iutf8 = on
def desktop_notify(self, osc_code: int, raw_data: str) -> None:
self.notifications.append((osc_code, raw_data))
def open_url(self, url: str, hyperlink_id: int) -> None:
self.open_urls.append((url, hyperlink_id))
def clear(self):
self.wtcbuf = b''
self.iconbuf = self.titlebuf = self.colorbuf = self.ctbuf = ''
self.iutf8 = True
self.notifications = []
self.open_urls = []
def filled_line_buf(ynum=5, xnum=5, cursor=Cursor()):

View File

@@ -354,9 +354,12 @@ class TestDataTypes(BaseTest):
def test_utils(self):
def w(x):
return wcwidth(ord(x))
self.ae(wcswidth('a\033[2mb'), 2)
self.ae(wcswidth('\033a\033[2mb'), 2)
self.ae(wcswidth('a\033]8;id=moo;https://foo\033\\a'), 2)
self.ae(wcswidth('a\033x'), 2)
self.ae(tuple(map(w, 'a1\0コニチ ✔')), (1, 1, 0, 2, 2, 2, 1, 1))
self.ae(wcswidth('\u2716\u2716\ufe0f\U0001f337'), 5)
self.ae(wcswidth('\033a\033[2mb'), 2)
self.ae(wcswidth('\u25b6\ufe0f'), 2)
self.ae(wcswidth('\U0001f610\ufe0e'), 1)
self.ae(wcswidth('\U0001f1e6a'), 3)

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