Compare commits

...

299 Commits

Author SHA1 Message Date
Kovid Goyal
6fa4110da9 version 0.24.2 2022-02-03 11:44:12 +05:30
Kovid Goyal
43ceaf0b7e Merge branch 'shorten-long-line' of https://github.com/Luflosi/kitty 2022-02-01 17:15:09 +05:30
Luflosi
301ca76d14 Shorten a long line
This line is getting really close to the limit of 160 characters.
2022-02-01 12:16:13 +01:00
Kovid Goyal
a591304412 ... 2022-02-01 13:39:42 +05:30
Kovid Goyal
8fffd6070a Fix #4605 2022-02-01 12:01:08 +05:30
Kovid Goyal
a899a8ab6d Sanitize window sizes read from cached values 2022-02-01 11:23:36 +05:30
Kovid Goyal
9c13e8a549 Allow controlling placement of bell and activity symbols in the tab title template
Fixes #4581
2022-02-01 08:58:49 +05:30
Kovid Goyal
68d2ea3f27 Get config generation working with py3.10
The typing classes now have a __name__ attribute
2022-02-01 08:29:34 +05:30
Kovid Goyal
2c0269930f kitty @ get-text: Add a n option to clear the current selection after getting text
See #4600
2022-01-31 08:22:05 +05:30
Kovid Goyal
c4953504ba ... 2022-01-31 08:19:45 +05:30
Kovid Goyal
e7ee6d9e26 A new action to clear the current selection
Fixes #4600
2022-01-31 08:17:40 +05:30
Kovid Goyal
1cb3875cb5 Merge branch 'minor-tweaks' of https://github.com/page-down/kitty 2022-01-30 15:51:02 +05:30
pagedown
14ae2d0862 Make the colons colored like the others 2022-01-30 17:37:17 +08:00
pagedown
539ec69d3f Remove a space that was introduced by mistake 2022-01-30 17:34:58 +08:00
Kovid Goyal
bc873ad838 bash shell integration: To title and cursor escape codes at end of PS1/PS0 so that they override pre-existing ones
This is needed because some distros like Debian patch the default bashrc
to set a user@host:/pwd style title which is extremely unsuitable for
kitty.
2022-01-30 11:13:46 +05:30
Kovid Goyal
f056b7c1da Improve docs for splits layout
Fixes #4592
2022-01-30 09:44:33 +05:30
Kovid Goyal
2e790a119a Merge branch 'refactor-fstring' of https://github.com/page-down/kitty 2022-01-30 09:26:17 +05:30
pagedown
4a3ed62809 Refactor: More f-string for kittens 2022-01-29 20:23:34 +08:00
pagedown
4110074580 Refactor: More f-string for kitty client.py 2022-01-29 20:22:44 +08:00
pagedown
c21f00c476 Refactor: More f-string for kitty fonts and rc send_text 2022-01-29 20:22:22 +08:00
pagedown
1ca1c2edad Refactor: More f-string for conf 2022-01-29 20:18:11 +08:00
pagedown
ba0f61d752 Refactor: More f-string for kitty 2022-01-29 20:17:46 +08:00
pagedown
74e70d2548 Refactor: More f-string for bypy scripts 2022-01-29 20:15:17 +08:00
pagedown
74921c1373 Refactor: Sorting items when generating files
Make the files generated in different environments consistent.
2022-01-29 20:14:56 +08:00
pagedown
a22f37b919 Refactor: More f-string for tools 2022-01-29 20:14:36 +08:00
Kovid Goyal
9f250fdf26 Only dump env var names not values 2022-01-29 11:55:26 +05:30
Kovid Goyal
d92eba82bc Dump env vars seen by kitty for debug config 2022-01-29 11:36:58 +05:30
Kovid Goyal
a052958a20 Merge branch 'macos' of https://github.com/page-down/kitty 2022-01-29 11:26:09 +05:30
pagedown
81e7e6a356 macOS: Disable sudden termination
Since we have the quit confirmation feature, so we explicitly specify
that sudden termination is not supported.
2022-01-29 13:41:59 +08:00
pagedown
953cf26369 macOS: Fix info plist structure version
Set the InfoDictionary version explicitly, hopefully the behavior will
be unchanged when the default version is upgraded.
2022-01-29 13:37:21 +08:00
pagedown
7649e2f3ea Docs: Fix macOS shortcut for debug_config 2022-01-29 13:32:36 +08:00
Kovid Goyal
2a6a77467c Merge branch 'refactor-fstring' of https://github.com/page-down/kitty 2022-01-28 17:26:43 +05:30
pagedown
dc61adf9d8 Refactor: More f-string 2022-01-28 19:34:13 +08:00
Kovid Goyal
e0c4a90aa3 DRYer 2022-01-28 13:21:54 +05:30
Kovid Goyal
5cdff7d6d0 themes kitten: Allow writing to a different file than kitty.conf 2022-01-27 20:24:39 +05:30
Kovid Goyal
4f6c16984a Do not send arrow key presses to shells when clicking on a window with the mouse to focus it
Fixes #4128
2022-01-27 20:07:17 +05:30
Kovid Goyal
7e364b1606 Fix incorrect shortcut in docs 2022-01-27 19:45:35 +05:30
Kovid Goyal
a24df07d25 Merge branch 'fix-on-macOS-10.12' of https://github.com/Luflosi/kitty 2022-01-27 17:35:20 +05:30
Luflosi
347c0d4de4 Fix compilation on macOS 10.12
Without this commit, kitty would fail to compile on macOS 10.12 with the error "use of undeclared identifier".
This problem was introduced in f7be4fab48, where some code was moved to a different file, without moving the corresponding `#define` statements.
2022-01-27 12:21:17 +01:00
Kovid Goyal
ca2e9f571f Merge branch 'refactor' of https://github.com/page-down/kitty 2022-01-27 16:40:17 +05:30
pagedown
9be57de4a9 macOS: List kitty in the open with menu of an arbitrary file
Allow opening files without extension name following the rules in launch
actions configuration.
2022-01-27 18:16:35 +08:00
pagedown
cc84ee9734 Refactor: Replace string concat with f-string 2022-01-27 17:57:25 +08:00
pagedown
b5ad030a14 macOS: Categorize and update bundle info plist
Allow toggling input sources using CapsLock.
Allow programs to request photo, motion, speech recognition permissions.
Add local network unicast or multicast usage description.
Remove iOS-only items.
2022-01-27 17:55:43 +08:00
Kovid Goyal
40b5c43920 ... 2022-01-27 12:14:42 +05:30
Kovid Goyal
9f2be8b696 Fix #4573 2022-01-27 11:29:33 +05:30
Kovid Goyal
4397e60206 Add changelog entry for cursor shape restoration 2022-01-27 11:19:53 +05:30
Kovid Goyal
caab98e70b remove duplicate changelog entry 2022-01-27 11:18:15 +05:30
Kovid Goyal
11a4b75ffa Merge branch 'fix-ime' of https://github.com/page-down/kitty 2022-01-27 10:46:37 +05:30
Kovid Goyal
d25dceacdd Merge branch 'remote-control' of https://github.com/page-down/kitty 2022-01-27 10:38:43 +05:30
pagedown
b9a612fb9c macOS: Clean up any text that may not have been sent when loses focus 2022-01-27 12:37:17 +08:00
pagedown
3b9a7914ee Also add alias --dont-take-focus to new_window 2022-01-27 12:28:40 +08:00
pagedown
f646dfa238 Cancel IME for active kitty or OS window only 2022-01-27 11:48:48 +08:00
Kovid Goyal
0b2a533b44 Merge branch 'fix-ime' of https://github.com/page-down/kitty 2022-01-27 08:03:08 +05:30
Kovid Goyal
0e1246c15b Merge branch 'fix-cache-defrag' of https://github.com/Luflosi/kitty 2022-01-27 07:49:54 +05:30
pagedown
a987fb81a0 Emits an IME lost focus event when the window lost focus or closed 2022-01-27 10:13:57 +08:00
pagedown
4d920b182f macOS: Cancel IME composition when receive loss of focus event 2022-01-27 10:07:18 +08:00
pagedown
22f077b384 Clear overlay text after screen loses focus 2022-01-27 09:53:40 +08:00
Luflosi
4c78a50dcf Add more copy_file_range() and sendfile() errno exceptions
On ZFS `copy_file_range()` and `sendfile()` fail for some reason, so add `EINVAL` to the list of error codes for which the code will fall back to a slower implementation.
While we are at it, add all the error codes from https://go.dev/src/internal/poll/copy_file_range_linux.go for different filesystems and older kernel versions to the list of error codes from `copy_file_range()`.
Without this, the cache defragmentation and the test testing it (`test_disk_cache`) fail with the error message "Failed to copy data to new disk cache file during defrag: Invalid argument" when the cache directory is stored on ZFS.
Perhaps this is caused by me running ZFS 2.1.2 on Linux 5.16.0, which is not a supported combination. This ZFS version is only "compatible with 3.10 - 5.15 kernels" according to the release notes. Or maybe it's just not implemented.
To reproduce the problem, execute `KITTY_CACHE_DIRECTORY=/path/to/zfs python3 test.py` with `KITTY_CACHE_DIRECTORY` pointing to a directory on ZFS.
2022-01-27 00:45:30 +01:00
Luflosi
9d87b08c52 Fix logic error in file copy helper functions
If `errno` is `EAGAIN`, the second if statement is never true and if `errno` is not `EAGAIN`, the function returns immediately, not checking the second if statement.
This commit fixes this.
A logically equivalent change would have been to simply swap the two if statements but I find this version to be more readable.
2022-01-26 23:35:54 +01:00
Kovid Goyal
97e58c5b86 Merge branch 'macos-ime' of https://github.com/page-down/kitty 2022-01-26 16:49:10 +05:30
pagedown
b3f678cf6f Improve debug log 2022-01-26 18:28:37 +08:00
pagedown
7f3715cd3e Use the input context instance from GLFWContentView 2022-01-26 18:25:37 +08:00
pagedown
a3aaaa1efb macOS: Explicitly cancel IME composition when losing the focus 2022-01-26 18:21:20 +08:00
Kovid Goyal
d1531632a2 Add some notes on how to run IBUS for testing 2022-01-26 10:47:18 +05:30
Kovid Goyal
2151969ae5 IBUS: Hide the pre-edit text when receiving a HidePreeditText message 2022-01-26 10:12:48 +05:30
Kovid Goyal
44e3a6f0fb Shell integration: Set cursor shape to whatever is specified in the cursor_shape option when executing a command rather than the block shape 2022-01-25 13:30:48 +05:30
Kovid Goyal
364bad504c CSI 0 q should blink cursor as per xterm docs and behavior 2022-01-25 13:29:44 +05:30
Kovid Goyal
0648b4f568 Merge branch 'ksi-fish' of https://github.com/page-down/kitty 2022-01-25 11:33:06 +05:30
pagedown
a47b3bf578 Fix the blinking block cursor shape for commands in bash integration 2022-01-25 14:00:28 +08:00
pagedown
3aec573d40 Use blinking block for commands consistent with zsh and bash integration 2022-01-25 13:58:11 +08:00
pagedown
5d4b0c0bef Set the pre-exec cursor shape in fish vi mode and config on demand 2022-01-25 13:01:54 +08:00
pagedown
11c0343420 Add comments and minor tweaks to fish integration 2022-01-25 11:52:34 +08:00
Kovid Goyal
287b843d13 add a comment 2022-01-24 22:14:56 +05:30
Kovid Goyal
1fea4777fa zsh shell integration: Restore cursor to blinking block before running external commands 2022-01-24 22:06:52 +05:30
Kovid Goyal
b5a2f94185 The open_actions() test needs options set 2022-01-24 15:21:32 +05:30
Kovid Goyal
1000376a4f ... 2022-01-24 09:50:37 +05:30
Kovid Goyal
02e1272502 Slightly improve debug text 2022-01-23 20:40:41 +05:30
Kovid Goyal
393e5eb158 Merge branch 'fix-macos-ime' of https://github.com/page-down/kitty 2022-01-23 20:38:43 +05:30
pagedown
b2d0c35fc2 Clear the pre-edit text only when inserting non-control characters
Maintains the pre-edit input state after pressing the modifier key
combination or function key while using the IME.
2022-01-23 22:22:24 +08:00
pagedown
3ee12082f2 No need to update the pre-edit text 2022-01-23 22:16:26 +08:00
pagedown
1ce4fc9c8c Check for ASCII control chars before sending text 2022-01-23 21:40:27 +08:00
pagedown
57e2866b68 macOS: Fix caps_lock not clearing pre-edit text when using IME 2022-01-23 18:50:55 +08:00
pagedown
195fc1a270 Minor tweaks to macOS key event debug log 2022-01-23 18:45:18 +08:00
Kovid Goyal
39e56f1864 Dont check the return value of handleEvent
It's apparently meaningless, despite what the docs say
2022-01-23 12:47:29 +05:30
Kovid Goyal
da96dfcad2 Forgot to use override 2022-01-23 11:33:58 +05:30
Kovid Goyal
053bb999d3 Merge branch 'fix-macos-ime-modifiers' of https://github.com/page-down/kitty 2022-01-23 10:41:45 +05:30
Kovid Goyal
e914f0df8e Make FAIL_WARN a boolean 2022-01-23 10:12:11 +05:30
Kovid Goyal
78cd83845f Fix sphinx failing to highlight fish 2022-01-23 09:43:15 +05:30
Kovid Goyal
f6228cc81e Merge branch 'fix-macos-ime' of https://github.com/page-down/kitty 2022-01-23 08:17:16 +05:30
pagedown
83884f42f8 macOS: Fix text insertion by modifier keys when using IME 2022-01-23 01:50:37 +08:00
pagedown
fbaf3e2f49 Update preedit text with markedText
This fixes the preedit text being cleared when using shortcut keys to
switch candidate text in the input state.
2022-01-23 00:01:08 +08:00
Kovid Goyal
c0be0f74d8 Use commit text instead of a zero key event
Thanks @page-down
2022-01-22 21:11:26 +05:30
Kovid Goyal
a9e014226e Merge branch 'docs' of https://github.com/page-down/kitty 2022-01-22 20:58:30 +05:30
pagedown
b498d86436 Docs: Add modifier key names and aliases 2022-01-22 23:08:44 +08:00
Kovid Goyal
ff836a85aa Special case the IME injecting text in on_key_input 2022-01-22 20:00:03 +05:30
Kovid Goyal
688a3eaef7 Check if input context handles flagschanged event 2022-01-22 16:17:01 +05:30
Kovid Goyal
3b15b46d08 Also deal with setMarkedText called from event loop with actual marked text 2022-01-22 16:13:44 +05:30
Kovid Goyal
f21ddae94f Handle setMarkedText called from the event loop
Also pass through text with zero key
2022-01-22 16:08:50 +05:30
Kovid Goyal
cd9b752926 ... 2022-01-22 16:02:05 +05:30
Kovid Goyal
655a5a2808 Handle insertText called from the event loop by the text input system 2022-01-22 15:51:43 +05:30
Kovid Goyal
5f4e519d61 ... 2022-01-22 15:13:53 +05:30
Kovid Goyal
720bf7efd6 Fix mouse position reported incorrectly on click event in a newly created window if the mouse has never moved in that window 2022-01-22 15:10:14 +05:30
Kovid Goyal
3867620be5 ... 2022-01-22 14:58:10 +05:30
Kovid Goyal
03a7361325 Some debug prints for flagsChanged 2022-01-22 14:57:13 +05:30
Kovid Goyal
e8c683db8b A new approach to handling flagsChanged with IME
Bypass cocoa's flagsChanged handling and call handleevent ourselves,
this allows us to know exactly what the IME is doing. Hopefully fixes
issues with IMEs that change state on modifier key presses.

Fixes #4541
2022-01-22 14:47:57 +05:30
Kovid Goyal
0a3acd9738 ask kitten: Use buttons for choices as well 2022-01-22 13:57:25 +05:30
Kovid Goyal
08f159c2ba TypeCheck underline style names 2022-01-22 12:41:26 +05:30
Kovid Goyal
d4e7587686 use underline instead of color for default choice 2022-01-22 12:35:43 +05:30
Kovid Goyal
d219654387 Use a dedicated choose() method matching confirm() 2022-01-22 12:27:20 +05:30
Kovid Goyal
32e31a3c6b Cancelling the choice kitten should return the empty response 2022-01-22 12:13:32 +05:30
Kovid Goyal
9419125387 Highlight the default choice in yellow 2022-01-22 11:45:01 +05:30
Kovid Goyal
1bbab217d0 ask kitten: Always have a default choice 2022-01-22 11:39:54 +05:30
Kovid Goyal
05d648d411 ask kitten: Draw yes/no buttons 2022-01-22 11:32:26 +05:30
Kovid Goyal
5c832d3190 Allow multiple clickable ranges 2022-01-22 10:52:44 +05:30
Kovid Goyal
ce7282a53c DRYer 2022-01-22 10:47:52 +05:30
Kovid Goyal
0d33380e6a Cleanup previous PR
yesno default should be yes.
Pressing q to quit is undiscoverable
Clicking empty cells should not trigger buttons as that is
undiscoverable and possibly surprising
2022-01-22 10:44:01 +05:30
Kovid Goyal
9944b895a6 Merge branch 'kitten-ask' of https://github.com/page-down/kitty 2022-01-22 10:06:52 +05:30
Kovid Goyal
fe7045dd2f Ask kitten: Fix rendering of multiline messages 2022-01-22 09:40:05 +05:30
Kovid Goyal
d17757cc84 Merge branch 'feat-underline' of https://github.com/page-down/kitty 2022-01-22 09:28:13 +05:30
pagedown
7ef73babdd Use fixed values for unchanging dict to replace split and for loops 2022-01-22 11:36:04 +08:00
pagedown
a47aca6fdb Add new dotted and dashed underline enum
Also use "straight" for single underline, consistent with the protocol
extension document.
2022-01-22 11:00:49 +08:00
pagedown
bf9d47fddd ... 2022-01-22 09:51:28 +08:00
pagedown
4fe10fadb0 ... 2022-01-22 09:45:32 +08:00
pagedown
d29faa0986 Allow tab title to be set to empty to use window title 2022-01-22 03:28:42 +08:00
pagedown
88ee5e95fc Add default choices for features that use ask kitten 2022-01-22 03:21:59 +08:00
pagedown
9a119255fe ask kitten: Use the default value as input text
Prefill the current value when changing the tab title.
2022-01-22 03:11:39 +08:00
pagedown
7ce11050cd ask kitten: Esc to abort and Enter to confirm the default choice 2022-01-22 02:50:50 +08:00
pagedown
9b4d1219b8 Add default choice option for ask kitten 2022-01-22 02:46:37 +08:00
pagedown
4dd29c726a Make the clickable range of No the same as Yes
Since the two options are treated equally, the same amount of mouse
clickable range are given.
2022-01-22 02:41:52 +08:00
Kovid Goyal
17e31565e2 Get it working even with 2 row windows assuming they are wide enough to fit the title on a single line 2022-01-21 21:46:27 +05:30
Kovid Goyal
dd72deb546 Allow ask kitten to function in a 3 line window 2022-01-21 21:43:25 +05:30
Kovid Goyal
7e82179c79 ... 2022-01-21 21:20:49 +05:30
Kovid Goyal
d61e655953 Changelog entry for new underline styles 2022-01-21 21:16:23 +05:30
Kovid Goyal
cd0ca95877 Improve the UI of the ask kitten
Center the text in the window and allow pressing enter/esc when doing a
yes/no question

Fixes #4545
2022-01-21 20:02:13 +05:30
Kovid Goyal
d70610f5d9 more uses of extlinks 2022-01-21 08:45:03 +05:30
Kovid Goyal
f9cfb6f989 use sphinx extlinks instead of custon roles 2022-01-21 08:19:57 +05:30
Kovid Goyal
1f4372b34c Test if text is being inserted rather than if text is present 2022-01-20 11:16:05 +05:30
Kovid Goyal
76e7e7d61f ... 2022-01-20 08:45:03 +05:30
Kovid Goyal
c09dc73476 ... 2022-01-20 08:33:44 +05:30
Kovid Goyal
b090602fdb macOS: Fix the shift key not clearing pre-edit text in IME
Fixes #4541
2022-01-20 08:00:17 +05:30
Kovid Goyal
16d3e82515 Add debug output when ignoring key press during IME 2022-01-19 14:50:12 +05:30
Kovid Goyal
9caa6bb306 Nicer event printing 2022-01-19 14:32:03 +05:30
Kovid Goyal
acf227803f Move event formatting into the types 2022-01-19 14:30:08 +05:30
Kovid Goyal
f255c15fa9 DRYer 2022-01-19 14:04:14 +05:30
Kovid Goyal
7ce74ac688 Also change string based on event type 2022-01-19 13:58:45 +05:30
Kovid Goyal
6090673bb5 DRYer 2022-01-19 13:56:02 +05:30
Kovid Goyal
d1b5420b08 Display space as a word when printing keys 2022-01-19 13:33:53 +05:30
Kovid Goyal
e0f2b96fd2 Merge branch 'debug-config' of https://github.com/page-down/kitty 2022-01-19 13:16:26 +05:30
Kovid Goyal
47fdce6483 remove executable perms from boss.py 2022-01-19 13:12:52 +05:30
pagedown
bac84f31b3 Refactor the print functions to make it easier to use in kitten 2022-01-19 15:26:02 +08:00
Kovid Goyal
21bc5bf8c0 Dont leak a reference to input_source_at_last_key_event 2022-01-19 12:06:54 +05:30
Kovid Goyal
0ae928f409 Increase max cache size for distribute_dots
Since it is now also used to render dotted underlines
2022-01-19 12:00:17 +05:30
pagedown
7bf7cc284d Add the missing mouse triggers click (-2) and double-click (-3) 2022-01-19 14:12:39 +08:00
pagedown
ff1efebf70 Fix mouse maps are indented with one more space 2022-01-19 14:07:45 +08:00
pagedown
9334ed773e Adjust the order of modifier key names to match the config and docs 2022-01-19 14:05:51 +08:00
Kovid Goyal
5d8a0c4b09 Merge branch 'ksi-fish' of https://github.com/page-down/kitty 2022-01-19 08:38:15 +05:30
pagedown
c5e8a36f9b Shell integration: Improvements for fish integration
Move the completion function to the autocomplete script.
Fix some issues with cursor shape setting, respect user configuration.
Functions and vars start with two underscores, following the convention.
Other minor refactoring.
2022-01-19 09:39:23 +08:00
Kovid Goyal
39f9efc816 Add changelog entry for last PR 2022-01-18 21:10:02 +05:30
Kovid Goyal
17a3be8cb1 Merge branch 'dotted-dashed-underline' of https://github.com/jcla1/kitty 2022-01-18 21:03:30 +05:30
Joseph Adams
ee2c9775a7 Use distribute_dots() to even out spacing in underline 2022-01-18 15:46:31 +01:00
Joseph Adams
da486153de Add (possibly erroneous) test for new underline.
Also make more clear *what* exactly is rendered in the cell (i.e. a
strikethrough).
2022-01-18 12:52:59 +01:00
Joseph Adams
dd192ad0b7 Make old tests run
Previously, because of the new underline styles a couple of tests were
failing due to an unexpected number of sprites being returned from the
test-set-up. No new tests were added.
2022-01-18 12:28:24 +01:00
Joseph Adams
3c6c36487f Update underline documentation 2022-01-18 12:13:51 +01:00
Kovid Goyal
85eedb96ac Merge branch 'docs' of https://github.com/page-down/kitty 2022-01-18 16:37:11 +05:30
pagedown
3a8d867c87 Docs: Add macOS shortcuts and nth_os_window 2022-01-18 18:59:26 +08:00
Kovid Goyal
6a2522452f Allow including config files matching glob patterns in conf files
Fixes #4533
2022-01-18 16:28:19 +05:30
Kovid Goyal
f9a4b6bb0d Use an enum for state as well 2022-01-18 13:58:55 +05:30
Kovid Goyal
f1fbfe297d Use an enum for background work 2022-01-18 13:50:05 +05:30
Kovid Goyal
2c0ac895e6 diff kitten: Fix exiting while highlighting is incomplete causes kitten to hang until highlighting is done 2022-01-18 13:43:39 +05:30
Joseph Adams
e86c7d668c Add rendering functions for dotted and dashed underline.
Dashed underline looks pretty good regardless of conditions, but the
dotted underline only looks good/correct on certain font-sizes. This is
due to the underline being rendered on a per cell/glyph basis (so one
can not place a dot directly between two letters, say. Could be remedied
by pulling the rendering of the underlines into the shader, but that is
more work.
2022-01-17 13:25:08 +01:00
Kovid Goyal
4778e0eb23 Move the section about integration to a better location 2022-01-17 10:41:35 +05:30
Kovid Goyal
c3ded5eb72 ... 2022-01-17 10:39:13 +05:30
Kovid Goyal
323355d4bb Add a press mentions section to the kitty website 2022-01-17 10:26:49 +05:30
Kovid Goyal
92075a44d8 Add a note about using icat from other programs 2022-01-17 08:35:48 +05:30
Kovid Goyal
386bb837d8 Merge branch 'fix-macos-kbd-shortcuts' of https://github.com/page-down/kitty 2022-01-17 08:23:34 +05:30
Kovid Goyal
ea28029581 ... 2022-01-17 08:05:00 +05:30
pagedown
507e570887 No longer need the patch for Sphinx KeyboardTransform
The issue with KeyboardTransform with dirhtml has been fixed in Sphinx 4.4.0.
2022-01-17 02:49:25 +08:00
pagedown
5182f79152 macOS: Fix global shortcuts with shift modifier key not working 2022-01-17 02:23:00 +08:00
Kovid Goyal
71106bcd39 Shell integration: Dont override fish's native title setting functionality
See discussion: https://github.com/fish-shell/fish-shell/issues/8641
2022-01-16 15:58:11 +05:30
Kovid Goyal
cf91e1973b Show number of windows per tab in the select_tab action
Fixes #4523
2022-01-16 10:03:20 +05:30
Kovid Goyal
2170d4e21a DRYer 2022-01-16 09:47:44 +05:30
Kovid Goyal
2538e94390 Use a single SGR regex pattern cache 2022-01-16 09:46:52 +05:30
Kovid Goyal
0be0836890 Add changelog entry for last PR 2022-01-16 03:48:00 +05:30
Kovid Goyal
725ddf4f8b Merge branch 'fix-ksi-last-cmd' of https://github.com/page-down/kitty 2022-01-16 03:46:17 +05:30
pagedown
ce6f99044e ... 2022-01-16 04:14:59 +08:00
pagedown
30b81d98f1 Add tests for finding command output 2022-01-16 04:10:16 +08:00
pagedown
f0d2d01a36 Fix calculating the end of the command output 2022-01-16 04:02:13 +08:00
pagedown
ce32e64692 Check the continued attribute when searching for prompt marks 2022-01-15 20:19:12 +08:00
pagedown
7ea8a7d45f Fix searching the last command output 2022-01-15 18:38:05 +08:00
Kovid Goyal
b32edf54f8 hints kitten: Document how to choose the empty hint for multi key hints 2022-01-15 15:17:30 +05:30
Kovid Goyal
06ff685f8e oops 2022-01-15 14:54:30 +05:30
Kovid Goyal
c6ecdf0fa4 Ensure ignoreOSKeyboardProcessing is initialized 2022-01-15 14:43:10 +05:30
Kovid Goyal
ca4840717b macOS: Fix using shortcuts from the global menu bar as subsequent key presses in a multi key mapping not working
Fixes #4519
2022-01-15 13:56:18 +05:30
Kovid Goyal
c473df4393 Ignore NSEventModifierFlagFunction when parsing global shortcuts 2022-01-15 12:16:18 +05:30
Kovid Goyal
fa6c74a9b1 ... 2022-01-15 11:49:02 +05:30
Kovid Goyal
8174ecb318 Use only bash builtins to get currently executing command 2022-01-15 11:46:25 +05:30
Kovid Goyal
5f20d69309 Move extraction of last cmd into its own function 2022-01-15 10:58:16 +05:30
Kovid Goyal
aab121b35d Also check for shiftable shortcuts in the virtual key code case 2022-01-15 10:23:18 +05:30
Kovid Goyal
1a374a7678 Merge branch 'ksi-fish' of https://github.com/page-down/kitty 2022-01-15 10:17:18 +05:30
Kovid Goyal
396906860d macOS: When checking for global shortcuts handle shifted form of cmd+` 2022-01-15 10:14:06 +05:30
pagedown
85254746bf Shell integration: Remove functions that run only once at start 2022-01-15 12:34:52 +08:00
Kovid Goyal
c3c7ad78c1 Merge branch 'ksi-fish-cursor' of https://github.com/page-down/kitty 2022-01-15 09:40:37 +05:30
pagedown
77787bdc7c Shell integration: Improvements in fish integration 2022-01-15 12:01:47 +08:00
pagedown
9813ad04c2 Shell integration: Set the fish vi mode cursor to blink 2022-01-15 10:29:26 +08:00
Kovid Goyal
d2c20f9548 Add changelog entry for previous PR 2022-01-14 23:31:10 +05:30
Kovid Goyal
e51b520897 Merge branch 'ksi-fish-title' of https://github.com/page-down/kitty 2022-01-14 23:29:48 +05:30
Kovid Goyal
35207af3de Another use of builtin 2022-01-14 23:28:10 +05:30
Kovid Goyal
96269510d3 Abort before defining anything and dont touch global state via set 2022-01-14 23:26:40 +05:30
pagedown
ceb7c440a1 Shell integration: Only override default fish_title 2022-01-15 01:46:14 +08:00
Kovid Goyal
25e1edf52e ... 2022-01-14 23:03:44 +05:30
Kovid Goyal
7a903adeea let _ksi_debug_print handle multiple args 2022-01-14 22:53:39 +05:30
Kovid Goyal
914f4e9ec9 Dont break if user aliases or redefines various builtins we use 2022-01-14 22:47:49 +05:30
Kovid Goyal
4c5673dc4d Freeze _ksi_prompt to prevent it from being edited 2022-01-14 22:35:37 +05:30
Kovid Goyal
8529a60d77 Use issue number in changelog 2022-01-14 22:26:54 +05:30
Kovid Goyal
58da5d4d29 Placate mypy 2022-01-14 22:26:05 +05:30
Kovid Goyal
4ce6d718c9 icat kitten: Add options to mirror images and remove their transparency before displaying them
Fixes #4513
2022-01-14 22:02:35 +05:30
Kovid Goyal
dd31ee60f2 Fix typing of to_color 2022-01-14 21:54:36 +05:30
Kovid Goyal
8bb2da3c37 Use with instead of closing manually 2022-01-14 21:12:08 +05:30
Kovid Goyal
47dbaac54f Add changelog entry for previous PR 2022-01-14 20:36:15 +05:30
Kovid Goyal
346405299b Merge branch 'ksi-fish-cursor' of https://github.com/page-down/kitty 2022-01-14 20:35:23 +05:30
pagedown
a0476c03c3 Improve fish cursor shape integration 2022-01-14 22:57:31 +08:00
Kovid Goyal
9dd5410433 Dont rely on declare -g existing in bash 2022-01-14 18:21:21 +05:30
Kovid Goyal
72745dfb62 Use red fg for activity symbol by default
Can be overriden via template, but for backwards compat keep it red
2022-01-14 13:27:35 +05:30
Kovid Goyal
3a0d8ada6a Merge branch 'feat-macos-kbd-shortcuts' of https://github.com/page-down/kitty 2022-01-14 12:36:01 +05:30
Kovid Goyal
b53d756bc2 Shell integration: Fix bash integration not working when PROMPT_COMMAND is used to change the prompt variables
Fixes #4476
2022-01-14 12:14:48 +05:30
Kovid Goyal
bb8ebf32ae Merge branch 'fix-macos-text-input-beep' of https://github.com/page-down/kitty 2022-01-14 11:53:37 +05:30
pagedown
704487f0ce macOS: Release input context 2022-01-14 14:18:03 +08:00
pagedown
a9bdf70275 macOS: Watch for system preferences changes and update global_shortcuts 2022-01-14 13:51:46 +08:00
Kovid Goyal
017da1159c Allow using templates with text formatting for tab_activity_symbol
Fixes #4507
2022-01-14 08:18:04 +05:30
pagedown
ab5b9fb329 macOS: Only check the "standard" type of the system shortcuts 2022-01-14 10:07:38 +08:00
pagedown
e0700d45de macOS: Update to the latest and categorize the system shortcut names 2022-01-14 10:05:49 +08:00
Joseph Adams
a2c4f830b3 Enable use of higher options for underlining text.
In `Colored and styled underlines` it's proposed that the SGR codes
\e[4:4m and \e[4:5m are used to add a dotted or dashed underline to the
rendering context respectively. This commit prepares the necessary
changes to add the two additional underline style, while still rendering
them as a normal underline and curly underline.
2022-01-13 17:27:02 +01:00
Kovid Goyal
ecf4fcdeb0 Add changelog entry for previous PR
Fixes #4489
2022-01-13 20:24:17 +05:30
Kovid Goyal
0a89fe2e17 Merge branch 'fix-macos-text-input-beep' of https://github.com/page-down/kitty 2022-01-13 20:23:23 +05:30
pagedown
5b5651ca7d macOS: Suppress the key combinations beep sound 2022-01-13 22:51:53 +08:00
Kovid Goyal
5e5cd1acc8 Simplify flagsChanged and make it more robust
Now it only sends a key event to kitty if an actual modifier key was
pressed/released.
2022-01-13 20:13:55 +05:30
Kovid Goyal
79fd01093a fix typo in generating lookup keys for vk code based shortcuts 2022-01-13 19:21:49 +05:30
Kovid Goyal
ce63ddfe54 macOS: Print out unhandled exceptions 2022-01-13 16:54:36 +05:30
Kovid Goyal
a56471f7ed Validate the parameters array 2022-01-13 16:44:43 +05:30
Kovid Goyal
96b46c554e Add type annotations to dictionaries 2022-01-13 16:29:13 +05:30
Kovid Goyal
a819023ab7 remove redundant cast 2022-01-13 16:05:48 +05:30
Kovid Goyal
9504f50bd4 ... 2022-01-13 16:04:50 +05:30
Kovid Goyal
8fe71e3dc8 Dont use a block for dictionary enumeration
Maybe that will fix the mysterious issue @page-down is having
2022-01-13 16:02:29 +05:30
Kovid Goyal
34d7643bed Ensure global_shosrtcuts is initialized 2022-01-13 15:10:19 +05:30
Kovid Goyal
682eb7d802 Log when events are ignored because no keyWindow is present 2022-01-13 14:22:52 +05:30
Kovid Goyal
f86a954131 ooops 2022-01-13 14:21:58 +05:30
Kovid Goyal
8e3f6d37d9 ... 2022-01-13 14:15:39 +05:30
Kovid Goyal
2c38360ca0 Handle no keyWindow case 2022-01-13 13:43:01 +05:30
Kovid Goyal
f9d2167c97 macOS: Respect the users system-wide global keyboard shortcut preferences
Note that we currently dont handle live reload of global shortcut
preferences. I cant be bothered. Patches welcome.
Fixes #4501
2022-01-13 13:36:58 +05:30
Kovid Goyal
17cada7a6a Micro-optimization 2022-01-12 23:32:03 +05:30
Kovid Goyal
19e6f70655 Draw the dots for braille characters more evenly spaced at all font sizes
Fixes #4499
2022-01-12 23:18:18 +05:30
Kovid Goyal
d106089cb9 Micro-optimization 2022-01-12 22:25:58 +05:30
Kovid Goyal
0f7f69c218 move menu case outside auto-generated section 2022-01-11 22:57:05 +05:30
Kovid Goyal
f9171a767c Also document menu key encoding 2022-01-11 22:48:53 +05:30
Kovid Goyal
34e939f9a1 Encode menu key the same as xterm in legacy mode
See #597
2022-01-11 22:44:56 +05:30
Kovid Goyal
ba76cacd74 Add changelog entry for previous PR 2022-01-11 20:10:38 +05:30
Kovid Goyal
fd1b64dcc6 Merge branch 'fixes-fullscreen-hide-macos-10.15' of https://github.com/npearson72/kitty 2022-01-11 20:09:10 +05:30
Nathan Pearson
7c33d7ef17 Bumps version from 10.15 to 10.15.7 (latest minor) 2022-01-11 07:31:37 -07:00
Nathan Pearson
2ccfecb5ae Fixes comment 2022-01-11 06:39:48 -07:00
Nathan Pearson
a40ff85e1a Fixes traditional fullscreen hide for Mac OS >= 10.15 2022-01-11 06:25:39 -07:00
Kovid Goyal
62953c2057 Mark parameters as unused 2022-01-11 11:26:08 +05:30
Kovid Goyal
0e6091773d Add a poster for the demo video 2022-01-11 11:09:43 +05:30
Kovid Goyal
4c8ef26141 For the vertical and horizontal layouts have the windows arranged on a ring rather than a plane. This means the first and last window are considered neighbors
Fixes #4494
2022-01-11 08:08:11 +05:30
Kovid Goyal
7023c7a8ae ... 2022-01-10 21:15:24 +05:30
Kovid Goyal
b2bfc4408e Fix common single letter extension files not being detected
Fixes #4491
Fixes #4492
2022-01-10 14:20:18 +05:30
Kovid Goyal
5b8aca3a2f Use an f-string 2022-01-10 08:35:43 +05:30
Kovid Goyal
6715ee888e Merge branch 'cleanup' of https://github.com/page-down/kitty 2022-01-10 08:01:13 +05:30
pagedown
4293325539 Cleanup previous PR
Revert the changes related to set_options debug_keyboard.
Remove the Carbon framework flag for glfw.
2022-01-10 10:24:10 +08:00
Kovid Goyal
24c44861ef Cleanup previous PR
Simplify wrapping of toggle_secure_input
Dont duplicate debug_keyboard flag in two places
2022-01-09 22:11:22 +05:30
Kovid Goyal
eafd20b4b3 Merge branch 'feat-macos-sec-kbd' of https://github.com/page-down/kitty 2022-01-09 22:03:31 +05:30
pagedown
d9e5edac01 Refactor: Replace double quotes with single quotes 2022-01-09 22:54:33 +08:00
pagedown
f7be4fab48 macOS: Allow mapping secure keyboard entry shortcut 2022-01-09 22:54:03 +08:00
Kovid Goyal
50c822e05a Merge branch 'zsh-integration' of https://github.com/romkatv/kitty 2022-01-09 15:44:43 +05:30
Roman Perepelitsa
6c1fd3015f Fix a minor bug in zsh integration
This bug is extremely unlikely to cause issues.
2022-01-09 10:45:47 +01:00
Kovid Goyal
de3b9634ac Add links to third party shell integrations 2022-01-09 04:30:32 +05:30
Kovid Goyal
722f6163aa Use tabs for zsh/fish in hg kitten docs 2022-01-09 03:54:33 +05:30
Kovid Goyal
82a4e72c85 Merge branch 'patch-1' of https://github.com/ronjouch/kitty 2022-01-09 03:51:54 +05:30
Ronan Jouchet
c40bd7ce82 kittens/hyperlinked_grep: document how to tell fish to autocomplete with rg
It was documented for zsh but not fish (2 lines above).

While looking for how to do the same in fish, I couldn't find how to do
*exactly the same*, but turns out there's a neat way to do both the
aliasing and autocompletion mapping at the same time.

See https://fishshell.com/docs/current/cmds/function.html :

> `-w WRAPPED_COMMAND` or `--wraps=WRAPPED_COMMAND` causes the function
> to inherit completions from the given wrapped command. See the
> documentation for [complete](https://fishshell.com/docs/current/cmds/complete.html#cmd-complete)
> for more information.
2022-01-08 14:43:36 -05:00
Kovid Goyal
356897a32d macOS: Persist "Secure Keyboard Entry" across restarts to match the behavior of Terminal.app
Fixes #4471
2022-01-08 14:13:14 +05:30
Kovid Goyal
ac0c10c170 Simplify if condition 2022-01-08 14:06:22 +05:30
Kovid Goyal
362c4147e4 ... 2022-01-08 12:21:55 +05:30
Kovid Goyal
9b9eae7923 icat: Fix --hold not working with critical errors 2022-01-08 10:22:22 +05:30
Kovid Goyal
8622c47348 Fix #4430 2022-01-08 10:00:25 +05:30
Kovid Goyal
1bfc46a24f ... 2022-01-08 09:16:32 +05:30
Kovid Goyal
8de787a2f3 Create shlex.join if it doesnt exist 2022-01-07 20:17:23 +05:30
Kovid Goyal
fcc75689d8 ... 2022-01-07 18:48:26 +05:30
Kovid Goyal
1eed35b8bb Document launch-actions.conf 2022-01-07 18:47:37 +05:30
Kovid Goyal
1454af2d41 macOS: Allow customizing the launch actions 2022-01-07 18:43:07 +05:30
Kovid Goyal
cbb2597667 Merge branch 'feat-macos-open-with' of https://github.com/page-down/kitty 2022-01-07 17:11:42 +05:30
pagedown
9ca691c0c6 macOS: Add kitty to the open with list for text files and image files 2022-01-07 19:33:29 +08:00
Kovid Goyal
b9f21f285c Remove non-namespaced launch points
As far as I can tell these were non-namespaced by accident
2022-01-07 14:34:41 +05:30
Kovid Goyal
bf84f95a94 macOS: When launching a shell script use its shebang, if present 2022-01-07 13:52:13 +05:30
Kovid Goyal
d42f2071c3 hold windows when running shell scripts via launch services 2022-01-07 13:45:02 +05:30
Kovid Goyal
5c343c918d Add a --hold option for the launch command 2022-01-07 13:32:25 +05:30
Kovid Goyal
5bfed4fe2f Also launch bash/zsh/fish scripts in their respective shells 2022-01-07 12:48:59 +05:30
Kovid Goyal
6c139600a0 macOS: Also open .sh and .command files in the default shell 2022-01-07 12:32:26 +05:30
Kovid Goyal
b80b8524e6 oops 2022-01-07 12:29:57 +05:30
Kovid Goyal
ffa63e27ea macOS: Allow opening text files, images and directories with kitty when launched using "Open with" in Finder
Fixes #4460
2022-01-07 12:25:30 +05:30
Kovid Goyal
a155b081b7 Make hold_till_enter re-useable 2022-01-07 12:13:20 +05:30
140 changed files with 2660 additions and 1354 deletions

View File

@@ -48,7 +48,7 @@ def install_deps():
def build_kitty(): def build_kitty():
python = shutil.which('python3') if is_bundle else sys.executable python = shutil.which('python3') if is_bundle else sys.executable
cmd = '{} setup.py build --verbose'.format(python) cmd = f'{python} setup.py build --verbose'
if os.environ.get('KITTY_SANITIZE') == '1': if os.environ.get('KITTY_SANITIZE') == '1':
cmd += ' --debug --sanitize' cmd += ' --debug --sanitize'
run(cmd) run(cmd)
@@ -59,8 +59,8 @@ def test_kitty():
def package_kitty(): def package_kitty():
py = 'python3' if is_macos else 'python' python = 'python3' if is_macos else 'python'
run(py + ' setup.py linux-package --update-check-interval=0 --verbose') run(f'{python} setup.py linux-package --update-check-interval=0 --verbose')
if is_macos: if is_macos:
run('python3 setup.py kitty.app --update-check-interval=0 --verbose') run('python3 setup.py kitty.app --update-check-interval=0 --verbose')
run('kitty.app/Contents/MacOS/kitty +runpy "from kitty.constants import *; print(kitty_exe())"') run('kitty.app/Contents/MacOS/kitty +runpy "from kitty.constants import *; print(kitty_exe())"')
@@ -76,11 +76,11 @@ def replace_in_file(path, src, dest):
def setup_bundle_env(): def setup_bundle_env():
global SW global 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['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' os.environ['PKG_CONFIG_PATH'] = os.path.join(SW, 'lib', 'pkgconfig')
if is_macos: if is_macos:
os.environ['PATH'] = '{}:{}'.format('/usr/local/opt/sphinx-doc/bin', os.environ['PATH']) os.environ['PATH'] = '{}:{}'.format('/usr/local/opt/sphinx-doc/bin', os.environ['PATH'])
else: else:
os.environ['LD_LIBRARY_PATH'] = SW + '/lib' os.environ['LD_LIBRARY_PATH'] = os.path.join(SW, 'lib')
os.environ['PYTHONHOME'] = SW os.environ['PYTHONHOME'] = SW
os.environ['PATH'] = '{}:{}'.format(os.path.join(SW, 'bin'), os.environ['PATH']) os.environ['PATH'] = '{}:{}'.format(os.path.join(SW, 'bin'), os.environ['PATH'])
@@ -111,7 +111,7 @@ def main():
setup_bundle_env() setup_bundle_env()
else: else:
if not is_macos and 'pythonLocation' in os.environ: if not is_macos and 'pythonLocation' in os.environ:
os.environ['LD_LIBRARY_PATH'] = '{}/lib'.format(os.environ['pythonLocation']) os.environ['LD_LIBRARY_PATH'] = os.path.join(os.environ['pythonLocation'], 'lib')
action = sys.argv[-1] action = sys.argv[-1]
if action in ('build', 'package'): if action in ('build', 'package'):
install_deps() install_deps()
@@ -122,7 +122,7 @@ def main():
elif action == 'test': elif action == 'test':
test_kitty() test_kitty()
else: else:
raise SystemExit('Unknown action: ' + action) raise SystemExit(f'Unknown action: {action}')
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -88,10 +88,10 @@ jobs:
run: python test.py mypy run: python test.py mypy
- name: Build man page - name: Build man page
run: make FAIL_WARN=-W man run: make FAIL_WARN=1 man
- name: Build HTML docs - name: Build HTML docs
run: make FAIL_WARN=-W html run: make FAIL_WARN=1 html
bundle: bundle:
name: Bundle test (${{ matrix.os }}) name: Bundle test (${{ matrix.os }})

View File

@@ -5,6 +5,10 @@ ifdef VERBOSE
VVAL=--verbose VVAL=--verbose
endif endif
ifdef FAIL_WARN
export FAIL_WARN
endif
all: all:
python3 setup.py $(VVAL) python3 setup.py $(VVAL)
@@ -32,13 +36,16 @@ app:
python3 setup.py kitty.app $(VVAL) python3 setup.py kitty.app $(VVAL)
man: man:
$(MAKE) FAIL_WARN=$(FAIL_WARN) -C docs man $(MAKE) -C docs man
html: html:
$(MAKE) FAIL_WARN=$(FAIL_WARN) -C docs html $(MAKE) -C docs html
dirhtml:
$(MAKE) -C docs dirhtml
linkcheck: linkcheck:
$(MAKE) FAIL_WARN=$(FAIL_WARN) -C docs linkcheck $(MAKE) -C docs linkcheck
website: website:
./publish.py --only website ./publish.py --only website

View File

@@ -31,26 +31,9 @@ def runpy(args: List[str]) -> None:
def hold(args: List[str]) -> None: def hold(args: List[str]) -> None:
import subprocess import subprocess
import termios
from contextlib import suppress
from kittens.tui.operations import init_state, set_cursor_visible
ret = subprocess.Popen(args[1:]).wait() ret = subprocess.Popen(args[1:]).wait()
with suppress(BaseException): from kitty.utils import hold_till_enter
print( hold_till_enter()
'\n\x1b[1;32mPress Enter to exit',
end=init_state(alternate_screen=False, kitty_keyboard_mode=False) + set_cursor_visible(False),
flush=True)
with suppress(BaseException):
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
new = old[:]
new[3] &= ~termios.ECHO # 3 == 'lflags'
tcsetattr_flags = termios.TCSAFLUSH
if hasattr(termios, 'TCSASOFT'):
tcsetattr_flags |= getattr(termios, 'TCSASOFT')
termios.tcsetattr(fd, tcsetattr_flags, new)
with suppress(KeyboardInterrupt):
input()
raise SystemExit(ret) raise SystemExit(ret)
@@ -81,6 +64,27 @@ def launch(args: List[str]) -> None:
runpy.run_path(exe, run_name='__main__') runpy.run_path(exe, run_name='__main__')
def shebang(args: List[str]) -> None:
script_path = args[1]
cmd = args[2:]
if cmd == ['__ext__']:
cmd = [os.path.splitext(script_path)[1][1:].lower()]
try:
f = open(script_path)
except FileNotFoundError:
raise SystemExit(f'The file {script_path} does not exist')
with f:
if f.read(2) == '#!':
line = f.readline().strip()
_plat = sys.platform.lower()
is_macos: bool = 'darwin' in _plat
if is_macos:
cmd = line.split(' ')
else:
cmd = line.split(' ', maxsplit=1)
os.execvp(cmd[0], cmd + [script_path])
def run_kitten(args: List[str]) -> None: def run_kitten(args: List[str]) -> None:
try: try:
kitten = args[1] kitten = args[1]
@@ -116,10 +120,6 @@ entry_points = {
# These two are here for backwards compat # These two are here for backwards compat
'icat': icat, 'icat': icat,
'list-fonts': list_fonts, 'list-fonts': list_fonts,
'runpy': runpy,
'launch': launch,
'kitten': run_kitten,
'edit-config': edit_config_file,
'@': remote_control, '@': remote_control,
'+': namespaced, '+': namespaced,
@@ -127,6 +127,11 @@ entry_points = {
namespaced_entry_points = {k: v for k, v in entry_points.items() if k[0] not in '+@'} namespaced_entry_points = {k: v for k, v in entry_points.items() if k[0] not in '+@'}
namespaced_entry_points['hold'] = hold namespaced_entry_points['hold'] = hold
namespaced_entry_points['complete'] = complete namespaced_entry_points['complete'] = complete
namespaced_entry_points['runpy'] = runpy
namespaced_entry_points['launch'] = launch
namespaced_entry_points['kitten'] = run_kitten
namespaced_entry_points['edit-config'] = edit_config_file
namespaced_entry_points['shebang'] = shebang
def setup_openssl_environment() -> None: def setup_openssl_environment() -> None:

View File

@@ -13,7 +13,7 @@ import tempfile
def compile_terminfo(base): def compile_terminfo(base):
with tempfile.TemporaryDirectory() as tdir: with tempfile.TemporaryDirectory() as tdir:
proc = subprocess.run(['tic', '-x', '-o' + tdir, 'terminfo/kitty.terminfo'], check=True, stderr=subprocess.PIPE) proc = subprocess.run(['tic', '-x', f'-o{tdir}', 'terminfo/kitty.terminfo'], check=True, stderr=subprocess.PIPE)
regex = '^"terminfo/kitty.terminfo", line [0-9]+, col [0-9]+, terminal \'xterm-kitty\': older tic versions may treat the description field as an alias$' regex = '^"terminfo/kitty.terminfo", line [0-9]+, col [0-9]+, terminal \'xterm-kitty\': older tic versions may treat the description field as an alias$'
for error in proc.stderr.decode('utf-8').splitlines(): for error in proc.stderr.decode('utf-8').splitlines():
if not re.match(regex, error): if not re.match(regex, error):

View File

@@ -26,7 +26,7 @@ def initialize_constants():
kitty_constants = {} kitty_constants = {}
src = read_src_file('constants.py') src = read_src_file('constants.py')
nv = re.search(r'Version\((\d+), (\d+), (\d+)\)', src) nv = re.search(r'Version\((\d+), (\d+), (\d+)\)', src)
kitty_constants['version'] = '%s.%s.%s' % (nv.group(1), nv.group(2), nv.group(3)) kitty_constants['version'] = f'{nv.group(1)}.{nv.group(2)}.{nv.group(3)}'
kitty_constants['appname'] = re.search( kitty_constants['appname'] = re.search(
r'appname: str\s+=\s+(u{0,1})[\'"]([^\'"]+)[\'"]', src r'appname: str\s+=\s+(u{0,1})[\'"]([^\'"]+)[\'"]', src
).group(2) ).group(2)

View File

@@ -38,7 +38,7 @@ def binary_includes():
'ncursesw', 'readline', 'brotlicommon', 'brotlienc', 'brotlidec' 'ncursesw', 'readline', 'brotlicommon', 'brotlienc', 'brotlidec'
))) + ( ))) + (
get_dll_path('bz2', 2), get_dll_path('ssl', 2), get_dll_path('crypto', 2), get_dll_path('bz2', 2), get_dll_path('ssl', 2), get_dll_path('crypto', 2),
get_dll_path('python' + py_ver, 2), get_dll_path(f'python{py_ver}', 2),
) )
@@ -47,7 +47,7 @@ class Env:
def __init__(self, package_dir): def __init__(self, package_dir):
self.base = package_dir self.base = package_dir
self.lib_dir = j(self.base, 'lib') self.lib_dir = j(self.base, 'lib')
self.py_dir = j(self.lib_dir, 'python' + py_ver) self.py_dir = j(self.lib_dir, f'python{py_ver}')
os.makedirs(self.py_dir) os.makedirs(self.py_dir)
self.bin_dir = j(self.base, 'bin') self.bin_dir = j(self.base, 'bin')
self.obj_dir = mkdtemp('launchers-') self.obj_dir = mkdtemp('launchers-')
@@ -107,7 +107,7 @@ def add_ca_certs(env):
def copy_python(env): def copy_python(env):
print('Copying python...') print('Copying python...')
srcdir = j(PREFIX, 'lib/python' + py_ver) srcdir = j(PREFIX, f'lib/python{py_ver}')
for x in os.listdir(srcdir): for x in os.listdir(srcdir):
y = j(srcdir, x) y = j(srcdir, x)
@@ -187,11 +187,11 @@ def strip_files(files, argv_max=(256 * 1024)):
def strip_binaries(files): def strip_binaries(files):
print('Stripping %d files...' % len(files)) print(f'Stripping {len(files)} files...')
before = sum(os.path.getsize(x) for x in files) before = sum(os.path.getsize(x) for x in files)
strip_files(files) strip_files(files)
after = sum(os.path.getsize(x) for x in files) after = sum(os.path.getsize(x) for x in files)
print('Stripped %.1f MB' % ((before - after) / (1024 * 1024.))) print('Stripped {:.1f} MB'.format((before - after) / (1024 * 1024.)))
def create_tarfile(env, compression_level='9'): def create_tarfile(env, compression_level='9'):
@@ -203,7 +203,7 @@ def create_tarfile(env, compression_level='9'):
if err.errno != errno.ENOENT: if err.errno != errno.ENOENT:
raise raise
os.mkdir(base) os.mkdir(base)
dist = os.path.join(base, '%s-%s-%s.tar' % (kitty_constants['appname'], kitty_constants['version'], arch)) dist = os.path.join(base, f'{kitty_constants["appname"]}-{kitty_constants["version"]}-{arch}.tar')
with tarfile.open(dist, mode='w', format=tarfile.PAX_FORMAT) as tf: with tarfile.open(dist, mode='w', format=tarfile.PAX_FORMAT) as tf:
cwd = os.getcwd() cwd = os.getcwd()
os.chdir(env.base) os.chdir(env.base)
@@ -213,13 +213,13 @@ def create_tarfile(env, compression_level='9'):
finally: finally:
os.chdir(cwd) os.chdir(cwd)
print('Compressing archive...') print('Compressing archive...')
ans = dist.rpartition('.')[0] + '.txz' ans = f'{dist.rpartition(".")[0]}.txz'
start_time = time.time() start_time = time.time()
subprocess.check_call(['xz', '--threads=0', '-f', '-' + compression_level, dist]) subprocess.check_call(['xz', '--threads=0', '-f', f'-{compression_level}', dist])
secs = time.time() - start_time secs = time.time() - start_time
print('Compressed in %d minutes %d seconds' % (secs // 60, secs % 60)) print('Compressed in {} minutes {} seconds'.format(secs // 60, secs % 60))
os.rename(dist + '.xz', ans) os.rename(f'{dist}.xz', ans)
print('Archive %s created: %.2f MB' % ( print('Archive {} created: {:.2f} MB'.format(
os.path.basename(ans), os.stat(ans).st_size / (1024.**2))) os.path.basename(ans), os.stat(ans).st_size / (1024.**2)))

View File

@@ -90,7 +90,7 @@ def strip_files(files, argv_max=(256 * 1024)):
def files_in(folder): def files_in(folder):
for record in os.walk(folder): for record in os.walk(folder):
for f in record[-1]: for f in record[-1]:
yield os.path.join(record[0], f) yield join(record[0], f)
def expand_dirs(items, exclude=lambda x: x.endswith('.so')): def expand_dirs(items, exclude=lambda x: x.endswith('.so')):
@@ -103,7 +103,7 @@ def expand_dirs(items, exclude=lambda x: x.endswith('.so')):
def do_sign(app_dir): def do_sign(app_dir):
with current_dir(os.path.join(app_dir, 'Contents')): with current_dir(join(app_dir, 'Contents')):
# Sign all .so files # Sign all .so files
so_files = {x for x in files_in('.') if x.endswith('.so')} so_files = {x for x in files_in('.') if x.endswith('.so')}
codesign(so_files) codesign(so_files)
@@ -153,7 +153,7 @@ class Freeze(object):
self.to_strip = [] self.to_strip = []
self.warnings = [] self.warnings = []
self.py_ver = py_ver self.py_ver = py_ver
self.python_stdlib = join(self.resources_dir, 'Python', 'lib', 'python' + self.py_ver) self.python_stdlib = join(self.resources_dir, 'Python', 'lib', f'python{self.py_ver}')
self.site_packages = self.python_stdlib # hack to avoid needing to add site-packages to path self.site_packages = self.python_stdlib # hack to avoid needing to add site-packages to path
self.obj_dir = mkdtemp('launchers-') self.obj_dir = mkdtemp('launchers-')
@@ -177,7 +177,7 @@ class Freeze(object):
self.run_tests() self.run_tests()
# self.run_shell() # self.run_shell()
ret = self.makedmg(self.build_dir, APPNAME + '-' + VERSION) ret = self.makedmg(self.build_dir, f'{APPNAME}-{VERSION}')
return ret return ret
@@ -186,7 +186,7 @@ class Freeze(object):
print('\nDownloading CA certs...') print('\nDownloading CA certs...')
from urllib.request import urlopen from urllib.request import urlopen
cdata = urlopen(kitty_constants['cacerts_url']).read() cdata = urlopen(kitty_constants['cacerts_url']).read()
dest = os.path.join(self.contents_dir, 'Resources', 'cacert.pem') dest = join(self.contents_dir, 'Resources', 'cacert.pem')
with open(dest, 'wb') as f: with open(dest, 'wb') as f:
f.write(cdata) f.write(cdata)
@@ -197,7 +197,7 @@ class Freeze(object):
@flush @flush
def run_tests(self): def run_tests(self):
iv['run_tests'](os.path.join(self.contents_dir, 'MacOS', 'kitty')) iv['run_tests'](join(self.contents_dir, 'MacOS', 'kitty'))
@flush @flush
def set_id(self, path_to_lib, new_id): def set_id(self, path_to_lib, new_id):
@@ -222,10 +222,10 @@ class Freeze(object):
@flush @flush
def get_local_dependencies(self, path_to_lib): def get_local_dependencies(self, path_to_lib):
for x, is_id in self.get_dependencies(path_to_lib): for x, is_id in self.get_dependencies(path_to_lib):
for y in (PREFIX + '/lib/', PREFIX + '/python/Python.framework/', '@rpath/'): for y in (f'{PREFIX}/lib/', f'{PREFIX}/python/Python.framework/', '@rpath/'):
if x.startswith(y): if x.startswith(y):
if y == PREFIX + '/python/Python.framework/': if y == f'{PREFIX}/python/Python.framework/':
y = PREFIX + '/python/' y = f'{PREFIX}/python/'
yield x, x[len(y):], is_id yield x, x[len(y):], is_id
break break
@@ -239,7 +239,7 @@ class Freeze(object):
self.to_strip.append(path_to_lib) self.to_strip.append(path_to_lib)
old_mode = flipwritable(path_to_lib) old_mode = flipwritable(path_to_lib)
for dep, bname, is_id in self.get_local_dependencies(path_to_lib): for dep, bname, is_id in self.get_local_dependencies(path_to_lib):
ndep = self.FID + '/' + bname ndep = f'{self.FID}/{bname}'
self.change_dep(dep, ndep, is_id, path_to_lib) self.change_dep(dep, ndep, is_id, path_to_lib)
ldeps = list(self.get_local_dependencies(path_to_lib)) ldeps = list(self.get_local_dependencies(path_to_lib))
if ldeps: if ldeps:
@@ -252,7 +252,7 @@ class Freeze(object):
@flush @flush
def add_python_framework(self): def add_python_framework(self):
print('\nAdding Python framework') print('\nAdding Python framework')
src = join(PREFIX + '/python', 'Python.framework') src = join(f'{PREFIX}/python', 'Python.framework')
x = join(self.frameworks_dir, 'Python.framework') x = join(self.frameworks_dir, 'Python.framework')
curr = os.path.realpath(join(src, 'Versions', 'Current')) curr = os.path.realpath(join(src, 'Versions', 'Current'))
currd = join(x, 'Versions', basename(curr)) currd = join(x, 'Versions', basename(curr))
@@ -262,12 +262,12 @@ class Freeze(object):
shutil.copy2(join(curr, 'Python'), currd) shutil.copy2(join(curr, 'Python'), currd)
self.set_id( self.set_id(
join(currd, 'Python'), join(currd, 'Python'),
self.FID + '/Python.framework/Versions/%s/Python' % basename(curr)) f'{self.FID}/Python.framework/Versions/{basename(curr)}/Python')
# The following is needed for codesign # The following is needed for codesign
with current_dir(x): with current_dir(x):
os.symlink(basename(curr), 'Versions/Current') os.symlink(basename(curr), 'Versions/Current')
for y in ('Python', 'Resources'): for y in ('Python', 'Resources'):
os.symlink('Versions/Current/%s' % y, y) os.symlink(f'Versions/Current/{y}', y)
@flush @flush
def install_dylib(self, path, set_id=True): def install_dylib(self, path, set_id=True):
@@ -275,7 +275,7 @@ class Freeze(object):
if set_id: if set_id:
self.set_id( self.set_id(
join(self.frameworks_dir, basename(path)), join(self.frameworks_dir, basename(path)),
self.FID + '/' + basename(path)) f'{self.FID}/{basename(path)}')
self.fix_dependencies_in_lib(join(self.frameworks_dir, basename(path))) self.fix_dependencies_in_lib(join(self.frameworks_dir, basename(path)))
@flush @flush
@@ -291,11 +291,11 @@ class Freeze(object):
'rsync.2', 'rsync.2',
): ):
print('\nAdding', x) print('\nAdding', x)
x = 'lib%s.dylib' % x x = f'lib{x}.dylib'
src = join(PREFIX, 'lib', x) src = join(PREFIX, 'lib', x)
shutil.copy2(src, self.frameworks_dir) shutil.copy2(src, self.frameworks_dir)
dest = join(self.frameworks_dir, x) dest = join(self.frameworks_dir, x)
self.set_id(dest, self.FID + '/' + x) self.set_id(dest, f'{self.FID}/{x}')
self.fix_dependencies_in_lib(dest) self.fix_dependencies_in_lib(dest)
@flush @flush
@@ -321,7 +321,7 @@ class Freeze(object):
@flush @flush
def add_stdlib(self): def add_stdlib(self):
print('\nAdding python stdlib') print('\nAdding python stdlib')
src = PREFIX + '/python/Python.framework/Versions/Current/lib/python' + self.py_ver src = f'{PREFIX}/python/Python.framework/Versions/Current/lib/python{self.py_ver}'
dest = self.python_stdlib dest = self.python_stdlib
if not os.path.exists(dest): if not os.path.exists(dest):
os.makedirs(dest) os.makedirs(dest)
@@ -345,19 +345,19 @@ class Freeze(object):
kitty_dir = join(self.resources_dir, 'kitty') kitty_dir = join(self.resources_dir, 'kitty')
bases = ('kitty', 'kittens', 'kitty_tests') bases = ('kitty', 'kittens', 'kitty_tests')
for x in bases: for x in bases:
dest = os.path.join(self.python_stdlib, x) dest = join(self.python_stdlib, x)
os.rename(os.path.join(kitty_dir, x), dest) os.rename(join(kitty_dir, x), dest)
if x == 'kitty': if x == 'kitty':
shutil.rmtree(os.path.join(dest, 'launcher')) shutil.rmtree(join(dest, 'launcher'))
os.rename(os.path.join(kitty_dir, '__main__.py'), os.path.join(self.python_stdlib, 'kitty_main.py')) os.rename(join(kitty_dir, '__main__.py'), join(self.python_stdlib, 'kitty_main.py'))
shutil.rmtree(os.path.join(kitty_dir, '__pycache__')) shutil.rmtree(join(kitty_dir, '__pycache__'))
pdir = os.path.join(dirname(self.python_stdlib), 'kitty-extensions') pdir = join(dirname(self.python_stdlib), 'kitty-extensions')
os.mkdir(pdir) os.mkdir(pdir)
print('Extracting extension modules from', self.python_stdlib, 'to', pdir) print('Extracting extension modules from', self.python_stdlib, 'to', pdir)
ext_map = extract_extension_modules(self.python_stdlib, pdir) ext_map = extract_extension_modules(self.python_stdlib, pdir)
shutil.copy(os.path.join(os.path.dirname(self_dir), 'site.py'), os.path.join(self.python_stdlib, 'site.py')) shutil.copy(join(os.path.dirname(self_dir), 'site.py'), join(self.python_stdlib, 'site.py'))
for x in bases: for x in bases:
iv['sanitize_source_folder'](os.path.join(self.python_stdlib, x)) iv['sanitize_source_folder'](join(self.python_stdlib, x))
self.compile_py_modules() self.compile_py_modules()
freeze_python(self.python_stdlib, pdir, self.obj_dir, ext_map, develop_mode_env_var='KITTY_DEVELOP_FROM', remove_pyc_files=True) freeze_python(self.python_stdlib, pdir, self.obj_dir, ext_map, develop_mode_env_var='KITTY_DEVELOP_FROM', remove_pyc_files=True)
iv['build_frozen_launcher']([path_to_freeze_dir(), self.obj_dir]) iv['build_frozen_launcher']([path_to_freeze_dir(), self.obj_dir])
@@ -434,23 +434,23 @@ class Freeze(object):
''' Copy a directory d into a dmg named volname ''' ''' Copy a directory d into a dmg named volname '''
print('\nMaking dmg...') print('\nMaking dmg...')
sys.stdout.flush() sys.stdout.flush()
destdir = os.path.join(SW, 'dist') destdir = join(SW, 'dist')
try: try:
shutil.rmtree(destdir) shutil.rmtree(destdir)
except FileNotFoundError: except FileNotFoundError:
pass pass
os.mkdir(destdir) os.mkdir(destdir)
dmg = os.path.join(destdir, volname + '.dmg') dmg = join(destdir, f'{volname}.dmg')
if os.path.exists(dmg): if os.path.exists(dmg):
os.unlink(dmg) os.unlink(dmg)
tdir = tempfile.mkdtemp() tdir = tempfile.mkdtemp()
appdir = os.path.join(tdir, os.path.basename(d)) appdir = join(tdir, os.path.basename(d))
shutil.copytree(d, appdir, symlinks=True) shutil.copytree(d, appdir, symlinks=True)
if self.sign_installers: if self.sign_installers:
with timeit() as times: with timeit() as times:
sign_app(appdir, self.notarize) sign_app(appdir, self.notarize)
print('Signing completed in %d minutes %d seconds' % tuple(times)) print('Signing completed in {} minutes {} seconds'.format(*times))
os.symlink('/Applications', os.path.join(tdir, 'Applications')) os.symlink('/Applications', join(tdir, 'Applications'))
size_in_mb = int( size_in_mb = int(
subprocess.check_output(['du', '-s', '-k', tdir]).decode('utf-8') subprocess.check_output(['du', '-s', '-k', tdir]).decode('utf-8')
.split()[0]) / 1024. .split()[0]) / 1024.
@@ -466,10 +466,10 @@ class Freeze(object):
print('\nCreating dmg...') print('\nCreating dmg...')
with timeit() as times: with timeit() as times:
subprocess.check_call(cmd + [dmg]) subprocess.check_call(cmd + [dmg])
print('dmg created in %d minutes and %d seconds' % tuple(times)) print('dmg created in {} minutes and {} seconds'.format(*times))
shutil.rmtree(tdir) shutil.rmtree(tdir)
size = os.stat(dmg).st_size / (1024 * 1024.) size = os.stat(dmg).st_size / (1024 * 1024.)
print('\nInstaller size: %.2fMB\n' % size) print(f'\nInstaller size: {size:.2f}MB\n')
return dmg return dmg
@@ -477,7 +477,7 @@ def main():
args = globals()['args'] args = globals()['args']
ext_dir = globals()['ext_dir'] ext_dir = globals()['ext_dir']
Freeze( Freeze(
os.path.join(ext_dir, kitty_constants['appname'] + '.app'), join(ext_dir, f'{kitty_constants["appname"]}.app'),
dont_strip=args.dont_strip, dont_strip=args.dont_strip,
sign_installers=args.sign_installers, sign_installers=args.sign_installers,
notarize=args.notarize, notarize=args.notarize,

View File

@@ -1,6 +1,10 @@
# Minimal makefile for Sphinx documentation # Minimal makefile for Sphinx documentation
# #
ifdef FAIL_WARN
override FAIL_WARN=-W
endif
# You can set these variables from the command line. # You can set these variables from the command line.
SPHINXOPTS = -n -q -j auto -T $(FAIL_WARN) $(OPTS) SPHINXOPTS = -n -q -j auto -T $(FAIL_WARN) $(OPTS)
SPHINXBUILD = sphinx-build SPHINXBUILD = sphinx-build

BIN
docs/_static/poster.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -77,8 +77,11 @@ You can also define a shortcut to switch to the previously active window::
map ctrl+p nth_window -1 map ctrl+p nth_window -1
``nth_window`` will focus the nth window for positive numbers and the ``nth_window`` will focus the nth window for positive numbers (starting from
previously active windows for negative numbers. zero) and the previously active windows for negative numbers.
To switch to the nth OS window, you can define ``nth_os_window``. Only positive
numbers are accepted, starting from one.
.. _detach_window: .. _detach_window:
@@ -127,12 +130,12 @@ Restore font size :sc:`reset_font_size` (also :kbd:`⌘+0` on
Toggle fullscreen :sc:`toggle_fullscreen` (also :kbd:`⌃+⌘+f` on macOS) Toggle fullscreen :sc:`toggle_fullscreen` (also :kbd:`⌃+⌘+f` on macOS)
Toggle maximized :sc:`toggle_maximized` Toggle maximized :sc:`toggle_maximized`
Input unicode character :sc:`input_unicode_character` (also :kbd:`⌃+⌘+space` on macOS) Input unicode character :sc:`input_unicode_character` (also :kbd:`⌃+⌘+space` on macOS)
Click URL using the keyboard :sc:`open_url` Open URL in web browser :sc:`open_url`
Reset the terminal :sc:`reset_terminal` Reset the terminal :sc:`reset_terminal` (also :kbd:`⌥+⌘+r` on macOS)
Reload :file:`kitty.conf` :sc:`reload_config_file` (also :kbd:`⌃+⌘+,` on macOS) Reload :file:`kitty.conf` :sc:`reload_config_file` (also :kbd:`⌃+⌘+,` on macOS)
Debug :file:`kitty.conf` :sc:`debug_config` (also :kbd:`⌘+⌥+f6` on macOS) Debug :file:`kitty.conf` :sc:`debug_config` (also :kbd:`⌥+⌘+,` on macOS)
Pass current selection to program :sc:`pass_selection_to_program` Pass current selection to program :sc:`pass_selection_to_program`
Edit |kitty| config file :sc:`edit_config_file` Edit |kitty| config file :sc:`edit_config_file` (also :kbd:`⌘+,` on macOS)
Open a |kitty| shell :sc:`kitty_shell` Open a |kitty| shell :sc:`kitty_shell`
Increase background opacity :sc:`increase_background_opacity` Increase background opacity :sc:`increase_background_opacity`
Decrease background opacity :sc:`decrease_background_opacity` Decrease background opacity :sc:`decrease_background_opacity`

View File

@@ -4,6 +4,8 @@ Changelog
|kitty| is a feature-rich, cross-platform, *fast*, GPU based terminal. |kitty| is a feature-rich, cross-platform, *fast*, GPU based terminal.
To update |kitty|, :doc:`follow the instructions <binary>`. To update |kitty|, :doc:`follow the instructions <binary>`.
.. recent major features {{{
Recent major new features Recent major new features
--------------------------- ---------------------------
@@ -16,7 +18,7 @@ Recent major new features
.. raw:: html .. raw:: html
<video controls width="640" height="360"> <video controls width="640" height="360" poster="../_static/poster.png">
<source src="https://download.calibre-ebook.com/videos/kitty.mp4" type="video/mp4"> <source src="https://download.calibre-ebook.com/videos/kitty.mp4" type="video/mp4">
<source src="https://download.calibre-ebook.com/videos/kitty.webm" type="video/webm"> <source src="https://download.calibre-ebook.com/videos/kitty.webm" type="video/webm">
</video> </video>
@@ -68,11 +70,81 @@ command.
Hints for selecting kitty windows visually Hints for selecting kitty windows visually
.. }}}
Detailed list of changes Detailed list of changes
------------------------------------- -------------------------------------
0.24.2 [2022-02-03]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- macOS: Allow opening text files, images and directories with kitty when
launched using "Open with" in Finder (:iss:`4460`)
- Allow including config files matching glob patterns in :file:`kitty.conf`
(:iss:`4533`)
- Shell integration: Fix bash integration not working when ``PROMPT_COMMAND``
is used to change the prompt variables (:iss:`4476`)
- Shell integration: Fix cursor shape not being restored to default when
running commands in the shell
- Improve the UI of the ask kitten (:iss:`4545`)
- Allow customizing the placement and formatting of the
:opt:`tab_activity_symbol` and :opt:`bell_on_tab` symbols
by adding them to the :opt:`tab_title_template` (:iss:`4581`, :pull:`4507`)
- macOS: Persist "Secure Keyboard Entry" across restarts to match the behavior
of Terminal.app (:iss:`4471`)
- hints kitten: Fix common single letter extension files not being detected
(:iss:`4491`)
- Support dotted and dashed underline styles (:pull:`4529`)
- For the vertical and horizontal layouts have the windows arranged on a ring
rather than a plane. This means the first and last window are considered
neighbors (:iss:`4494`)
- A new action to clear the current selection (:iss:`4600`)
- Shell integration: fish: Fix cursor shape not working with fish's vi mode
(:iss:`4508`)
- Shell integration: fish: Dont override fish's native title setting functionality.
See `discussion <https://github.com/fish-shell/fish-shell/issues/8641>`__.
- macOS: Fix hiding via :kbd:`cmd+h` not working on macOS 10.15.7 (:iss:`4472`)
- Draw the dots for braille characters more evenly spaced at all font sizes (:iss:`4499`)
- icat kitten: Add options to mirror images and remove their transparency
before displaying them (:iss:`4513`)
- macOS: Respect the users system-wide global keyboard shortcut preferences
(:iss:`4501`)
- macOS: Fix a few key-presses causing beeps from Cocoa's text input system
(:iss:`4489`)
- macOS: Fix using shortcuts from the global menu bar as subsequent key presses
in a multi key mapping not working (:iss:`4519`)
- Fix getting last command output not working correctly when the screen is
scrolled (:pull:`4522`)
- Show number of windows per tab in the :ac:`select_tab` action (:pull:`4523`)
- macOS: Fix the shift key not clearing pre-edit text in IME (:iss:`4541`)
- Fix clicking in a window to focus it and typing immediately sometimes having
unexpected effects if at a shell prompt (:iss:`4128`)
- themes kitten: Allow writing to a different file than :file:`kitty.conf`.
0.24.1 [2022-01-06] 0.24.1 [2022-01-06]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -13,7 +13,7 @@ import subprocess
import sys import sys
import time import time
from functools import partial from functools import partial
from typing import Any, Callable, Dict, Iterable, List, Match, Optional, Tuple from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
from docutils import nodes from docutils import nodes
from docutils.parsers.rst.roles import set_classes from docutils.parsers.rst.roles import set_classes
@@ -22,16 +22,13 @@ from pygments.token import ( # type: ignore
Comment, Keyword, Literal, Name, Number, String, Whitespace Comment, Keyword, Literal, Name, Number, String, Whitespace
) )
from sphinx import addnodes, version_info from sphinx import addnodes, version_info
from sphinx.builders.html.transforms import KeyboardTransform
from sphinx.util.logging import getLogger from sphinx.util.logging import getLogger
KeyboardTransform.builders = ('html', 'dirhtml') # type: ignore
kitty_src = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) kitty_src = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if kitty_src not in sys.path: if kitty_src not in sys.path:
sys.path.insert(0, kitty_src) sys.path.insert(0, kitty_src)
from kitty.conf.types import Definition # noqa from kitty.conf.types import Definition, expand_opt_references # noqa
from kitty.constants import str_version, website_url # noqa from kitty.constants import str_version, website_url # noqa
# config {{{ # config {{{
@@ -62,6 +59,7 @@ extensions = [
'sphinx.ext.ifconfig', 'sphinx.ext.ifconfig',
'sphinx.ext.viewcode', 'sphinx.ext.viewcode',
'sphinx.ext.githubpages', 'sphinx.ext.githubpages',
'sphinx.ext.extlinks',
'sphinx_copybutton', 'sphinx_copybutton',
'sphinx_inline_tabs', 'sphinx_inline_tabs',
"sphinxext.opengraph", "sphinxext.opengraph",
@@ -170,24 +168,11 @@ texinfo_documents = [
# GitHub linking inline roles {{{ # GitHub linking inline roles {{{
def num_role( extlinks = {
which: str, name: str, rawtext: str, text: str, lineno: int, inliner: Any, options: Any = {}, content: Any = [] 'iss': ('https://github.com/kovidgoyal/kitty/issues/%s', '#%s'),
) -> Tuple[List[nodes.reference], List[nodes.problematic]]: 'pull': ('https://github.com/kovidgoyal/kitty/pull/%s', '#%s'),
' Link to a github issue ' 'disc': ('https://github.com/kovidgoyal/kitty/discussions/%s', '#%s'),
try: }
issue_num = int(text)
if issue_num <= 0:
raise ValueError
except ValueError:
msg = inliner.reporter.error(
'GitHub issue number must be a number greater than or equal to 1; '
'"%s" is invalid.' % text, line=lineno)
prb = inliner.problematic(rawtext, rawtext, msg)
return [prb], [msg]
url = f'https://github.com/kovidgoyal/kitty/{which}/{issue_num}'
set_classes(options)
node = nodes.reference(rawtext, f'#{issue_num}', refuri=url, **options)
return [node], []
def commit_role( def commit_role(
@@ -232,7 +217,8 @@ if you specify a program-to-run you can use the special placeholder
from kitty.remote_control import cli_msg, global_options_spec from kitty.remote_control import cli_msg, global_options_spec
with open('generated/cli-kitty-at.rst', 'w') as f: with open('generated/cli-kitty-at.rst', 'w') as f:
p = partial(print, file=f) p = partial(print, file=f)
p('kitty @\n' + '-' * 80) p('kitty @')
p('-' * 80)
p('.. program::', 'kitty @') p('.. program::', 'kitty @')
p('\n\n' + as_rst( p('\n\n' + as_rst(
global_options_spec, message=cli_msg, usage='command ...', appname='kitty @')) global_options_spec, message=cli_msg, usage='command ...', appname='kitty @'))
@@ -240,7 +226,8 @@ if you specify a program-to-run you can use the special placeholder
for cmd_name in sorted(all_command_names()): for cmd_name in sorted(all_command_names()):
func = command_for_name(cmd_name) func = command_for_name(cmd_name)
p(f'.. _at_{func.name}:\n') p(f'.. _at_{func.name}:\n')
p('kitty @', func.name + '\n' + '-' * 120) p('kitty @', func.name)
p('-' * 120)
p('.. program::', 'kitty @', func.name) p('.. program::', 'kitty @', func.name)
p('\n\n' + as_rst(*cli_params_for(func))) p('\n\n' + as_rst(*cli_params_for(func)))
from kittens.runner import get_kitten_cli_docs from kittens.runner import get_kitten_cli_docs
@@ -249,12 +236,12 @@ if you specify a program-to-run you can use the special placeholder
if data: if data:
with open(f'generated/cli-kitten-{kitten}.rst', 'w') as f: with open(f'generated/cli-kitten-{kitten}.rst', 'w') as f:
p = partial(print, file=f) p = partial(print, file=f)
p('.. program::', f'kitty +kitten {kitten}') p('.. program::', 'kitty +kitten', kitten)
p(f'\nSource code for {kitten}') p('\nSource code for', kitten)
p('-' * 72) p('-' * 72)
p(f'\nThe source code for this kitten is `available on GitHub <https://github.com/kovidgoyal/kitty/tree/master/kittens/{kitten}>`_.') p(f'\nThe source code for this kitten is `available on GitHub <https://github.com/kovidgoyal/kitty/tree/master/kittens/{kitten}>`_.')
p('\nCommand Line Interface') p('\nCommand Line Interface')
p('-' * 72, file=f) p('-' * 72)
p('\n\n' + option_spec_as_rst( p('\n\n' + option_spec_as_rst(
data['options'], message=data['help_text'], usage=data['usage'], appname=f'kitty +kitten {kitten}', data['options'], message=data['help_text'], usage=data['usage'], appname=f'kitty +kitten {kitten}',
heading_char='^')) heading_char='^'))
@@ -379,19 +366,6 @@ def link_role(
return [node], [] return [node], []
def expand_opt_references(conf_name: str, text: str) -> str:
conf_name += '.'
def expand(m: Match[str]) -> str:
ref = m.group(1)
if '<' not in ref and '.' not in ref:
full_ref = conf_name + ref
return ':opt:`{} <{}>`'.format(ref, full_ref)
return str(m.group())
return re.sub(r':opt:`(.+?)`', expand, text)
opt_aliases: Dict[str, str] = {} opt_aliases: Dict[str, str] = {}
shortcut_slugs: Dict[str, Tuple[str, str]] = {} shortcut_slugs: Dict[str, Tuple[str, str]] = {}
@@ -414,7 +388,7 @@ def parse_opt_node(env: Any, sig: str, signode: Any) -> str:
opt_aliases[optname] = firstname opt_aliases[optname] = firstname
count += 1 count += 1
if not firstname: if not firstname:
raise ValueError('{} is not a valid opt'.format(sig)) raise ValueError(f'{sig} is not a valid opt')
return firstname return firstname
@@ -429,7 +403,7 @@ def process_opt_link(env: Any, refnode: Any, has_explicit_title: bool, title: st
conf_name, opt = target.partition('.')[::2] conf_name, opt = target.partition('.')[::2]
if not opt: if not opt:
conf_name, opt = 'kitty', conf_name conf_name, opt = 'kitty', conf_name
full_name = conf_name + '.' + opt full_name = f'{conf_name}.{opt}'
return title, opt_aliases.get(full_name, full_name) return title, opt_aliases.get(full_name, full_name)
@@ -437,11 +411,11 @@ def process_shortcut_link(env: Any, refnode: Any, has_explicit_title: bool, titl
conf_name, slug = target.partition('.')[::2] conf_name, slug = target.partition('.')[::2]
if not slug: if not slug:
conf_name, slug = 'kitty', conf_name conf_name, slug = 'kitty', conf_name
full_name = conf_name + '.' + slug full_name = f'{conf_name}.{slug}'
try: try:
target, stitle = shortcut_slugs[full_name] target, stitle = shortcut_slugs[full_name]
except KeyError: except KeyError:
logger.warning('Unknown shortcut: {}'.format(target), location=refnode) logger.warning(f'Unknown shortcut: {target}', location=refnode)
else: else:
if not has_explicit_title: if not has_explicit_title:
title = stitle title = stitle
@@ -520,9 +494,6 @@ def setup(app: Any) -> None:
app.connect('html-page-context', add_html_context) app.connect('html-page-context', add_html_context)
app.add_lexer('session', SessionLexer() if version_info[0] < 3 else SessionLexer) app.add_lexer('session', SessionLexer() if version_info[0] < 3 else SessionLexer)
app.add_role('link', link_role) app.add_role('link', link_role)
app.add_role('iss', partial(num_role, 'issues'))
app.add_role('pull', partial(num_role, 'pull'))
app.add_role('disc', partial(num_role, 'discussions'))
app.add_role('commit', commit_role) app.add_role('commit', commit_role)
# monkey patch sphinx_inline_tabs to avoid a warning about parallel reads # monkey patch sphinx_inline_tabs to avoid a warning about parallel reads
# see https://github.com/pradyunsg/sphinx-inline-tabs/issues/26 # see https://github.com/pradyunsg/sphinx-inline-tabs/issues/26

View File

@@ -7,11 +7,11 @@ kitty.conf
rendering frames-per-second. See below for an overview of all customization rendering frames-per-second. See below for an overview of all customization
possibilities. possibilities.
You can open the config file within kitty by pressing :sc:`edit_config_file`. You can open the config file within kitty by pressing :sc:`edit_config_file` (:kbd:`⌘+,` on macOS).
You can reload the config file within kitty by pressing You can reload the config file within kitty by pressing
:sc:`reload_config_file` (:kbd:`⌃+⌘+,` on macOS) or sending kitty the ``SIGUSR1`` signal. :sc:`reload_config_file` (:kbd:`⌃+⌘+,` on macOS) or sending kitty the ``SIGUSR1`` signal.
You can also display the current configuration by pressing the :sc:`debug_config` You can also display the current configuration by pressing :sc:`debug_config`
(:kbd:`⌘+⌥+f6` on macOS) key. (:kbd:`⌥+⌘+,` on macOS).
.. _confloc: .. _confloc:
@@ -25,12 +25,14 @@ character. This works only if the ``#`` character is the first character
in the line. in the line.
You can include secondary config files via the :code:`include` directive. If You can include secondary config files via the :code:`include` directive. If
you use a relative path for include, it is resolved with respect to the you use a relative path for :code:`include`, it is resolved with respect to the
location of the current config file. Note that environment variables are location of the current config file. Note that environment variables are
expanded, so :code:`${USER}.conf` becomes :file:`name.conf` if expanded, so :code:`${USER}.conf` becomes :file:`name.conf` if
:code:`USER=name`. For example:: :code:`USER=name`. Also, you can use :code:`globinclude` to include files
matching a shell glob pattern. For example::
include other.conf include other.conf
globinclude kitty.d/**/*.conf
.. note:: Syntax highlighting for :file:`kitty.conf` in vim is available via .. note:: Syntax highlighting for :file:`kitty.conf` in vim is available via

View File

@@ -11,7 +11,7 @@ region of the screen.
The motivation for this extension is the various problems with the existing The motivation for this extension is the various problems with the existing
solution for erasing to background color, namely the *background color erase solution for erasing to background color, namely the *background color erase
(bce)* capability. See (bce)* capability. See
`this discussion <https://github.com/kovidgoyal/kitty/issues/160#issuecomment-346470545>`_ :iss:`this discussion <160#issuecomment-346470545>`
and `this FAQ <https://invisible-island.net/ncurses/ncurses.faq.html#bce_mismatches>`_ and `this FAQ <https://invisible-island.net/ncurses/ncurses.faq.html#bce_mismatches>`_
for a summary of problems with *bce*. for a summary of problems with *bce*.

View File

@@ -245,7 +245,7 @@ How can I assign a single global shortcut to bring up the kitty terminal?
Bringing up applications on a single key press is the job of the window Bringing up applications on a single key press is the job of the window
manager/desktop environment. For ways to do it with kitty (or indeed any manager/desktop environment. For ways to do it with kitty (or indeed any
terminal) in different environments, terminal) in different environments,
see `here <https://github.com/kovidgoyal/kitty/issues/45>`_. see :iss:`here <45>`.
I do not like the kitty icon! I do not like the kitty icon!
@@ -337,8 +337,7 @@ sorts of places where they may or may not work.
I am using tmux and have a problem I am using tmux and have a problem
-------------------------------------- --------------------------------------
First, terminal multiplexers are `a bad idea First, terminal multiplexers are :iss:`a bad idea <391#issuecomment-638320745>`, do
<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 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`). tmux does, but better, with the exception of remote persistence (:iss:`391`).
If you still want to use tmux, read on. If you still want to use tmux, read on.

View File

@@ -12,8 +12,7 @@ emulator. The major design goals are:
below as well as above the text, with alpha blending. The graphics should also scroll with the text, automatically. below as well as above the text, with alpha blending. The graphics should also scroll with the text, automatically.
* Should use optimizations when the client is running on the same computer as the terminal emulator. * Should use optimizations when the client is running on the same computer as the terminal emulator.
For some discussion regarding the design choices, see `#33 For some discussion regarding the design choices, see :iss:`33`.
<https://github.com/kovidgoyal/kitty/issues/33>`_.
To see a quick demo, inside a |kitty| terminal run:: To see a quick demo, inside a |kitty| terminal run::
@@ -109,7 +108,7 @@ features of the graphics protocol:
def serialize_gr_command(**cmd): def serialize_gr_command(**cmd):
payload = cmd.pop('payload', None) payload = cmd.pop('payload', None)
cmd = ','.join('{}={}'.format(k, v) for k, v in cmd.items()) cmd = ','.join(f'{k}={v}' for k, v in cmd.items())
ans = [] ans = []
w = ans.append w = ans.append
w(b'\033_G'), w(cmd.encode('ascii')) w(b'\033_G'), w(cmd.encode('ascii'))

View File

@@ -14,13 +14,13 @@ kitty
changelog changelog
integrations integrations
protocol-extensions protocol-extensions
press-mentions
.. tab:: Fast .. tab:: Fast
* Offloads rendering to the GPU for :doc:`lower system load <performance>` * Offloads rendering to the GPU for :doc:`lower system load <performance>`
* Uses threaded rendering for * Uses threaded rendering for :iss:`absolutely minimal latency <2701#issuecomment-636497270>`
`absolutely minimal latency <https://github.com/kovidgoyal/kitty/issues/2701#issuecomment-636497270>`_
* Performance tradeoffs can be :ref:`tuned <conf-kitty-performance>` * Performance tradeoffs can be :ref:`tuned <conf-kitty-performance>`
.. tab:: Capable .. tab:: Capable
@@ -61,7 +61,7 @@ kitty
.. raw:: html .. raw:: html
<video controls width="640" height="360"> <video controls width="640" height="360" poster="_static/poster.png">
<source src="https://download.calibre-ebook.com/videos/kitty.mp4" type="video/mp4"> <source src="https://download.calibre-ebook.com/videos/kitty.mp4" type="video/mp4">
<source src="https://download.calibre-ebook.com/videos/kitty.webm" type="video/webm"> <source src="https://download.calibre-ebook.com/videos/kitty.webm" type="video/webm">
</video> </video>

View File

@@ -441,6 +441,7 @@ mode* (the ``smkx/rmkx`` terminfo capabilities). This form is used only in
"F10", "kf10", "CSI 21 ~" "F10", "kf10", "CSI 21 ~"
"F11", "kf11", "CSI 23 ~" "F11", "kf11", "CSI 23 ~"
"F12", "kf12", "CSI 24 ~" "F12", "kf12", "CSI 24 ~"
"MENU", "kf16", "CSI 29 ~"
There are a few more functional keys that have special cased legacy encodings. There are a few more functional keys that have special cased legacy encodings.
These are present because they are commonly used and for the sake of legacy These are present because they are commonly used and for the sake of legacy

View File

@@ -325,7 +325,7 @@ Kittens created by kitty users
`smart-scroll <https://github.com/yurikhan/kitty-smart-scroll>`_ `smart-scroll <https://github.com/yurikhan/kitty-smart-scroll>`_
Makes the kitty scroll bindings work in full screen applications Makes the kitty scroll bindings work in full screen applications
`insert password <https://github.com/kovidgoyal/kitty/issues/1222>`_ :iss:`insert password <1222>`
Insert a password from a CLI password manager, taking care to only do it at Insert a password from a CLI password manager, taking care to only do it at
a password prompt. a password prompt.

View File

@@ -33,6 +33,12 @@ that has been marked as such by the program running in the terminal,
for example, by ``ls --hyperlink=auto``. You can also :doc:`customize what actions are for example, by ``ls --hyperlink=auto``. You can also :doc:`customize what actions are
taken for different types of URLs <../open_actions>`. taken for different types of URLs <../open_actions>`.
.. note:: If there are more hints than letters, hints will use multiple
letters. In this case, when you press the first letter, only hints
starting with that letter are displayed. Pressing the second letter will
select that hint or press :kbd:`Enter` or :kbd:`Space` to select the empty
hint.
The hints kitten is very powerful to see more detailed help on its various The hints kitten is very powerful to see more detailed help on its various
options and modes of operation, see below. You can use these options to options and modes of operation, see below. You can use these options to
create mappings in :file:`kitty.conf` to select various different text create mappings in :file:`kitty.conf` to select various different text

View File

@@ -45,17 +45,31 @@ You can now run searches with::
hg some-search-term hg some-search-term
If you want to enable completion, for the kitten, you can delegate completion 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 to rg. How to do that varies based on the shell:
:file:`hg` somewhere in your ``PATH``:
.. code-block:: sh
#!/bin/sh .. tab:: zsh
exec kitty +kitten hyperlinked_grep "$@"
Then, for example, for ZSH, add the following to :file:`.zshrc`:: Instead of using an alias create a simple wrapper script named
:file:`hg` somewhere in your ``PATH``:
compdef _rg hg .. code-block:: sh
#!/bin/sh
exec kitty +kitten hyperlinked_grep "$@"
Then, add the following to :file:`.zshrc`::
compdef _rg hg
.. tab:: fish
You can combine both the aliasing/wrapping and pointing fish
to rg's autocompletion with a fish "wrapper" function in your :file:`config.fish`:
.. code-block:: sh
function hg --wraps rg; kitty +kitten hyperlinked_grep $argv; end
To learn more about kitty's powerful framework for customizing URL click To learn more about kitty's powerful framework for customizing URL click
actions, :doc:`see here </open_actions>`. actions, :doc:`see here </open_actions>`.

View File

@@ -35,6 +35,18 @@ Then you can simply use ``icat image.png`` to view images.
The ``icat`` kitten has various command line arguments to allow it to be used The ``icat`` kitten has various command line arguments to allow it to be used
from inside other programs to display images. In particular, :option:`--place`, from inside other programs to display images. In particular, :option:`--place`,
:option:`--detect-support` and :option:`--print-window-size`. :option:`--detect-support`, :option:`--silent` and :option:`--print-window-size`.
If you are trying to integrate icat into a complex program like a file
manager or editor, there are a few things to keep in mind. icat works by
communicating over the TTY device, it both writes to and reads from the TTY.
So it is imperative that while it is running the host program does not do
any TTY I/O. Any key presses or other input from the user on the TTY device
will be discarded. At a minimum, you should use the :option:`--silent` and
:option:`--transfer-mode` command line arguments. To be
really robust you should consider writing proper support for the
:doc:`../graphics-protocol` in the program instead. Nowadays there are many
libraries that have support for it.
.. include:: /generated/cli-kitten-icat.rst .. include:: /generated/cli-kitten-icat.rst

View File

@@ -128,33 +128,42 @@ The Splits Layout
This is the most flexible layout. You can create any arrangement of windows This is the most flexible layout. You can create any arrangement of windows
by splitting exiting windows repeatedly. To best use this layout you should by splitting exiting windows repeatedly. To best use this layout you should
define a few extra keybindings in :file:`kitty.conf`:: define a few extra key bindings in :file:`kitty.conf`::
# Create a new window splitting the space used by the existing one so that
# the two windows are placed one above the other
map F5 launch --location=hsplit map F5 launch --location=hsplit
# Create a new window splitting the space used by the existing one so that
# the two windows are placed side by side
map F6 launch --location=vsplit map F6 launch --location=vsplit
# Rotate the current split, chaging its split axis from vertical to
# horizontal or vice versa
map F7 layout_action rotate map F7 layout_action rotate
# Move the active window in the indicated direction
map shift+up move_window up map shift+up move_window up
map shift+left move_window left map shift+left move_window left
map shift+right move_window right map shift+right move_window right
map shift+down move_window down map shift+down move_window down
# Switch focus to the neighboring window in the indicated direction
map ctrl+left neighboring_window left map ctrl+left neighboring_window left
map ctrl+right neighboring_window right map ctrl+right neighboring_window right
map ctrl+up neighboring_window up map ctrl+up neighboring_window up
map ctrl+down neighboring_window down map ctrl+down neighboring_window down
Now you can create horizontal and vertical splits by using :kbd:`F5` and Windows can be resized using :ref:`window_resizing`. You can swap the windows
:kbd:`F6`. You can move them around using :kbd:`shift+arrow` keys in a split using the ``rotate`` action with an argument of ``180`` and rotate
and you can move focus to neighboring windows using :kbd:`ctrl+arrow` keys. and swap with an argument of ``270``.
You can switch an existing split from horizontal to vertical and vice versa
using :kbd:`F7`. Finally, windows can be resized using :ref:`window_resizing`.
Note that you can swap the windows in a split using the ``rotate`` action with
an argument of ``180`` and rotate and swap with an argument of ``270``.
This layout takes one option, ``split_axis`` that controls whether new windows This layout takes one option, ``split_axis`` that controls whether new windows
are placed into vertical or horizontal splits, by default:: are placed into vertical or horizontal splits when a ``--location`` is not
specified. A value of ``horizontal`` (same as ``--location=vsplit``)
means when a new split is created the two windows will be placed side by side
and a value of ``vertical`` (same as ``--location=hsplit``) means the two
windows will be placed one on top of the other. By default::
enabled_layouts splits:split_axis=horizontal enabled_layouts splits:split_axis=horizontal

View File

@@ -100,3 +100,41 @@ lines. The various available criteria are:
``file`` ``file``
A shell glob pattern that must match the filename, for example: A shell glob pattern that must match the filename, for example:
``image-??.png`` ``image-??.png``
Scripting the opening of files with kitty on macOS
-------------------------------------------------------
On macOS you can use :guilabel:`Open With` in Finder or drag and drop files
onto the kitty dock icon to open them with kitty. The default action
is to open text files in your editor and images using the icat kitten.
Shell scripts are run in a shell. You can customize these actions by creating
a :file:`launch-actions.conf` file in the kitty config directory, just like
the :file:`open-actions.conf` file above. For example:
.. code:: conf
# Open script files based on their shebangs
protocol file
ext sh,command,tool
action launch --hold --type=os-window kitty +shebang $FILE_PATH {SHELL}
# Open shell specific script files
protocol file
ext fish,bash,zsh
action launch --hold --type=os-window kitty +shebang $FILE_PATH __ext__
# Open directories
protocol file
mime inode/directory
action launch --type=os-window --cwd $FILE_PATH
# Open text files without fragments in the editor
protocol file
mime text/*
action launch --type=os-window $EDITOR $FILE_PATH
# Open image files with icat
protocol file
mime image/*
action launch --type=os-window kitty +kitten icat --hold $FILE_PATH

View File

@@ -247,8 +247,7 @@ Would open the scrollback buffer in a new :term:`window` when you press the :kbd
key. See :sc:`show_scrollback <show_scrollback>` for details. key. See :sc:`show_scrollback <show_scrollback>` for details.
If you want to use it with an editor such as vim to get more powerful features, If you want to use it with an editor such as vim to get more powerful features,
you can see tips for doing so, in you can see tips for doing so, in :iss:`this thread <719>`.
`this thread <https://github.com/kovidgoyal/kitty/issues/719>`_.
If you wish to store very large amounts of scrollback to view using the piping or If you wish to store very large amounts of scrollback to view using the piping or
:sc:`show_scrollback <show_scrollback>` features, you can use the :sc:`show_scrollback <show_scrollback>` features, you can use the

5
docs/press-mentions.rst Normal file
View File

@@ -0,0 +1,5 @@
Press mentions of kitty
========================
`Console #88 <https://console.substack.com/p/console-88>`__
An interview with Kovid about kitty

View File

@@ -26,7 +26,7 @@ Features
to view it in a pager to view it in a pager
* The current working directory or the command being executed are automatically * The current working directory or the command being executed are automatically
displayed in the kitty window titlebar/tab title. displayed in the kitty window titlebar/tab title
* The text cursor is changed to a bar when editing commands at the shell prompt * The text cursor is changed to a bar when editing commands at the shell prompt
@@ -58,7 +58,9 @@ no-cursor
Turn off changing of the text cursor to a bar when editing text Turn off changing of the text cursor to a bar when editing text
no-title no-title
Turn off setting the kitty window/tab title based on shell state Turn off setting the kitty window/tab title based on shell state.
Note that for the ``fish`` shell kitty relies on fish's native title
setting functionality instead.
no-prompt-mark no-prompt-mark
Turn off marking of prompts. This disables jumping to prompt, browsing Turn off marking of prompts. This disables jumping to prompt, browsing
@@ -66,6 +68,8 @@ no-prompt-mark
no-complete no-complete
Turn off completion for the kitty command. Turn off completion for the kitty command.
Note that for the ``fish`` shell this does not take effect, since fish
already comes with a kitty completion script.
More ways to browse command output More ways to browse command output
@@ -169,6 +173,7 @@ code used for each shell below:
.. literalinclude:: ../shell-integration/fish/vendor_conf.d/kitty-shell-integration.fish .. literalinclude:: ../shell-integration/fish/vendor_conf.d/kitty-shell-integration.fish
:language: fish :language: fish
:force:
.. tab:: bash .. tab:: bash
@@ -235,11 +240,20 @@ completely, in which case simply do not set the
:envvar:`KITTY_SHELL_INTEGRATION` variable at all. :envvar:`KITTY_SHELL_INTEGRATION` variable at all.
If you want this to work while SSHing into a remote system, then you will If you want this to work while SSHing into a remote system, then you will
need to add some code to the snippets above to check if :code:`KITTY_INSTALLATION_DIR` need to add some code to the snippets above to check if :envvar:`KITTY_INSTALLATION_DIR`
is empty and if so to set it to some hard coded location with the shell is empty and if so to set it to some hard coded location with the shell
integration scripts that need to be copied onto the remote system. integration scripts that need to be copied onto the remote system.
Integration with other shells
-------------------------------
There exist third-party integrations to use these features for various other shells:
* Jupyter console via a patch (:iss:`4475`)
* `xonsh <https://github.com/xonsh/xonsh/issues/4623>`__
Notes for shell developers Notes for shell developers
----------------------------- -----------------------------

View File

@@ -13,8 +13,8 @@ To set the underline style::
<ESC>[4:1m # this is a straight underline <ESC>[4:1m # this is a straight underline
<ESC>[4:2m # this is a double underline <ESC>[4:2m # this is a double underline
<ESC>[4:3m # this is a curly underline <ESC>[4:3m # this is a curly underline
<ESC>[4:4m # this is a dotted underline (not implemented in kitty) <ESC>[4:4m # this is a dotted underline
<ESC>[4:5m # this is a dashed underline (not implemented in kitty) <ESC>[4:5m # this is a dashed underline
<ESC>[4m # this is a straight underline (for backwards compat) <ESC>[4m # this is a straight underline (for backwards compat)
<ESC>[24m # this is no underline (for backwards compat) <ESC>[24m # this is no underline (for backwards compat)

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# License: GPLv3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net> # License: GPLv3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import os
import subprocess import subprocess
from collections import defaultdict from collections import defaultdict
from typing import Any, DefaultDict, Dict, FrozenSet, List, Tuple, Union from typing import Any, DefaultDict, Dict, FrozenSet, List, Tuple, Union
@@ -42,7 +43,7 @@ def parse_flag(keymap: KeymapType, type_map: Dict[str, Any], command_class: str)
lines = [] lines = []
for ch in type_map['flag']: for ch in type_map['flag']:
attr, allowed_values = keymap[ch] attr, allowed_values = keymap[ch]
q = ' && '.join(f"g.{attr} != '{x}'" for x in allowed_values) q = ' && '.join(f"g.{attr} != '{x}'" for x in sorted(allowed_values))
lines.append(f''' lines.append(f'''
case {attr}: {{ case {attr}: {{
g.{attr} = screen->parser_buf[pos++] & 0xff; g.{attr} = screen->parser_buf[pos++] & 0xff;
@@ -67,7 +68,7 @@ def cmd_for_report(report_name: str, keymap: KeymapType, type_map: Dict[str, Any
flag_fmt, flag_attrs = [], [] flag_fmt, flag_attrs = [], []
cv = {'flag': 'c', 'int': 'i', 'uint': 'I'}[atype] cv = {'flag': 'c', 'int': 'i', 'uint': 'I'}[atype]
for ch in type_map[atype]: for ch in type_map[atype]:
flag_fmt.append('s' + cv) flag_fmt.append(f's{cv}')
attr = keymap[ch][0] attr = keymap[ch][0]
flag_attrs.append(f'"{attr}", {conv}g.{attr}') flag_attrs.append(f'"{attr}", {conv}g.{attr}')
return ' '.join(flag_fmt), ', '.join(flag_attrs) return ' '.join(flag_fmt), ', '.join(flag_attrs)
@@ -240,7 +241,7 @@ static inline void
def write_header(text: str, path: str) -> None: def write_header(text: str, path: str) -> None:
with open(path, 'w') as f: with open(path, 'w') as f:
print(f'// This file is generated by {__file__} do not edit!', file=f, end='\n\n') print(f'// This file is generated by {os.path.basename(__file__)} do not edit!', file=f, end='\n\n')
print('#pragma once', file=f) print('#pragma once', file=f)
print(text, file=f) print(text, file=f)
subprocess.check_call(['clang-format', '-i', path]) subprocess.check_call(['clang-format', '-i', path])

View File

@@ -238,7 +238,7 @@ def patch_file(path: str, what: str, text: str, start_marker: str = '/* ', end_m
end = raw.index(end_q) end = raw.index(end_q)
except ValueError: except ValueError:
raise SystemExit(f'Failed to find "{end_q}" in {path}') raise SystemExit(f'Failed to find "{end_q}" in {path}')
raw = raw[:start] + start_q + '\n' + text + '\n' + raw[end:] raw = f'{raw[:start]}{start_q}\n{text}\n{raw[end:]}'
f.seek(0) f.seek(0)
f.truncate(0) f.truncate(0)
f.write(raw) f.write(raw)
@@ -368,7 +368,7 @@ def generate_ctrl_mapping() -> None:
val = str(ctrl_mapping[k]) val = str(ctrl_mapping[k])
items.append(val) items.append(val)
if k in "\\'": if k in "\\'":
k = '\\' + k k = f'\\{k}'
mi.append(f" case '{k}': return {val};") mi.append(f" case '{k}': return {val};")
for line_items in chunks(items, 6): for line_items in chunks(items, 6):

View File

@@ -264,6 +264,8 @@ display_reconfigured(CGDirectDisplayID display UNUSED, CGDisplayChangeSummaryFla
} }
} }
static NSDictionary<NSString*,NSNumber*> *global_shortcuts = nil;
@interface GLFWHelper : NSObject @interface GLFWHelper : NSObject
@end @end
@@ -280,6 +282,16 @@ display_reconfigured(CGDirectDisplayID display UNUSED, CGDisplayChangeSummaryFla
(void)object; (void)object;
} }
// watch for settings change and rebuild global_shortcuts using key/value observing on NSUserDefaults
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
(void)keyPath; (void)object; (void)change; (void)context;
if (global_shortcuts != nil) {
[global_shortcuts release];
global_shortcuts = nil;
}
}
@end // GLFWHelper @end // GLFWHelper
// Delegate for application related notifications {{{ // Delegate for application related notifications {{{
@@ -453,72 +465,253 @@ void* _glfwLoadLocalVulkanLoaderNS(void)
////// GLFW platform API ////// ////// GLFW platform API //////
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
static bool /**
is_modified_tab(NSEvent *event, NSEventModifierFlags modifierFlags) { * Apple Symbolic HotKeys Ids
switch ((NSUInteger)modifierFlags) { * To find this symbolic hot keys indices do:
// No need to handle shift+tab, [shift]+option+tab * 1. open Terminal
case NSEventModifierFlagShift: * 2. restore defaults in System Preferences > Keyboard > Shortcuts
case NSEventModifierFlagOption: * 3. defaults read com.apple.symbolichotkeys > current.txt
case (NSEventModifierFlagShift | NSEventModifierFlagOption): * 4. enable/disable given symbolic hot key in System Preferences > Keyboard > Shortcuts
// Do not intercept cmd+tab, shift+cmd+tab * 5. defaults read com.apple.symbolichotkeys | diff -C 5 current.txt -
case NSEventModifierFlagCommand: * 6. restore defaults in System Preferences > Keyboard > Shortcuts
case (NSEventModifierFlagShift | NSEventModifierFlagCommand): */
return false; typedef enum AppleShortcutNames {
default: // launchpad & dock
break; kSHKTurnDockHidingOnOrOff = 52, // Opt, Cmd, D
} kSHKShowLaunchpad = 160, //
// ctrl+whatever+tab, option+cmd+tab
if ( // display
( kSHKDecreaseDisplayBrightness1 = 53, // F14 (Fn)
(modifierFlags & NSEventModifierFlagControl) || kSHKDecreaseDisplayBrightness2 = 55, // F14 (Fn, Ctrl)
modifierFlags == (NSEventModifierFlagOption | NSEventModifierFlagCommand) kSHKIncreaseDisplayBrightness1 = 54, // F15 (Fn)
) && [event.charactersIgnoringModifiers isEqualToString:@"\t"] kSHKIncreaseDisplayBrightness2 = 56, // F15 (Fn, Ctrl)
) return true;
// shift+whatever+tab // mission control
if ( kSHKMissionControl = 32, // Ctrl, Arrow Up
(modifierFlags & NSEventModifierFlagShift) && kSHKShowNotificationCenter = 163, //
[event.charactersIgnoringModifiers isEqualToString:@"\x19"] kSHKTurnDoNotDisturbOnOrOff = 175, //
) return true; kSHKApplicationWindows = 33, // Ctrl, Arrow Down
return false; kSHKShowDesktop = 36, // F11
} kSHKMoveLeftASpace = 79, // Ctrl, Arrow Left
kSHKMoveRightASpace = 81, // Ctrl, Arrow Right
kSHKSwitchToDesktop1 = 118, // Ctrl, 1
kSHKSwitchToDesktop2 = 119, // Ctrl, 2
kSHKSwitchToDesktop3 = 120, // Ctrl, 3
kSHKSwitchToDesktop4 = 121, // Ctrl, 4
kSHKQuickNote = 190, // Fn, Q
// keyboard
kSHKChangeTheWayTabMovesFocus = 13, // Ctrl, F7
kSHKTurnKeyboardAccessOnOrOff = 12, // Ctrl, F1
kSHKMoveFocusToTheMenuBar = 7, // Ctrl, F2
kSHKMoveFocusToTheDock = 8, // Ctrl, F3
kSHKMoveFocusToActiveOrNextWindow = 9, // Ctrl, F4
kSHKMoveFocusToTheWindowToolbar = 10, // Ctrl, F5
kSHKMoveFocusToTheFloatingWindow = 11, // Ctrl, F6
kSHKMoveFocusToNextWindow = 27, // Cmd, `
kSHKMoveFocusToStatusMenus = 57, // Ctrl, F8
// input sources
kSHKSelectThePreviousInputSource = 60, // Ctrl, Space bar
kSHKSelectNextSourceInInputMenu = 61, // Ctrl, Opt, Space bar
// screenshots
kSHKSavePictureOfScreenAsAFile = 28, // Shift, Cmd, 3
kSHKCopyPictureOfScreenToTheClipboard = 29, // Ctrl, Shift, Cmd, 3
kSHKSavePictureOfSelectedAreaAsAFile = 30, // Shift, Cmd, 4
kSHKCopyPictureOfSelectedAreaToTheClipboard = 31, // Ctrl, Shift, Cmd, 4
kSHKScreenshotAndRecordingOptions = 184, // Shift, Cmd, 5
// spotlight
kSHKShowSpotlightSearch = 64, // Cmd, Space bar
kSHKShowFinderSearchWindow = 65, // Opt, Cmd, Space bar
// accessibility
kSHKTurnZoomOnOrOff = 15, // Opt, Cmd, 8
kSHKTurnImageSmoothingOnOrOff = 23, // Opt, Cmd, Backslash "\"
kSHKZoomOut = 19, // Opt, Cmd, -
kSHKZoomIn = 17, // Opt, Cmd, =
kSHKTurnFocusFollowingOnOrOff = 179, //
kSHKIncreaseContrast = 25, // Ctrl, Opt, Cmd, .
kSHKDecreaseContrast = 26, // Ctrl, Opt, Cmd, ,
kSHKInvertColors = 21, // Ctrl, Opt, Cmd, 8
kSHKTurnVoiceOverOnOrOff = 59, // Cmd, F5
kSHKShowAccessibilityControls = 162, // Opt, Cmd, F5
// app shortcuts
kSHKShowHelpMenu = 98, // Shift, Cmd, /
// deprecated (Not shown on macOS Monterey)
kSHKMoveFocusToTheWindowDrawer = 51, // Opt, Cmd, `
kSHKShowDashboard = 62, // F12
kSHKLookUpInDictionary = 70, // Shift, Cmd, E
kSHKHideAndShowFrontRow = 73, // Cmd, Esc
kSHKActivateSpaces = 75, // F8
// unknown
kSHKUnknown = 0, //
} AppleShortcutNames;
static bool static bool
is_cmd_period(NSEvent *event, NSEventModifierFlags modifierFlags) { is_shiftable_shortcut(int scv) {
if (modifierFlags != NSEventModifierFlagCommand) return false; return scv == kSHKMoveFocusToActiveOrNextWindow || scv == kSHKMoveFocusToNextWindow;
if ([event.charactersIgnoringModifiers isEqualToString:@"."]) return true;
return false;
} }
static bool #define USEFUL_MODS(x) (x & (NSEventModifierFlagShift | NSEventModifierFlagOption | NSEventModifierFlagCommand | NSEventModifierFlagControl))
is_modified_special_key(NSEvent *event, NSEventModifierFlags modifierFlags) {
// really one should use [[NSUserDefaults standardUserDefaults] valueForDefaultsDomain:@"com.apple.symbolichotkeys" key:@"AppleSymbolicHotKeys"] static void
// to get the list of global shortcuts and pass through the important ones, build_global_shortcuts_lookup(void) {
// see https://stackoverflow.com/questions/21878482/what-do-the-parameter-values-in-applesymbolichotkeys-plist-dict-represent NSMutableDictionary<NSString*, NSNumber*> *temp = [NSMutableDictionary dictionaryWithCapacity:128]; // will be autoreleased
// however given that in order to know which integers are which actions in that dict one needs reverse engineering NSDictionary *apple_settings = [[NSUserDefaults standardUserDefaults] persistentDomainForName:@"com.apple.symbolichotkeys"];
// see https://stackoverflow.com/questions/866056/how-do-i-programmatically-get-the-shortcut-keys-reserved-by-mac-os-x if (apple_settings) {
// it's too much effort. NSDictionary<NSString*, id> *symbolic_hotkeys = [apple_settings objectForKey:@"AppleSymbolicHotKeys"];
if ([event.charactersIgnoringModifiers length] != 1) return false; if (symbolic_hotkeys) {
const unichar ch = [event.charactersIgnoringModifiers characterAtIndex:0]; for (NSString *key in symbolic_hotkeys) {
if (modifierFlags == (NSEventModifierFlagControl | NSEventModifierFlagShift)) { id obj = symbolic_hotkeys[key];
switch (ch) { if (![key isKindOfClass:[NSString class]] || ![obj isKindOfClass:[NSDictionary class]]) continue;
case 0x1b: // Esc NSInteger sc = [key integerValue];
case NSF1FunctionKey: case NSF2FunctionKey: case NSF3FunctionKey: case NSF4FunctionKey: NSDictionary *sc_value = obj;
case NSF5FunctionKey: case NSF6FunctionKey: case NSF7FunctionKey: case NSF8FunctionKey: id enabled = [sc_value objectForKey:@"enabled"];
case NSF9FunctionKey: case NSF10FunctionKey: case NSF11FunctionKey: case NSF12FunctionKey: if (!enabled || ![enabled isKindOfClass:[NSNumber class]] || ![(NSNumber*)enabled boolValue]) continue;
case NSF13FunctionKey: case NSF14FunctionKey: case NSF15FunctionKey: case NSF16FunctionKey: id v = [sc_value objectForKey:@"value"];
case NSF17FunctionKey: case NSF18FunctionKey: case NSF19FunctionKey: if (!v || ![v isKindOfClass:[NSDictionary class]]) continue;
return true; NSDictionary *value = v;
id t = [value objectForKey:@"type"];
if (!t || ![t isKindOfClass:[NSString class]] || ![t isEqualToString:@"standard"]) continue;
id p = [value objectForKey:@"parameters"];
if (!p || ![p isKindOfClass:[NSArray class]] || [(NSArray*)p count] < 2) continue;
NSArray<NSNumber*> *parameters = p;
NSInteger ch = [parameters[0] isKindOfClass:[NSNumber class]] ? [parameters[0] integerValue] : 0xffff;
NSInteger vk = [parameters[1] isKindOfClass:[NSNumber class]] ? [parameters[1] integerValue] : 0xffff;
NSEventModifierFlags mods = ([parameters count] > 2 && [parameters[2] isKindOfClass:[NSNumber class]]) ? [parameters[2] unsignedIntegerValue] : 0;
mods = USEFUL_MODS(mods);
static char buf[64];
#define S(x, k) snprintf(buf, sizeof(buf) - 1, #x":%lx:%ld", (unsigned long)mods, (long)k)
if (ch == 0xffff) { if (vk == 0xffff) continue; S(v, vk); } else S(c, ch);
temp[@(buf)] = @(sc);
// the move to next window shortcuts also respond to the same shortcut + shift
if (is_shiftable_shortcut([key intValue]) && !(mods & NSEventModifierFlagShift)) {
mods |= NSEventModifierFlagShift;
if (ch == 0xffff) S(v, vk); else S(c, ch);
temp[@(buf)] = @(sc);
}
#undef S
}
} }
} }
switch (ch) { global_shortcuts = [[NSDictionary dictionaryWithDictionary:temp] retain];
case 0x1b: // Esc /* NSLog(@"global_shortcuts: %@", global_shortcuts); */
if (modifierFlags & (NSEventModifierFlagCommand | NSEventModifierFlagControl)) return true; }
break;
case NSHelpFunctionKey: // For some reason keyboards with an insert key have it mapped to help static int
if (!modifierFlags || modifierFlags == NSEventModifierFlagShift) return true; is_active_apple_global_shortcut(NSEvent *event) {
break; if (global_shortcuts == nil) build_global_shortcuts_lookup();
NSEventModifierFlags modifierFlags = USEFUL_MODS([event modifierFlags]);
static char lookup_key[64];
#define LOOKUP(t, k) \
snprintf(lookup_key, sizeof(lookup_key) - 1, #t":%lx:%ld", (unsigned long)modifierFlags, (long)k); \
NSNumber *sc = global_shortcuts[@(lookup_key)]; \
if (sc != nil) return [sc intValue];
if ([event.charactersIgnoringModifiers length] == 1) {
if (modifierFlags & NSEventModifierFlagShift) {
const uint32_t ch_without_shift = vk_to_unicode_key_with_current_layout([event keyCode]);
if (ch_without_shift < GLFW_FKEY_FIRST || ch_without_shift > GLFW_FKEY_LAST) {
LOOKUP(c, ch_without_shift);
}
}
const unichar ch = [event.charactersIgnoringModifiers characterAtIndex:0];
LOOKUP(c, ch);
}
unsigned short vk = [event keyCode];
if (vk != 0xffff) {
LOOKUP(v, vk);
}
#undef LOOKUP
return kSHKUnknown;
}
static bool
is_useful_apple_global_shortcut(int sc) {
switch(sc) {
// launchpad & dock
case kSHKTurnDockHidingOnOrOff: // Opt, Cmd, D
case kSHKShowLaunchpad: //
// display
case kSHKDecreaseDisplayBrightness1: // F14 (Fn)
case kSHKDecreaseDisplayBrightness2: // F14 (Fn, Ctrl)
case kSHKIncreaseDisplayBrightness1: // F15 (Fn)
case kSHKIncreaseDisplayBrightness2: // F14 (Fn, Ctrl)
// mission control
case kSHKMissionControl: // Ctrl, Arrow Up
case kSHKShowNotificationCenter: //
case kSHKTurnDoNotDisturbOnOrOff: //
case kSHKApplicationWindows: // Ctrl, Arrow Down
case kSHKShowDesktop: // F11
case kSHKMoveLeftASpace: // Ctrl, Arrow Left
case kSHKMoveRightASpace: // Ctrl, Arrow Right
case kSHKSwitchToDesktop1: // Ctrl, 1
case kSHKSwitchToDesktop2: // Ctrl, 2
case kSHKSwitchToDesktop3: // Ctrl, 3
case kSHKSwitchToDesktop4: // Ctrl, 4
case kSHKQuickNote: // Fn, Q
// keyboard
/* case kSHKChangeTheWayTabMovesFocus: // Ctrl, F7 */
/* case kSHKTurnKeyboardAccessOnOrOff: // Ctrl, F1 */
case kSHKMoveFocusToTheMenuBar: // Ctrl, F2
case kSHKMoveFocusToTheDock: // Ctrl, F3
case kSHKMoveFocusToActiveOrNextWindow: // Ctrl, F4
/* case kSHKMoveFocusToTheWindowToolbar: // Ctrl, F5 */
/* case kSHKMoveFocusToTheFloatingWindow: // Ctrl, F6 */
case kSHKMoveFocusToNextWindow: // Cmd, `
case kSHKMoveFocusToStatusMenus: // Ctrl, F8
// input sources
case kSHKSelectThePreviousInputSource: // Ctrl, Space bar
case kSHKSelectNextSourceInInputMenu: // Ctrl, Opt, Space bar
// screenshots
/* case kSHKSavePictureOfScreenAsAFile: // Shift, Cmd, 3 */
/* case kSHKCopyPictureOfScreenToTheClipboard: // Ctrl, Shift, Cmd, 3 */
/* case kSHKSavePictureOfSelectedAreaAsAFile: // Shift, Cmd, 4 */
/* case kSHKCopyPictureOfSelectedAreaToTheClipboard: // Ctrl, Shift, Cmd, 4 */
/* case kSHKScreenshotAndRecordingOptions: // Shift, Cmd, 5 */
// spotlight
case kSHKShowSpotlightSearch: // Cmd, Space bar
case kSHKShowFinderSearchWindow: // Opt, Cmd, Space bar
// accessibility
/* case kSHKTurnZoomOnOrOff: // Opt, Cmd, 8 */
/* case kSHKTurnImageSmoothingOnOrOff: // Opt, Cmd, Backslash "\" */
/* case kSHKZoomOut: // Opt, Cmd, - */
/* case kSHKZoomIn: // Opt, Cmd, = */
/* case kSHKTurnFocusFollowingOnOrOff: // */
/* case kSHKIncreaseContrast: // Ctrl, Opt, Cmd, . */
/* case kSHKDecreaseContrast: // Ctrl, Opt, Cmd, , */
/* case kSHKInvertColors: // Ctrl, Opt, Cmd, 8 */
/* case kSHKTurnVoiceOverOnOrOff: // Cmd, F5 */
/* case kSHKShowAccessibilityControls: // Opt, Cmd, F5 */
// app shortcuts
/* case kSHKShowHelpMenu: // Shift, Cmd, / */
// deprecated (Not shown on macOS Monterey)
/* case kSHKMoveFocusToTheWindowDrawer: // Opt, Cmd, ` */
/* case kSHKShowDashboard: // F12 */
/* case kSHKLookUpInDictionary: // Shift, Cmd, E */
/* case kSHKHideAndShowFrontRow: // Cmd, Esc */
/* case kSHKActivateSpaces: // F8 */
return true;
default:
return false;
} }
return false;
} }
GLFWAPI GLFWapplicationshouldhandlereopenfun glfwSetApplicationShouldHandleReopen(GLFWapplicationshouldhandlereopenfun callback) { GLFWAPI GLFWapplicationshouldhandlereopenfun glfwSetApplicationShouldHandleReopen(GLFWapplicationshouldhandlereopenfun callback) {
@@ -557,35 +750,65 @@ int _glfwPlatformInit(void)
} }
[NSApp setDelegate:_glfw.ns.delegate]; [NSApp setDelegate:_glfw.ns.delegate];
static struct {
unsigned short virtual_key_code;
NSTimeInterval timestamp;
} last_keydown_shortcut_event;
last_keydown_shortcut_event.virtual_key_code = 0xffff;
NSEvent* (^keydown_block)(NSEvent*) = ^ NSEvent* (NSEvent* event) NSEvent* (^keydown_block)(NSEvent*) = ^ NSEvent* (NSEvent* event)
{ {
NSEventModifierFlags modifierFlags = [event modifierFlags] & (NSEventModifierFlagShift | NSEventModifierFlagOption | NSEventModifierFlagCommand | NSEventModifierFlagControl); debug_key("---------------- key down -------------------\n");
if (is_modified_special_key(event, modifierFlags) || is_modified_tab(event, modifierFlags) || is_cmd_period(event, modifierFlags)) { debug_key("%s\n", [[event description] UTF8String]);
// Cocoa swallows various key presses, so route them explicitly if (!_glfw.ignoreOSKeyboardProcessing) {
[[NSApp keyWindow].contentView keyDown:event]; // first check if there is a global menu bar shortcut
return nil; if ([[NSApp mainMenu] performKeyEquivalent:event]) {
debug_key("keyDown triggerred global menu bar action ignoring\n");
last_keydown_shortcut_event.virtual_key_code = [event keyCode];
last_keydown_shortcut_event.timestamp = [event timestamp];
return nil;
}
// now check if there is a useful apple shortcut
int global_shortcut = is_active_apple_global_shortcut(event);
if (is_useful_apple_global_shortcut(global_shortcut)) {
debug_key("keyDown triggerred global macOS shortcut ignoring\n");
last_keydown_shortcut_event.virtual_key_code = [event keyCode];
last_keydown_shortcut_event.timestamp = [event timestamp];
return event;
}
} }
last_keydown_shortcut_event.virtual_key_code = 0xffff;
return event; NSWindow *kw = [NSApp keyWindow];
if (kw && kw.contentView) [kw.contentView keyDown:event];
else debug_key("keyDown ignored as no keyWindow present\n");
return nil;
}; };
NSEvent* (^keyup_block)(NSEvent*) = ^ NSEvent* (NSEvent* event) NSEvent* (^keyup_block)(NSEvent*) = ^ NSEvent* (NSEvent* event)
{ {
NSEventModifierFlags modifierFlags = [event modifierFlags] & (NSEventModifierFlagShift | NSEventModifierFlagOption | NSEventModifierFlagCommand | NSEventModifierFlagControl); debug_key("----------------- key up --------------------\n");
if (modifierFlags & NSEventModifierFlagCommand) { debug_key("%s\n", [[event description] UTF8String]);
// From https://cocoadev.github.io/GameKeyboardHandlingAlmost/ if (last_keydown_shortcut_event.virtual_key_code != 0xffff && last_keydown_shortcut_event.virtual_key_code == [event keyCode]) {
// This works around an AppKit bug, where key up events while holding // ignore as the corresponding key down event triggered a menu bar or macOS shortcut
// down the command key don't get sent to the key window. last_keydown_shortcut_event.virtual_key_code = 0xffff;
[[NSApp keyWindow] sendEvent:event]; debug_key("keyUp ignored as corresponds to previous keyDown that trigerred a shortcut\n");
}
if (is_modified_special_key(event, modifierFlags) || is_modified_tab(event, modifierFlags) || is_cmd_period(event, modifierFlags)) {
// Cocoa swallows various key presses, so route them explicitly
[[NSApp keyWindow].contentView keyUp:event];
return nil; return nil;
} }
NSWindow *kw = [NSApp keyWindow];
if (kw && kw.contentView) [kw.contentView keyUp:event];
else debug_key("keyUp ignored as no keyWindow present\n");
return nil;
};
return event; NSEvent* (^flags_changed_block)(NSEvent*) = ^ NSEvent* (NSEvent* event)
{
debug_key("-------------- flags changed -----------------\n");
debug_key("%s\n", [[event description] UTF8String]);
last_keydown_shortcut_event.virtual_key_code = 0xffff;
NSWindow *kw = [NSApp keyWindow];
if (kw && kw.contentView) [kw.contentView flagsChanged:event];
else debug_key("flagsChanged ignored as no keyWindow present\n");
return nil;
}; };
_glfw.ns.keyUpMonitor = _glfw.ns.keyUpMonitor =
@@ -594,6 +817,9 @@ int _glfwPlatformInit(void)
_glfw.ns.keyDownMonitor = _glfw.ns.keyDownMonitor =
[NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown
handler:keydown_block]; handler:keydown_block];
_glfw.ns.flagsChangedMonitor =
[NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskFlagsChanged
handler:flags_changed_block];
if (_glfw.hints.init.ns.chdir) if (_glfw.hints.init.ns.chdir)
changeToResourcesDirectory(); changeToResourcesDirectory();
@@ -606,6 +832,13 @@ int _glfwPlatformInit(void)
}; };
[[NSUserDefaults standardUserDefaults] registerDefaults:defaults]; [[NSUserDefaults standardUserDefaults] registerDefaults:defaults];
NSUserDefaults *apple_settings = [[NSUserDefaults alloc] initWithSuiteName:@"com.apple.symbolichotkeys"];
[apple_settings addObserver:_glfw.ns.helper
forKeyPath:@"AppleSymbolicHotKeys"
options:NSKeyValueObservingOptionNew
context:NULL];
_glfw.ns.appleSettings = apple_settings;
[[NSNotificationCenter defaultCenter] [[NSNotificationCenter defaultCenter]
addObserver:_glfw.ns.helper addObserver:_glfw.ns.helper
selector:@selector(selectedKeyboardInputSourceChanged:) selector:@selector(selectedKeyboardInputSourceChanged:)
@@ -661,6 +894,8 @@ void _glfwPlatformTerminate(void)
object:nil]; object:nil];
[[NSNotificationCenter defaultCenter] [[NSNotificationCenter defaultCenter]
removeObserver:_glfw.ns.helper]; removeObserver:_glfw.ns.helper];
if (_glfw.ns.appleSettings)
[_glfw.ns.appleSettings removeObserver:_glfw.ns.helper forKeyPath:@"AppleSymbolicHotKeys"];
[_glfw.ns.helper release]; [_glfw.ns.helper release];
_glfw.ns.helper = nil; _glfw.ns.helper = nil;
} }
@@ -669,10 +904,18 @@ void _glfwPlatformTerminate(void)
[NSEvent removeMonitor:_glfw.ns.keyUpMonitor]; [NSEvent removeMonitor:_glfw.ns.keyUpMonitor];
if (_glfw.ns.keyDownMonitor) if (_glfw.ns.keyDownMonitor)
[NSEvent removeMonitor:_glfw.ns.keyDownMonitor]; [NSEvent removeMonitor:_glfw.ns.keyDownMonitor];
if (_glfw.ns.flagsChangedMonitor)
[NSEvent removeMonitor:_glfw.ns.flagsChangedMonitor];
if (_glfw.ns.appleSettings != nil) {
[_glfw.ns.appleSettings release];
_glfw.ns.appleSettings = nil;
}
free(_glfw.ns.clipboardString); free(_glfw.ns.clipboardString);
_glfwTerminateNSGL(); _glfwTerminateNSGL();
if (global_shortcuts != nil) { [global_shortcuts release]; global_shortcuts = nil; }
} // autoreleasepool } // autoreleasepool
} }

View File

@@ -64,6 +64,7 @@ typedef void* CVDisplayLinkRef;
#define NSOpenGLContextParameterSurfaceOpacity NSOpenGLCPSurfaceOpacity #define NSOpenGLContextParameterSurfaceOpacity NSOpenGLCPSurfaceOpacity
#endif #endif
#define debug_key(...) if (_glfw.hints.init.debugKeyboard) { fprintf(stderr, __VA_ARGS__); fflush(stderr); }
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int, unsigned long); typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int, unsigned long);
typedef bool (* GLFWapplicationshouldhandlereopenfun)(int); typedef bool (* GLFWapplicationshouldhandlereopenfun)(int);
@@ -173,8 +174,8 @@ typedef struct _GLFWlibraryNS
IOHIDManagerRef hidManager; IOHIDManagerRef hidManager;
id unicodeData; id unicodeData;
id helper; id helper;
id keyUpMonitor; id keyUpMonitor, keyDownMonitor, flagsChangedMonitor;
id keyDownMonitor; id appleSettings;
id nibObjects; id nibObjects;
char keyName[64]; char keyName[64];
@@ -231,7 +232,6 @@ typedef struct _GLFWtimerNS
} _GLFWtimerNS; } _GLFWtimerNS;
void _glfwPollMonitorsNS(void); void _glfwPollMonitorsNS(void);
void _glfwSetVideoModeNS(_GLFWmonitor* monitor, const GLFWvidmode* desired); void _glfwSetVideoModeNS(_GLFWmonitor* monitor, const GLFWvidmode* desired);
void _glfwRestoreVideoModeNS(_GLFWmonitor* monitor); void _glfwRestoreVideoModeNS(_GLFWmonitor* monitor);
@@ -248,3 +248,4 @@ void _glfwShutdownCVDisplayLink(unsigned long long, void*);
void _glfwCocoaPostEmptyEvent(void); void _glfwCocoaPostEmptyEvent(void);
void _glfw_create_cv_display_link(_GLFWDisplayLinkNS *entry); void _glfw_create_cv_display_link(_GLFWDisplayLinkNS *entry);
_GLFWDisplayLinkNS* _glfw_create_display_link(CGDirectDisplayID); _GLFWDisplayLinkNS* _glfw_create_display_link(CGDirectDisplayID);
uint32_t vk_to_unicode_key_with_current_layout(uint16_t keycode);

View File

@@ -32,11 +32,6 @@
#include <float.h> #include <float.h>
#include <string.h> #include <string.h>
#if (MAC_OS_X_VERSION_MAX_ALLOWED < 101300)
#define NSControlStateValueOn NSOnState
#define NSControlStateValueOff NSOffState
#define NSControlStateValueMixed NSMixedState
#endif
static const char* static const char*
polymorphic_string_as_utf8(id string) { polymorphic_string_as_utf8(id string) {
@@ -259,7 +254,7 @@ is_pua_char(uint32_t ch) {
return (0xE000 <= ch && ch <= 0xF8FF) || (0xF0000 <= ch && ch <= 0xFFFFF) || (0x100000 <= ch && ch <= 0x10FFFF); return (0xE000 <= ch && ch <= 0xF8FF) || (0xF0000 <= ch && ch <= 0xFFFFF) || (0x100000 <= ch && ch <= 0x10FFFF);
} }
static uint32_t uint32_t
vk_to_unicode_key_with_current_layout(uint16_t keycode) vk_to_unicode_key_with_current_layout(uint16_t keycode)
{ {
UInt32 dead_key_state = 0; UInt32 dead_key_state = 0;
@@ -513,8 +508,6 @@ translateFlags(NSUInteger flags)
return mods; return mods;
} }
#define debug_key(...) if (_glfw.hints.init.debugKeyboard) { fprintf(stderr, __VA_ARGS__); fflush(stderr); }
static const char* static const char*
format_mods(int mods) { format_mods(int mods) {
static char buf[128]; static char buf[128];
@@ -576,166 +569,11 @@ get_window_size_without_border_in_logical_pixels(_GLFWwindow *window) {
return [window->ns.object contentRectForFrameRect:[window->ns.object frame]]; return [window->ns.object contentRectForFrameRect:[window->ns.object frame]];
} }
// Translate a GLFW keycode to a Cocoa modifier flag
//
static NSUInteger
translateKeyToModifierFlag(uint32_t key)
{
switch (key)
{
case GLFW_FKEY_LEFT_SHIFT:
case GLFW_FKEY_RIGHT_SHIFT:
return NSEventModifierFlagShift;
case GLFW_FKEY_LEFT_CONTROL:
case GLFW_FKEY_RIGHT_CONTROL:
return NSEventModifierFlagControl;
case GLFW_FKEY_LEFT_ALT:
case GLFW_FKEY_RIGHT_ALT:
return NSEventModifierFlagOption;
case GLFW_FKEY_LEFT_SUPER:
case GLFW_FKEY_RIGHT_SUPER:
return NSEventModifierFlagCommand;
case GLFW_FKEY_CAPS_LOCK:
return NSEventModifierFlagCapsLock;
}
return 0;
}
// Defines a constant for empty ranges in NSTextInputClient // Defines a constant for empty ranges in NSTextInputClient
// //
static const NSRange kEmptyRange = { NSNotFound, 0 }; static const NSRange kEmptyRange = { NSNotFound, 0 };
// SecureKeyboardEntryController {{{
@interface SecureKeyboardEntryController : NSObject
@property (nonatomic, readonly) BOOL isDesired;
@property (nonatomic, readonly, getter=isEnabled) BOOL enabled;
+ (instancetype)sharedInstance;
- (void)toggle;
- (void)update;
@end
@implementation SecureKeyboardEntryController {
int _count;
BOOL _desired;
}
+ (instancetype)sharedInstance {
static id instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (instancetype)init {
self = [super init];
if (self) {
_desired = false;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidResignActive:)
name:NSApplicationDidResignActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidBecomeActive:)
name:NSApplicationDidBecomeActiveNotification
object:nil];
if ([NSApp isActive]) {
[self update];
}
}
return self;
}
#pragma mark - API
- (void)toggle {
// Set _desired to the opposite of the current state.
_desired = !_desired;
debug_key("toggle called. Setting desired to %d", _desired);
// Try to set the system's state of secure input to the desired state.
[self update];
}
- (BOOL)isEnabled {
return !!IsSecureEventInputEnabled();
}
- (BOOL)isDesired {
return _desired;
}
#pragma mark - Notifications
- (void)applicationDidResignActive:(NSNotification *)notification {
(void)notification;
if (_count > 0) {
debug_key("Application resigning active.");
[self update];
}
}
- (void)applicationDidBecomeActive:(NSNotification *)notification {
(void)notification;
if (self.isDesired) {
debug_key("Application became active.");
[self update];
}
}
#pragma mark - Private
- (BOOL)allowed {
return [NSApp isActive];
}
- (void)update {
debug_key("Update secure keyboard entry. desired=%d active=%d\n",
(int)self.isDesired, (int)[NSApp isActive]);
const BOOL secure = self.isDesired && [self allowed];
if (secure && _count > 0) {
debug_key("Want to turn on secure input but it's already on\n");
return;
}
if (!secure && _count == 0) {
debug_key("Want to turn off secure input but it's already off\n");
return;
}
debug_key("Before: IsSecureEventInputEnabled returns %d ", (int)self.isEnabled);
if (secure) {
OSErr err = EnableSecureEventInput();
debug_key("EnableSecureEventInput err=%d ", (int)err);
if (err) {
debug_key("EnableSecureEventInput failed with error %d ", (int)err);
} else {
_count += 1;
}
} else {
OSErr err = DisableSecureEventInput();
debug_key("DisableSecureEventInput err=%d ", (int)err);
if (err) {
debug_key("DisableSecureEventInput failed with error %d ", (int)err);
} else {
_count -= 1;
}
}
debug_key("After: IsSecureEventInputEnabled returns %d\n", (int)self.isEnabled);
}
@end
// }}}
// Delegate for window related notifications {{{ // Delegate for window related notifications {{{
@interface GLFWWindowDelegate : NSObject @interface GLFWWindowDelegate : NSObject
@@ -866,9 +704,11 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
_glfwInputWindowFocus(window, false); _glfwInputWindowFocus(window, false);
// IME is cancelled when losing the focus // IME is cancelled when losing the focus
if ([window->ns.view hasMarkedText]) { if ([window->ns.view hasMarkedText]) {
[[window->ns.view inputContext] discardMarkedText];
[window->ns.view unmarkText]; [window->ns.view unmarkText];
GLFWkeyevent dummy = {.action = GLFW_RELEASE, .ime_state = GLFW_IME_PREEDIT_CHANGED}; GLFWkeyevent dummy = {.action = GLFW_RELEASE, .ime_state = GLFW_IME_PREEDIT_CHANGED};
_glfwInputKeyboard(window, &dummy); _glfwInputKeyboard(window, &dummy);
_glfw.ns.text[0] = 0;
} }
} }
@@ -884,6 +724,20 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
@end // }}} @end // }}}
// Text input context class for the GLFW content view {{{
@interface GLFWTextInputContext : NSTextInputContext
@end
@implementation GLFWTextInputContext
- (void)doCommandBySelector:(SEL)selector
{
// interpretKeyEvents: May call insertText: or doCommandBySelector:.
// With the default macOS keybindings, pressing certain key combinations
// (e.g. Ctrl+/, Ctrl+Cmd+Down/Left/Right) will produce a beep sound.
debug_key("\n\tTextInputCtx: doCommandBySelector: (%s)\n", [NSStringFromSelector(selector) UTF8String]);
}
@end // }}}
// Content view class for the GLFW window {{{ // Content view class for the GLFW window {{{
@@ -891,9 +745,11 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
{ {
_GLFWwindow* window; _GLFWwindow* window;
NSTrackingArea* trackingArea; NSTrackingArea* trackingArea;
GLFWTextInputContext* input_context;
NSMutableAttributedString* markedText; NSMutableAttributedString* markedText;
NSRect markedRect; NSRect markedRect;
bool marked_text_cleared_by_insert; bool marked_text_cleared_by_insert;
int in_key_handler;
NSString *input_source_at_last_key_event; NSString *input_source_at_last_key_event;
} }
@@ -911,9 +767,11 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
{ {
window = initWindow; window = initWindow;
trackingArea = nil; trackingArea = nil;
input_context = [[GLFWTextInputContext alloc] initWithClient:self];
markedText = [[NSMutableAttributedString alloc] init]; markedText = [[NSMutableAttributedString alloc] init];
markedRect = NSMakeRect(0.0, 0.0, 0.0, 0.0); markedRect = NSMakeRect(0.0, 0.0, 0.0, 0.0);
input_source_at_last_key_event = nil; input_source_at_last_key_event = nil;
in_key_handler = 0;
[self updateTrackingAreas]; [self updateTrackingAreas];
[self registerForDraggedTypes:@[NSPasteboardTypeFileURL, NSPasteboardTypeString]]; [self registerForDraggedTypes:@[NSPasteboardTypeFileURL, NSPasteboardTypeString]];
@@ -927,6 +785,7 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
[trackingArea release]; [trackingArea release];
[markedText release]; [markedText release];
if (input_source_at_last_key_event) [input_source_at_last_key_event release]; if (input_source_at_last_key_event) [input_source_at_last_key_event release];
[input_context release];
[super dealloc]; [super dealloc];
} }
@@ -1166,6 +1025,11 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
[super updateTrackingAreas]; [super updateTrackingAreas];
} }
- (NSTextInputContext *)inputContext
{
return input_context;
}
static UInt32 static UInt32
convert_cocoa_to_carbon_modifiers(NSUInteger flags) { convert_cocoa_to_carbon_modifiers(NSUInteger flags) {
UInt32 mods = 0; UInt32 mods = 0;
@@ -1231,15 +1095,16 @@ is_ascii_control_char(char x) {
#define UPDATE_PRE_EDIT_TEXT glfw_keyevent.text = [[markedText string] UTF8String]; glfw_keyevent.ime_state = GLFW_IME_PREEDIT_CHANGED; _glfwInputKeyboard(window, &glfw_keyevent); #define UPDATE_PRE_EDIT_TEXT glfw_keyevent.text = [[markedText string] UTF8String]; glfw_keyevent.ime_state = GLFW_IME_PREEDIT_CHANGED; _glfwInputKeyboard(window, &glfw_keyevent);
const bool previous_has_marked_text = [self hasMarkedText]; const bool previous_has_marked_text = [self hasMarkedText];
NSTextInputContext *inpctx = [NSTextInputContext currentInputContext]; if (input_context && (!input_source_at_last_key_event || ![input_source_at_last_key_event isEqualToString:input_context.selectedKeyboardInputSource])) {
if (inpctx && (!input_source_at_last_key_event || ![input_source_at_last_key_event isEqualToString:inpctx.selectedKeyboardInputSource])) {
if (input_source_at_last_key_event) { if (input_source_at_last_key_event) {
debug_key("Input source changed, clearing pre-edit text and resetting deadkey state\n"); debug_key("Input source changed, clearing pre-edit text and resetting deadkey state\n");
GLFWkeyevent dummy = {.action = GLFW_RELEASE, .ime_state = GLFW_IME_PREEDIT_CHANGED}; GLFWkeyevent dummy = {.action = GLFW_RELEASE, .ime_state = GLFW_IME_PREEDIT_CHANGED};
window->ns.deadKeyState = 0; window->ns.deadKeyState = 0;
_glfwInputKeyboard(window, &dummy); // clear pre-edit text _glfwInputKeyboard(window, &dummy); // clear pre-edit text
[input_source_at_last_key_event release];
input_source_at_last_key_event = nil;
} }
input_source_at_last_key_event = [inpctx.selectedKeyboardInputSource retain]; input_source_at_last_key_event = [input_context.selectedKeyboardInputSource retain];
[self unmarkText]; [self unmarkText];
} }
@@ -1247,7 +1112,7 @@ is_ascii_control_char(char x) {
const NSUInteger flags = [event modifierFlags]; const NSUInteger flags = [event modifierFlags];
const int mods = translateFlags(flags); const int mods = translateFlags(flags);
const uint32_t key = translateKey(keycode, true); const uint32_t key = translateKey(keycode, true);
const bool process_text = !window->ns.textInputFilterCallback || window->ns.textInputFilterCallback(key, mods, keycode, flags) != 1; const bool process_text = !_glfw.ignoreOSKeyboardProcessing && (!window->ns.textInputFilterCallback || window->ns.textInputFilterCallback(key, mods, keycode, flags) != 1);
_glfw.ns.text[0] = 0; _glfw.ns.text[0] = 0;
if (keycode == 0x33 /* backspace */ || keycode == 0x35 /* escape */) [self unmarkText]; if (keycode == 0x33 /* backspace */ || keycode == 0x35 /* escape */) [self unmarkText];
GLFWkeyevent glfw_keyevent = {.key = key, .native_key = keycode, .native_key_id = keycode, .action = GLFW_PRESS, .mods = mods}; GLFWkeyevent glfw_keyevent = {.key = key, .native_key = keycode, .native_key_id = keycode, .action = GLFW_PRESS, .mods = mods};
@@ -1257,7 +1122,7 @@ is_ascii_control_char(char x) {
// keyboard unicode data is not available. // keyboard unicode data is not available.
if (process_text) { if (process_text) {
// this will call insertText with the text for this event, if any // this will call insertText with the text for this event, if any
[self interpretKeyEvents:[NSArray arrayWithObject:event]]; [self interpretKeyEvents:@[event]];
} }
} else { } else {
static UniChar text[256]; static UniChar text[256];
@@ -1284,8 +1149,10 @@ is_ascii_control_char(char x) {
keycode, safe_name_for_keycode(keycode), key, format_mods(mods), char_count, window->ns.deadKeyState, event.ARepeat); keycode, safe_name_for_keycode(keycode), key, format_mods(mods), char_count, window->ns.deadKeyState, event.ARepeat);
marked_text_cleared_by_insert = false; marked_text_cleared_by_insert = false;
if (process_text) { if (process_text) {
in_key_handler = 1;
// this will call insertText which will fill up _glfw.ns.text // this will call insertText which will fill up _glfw.ns.text
[self interpretKeyEvents:[NSArray arrayWithObject:event]]; [self interpretKeyEvents:@[event]];
in_key_handler = 0;
} else { } else {
window->ns.deadKeyState = 0; window->ns.deadKeyState = 0;
} }
@@ -1314,6 +1181,7 @@ is_ascii_control_char(char x) {
} }
if (([self hasMarkedText] || previous_has_marked_text) && !_glfw.ns.text[0]) { if (([self hasMarkedText] || previous_has_marked_text) && !_glfw.ns.text[0]) {
// do not pass keys like BACKSPACE while there's pre-edit text, let IME handle it // do not pass keys like BACKSPACE while there's pre-edit text, let IME handle it
debug_key("Ignoring key press as IME is active and it generated no text\n");
return; return;
} }
} }
@@ -1329,35 +1197,60 @@ is_ascii_control_char(char x) {
// insertText followed by setMarkedText // insertText followed by setMarkedText
UPDATE_PRE_EDIT_TEXT; UPDATE_PRE_EDIT_TEXT;
} }
#undef CLEAR_PRE_EDIT_TEXT
#undef UPDATE_PRE_EDIT_TEXT
} }
- (void)flagsChanged:(NSEvent *)event - (void)flagsChanged:(NSEvent *)event
{ {
int action = GLFW_RELEASE; int action = GLFW_RELEASE;
const unsigned int modifierFlags = const char old_first_char = _glfw.ns.text[0];
[event modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask; _glfw.ns.text[0] = 0;
const uint32_t key = translateKey([event keyCode], false); const NSUInteger modifierFlags = [event modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask;
const uint32_t key = vk_code_to_functional_key_code([event keyCode]);
const unsigned int keycode = [event keyCode];
const int mods = translateFlags(modifierFlags); const int mods = translateFlags(modifierFlags);
const NSUInteger keyFlag = translateKeyToModifierFlag(key); const bool process_text = !_glfw.ignoreOSKeyboardProcessing && (!window->ns.textInputFilterCallback || window->ns.textInputFilterCallback(key, mods, keycode, modifierFlags) != 1);
const char *mod_name = "unknown";
if (keyFlag & modifierFlags) switch(key) {
{ case GLFW_FKEY_CAPS_LOCK:
int current_action = GLFW_RELEASE; mod_name = "capslock";
for (unsigned i = 0; i < arraysz(window->activated_keys); i++) { action = modifierFlags & NSEventModifierFlagCapsLock ? GLFW_PRESS : GLFW_RELEASE; break;
if (window->activated_keys[i].key == key) { case GLFW_FKEY_LEFT_SUPER:
current_action = window->activated_keys[i].action; case GLFW_FKEY_RIGHT_SUPER:
break; mod_name = "super";
} action = modifierFlags & NSEventModifierFlagCommand ? GLFW_PRESS : GLFW_RELEASE; break;
} case GLFW_FKEY_LEFT_CONTROL:
if (current_action == GLFW_PRESS) case GLFW_FKEY_RIGHT_CONTROL:
action = GLFW_RELEASE; mod_name = "ctrl";
else action = modifierFlags & NSEventModifierFlagControl ? GLFW_PRESS : GLFW_RELEASE; break;
action = GLFW_PRESS; case GLFW_FKEY_LEFT_ALT:
case GLFW_FKEY_RIGHT_ALT:
mod_name = "alt";
action = modifierFlags & NSEventModifierFlagOption ? GLFW_PRESS : GLFW_RELEASE; break;
case GLFW_FKEY_LEFT_SHIFT:
case GLFW_FKEY_RIGHT_SHIFT:
mod_name = "shift";
action = modifierFlags & NSEventModifierFlagShift ? GLFW_PRESS : GLFW_RELEASE; break;
default:
return;
} }
GLFWkeyevent glfw_keyevent = {.key = key, .native_key = keycode, .native_key_id = keycode, .action = action, .mods = mods};
GLFWkeyevent glfw_keyevent = {.key = key, .native_key = [event keyCode], .native_key_id = [event keyCode], .action = action, .mods = mods}; debug_key("\x1b[33mflagsChanged:\x1b[m modifier: %s native_key: 0x%x (%s) glfw_key: 0x%x %s\n",
mod_name, keycode, safe_name_for_keycode(keycode), key, format_mods(mods));
marked_text_cleared_by_insert = false;
if (process_text && input_context) {
// this will call insertText which will fill up _glfw.ns.text
in_key_handler = 2;
[input_context handleEvent:event];
in_key_handler = 0;
if (marked_text_cleared_by_insert) {
debug_key("Clearing pre-edit text because insertText called from flagsChanged\n");
CLEAR_PRE_EDIT_TEXT;
if (_glfw.ns.text[0]) glfw_keyevent.text = _glfw.ns.text;
else _glfw.ns.text[0] = old_first_char;
}
}
glfw_keyevent.ime_state = GLFW_IME_NONE;
_glfwInputKeyboard(window, &glfw_keyevent); _glfwInputKeyboard(window, &glfw_keyevent);
} }
@@ -1374,6 +1267,9 @@ is_ascii_control_char(char x) {
_glfwInputKeyboard(window, &glfw_keyevent); _glfwInputKeyboard(window, &glfw_keyevent);
} }
#undef CLEAR_PRE_EDIT_TEXT
#undef UPDATE_PRE_EDIT_TEXT
- (void)scrollWheel:(NSEvent *)event - (void)scrollWheel:(NSEvent *)event
{ {
double deltaX = [event scrollingDeltaX]; double deltaX = [event scrollingDeltaX];
@@ -1472,8 +1368,19 @@ is_ascii_control_char(char x) {
selectedRange:(NSRange)selectedRange selectedRange:(NSRange)selectedRange
replacementRange:(NSRange)replacementRange replacementRange:(NSRange)replacementRange
{ {
debug_key("\n\tsetMarkedText: %s selectedRange: (%lu, %lu) replacementRange: (%lu, %lu)\n", polymorphic_string_as_utf8(string), selectedRange.location, selectedRange.length, replacementRange.location, replacementRange.length); const char *s = polymorphic_string_as_utf8(string);
if (string == nil) { [self unmarkText]; return; } debug_key("\n\tsetMarkedText: %s selectedRange: (%lu, %lu) replacementRange: (%lu, %lu)\n", s, selectedRange.location, selectedRange.length, replacementRange.location, replacementRange.length);
if (string == nil || !s[0]) {
bool had_marked_text = [self hasMarkedText];
[self unmarkText];
if (had_marked_text && (!in_key_handler || in_key_handler == 2)) {
debug_key("Clearing pre-edit because setMarkedText called from %s\n", in_key_handler ? "flagsChanged" : "event loop");
GLFWkeyevent glfw_keyevent = {.ime_state = GLFW_IME_PREEDIT_CHANGED};
_glfwInputKeyboard(window, &glfw_keyevent);
_glfw.ns.text[0] = 0;
}
return;
}
if ([string isKindOfClass:[NSAttributedString class]]) { if ([string isKindOfClass:[NSAttributedString class]]) {
if (((NSMutableAttributedString*)string).length == 0) { [self unmarkText]; return; } if (((NSMutableAttributedString*)string).length == 0) { [self unmarkText]; return; }
[markedText release]; [markedText release];
@@ -1483,6 +1390,12 @@ is_ascii_control_char(char x) {
[markedText release]; [markedText release];
markedText = [[NSMutableAttributedString alloc] initWithString:string]; markedText = [[NSMutableAttributedString alloc] initWithString:string];
} }
if (!in_key_handler || in_key_handler == 2) {
debug_key("Updating IME text in kitty from setMarkedText called from %s: %s\n", in_key_handler ? "flagsChanged" : "event loop", _glfw.ns.text);
GLFWkeyevent glfw_keyevent = {.text=[[markedText string] UTF8String], .ime_state = GLFW_IME_PREEDIT_CHANGED};
_glfwInputKeyboard(window, &glfw_keyevent);
_glfw.ns.text[0] = 0;
}
} }
- (void)unmarkText - (void)unmarkText
@@ -1491,15 +1404,23 @@ is_ascii_control_char(char x) {
} }
void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) { void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
[w->ns.view updateIMEStateFor: ev->type left:(CGFloat)ev->cursor.left top:(CGFloat)ev->cursor.top cellWidth:(CGFloat)ev->cursor.width cellHeight:(CGFloat)ev->cursor.height]; [w->ns.view updateIMEStateFor: ev->type focused:(bool)ev->focused left:(CGFloat)ev->cursor.left top:(CGFloat)ev->cursor.top cellWidth:(CGFloat)ev->cursor.width cellHeight:(CGFloat)ev->cursor.height];
} }
- (void)updateIMEStateFor:(GLFWIMEUpdateType)which - (void)updateIMEStateFor:(GLFWIMEUpdateType)which
focused:(bool)focused
left:(CGFloat)left left:(CGFloat)left
top:(CGFloat)top top:(CGFloat)top
cellWidth:(CGFloat)cellWidth cellWidth:(CGFloat)cellWidth
cellHeight:(CGFloat)cellHeight cellHeight:(CGFloat)cellHeight
{ {
if (which == GLFW_IME_UPDATE_FOCUS && !focused && [self hasMarkedText] && window) {
[input_context discardMarkedText];
[self unmarkText];
GLFWkeyevent glfw_keyevent = {.ime_state = GLFW_IME_PREEDIT_CHANGED};
_glfwInputKeyboard(window, &glfw_keyevent);
_glfw.ns.text[0] = 0;
}
if (which != GLFW_IME_UPDATE_CURSOR_POSITION) return; if (which != GLFW_IME_UPDATE_CURSOR_POSITION) return;
left /= window->ns.xscale; left /= window->ns.xscale;
top /= window->ns.yscale; top /= window->ns.yscale;
@@ -1543,14 +1464,28 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
{ {
const char *utf8 = polymorphic_string_as_utf8(string); const char *utf8 = polymorphic_string_as_utf8(string);
debug_key("\n\tinsertText: %s replacementRange: (%lu, %lu)\n", utf8, replacementRange.location, replacementRange.length); debug_key("\n\tinsertText: %s replacementRange: (%lu, %lu)\n", utf8, replacementRange.location, replacementRange.length);
if ([self hasMarkedText]) { if ([self hasMarkedText] && !is_ascii_control_char(utf8[0])) {
[self unmarkText]; [self unmarkText];
marked_text_cleared_by_insert = true; marked_text_cleared_by_insert = true;
if (!in_key_handler) {
debug_key("Clearing pre-edit because insertText called from event loop\n");
GLFWkeyevent glfw_keyevent = {.ime_state = GLFW_IME_PREEDIT_CHANGED};
_glfwInputKeyboard(window, &glfw_keyevent);
_glfw.ns.text[0] = 0;
}
} }
// insertText can be called multiple times for a single key event // insertText can be called multiple times for a single key event
char *s = _glfw.ns.text + strnlen(_glfw.ns.text, sizeof(_glfw.ns.text)); char *s = _glfw.ns.text + strnlen(_glfw.ns.text, sizeof(_glfw.ns.text));
snprintf(s, sizeof(_glfw.ns.text) - (s - _glfw.ns.text), "%s", utf8); snprintf(s, sizeof(_glfw.ns.text) - (s - _glfw.ns.text), "%s", utf8);
_glfw.ns.text[sizeof(_glfw.ns.text) - 1] = 0; _glfw.ns.text[sizeof(_glfw.ns.text) - 1] = 0;
if ((!in_key_handler || in_key_handler == 2) && _glfw.ns.text[0]) {
if (!is_ascii_control_char(_glfw.ns.text[0])) {
debug_key("Sending text to kitty from insertText called from %s: %s\n", in_key_handler ? "flagsChanged" : "event loop", _glfw.ns.text);
GLFWkeyevent glfw_keyevent = {.text=_glfw.ns.text, .ime_state=GLFW_IME_COMMIT_TEXT};
_glfwInputKeyboard(window, &glfw_keyevent);
}
_glfw.ns.text[0] = 0;
}
} }
- (void)doCommandBySelector:(SEL)selector - (void)doCommandBySelector:(SEL)selector
@@ -1597,19 +1532,6 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
- (BOOL)validateMenuItem:(NSMenuItem *)item { - (BOOL)validateMenuItem:(NSMenuItem *)item {
if (item.action == @selector(performMiniaturize:)) return YES; if (item.action == @selector(performMiniaturize:)) return YES;
if (item.action == @selector(toggleSecureInput:)) {
SecureKeyboardEntryController *controller = [SecureKeyboardEntryController sharedInstance];
if (controller.isEnabled) {
if (controller.isDesired) {
item.state = NSControlStateValueOn;
} else {
item.state = NSControlStateValueMixed;
}
} else {
item.state = controller.isDesired ? NSControlStateValueOn : NSControlStateValueOff;
}
return YES;
}
return [super validateMenuItem:item]; return [super validateMenuItem:item];
} }
@@ -1619,11 +1541,6 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
else [super performMiniaturize:sender]; else [super performMiniaturize:sender];
} }
- (void)toggleSecureInput:(id)sender {
(void)sender;
[[SecureKeyboardEntryController sharedInstance] toggle];
}
- (BOOL)canBecomeKeyWindow - (BOOL)canBecomeKeyWindow
{ {
// Required for NSWindowStyleMaskBorderless windows // Required for NSWindowStyleMaskBorderless windows
@@ -2449,7 +2366,7 @@ void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor UNUSED)
bool _glfwPlatformIsFullscreen(_GLFWwindow* w, unsigned int flags) { bool _glfwPlatformIsFullscreen(_GLFWwindow* w, unsigned int flags) {
NSWindow *window = w->ns.object; NSWindow *window = w->ns.object;
bool traditional = !(flags & 1); bool traditional = !(flags & 1);
if (traditional) { if(@available(macOS 11.0, *)) return w->ns.in_traditional_fullscreen; } if (traditional) { if(@available(macOS 10.15.7, *)) return w->ns.in_traditional_fullscreen; }
NSWindowStyleMask sm = [window styleMask]; NSWindowStyleMask sm = [window styleMask];
return sm & NSWindowStyleMaskFullScreen; return sm & NSWindowStyleMaskFullScreen;
} }
@@ -2460,8 +2377,9 @@ bool _glfwPlatformToggleFullscreen(_GLFWwindow* w, unsigned int flags) {
bool traditional = !(flags & 1); bool traditional = !(flags & 1);
NSWindowStyleMask sm = [window styleMask]; NSWindowStyleMask sm = [window styleMask];
if (traditional) { if (traditional) {
if (@available(macOS 11.0, *)) { if (@available(macOS 10.15.7, *)) {
// As of Big Turd NSWindowStyleMaskFullScreen is no longer useable // As of Big Turd NSWindowStyleMaskFullScreen is no longer useable
// Also no longer compatible after a minor release of macOS 10.15.7
if (!w->ns.in_traditional_fullscreen) { if (!w->ns.in_traditional_fullscreen) {
w->ns.pre_full_screen_style_mask = sm; w->ns.pre_full_screen_style_mask = sm;
[window setStyleMask: NSWindowStyleMaskBorderless]; [window setStyleMask: NSWindowStyleMaskBorderless];

View File

@@ -67,7 +67,7 @@ class Env:
def wayland_protocol_file_name(base: str, ext: str = 'c') -> str: def wayland_protocol_file_name(base: str, ext: str = 'c') -> str:
base = os.path.basename(base).rpartition('.')[0] base = os.path.basename(base).rpartition('.')[0]
return 'wayland-{}-client-protocol.{}'.format(base, ext) return f'wayland-{base}-client-protocol.{ext}'
def init_env( def init_env(
@@ -80,7 +80,7 @@ def init_env(
) -> Env: ) -> Env:
ans = env.copy() ans = env.copy()
ans.cflags.append('-fPIC') ans.cflags.append('-fPIC')
ans.cppflags.append('-D_GLFW_' + module.upper()) ans.cppflags.append(f'-D_GLFW_{module.upper()}')
ans.cppflags.append('-D_GLFW_BUILD_DLL') ans.cppflags.append('-D_GLFW_BUILD_DLL')
with open(os.path.join(base, 'source-info.json')) as f: with open(os.path.join(base, 'source-info.json')) as f:
@@ -111,7 +111,7 @@ def init_env(
elif module == 'cocoa': elif module == 'cocoa':
ans.cppflags.append('-DGL_SILENCE_DEPRECATION') ans.cppflags.append('-DGL_SILENCE_DEPRECATION')
for f_ in 'Cocoa Carbon IOKit CoreFoundation CoreVideo'.split(): for f_ in 'Cocoa IOKit CoreFoundation CoreVideo'.split():
ans.ldpaths.extend(('-framework', f_)) ans.ldpaths.extend(('-framework', f_))
elif module == 'wayland': elif module == 'wayland':
@@ -150,14 +150,14 @@ def build_wayland_protocols(
for protocol in env.wayland_protocols: for protocol in env.wayland_protocols:
src = os.path.join(env.wayland_packagedir, protocol) src = os.path.join(env.wayland_packagedir, protocol)
if not os.path.exists(src): if not os.path.exists(src):
raise SystemExit('The wayland-protocols package on your system is missing the {} protocol definition file'.format(protocol)) raise SystemExit(f'The wayland-protocols package on your system is missing the {protocol} protocol definition file')
for ext in 'hc': for ext in 'hc':
dest = wayland_protocol_file_name(src, ext) dest = wayland_protocol_file_name(src, ext)
dest = os.path.join(dest_dir, dest) dest = os.path.join(dest_dir, dest)
if newer(dest, src): if newer(dest, src):
q = 'client-header' if ext == 'h' else env.wayland_scanner_code q = 'client-header' if ext == 'h' else env.wayland_scanner_code
items.append(Command( items.append(Command(
'Generating {} ...'.format(emphasis(os.path.basename(dest))), f'Generating {emphasis(os.path.basename(dest))} ...',
[env.wayland_scanner, q, src, dest], lambda: True)) [env.wayland_scanner, q, src, dest], lambda: True))
if items: if items:
parallel_run(items) parallel_run(items)
@@ -174,7 +174,7 @@ class Arg:
self.type = self.type + '*' self.type = self.type + '*'
def __repr__(self) -> str: def __repr__(self) -> str:
return 'Arg({}, {})'.format(self.type, self.name) return f'Arg({self.type}, {self.name})'
class Function: class Function:

3
glfw/glfw3.h vendored
View File

@@ -4119,6 +4119,9 @@ GLFWAPI GLFWwindowcontentscalefun glfwSetWindowContentScaleCallback(GLFWwindow*
*/ */
GLFWAPI void glfwPostEmptyEvent(void); GLFWAPI void glfwPostEmptyEvent(void);
GLFWAPI bool glfwGetIgnoreOSKeyboardProcessing(void);
GLFWAPI void glfwSetIgnoreOSKeyboardProcessing(bool enabled);
/*! @brief Returns the value of an input option for the specified window. /*! @brief Returns the value of an input option for the specified window.
* *
* This function returns the value of an input option for the specified window. * This function returns the value of an input option for the specified window.

12
glfw/ibus_glfw.c vendored
View File

@@ -24,6 +24,17 @@
// //
//======================================================================== //========================================================================
/* To test under X11 start IBUS as:
* ibus-daemon -drxR
* Setup the input sources you want with:
* ibus-setup
* Switch to the input source you want to test with:
* ibus engine name
* You can list available engines with:
* ibus list-engine
* Then run kitty as:
* GLFW_IM_MODULE=ibus kitty
*/
#define _GNU_SOURCE #define _GNU_SOURCE
#include <stdio.h> #include <stdio.h>
#include <errno.h> #include <errno.h>
@@ -226,6 +237,7 @@ message_handler(DBusConnection *conn UNUSED, DBusMessage *msg, void *user_data)
break; break;
case 2: case 2:
debug("IBUS: HidePreeditText\n"); debug("IBUS: HidePreeditText\n");
send_text("", GLFW_IME_PREEDIT_CHANGED);
break; break;
case 3: case 3:
debug("IBUS: ShowPreeditText\n"); debug("IBUS: ShowPreeditText\n");

1
glfw/init.c vendored
View File

@@ -227,6 +227,7 @@ GLFWAPI int glfwInit(monotonic_t start_time)
memset(&_glfw, 0, sizeof(_glfw)); memset(&_glfw, 0, sizeof(_glfw));
_glfw.hints.init = _glfwInitHints; _glfw.hints.init = _glfwInitHints;
_glfw.ignoreOSKeyboardProcessing = false;
if (!_glfwPlatformInit()) if (!_glfwPlatformInit())
{ {

8
glfw/input.c vendored
View File

@@ -667,6 +667,14 @@ void _glfwCenterCursorInContentArea(_GLFWwindow* window)
////// GLFW public API ////// ////// GLFW public API //////
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
GLFWAPI bool glfwGetIgnoreOSKeyboardProcessing(void) {
return _glfw.ignoreOSKeyboardProcessing;
}
GLFWAPI void glfwSetIgnoreOSKeyboardProcessing(bool enabled) {
_glfw.ignoreOSKeyboardProcessing = enabled;
}
GLFWAPI int glfwGetInputMode(GLFWwindow* handle, int mode) GLFWAPI int glfwGetInputMode(GLFWwindow* handle, int mode)
{ {
_GLFWwindow* window = (_GLFWwindow*) handle; _GLFWwindow* window = (_GLFWwindow*) handle;

2
glfw/internal.h vendored
View File

@@ -584,6 +584,8 @@ struct _GLFWlibrary
_GLFWtls contextSlot; _GLFWtls contextSlot;
_GLFWmutex errorLock; _GLFWmutex errorLock;
bool ignoreOSKeyboardProcessing;
struct { struct {
bool available; bool available;
void* handle; void* handle;

2
glfw/xkb_glfw.c vendored
View File

@@ -827,7 +827,7 @@ glfw_xkb_key_from_ime(_GLFWIBUSKeyEvent *ev, bool handled_by_ime, bool failed) {
xkb_keycode_t prev_handled_press = last_handled_press_keycode; xkb_keycode_t prev_handled_press = last_handled_press_keycode;
last_handled_press_keycode = 0; last_handled_press_keycode = 0;
bool is_release = ev->glfw_ev.action == GLFW_RELEASE; bool is_release = ev->glfw_ev.action == GLFW_RELEASE;
debug("From IBUS: native_key: 0x%x name: %s is_release: %d\n", ev->glfw_ev.native_key, glfw_xkb_keysym_name(ev->glfw_ev.key), is_release); debug("From IBUS: native_key: 0x%x name: %s is_release: %d handled_by_ime: %d\n", ev->glfw_ev.native_key, glfw_xkb_keysym_name(ev->glfw_ev.key), is_release, handled_by_ime);
if (window && !handled_by_ime && !(is_release && ev->glfw_ev.native_key == (int) prev_handled_press)) { if (window && !handled_by_ime && !(is_release && ev->glfw_ev.native_key == (int) prev_handled_press)) {
debug("↳ to application: glfw_keycode: 0x%x (%s) keysym: 0x%x (%s) action: %s %s text: %s\n", debug("↳ to application: glfw_keycode: 0x%x (%s) keysym: 0x%x (%s) action: %s %s text: %s\n",
ev->glfw_ev.native_key, _glfwGetKeyName(ev->glfw_ev.native_key), ev->glfw_ev.key, glfw_xkb_keysym_name(ev->glfw_ev.key), ev->glfw_ev.native_key, _glfwGetKeyName(ev->glfw_ev.native_key), ev->glfw_ev.key, glfw_xkb_keysym_name(ev->glfw_ev.key),

View File

@@ -4,13 +4,15 @@
import os import os
import sys import sys
from contextlib import suppress from contextlib import suppress
from typing import TYPE_CHECKING, Dict, List, NamedTuple, Optional, Tuple from typing import (
TYPE_CHECKING, Callable, Dict, List, NamedTuple, Optional, Tuple
)
from kitty.cli import parse_args from kitty.cli import parse_args
from kitty.cli_stub import AskCLIOptions from kitty.cli_stub import AskCLIOptions
from kitty.constants import cache_dir from kitty.constants import cache_dir
from kitty.fast_data_types import wcswidth from kitty.fast_data_types import truncate_point_for_length, wcswidth
from kitty.typing import BossType, TypedDict from kitty.typing import BossType, KeyEventType, TypedDict
from kitty.utils import ScreenSize from kitty.utils import ScreenSize
from ..tui.handler import Handler, result_handler from ..tui.handler import Handler, result_handler
@@ -95,6 +97,12 @@ 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 an optional color specification after the letter to indicate what color it should
be. be.
For example: y:Yes and n;red:No For example: y:Yes and n;red:No
--default -d
A default choice or text. If unspecified, it is "y" for :code:`yesno`, the first choice
for :code:`choices` and empty for others. The default choice is selected when the user
presses the Enter key.
''' '''
@@ -107,6 +115,7 @@ class Choice(NamedTuple):
text: str text: str
idx: int idx: int
color: str color: str
letter: str
class Range(NamedTuple): class Range(NamedTuple):
@@ -118,27 +127,44 @@ class Range(NamedTuple):
return y == self.y and self.start <= x <= self.end return y == self.y and self.start <= x <= self.end
def truncate_at_space(text: str, width: int) -> Tuple[str, str]:
p = truncate_point_for_length(text, width)
if p < len(text):
i = text.rfind(' ', 0, p + 1)
if i > 0 and p - i < 12:
p = i + 1
return text[:p], text[p:]
def extra_for(width: int, screen_width: int) -> int:
return max(0, screen_width - width) // 2 + 1
class Choose(Handler): class Choose(Handler):
mouse_tracking = MouseTracking.buttons_only mouse_tracking = MouseTracking.buttons_only
def __init__(self, cli_opts: AskCLIOptions) -> None: def __init__(self, cli_opts: AskCLIOptions) -> None:
self.cli_opts = cli_opts self.cli_opts = cli_opts
self.response = 'n' if cli_opts.type == 'yesno' else ''
self.allowed = frozenset('yn')
self.choices: Dict[str, Choice] = {} self.choices: Dict[str, Choice] = {}
self.clickable_ranges: Dict[str, Range] = {} self.clickable_ranges: Dict[str, List[Range]] = {}
if cli_opts.type != 'yesno': if cli_opts.type == 'yesno':
self.allowed = frozenset('yn')
else:
allowed = [] allowed = []
for choice in cli_opts.choices: for choice in cli_opts.choices:
color = 'green'
letter, text = choice.split(':', maxsplit=1) letter, text = choice.split(':', maxsplit=1)
color = ''
if ';' in letter: if ';' in letter:
letter, color = letter.split(';', maxsplit=1) letter, color = letter.split(';', maxsplit=1)
letter = letter.lower() letter = letter.lower()
idx = text.lower().index(letter) idx = text.lower().index(letter)
allowed.append(letter) allowed.append(letter)
self.choices[letter] = Choice(text, idx, color) self.choices[letter] = Choice(text, idx, color, letter)
self.allowed = frozenset(allowed) self.allowed = frozenset(allowed)
self.response = ''
self.response_on_accept = cli_opts.default or ''
if cli_opts.type in ('yesno', 'choices') and self.response_on_accept not in self.allowed:
self.response_on_accept = 'y' if cli_opts.type == 'yesno' else tuple(self.choices.keys())[0]
def initialize(self) -> None: def initialize(self) -> None:
self.cmd.set_cursor_visible(False) self.cmd.set_cursor_visible(False)
@@ -147,52 +173,164 @@ class Choose(Handler):
def finalize(self) -> None: def finalize(self) -> None:
self.cmd.set_cursor_visible(True) self.cmd.set_cursor_visible(True)
def draw_long_text(self, text: str) -> int:
y = 0
width = self.screen_size.cols - 2
while text:
t, text = truncate_at_space(text, width)
t = t.strip()
self.print(' ' * extra_for(wcswidth(t), width), styled(t, bold=True), sep='')
y += 1
return y
@Handler.atomic_update @Handler.atomic_update
def draw_screen(self) -> None: def draw_screen(self) -> None:
self.cmd.clear_screen() self.cmd.clear_screen()
y = 1 y = max(0, self.screen_size.rows // 2 - 2)
self.print(end='\r\n'*y)
if self.cli_opts.message: if self.cli_opts.message:
self.cmd.styled(self.cli_opts.message, bold=True) for line in self.cli_opts.message.splitlines():
y += wcswidth(self.cli_opts.message) // self.screen_size.cols y += self.draw_long_text(line)
self.print() if self.screen_size.rows > 2:
self.print()
y += 1
if self.cli_opts.type == 'yesno': if self.cli_opts.type == 'yesno':
self.draw_yesno(y) self.draw_yesno(y)
else: else:
self.draw_choice(y) self.draw_choice(y)
def draw_choice(self, y: int) -> None: def draw_choice_boxes(self, y: int, *choices: Choice) -> None:
x = 0
self.clickable_ranges.clear() self.clickable_ranges.clear()
width = self.screen_size.cols - 2
current_line_length = 0
current_line: List[Tuple[str, str]] = []
lines: List[List[Tuple[str, str]]] = []
sep, sep_sz = ' ', 2
for choice in choices:
self.clickable_ranges[choice.letter] = []
text = ' ' + choice.text[:choice.idx]
text += styled(choice.text[choice.idx], fg=choice.color or 'green')
text += choice.text[choice.idx + 1:] + ' '
sz = wcswidth(text)
if sz + sep_sz + current_line_length > width:
lines.append(current_line)
current_line = []
current_line_length = 0
current_line.append((choice.letter, text))
current_line_length += sz
if current_line:
lines.append(current_line)
def top(text: str) -> str:
return '' + '' * wcswidth(text) + ''
def middle(text: str) -> str:
return f'{text}'
def bottom(text: str) -> str:
return '' + '' * wcswidth(text) + ''
def highlight(text: str, only_edges: bool = False) -> str:
if only_edges:
return styled(text[0], fg='yellow') + text[1:-1] + styled(text[-1], fg='yellow')
return styled(text, fg='yellow')
def print_line(add_borders: Callable[[str], str], *items: Tuple[str, str], is_last: bool = False) -> None:
nonlocal y
texts = []
positions = []
x = 0
for (letter, text) in items:
positions.append((letter, x, wcswidth(text) + 2))
text = add_borders(text)
if letter == self.response_on_accept:
text = highlight(text, only_edges=add_borders is middle)
text += sep
x += wcswidth(text)
texts.append(text)
line = ''.join(texts).rstrip()
offset = extra_for(wcswidth(line), width)
for (letter, x, sz) in positions:
x += offset
self.clickable_ranges[letter].append(Range(x, x + sz - 1, y))
self.print(' ' * offset, line, sep='', end='' if is_last else '\r\n')
y += 1
for boxed_line in lines:
print_line(top, *boxed_line)
print_line(middle, *boxed_line)
print_line(bottom, *boxed_line, is_last=boxed_line is lines[-1])
def draw_choice(self, y: int) -> None:
if y + 3 <= self.screen_size.rows:
self.draw_choice_boxes(y, *self.choices.values())
return
self.clickable_ranges.clear()
current_line = ''
current_ranges: Dict[str, int] = {}
width = self.screen_size.cols - 2
def commit_line(end: str = '\r\n') -> None:
nonlocal current_line, y
x = extra_for(wcswidth(current_line), width)
self.print(' ' * x + current_line, end=end)
for letter, sz in current_ranges.items():
self.clickable_ranges[letter] = [Range(x, x + sz - 3, y)]
x += sz
current_ranges.clear()
y += 1
current_line = ''
for letter, choice in self.choices.items(): for letter, choice in self.choices.items():
text = choice.text[:choice.idx] text = choice.text[:choice.idx]
text += styled(choice.text[choice.idx], fg=choice.color) text += styled(choice.text[choice.idx], fg=choice.color or 'green', underline='straight' if letter == self.response_on_accept else None)
text += choice.text[choice.idx + 1:] text += choice.text[choice.idx + 1:]
text += ' ' text += ' '
sz = wcswidth(text) sz = wcswidth(text)
if sz + x >= self.screen_size.cols: if sz + wcswidth(current_line) >= width:
y += 1 commit_line()
x = 0 current_line += text
self.print() current_ranges[letter] = sz
self.clickable_ranges[letter] = Range(x, x + sz - 1, y) if current_line:
x += sz commit_line(end='')
self.print(text, end='')
def draw_yesno(self, y: int) -> None: def draw_yesno(self, y: int) -> None:
self.clickable_ranges = {'y': Range(2, 4, y), 'n': Range(8, 9, y)} yes = styled('Y', fg='green') + 'es'
self.print(' ', styled('Y', fg='green') + 'es', ' ', styled('N', fg='red') + 'o') no = styled('N', fg='red') + 'o'
if y + 3 <= self.screen_size.rows:
self.draw_choice_boxes(y, Choice('Yes', 0, 'green', 'y'), Choice('No', 0, 'red', 'n'))
return
sep = ' ' * 3
text = yes + sep + no
w = wcswidth(text)
x = extra_for(w, self.screen_size.cols - 2)
nx = x + wcswidth(yes) + len(sep)
self.clickable_ranges = {'y': [Range(x, x + wcswidth(yes) - 1, y)], 'n': [Range(nx, nx + wcswidth(no) - 1, y)]}
self.print(' ' * x + text, end='')
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None: def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
text = text.lower() text = text.lower()
if text in self.allowed: if text in self.allowed:
self.response = text self.response = text
self.quit_loop(0) self.quit_loop(0)
elif self.cli_opts.type == 'yesno':
self.on_interrupt()
def on_key(self, key_event: KeyEventType) -> None:
if key_event.matches('esc'):
self.on_interrupt()
elif key_event.matches('enter'):
self.response = self.response_on_accept
self.quit_loop(0)
def on_click(self, ev: MouseEvent) -> None: def on_click(self, ev: MouseEvent) -> None:
for letter, r in self.clickable_ranges.items(): for letter, ranges in self.clickable_ranges.items():
if r.has_point(ev.cell_x, ev.cell_y): for r in ranges:
self.response = letter if r.has_point(ev.cell_x, ev.cell_y):
self.quit_loop(0) self.response = letter
break self.quit_loop(0)
return
def on_resize(self, screen_size: ScreenSize) -> None: def on_resize(self, screen_size: ScreenSize) -> None:
self.screen_size = screen_size self.screen_size = screen_size
@@ -235,7 +373,15 @@ def main(args: List[str]) -> Response:
prompt = '> ' prompt = '> '
with suppress(KeyboardInterrupt, EOFError): with suppress(KeyboardInterrupt, EOFError):
response = input(prompt) if cli_opts.default:
def prefill_text() -> None:
readline.insert_text(cli_opts.default or '')
readline.redisplay()
readline.set_pre_input_hook(prefill_text)
response = input(prompt)
readline.set_pre_input_hook()
else:
response = input(prompt)
return {'items': items, 'response': response} return {'items': items, 'response': response}

View File

@@ -32,8 +32,7 @@ class Segment:
self.end_code: Optional[str] = None self.end_code: Optional[str] = None
def __repr__(self) -> str: def __repr__(self) -> str:
return 'Segment(start={!r}, start_code={!r}, end={!r}, end_code={!r})'.format( return f'Segment(start={self.start!r}, start_code={self.start_code!r}, end={self.end!r}, end_code={self.end_code!r})'
self.start, self.start_code, self.end, self.end_code)
class Collection: class Collection:
@@ -102,7 +101,7 @@ def remote_hostname(path: str) -> Tuple[Optional[str], Optional[str]]:
def resolve_remote_name(path: str, default: str) -> str: def resolve_remote_name(path: str, default: str) -> str:
remote_dir, rh = remote_hostname(path) remote_dir, rh = remote_hostname(path)
if remote_dir and rh: if remote_dir and rh:
return rh + ':' + os.path.relpath(path, remote_dir) return f'{rh}:{os.path.relpath(path, remote_dir)}'
return default return default

View File

@@ -4,7 +4,10 @@
import concurrent import concurrent
import os import os
import re import re
from typing import IO, Dict, Iterable, List, Optional, Tuple, Union, cast from concurrent.futures import ProcessPoolExecutor
from typing import (
IO, Dict, Iterable, Iterator, List, Optional, Tuple, Union, cast
)
from pygments import highlight # type: ignore from pygments import highlight # type: ignore
from pygments.formatter import Formatter # type: ignore from pygments.formatter import Formatter # type: ignore
@@ -94,7 +97,7 @@ def highlight_data(code: str, filename: str, aliases: Optional[Dict[str, str]] =
base, ext = os.path.splitext(filename) base, ext = os.path.splitext(filename)
alias = aliases.get(ext[1:]) alias = aliases.get(ext[1:])
if alias is not None: if alias is not None:
filename = base + '.' + alias filename = f'{base}.{alias}'
try: try:
lexer = get_lexer_for_filename(filename, stripnl=False) lexer = get_lexer_for_filename(filename, stripnl=False)
except ClassNotFound: except ClassNotFound:
@@ -136,10 +139,22 @@ def highlight_for_diff(path: str, aliases: Dict[str, str]) -> DiffHighlight:
return ans return ans
process_pool_executor: Optional[ProcessPoolExecutor] = None
def get_highlight_processes() -> Iterator[int]:
if process_pool_executor is None:
return
for pid in process_pool_executor._processes:
yield pid
def highlight_collection(collection: Collection, aliases: Optional[Dict[str, str]] = None) -> Union[str, Dict[str, DiffHighlight]]: def highlight_collection(collection: Collection, aliases: Optional[Dict[str, str]] = None) -> Union[str, Dict[str, DiffHighlight]]:
global process_pool_executor
jobs = {} jobs = {}
ans: Dict[str, DiffHighlight] = {} ans: Dict[str, DiffHighlight] = {}
with get_process_pool_executor(prefer_fork=True) as executor: with get_process_pool_executor(prefer_fork=True) as executor:
process_pool_executor = executor
for path, item_type, other_path in collection: for path, item_type, other_path in collection:
if item_type != 'rename': if item_type != 'rename':
for p in (path, other_path): for p in (path, other_path):
@@ -159,8 +174,9 @@ def highlight_collection(collection: Collection, aliases: Optional[Dict[str, str
def main() -> None: def main() -> None:
# kitty +runpy "from kittens.diff.highlight import main; main()" file # kitty +runpy "from kittens.diff.highlight import main; main()" file
from .options.types import defaults
import sys import sys
from .options.types import defaults
initialize_highlighter() initialize_highlighter()
with open(sys.argv[-1]) as f: with open(sys.argv[-1]) as f:
highlighted = highlight_data(f.read(), f.name, defaults.syntax_aliases) highlighted = highlight_data(f.read(), f.name, defaults.syntax_aliases)

View File

@@ -10,10 +10,11 @@ import tempfile
import warnings import warnings
from collections import defaultdict from collections import defaultdict
from contextlib import suppress from contextlib import suppress
from enum import Enum, auto
from functools import partial from functools import partial
from gettext import gettext as _ from gettext import gettext as _
from typing import ( from typing import (
Any, DefaultDict, Dict, Iterable, List, Optional, Tuple, Union Any, DefaultDict, Dict, Iterable, Iterator, List, Optional, Tuple, Union
) )
from kitty.cli import CONFIG_HELP, parse_args from kitty.cli import CONFIG_HELP, parse_args
@@ -44,7 +45,8 @@ from .search import BadRegex, Search
try: try:
from .highlight import ( from .highlight import (
DiffHighlight, highlight_collection, initialize_highlighter DiffHighlight, get_highlight_processes, highlight_collection,
initialize_highlighter
) )
has_highlighter = True has_highlighter = True
DiffHighlight DiffHighlight
@@ -54,8 +56,24 @@ except ImportError:
def highlight_collection(collection: 'Collection', aliases: Optional[Dict[str, str]] = None) -> Union[str, Dict[str, 'DiffHighlight']]: def highlight_collection(collection: 'Collection', aliases: Optional[Dict[str, str]] = None) -> Union[str, Dict[str, 'DiffHighlight']]:
return '' return ''
def get_highlight_processes() -> Iterator[int]:
if has_highlighter:
yield -1
INITIALIZING, COLLECTED, DIFFED, COMMAND, MESSAGE = range(5)
class State(Enum):
initializing = auto()
collected = auto()
diffed = auto()
command = auto()
message = auto()
class BackgroundWork(Enum):
none = auto()
collecting = auto()
diffing = auto()
highlighting = auto()
def generate_diff(collection: Collection, context: int) -> Union[str, Dict[str, Patch]]: def generate_diff(collection: Collection, context: int) -> Union[str, Dict[str, Patch]]:
@@ -76,7 +94,7 @@ class DiffHandler(Handler):
image_manager_class = ImageManager image_manager_class = ImageManager
def __init__(self, args: DiffCLIOptions, opts: DiffOptions, left: str, right: str) -> None: def __init__(self, args: DiffCLIOptions, opts: DiffOptions, left: str, right: str) -> None:
self.state = INITIALIZING self.state = State.initializing
self.message = '' self.message = ''
self.current_search_is_regex = True self.current_search_is_regex = True
self.current_search: Optional[Search] = None self.current_search: Optional[Search] = None
@@ -90,16 +108,20 @@ class DiffHandler(Handler):
if self.current_context_count < 0: if self.current_context_count < 0:
self.current_context_count = self.original_context_count = self.opts.num_context_lines self.current_context_count = self.original_context_count = self.opts.num_context_lines
self.highlighting_done = False self.highlighting_done = False
self.doing_background_work = BackgroundWork.none
self.restore_position: Optional[Reference] = None self.restore_position: Optional[Reference] = None
for key_def, action in self.opts.key_definitions.items(): for key_def, action in self.opts.key_definitions.items():
self.add_shortcut(action, key_def) self.add_shortcut(action, key_def)
def terminate(self, return_code: int = 0) -> None:
self.quit_loop(return_code)
def perform_action(self, action: KeyAction) -> None: def perform_action(self, action: KeyAction) -> None:
func, args = action func, args = action
if func == 'quit': if func == 'quit':
self.quit_loop(0) self.terminate()
return return
if self.state <= DIFFED: if self.state.value <= State.diffed.value:
if func == 'scroll_by': if func == 'scroll_by':
return self.scroll_lines(int(args[0] or 0)) return self.scroll_lines(int(args[0] or 0))
if func == 'scroll_to': if func == 'scroll_to':
@@ -130,8 +152,9 @@ class DiffHandler(Handler):
def create_collection(self) -> None: def create_collection(self) -> None:
def collect_done(collection: Collection) -> None: def collect_done(collection: Collection) -> None:
self.doing_background_work = BackgroundWork.none
self.collection = collection self.collection = collection
self.state = COLLECTED self.state = State.collected
self.generate_diff() self.generate_diff()
def collect(left: str, right: str) -> None: def collect(left: str, right: str) -> None:
@@ -139,15 +162,17 @@ class DiffHandler(Handler):
self.asyncio_loop.call_soon_threadsafe(collect_done, collection) self.asyncio_loop.call_soon_threadsafe(collect_done, collection)
self.asyncio_loop.run_in_executor(None, collect, self.left, self.right) self.asyncio_loop.run_in_executor(None, collect, self.left, self.right)
self.doing_background_work = BackgroundWork.collecting
def generate_diff(self) -> None: def generate_diff(self) -> None:
def diff_done(diff_map: Union[str, Dict[str, Patch]]) -> None: def diff_done(diff_map: Union[str, Dict[str, Patch]]) -> None:
self.doing_background_work = BackgroundWork.none
if isinstance(diff_map, str): if isinstance(diff_map, str):
self.report_traceback_on_exit = diff_map self.report_traceback_on_exit = diff_map
self.quit_loop(1) self.terminate(1)
return return
self.state = DIFFED self.state = State.diffed
self.diff_map = diff_map self.diff_map = diff_map
self.calculate_statistics() self.calculate_statistics()
self.render_diff() self.render_diff()
@@ -163,7 +188,7 @@ class DiffHandler(Handler):
initialize_highlighter(self.opts.pygments_style) initialize_highlighter(self.opts.pygments_style)
except StyleNotFound as e: except StyleNotFound as e:
self.report_traceback_on_exit = str(e) self.report_traceback_on_exit = str(e)
self.quit_loop(1) self.terminate(1)
return return
self.syntax_highlight() self.syntax_highlight()
@@ -172,13 +197,15 @@ class DiffHandler(Handler):
self.asyncio_loop.call_soon_threadsafe(diff_done, diff_map) self.asyncio_loop.call_soon_threadsafe(diff_done, diff_map)
self.asyncio_loop.run_in_executor(None, diff, self.collection, self.current_context_count) self.asyncio_loop.run_in_executor(None, diff, self.collection, self.current_context_count)
self.doing_background_work = BackgroundWork.diffing
def syntax_highlight(self) -> None: def syntax_highlight(self) -> None:
def highlighting_done(hdata: Union[str, Dict[str, 'DiffHighlight']]) -> None: def highlighting_done(hdata: Union[str, Dict[str, 'DiffHighlight']]) -> None:
self.doing_background_work = BackgroundWork.none
if isinstance(hdata, str): if isinstance(hdata, str):
self.report_traceback_on_exit = hdata self.report_traceback_on_exit = hdata
self.quit_loop(1) self.terminate(1)
return return
set_highlight_data(hdata) set_highlight_data(hdata)
self.render_diff() self.render_diff()
@@ -189,6 +216,7 @@ class DiffHandler(Handler):
self.asyncio_loop.call_soon_threadsafe(highlighting_done, result) self.asyncio_loop.call_soon_threadsafe(highlighting_done, result)
self.asyncio_loop.run_in_executor(None, highlight, self.collection, self.opts.syntax_aliases) self.asyncio_loop.run_in_executor(None, highlight, self.collection, self.opts.syntax_aliases)
self.doing_background_work = BackgroundWork.highlighting
def calculate_statistics(self) -> None: def calculate_statistics(self) -> None:
self.added_count = self.collection.added_count self.added_count = self.collection.added_count
@@ -304,7 +332,7 @@ class DiffHandler(Handler):
self.create_collection() self.create_collection()
def enforce_cursor_state(self) -> None: def enforce_cursor_state(self) -> None:
self.cmd.set_cursor_visible(self.state == COMMAND) self.cmd.set_cursor_visible(self.state is State.command)
def draw_lines(self, num: int, offset: int = 0) -> None: def draw_lines(self, num: int, offset: int = 0) -> None:
offset += self.scroll_pos offset += self.scroll_pos
@@ -319,7 +347,7 @@ class DiffHandler(Handler):
text = line.text text = line.text
if line.image_data is not None: if line.image_data is not None:
image_involved = True image_involved = True
self.write('\r\x1b[K' + text + '\x1b[0m') self.write(f'\r\x1b[K{text}\x1b[0m')
if self.current_search is not None: if self.current_search is not None:
self.current_search.highlight_line(self.write, lpos) self.current_search.highlight_line(self.write, lpos)
if i < num - 1: if i < num - 1:
@@ -407,7 +435,7 @@ class DiffHandler(Handler):
def draw_screen(self) -> None: def draw_screen(self) -> None:
self.enforce_cursor_state() self.enforce_cursor_state()
if self.state < DIFFED: if self.state.value < State.diffed.value:
self.cmd.clear_screen() self.cmd.clear_screen()
self.write(_('Calculating diff, please wait...')) self.write(_('Calculating diff, please wait...'))
return return
@@ -417,14 +445,14 @@ class DiffHandler(Handler):
self.draw_status_line() self.draw_status_line()
def draw_status_line(self) -> None: def draw_status_line(self) -> None:
if self.state < DIFFED: if self.state.value < State.diffed.value:
return return
self.enforce_cursor_state() self.enforce_cursor_state()
self.cmd.set_cursor_position(0, self.num_lines) self.cmd.set_cursor_position(0, self.num_lines)
self.cmd.clear_to_eol() self.cmd.clear_to_eol()
if self.state is COMMAND: if self.state is State.command:
self.line_edit.write(self.write) self.line_edit.write(self.write)
elif self.state is MESSAGE: elif self.state is State.message:
self.cmd.styled(self.message, reverse=True) self.cmd.styled(self.message, reverse=True)
else: else:
sp = f'{self.scroll_pos/self.max_scroll_pos:.0%}' if self.scroll_pos and self.max_scroll_pos else '0%' sp = f'{self.scroll_pos/self.max_scroll_pos:.0%}' if self.scroll_pos and self.max_scroll_pos else '0%'
@@ -437,7 +465,7 @@ class DiffHandler(Handler):
) )
else: else:
counts = styled(f'{len(self.current_search)} matches', fg=self.opts.margin_fg) counts = styled(f'{len(self.current_search)} matches', fg=self.opts.margin_fg)
suffix = counts + ' ' + scroll_frac suffix = f'{counts} {scroll_frac}'
prefix = styled(':', fg=self.opts.margin_fg) prefix = styled(':', fg=self.opts.margin_fg)
filler = self.screen_size.cols - wcswidth(prefix) - wcswidth(suffix) filler = self.screen_size.cols - wcswidth(prefix) - wcswidth(suffix)
text = '{}{}{}'.format(prefix, ' ' * filler, suffix) text = '{}{}{}'.format(prefix, ' ' * filler, suffix)
@@ -447,16 +475,16 @@ class DiffHandler(Handler):
new_ctx = max(0, new_ctx) new_ctx = max(0, new_ctx)
if new_ctx != self.current_context_count: if new_ctx != self.current_context_count:
self.current_context_count = new_ctx self.current_context_count = new_ctx
self.state = COLLECTED self.state = State.collected
self.generate_diff() self.generate_diff()
self.restore_position = self.current_position self.restore_position = self.current_position
self.draw_screen() self.draw_screen()
def start_search(self, is_regex: bool, is_backward: bool) -> None: def start_search(self, is_regex: bool, is_backward: bool) -> None:
if self.state != DIFFED: if self.state is not State.diffed:
self.cmd.bell() self.cmd.bell()
return return
self.state = COMMAND self.state = State.command
self.line_edit.clear() self.line_edit.clear()
self.line_edit.add_text('?' if is_backward else '/') self.line_edit.add_text('?' if is_backward else '/')
self.current_search_is_regex = is_regex self.current_search_is_regex = is_regex
@@ -470,50 +498,50 @@ class DiffHandler(Handler):
try: try:
self.current_search = Search(self.opts, query[1:], self.current_search_is_regex, query[0] == '?') self.current_search = Search(self.opts, query[1:], self.current_search_is_regex, query[0] == '?')
except BadRegex: except BadRegex:
self.state = MESSAGE self.state = State.message
self.message = sanitize(_('Bad regex: {}').format(query[1:])) self.message = sanitize(_('Bad regex: {}').format(query[1:]))
self.cmd.bell() self.cmd.bell()
else: else:
if self.current_search(self.diff_lines, self.margin_size, self.screen_size.cols): if self.current_search(self.diff_lines, self.margin_size, self.screen_size.cols):
self.scroll_to_next_match(include_current=True) self.scroll_to_next_match(include_current=True)
else: else:
self.state = MESSAGE self.state = State.message
self.message = sanitize(_('No matches found')) self.message = sanitize(_('No matches found'))
self.cmd.bell() self.cmd.bell()
def on_key_event(self, key_event: KeyEvent, in_bracketed_paste: bool = False) -> None: def on_key_event(self, key_event: KeyEvent, in_bracketed_paste: bool = False) -> None:
if key_event.text: if key_event.text:
if self.state is COMMAND: if self.state is State.command:
self.line_edit.on_text(key_event.text, in_bracketed_paste) self.line_edit.on_text(key_event.text, in_bracketed_paste)
self.draw_status_line() self.draw_status_line()
return return
if self.state is MESSAGE: if self.state is State.message:
self.state = DIFFED self.state = State.diffed
self.draw_status_line() self.draw_status_line()
return return
else: else:
if self.state is MESSAGE: if self.state is State.message:
if key_event.type is not EventType.RELEASE: if key_event.type is not EventType.RELEASE:
self.state = DIFFED self.state = State.diffed
self.draw_status_line() self.draw_status_line()
return return
if self.state is COMMAND: if self.state is State.command:
if self.line_edit.on_key(key_event): if self.line_edit.on_key(key_event):
if not self.line_edit.current_input: if not self.line_edit.current_input:
self.state = DIFFED self.state = State.diffed
self.draw_status_line() self.draw_status_line()
return return
if key_event.matches('enter'): if key_event.matches('enter'):
self.state = DIFFED self.state = State.diffed
self.do_search() self.do_search()
self.line_edit.clear() self.line_edit.clear()
self.draw_screen() self.draw_screen()
return return
if key_event.matches('esc'): if key_event.matches('esc'):
self.state = DIFFED self.state = State.diffed
self.draw_status_line() self.draw_status_line()
return return
if self.state >= DIFFED and self.current_search is not None and key_event.matches('esc'): if self.state.value >= State.diffed.value and self.current_search is not None and key_event.matches('esc'):
self.current_search = None self.current_search = None
self.draw_screen() self.draw_screen()
return return
@@ -526,16 +554,16 @@ class DiffHandler(Handler):
def on_resize(self, screen_size: ScreenSize) -> None: def on_resize(self, screen_size: ScreenSize) -> None:
self.screen_size = screen_size self.screen_size = screen_size
self.set_scrolling_region() self.set_scrolling_region()
if self.state > COLLECTED: if self.state.value > State.collected.value:
self.image_manager.delete_all_sent_images() self.image_manager.delete_all_sent_images()
self.render_diff() self.render_diff()
self.draw_screen() self.draw_screen()
def on_interrupt(self) -> None: def on_interrupt(self) -> None:
self.quit_loop(1) self.terminate(1)
def on_eot(self) -> None: def on_eot(self) -> None:
self.quit_loop(1) self.terminate(1)
OPTIONS = partial('''\ OPTIONS = partial('''\
@@ -639,9 +667,10 @@ def main(args: List[str]) -> None:
for message in showwarning.warnings: for message in showwarning.warnings:
from kitty.utils import safe_print from kitty.utils import safe_print
safe_print(message, file=sys.stderr) safe_print(message, file=sys.stderr)
highlight_processes = getattr(highlight_collection, 'processes', ()) if handler.doing_background_work is BackgroundWork.highlighting:
terminate_processes(tuple(highlight_processes)) terminate_processes(tuple(get_highlight_processes()))
terminate_processes(tuple(worker_processes)) elif handler.doing_background_work == BackgroundWork.diffing:
terminate_processes(tuple(worker_processes))
if loop.return_code != 0: if loop.return_code != 0:
if handler.report_traceback_on_exit: if handler.report_traceback_on_exit:
print(handler.report_traceback_on_exit, file=sys.stderr) print(handler.report_traceback_on_exit, file=sys.stderr)

View File

@@ -244,14 +244,14 @@ class Differ:
except Exception as e: except Exception as e:
return f'Running git diff for {left_path} vs. {right_path} generated an exception: {e}' return f'Running git diff for {left_path} vs. {right_path} generated an exception: {e}'
if not ok: if not ok:
return output + f'\nRunning git diff for {left_path} vs. {right_path} failed' return f'{output}\nRunning git diff for {left_path} vs. {right_path} failed'
left_lines = lines_for_path(left_path) left_lines = lines_for_path(left_path)
right_lines = lines_for_path(right_path) right_lines = lines_for_path(right_path)
try: try:
patch = parse_patch(output) patch = parse_patch(output)
except Exception: except Exception:
import traceback import traceback
return traceback.format_exc() + f'\nParsing diff for {left_path} vs. {right_path} failed' return f'{traceback.format_exc()}\nParsing diff for {left_path} vs. {right_path} failed'
else: else:
ans[key] = patch ans[key] = patch
return ans return ans

View File

@@ -101,7 +101,7 @@ def human_readable(size: int, sep: str = ' ') -> str:
s = s[:s.find(".")+2] s = s[:s.find(".")+2]
if s.endswith('.0'): if s.endswith('.0'):
s = s[:-2] s = s[:-2]
return s + sep + suffix return f'{s}{sep}{suffix}'
def fit_in(text: str, count: int) -> str: def fit_in(text: str, count: int) -> str:
@@ -110,7 +110,7 @@ def fit_in(text: str, count: int) -> str:
return text return text
if count > 1: if count > 1:
p = truncate_point_for_length(text, count - 1) p = truncate_point_for_length(text, count - 1)
return text[:p] + '' return f'{text[:p]}'
def fill_in(text: str, sz: int) -> str: def fill_in(text: str, sz: int) -> str:
@@ -127,8 +127,8 @@ def place_in(text: str, sz: int) -> str:
def format_func(which: str) -> Callable[[str], str]: def format_func(which: str) -> Callable[[str], str]:
def formatted(text: str) -> str: def formatted(text: str) -> str:
fmt = formats[which] fmt = formats[which]
return '\x1b[' + fmt + 'm' + text + '\x1b[0m' return f'\x1b[{fmt}m{text}\x1b[0m'
formatted.__name__ = which + '_format' formatted.__name__ = f'{which}_format'
return formatted return formatted
@@ -148,8 +148,8 @@ highlight_map = {'remove': ('removed_highlight', 'removed'), 'add': ('added_high
def highlight_boundaries(ltype: str) -> Tuple[str, str]: def highlight_boundaries(ltype: str) -> Tuple[str, str]:
s, e = highlight_map[ltype] s, e = highlight_map[ltype]
start = '\x1b[' + formats[s] + 'm' start = f'\x1b[{formats[s]}m'
stop = '\x1b[' + formats[e] + 'm' stop = f'\x1b[{formats[e]}m'
return start, stop return start, stop

View File

@@ -40,6 +40,8 @@ def kitty_common_opts() -> KittyCommonOpts:
DEFAULT_HINT_ALPHABET = string.digits + string.ascii_lowercase DEFAULT_HINT_ALPHABET = string.digits + string.ascii_lowercase
DEFAULT_REGEX = r'(?m)^\s*(.+)\s*$' DEFAULT_REGEX = r'(?m)^\s*(.+)\s*$'
FILE_EXTENSION = r'\.(?:[a-zA-Z0-9]{2,7}|[ahcmo])(?!\.)'
PATH_REGEX = fr'(?:\S*?/[\r\S]+)|(?:\S[\r\S]*{FILE_EXTENSION})\b'
class Mark: class Mark:
@@ -184,7 +186,7 @@ class Hints(Handler):
self.current_input = self.current_input[:-1] self.current_input = self.current_input[:-1]
self.current_text = None self.current_text = None
self.draw_screen() self.draw_screen()
elif key_event.matches('enter') and self.current_input: elif (key_event.matches('enter') or key_event.matches('space')) and self.current_input:
try: try:
idx = decode_hint(self.current_input, self.alphabet) idx = decode_hint(self.current_input, self.alphabet)
self.chosen.append(self.index_map[idx]) self.chosen.append(self.index_map[idx])
@@ -355,7 +357,7 @@ def functions_for(args: HintsCLIOptions) -> Tuple[str, List[PostprocessorFunc]]:
) )
post_processors.append(url) post_processors.append(url)
elif args.type == 'path': elif args.type == 'path':
pattern = r'(?:\S*?/[\r\S]+)|(?:\S[\r\S]*\.[a-zA-Z0-9\r]{2,7})' pattern = PATH_REGEX
post_processors.extend((brackets, quotes)) post_processors.extend((brackets, quotes))
elif args.type == 'line': elif args.type == 'line':
pattern = '(?m)^\\s*(.+)[\\s\0]*$' pattern = '(?m)^\\s*(.+)[\\s\0]*$'
@@ -412,7 +414,7 @@ def parse_input(text: str) -> str:
def linenum_marks(text: str, args: HintsCLIOptions, Mark: Type[Mark], extra_cli_args: Sequence[str], *a: Any) -> Generator[Mark, None, None]: def linenum_marks(text: str, args: HintsCLIOptions, Mark: Type[Mark], extra_cli_args: Sequence[str], *a: Any) -> Generator[Mark, None, None]:
regex = args.regex regex = args.regex
if regex == DEFAULT_REGEX: if regex == DEFAULT_REGEX:
regex = r'(?P<path>(?:\S*/\S+?)|(?:\S+[.][a-zA-Z0-9]{2,7})):(?P<line>\d+)' regex = fr'(?P<path>{PATH_REGEX}):(?P<line>\d+)'
yield from mark(regex, [brackets, quotes], text, args) yield from mark(regex, [brackets, quotes], text, args)
@@ -727,7 +729,7 @@ def linenum_handle_result(args: List[str], data: Dict[str, Any], target_window_i
else: else:
import shlex import shlex
text = ' '.join(shlex.quote(arg) for arg in cmd) text = ' '.join(shlex.quote(arg) for arg in cmd)
w.paste_bytes(text + '\r') w.paste_bytes(f'{text}\r')
elif action == 'background': elif action == 'background':
import subprocess import subprocess
subprocess.Popen(cmd, cwd=data['cwd']) subprocess.Popen(cmd, cwd=data['cwd'])

View File

@@ -19,6 +19,7 @@ from kitty.cli import parse_args
from kitty.cli_stub import IcatCLIOptions from kitty.cli_stub import IcatCLIOptions
from kitty.constants import appname from kitty.constants import appname
from kitty.guess_mime_type import guess_type from kitty.guess_mime_type import guess_type
from kitty.rgb import to_color
from kitty.types import run_once from kitty.types import run_once
from kitty.typing import GRT_f, GRT_t from kitty.typing import GRT_f, GRT_t
from kitty.utils import ( from kitty.utils import (
@@ -55,6 +56,19 @@ are smaller than the specified area to be scaled up to use as much
of the specified area as possible. of the specified area as possible.
--background
default=none
Specify a background color, this will cause transparent images to be composited on
top of the specified color.
--mirror
default=none
type=choices
choices=none,horizontal,vertical,both
Mirror the image about a horizontal or vertical axis or both.
--clear --clear
type=bool-set type=bool-set
Remove all images currently displayed on the screen. Remove all images currently displayed on the screen.
@@ -307,6 +321,9 @@ class ParsedOpts:
place: Optional['Place'] = None place: Optional['Place'] = None
z_index: int = 0 z_index: int = 0
remove_alpha: str = ''
flip: bool = False
flop: bool = False
def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfile: bool) -> bool: def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfile: bool) -> bool:
@@ -316,9 +333,10 @@ def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfil
available_height = parsed_opts.place.height * (ss.height // ss.rows) if parsed_opts.place else 10 * m.height available_height = parsed_opts.place.height * (ss.height // ss.rows) if parsed_opts.place else 10 * m.height
needs_scaling = m.width > available_width or m.height > available_height needs_scaling = m.width > available_width or m.height > available_height
needs_scaling = needs_scaling or args.scale_up needs_scaling = needs_scaling or args.scale_up
needs_conversion = needs_scaling or bool(parsed_opts.remove_alpha) or parsed_opts.flip or parsed_opts.flop
file_removed = False file_removed = False
use_number = 0 use_number = 0
if m.fmt == 'png' and not needs_scaling: if m.fmt == 'png' and not needs_conversion:
outfile = path outfile = path
transmit_mode: 'GRT_t' = 't' if is_tempfile else 'f' transmit_mode: 'GRT_t' = 't' if is_tempfile else 'f'
fmt: 'GRT_f' = 100 fmt: 'GRT_f' = 100
@@ -328,13 +346,17 @@ def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfil
fmt = 24 if m.mode == 'rgb' else 32 fmt = 24 if m.mode == 'rgb' else 32
transmit_mode = 't' transmit_mode = 't'
if len(m) == 1 or args.loop == 0: if len(m) == 1 or args.loop == 0:
outfile, width, height = render_as_single_image(path, m, available_width, available_height, args.scale_up) outfile, width, height = render_as_single_image(
path, m, available_width, available_height, args.scale_up,
remove_alpha=parsed_opts.remove_alpha, flip=parsed_opts.flip, flop=parsed_opts.flop)
else: else:
import struct import struct
use_number = max(1, struct.unpack('@I', os.urandom(4))[0]) use_number = max(1, struct.unpack('@I', os.urandom(4))[0])
with NamedTemporaryFile() as f: with NamedTemporaryFile() as f:
prefix = f.name prefix = f.name
frame_data = render_image(path, prefix, m, available_width, available_height, args.scale_up) frame_data = render_image(
path, prefix, m, available_width, available_height, args.scale_up,
remove_alpha=parsed_opts.remove_alpha, flip=parsed_opts.flip, flop=parsed_opts.flop)
outfile, width, height = frame_data.frames[0].path, frame_data.width, frame_data.height outfile, width, height = frame_data.frames[0].path, frame_data.width, frame_data.height
show( show(
outfile, width, height, parsed_opts.z_index, fmt, transmit_mode, outfile, width, height, parsed_opts.z_index, fmt, transmit_mode,
@@ -449,9 +471,8 @@ def process_single_item(
file_removed = False file_removed = False
try: try:
if isinstance(item, bytes): if isinstance(item, bytes):
tf = NamedTemporaryFile(prefix='stdin-image-data-', delete=False) with NamedTemporaryFile(prefix='stdin-image-data-', delete=False) as tf:
tf.write(item) tf.write(item)
tf.close()
item = tf.name item = tf.name
is_tempfile = True is_tempfile = True
if url_pat is not None and url_pat.match(item) is not None: if url_pat is not None and url_pat.match(item) is not None:
@@ -527,6 +548,13 @@ def main(args: List[str] = sys.argv) -> None:
parsed_opts.z_index = parse_z_index(cli_opts.z_index) parsed_opts.z_index = parse_z_index(cli_opts.z_index)
except Exception: except Exception:
raise SystemExit(f'Not a valid z-index specification: {cli_opts.z_index}') raise SystemExit(f'Not a valid z-index specification: {cli_opts.z_index}')
if cli_opts.background != 'none':
ra = to_color(cli_opts.background)
if ra is None:
raise SystemExit(f'Not a valid color specification: {cli_opts.background}')
parsed_opts.remove_alpha = ra.as_sharp
parsed_opts.flip = cli_opts.mirror in ('both', 'vertical')
parsed_opts.flop = cli_opts.mirror in ('both', 'horizontal')
if cli_opts.detect_support: if cli_opts.detect_support:
if not detect_support(wait_for=cli_opts.detection_timeout, silent=True): if not detect_support(wait_for=cli_opts.detection_timeout, silent=True):
@@ -550,16 +578,26 @@ def main(args: List[str] = sys.argv) -> None:
raise SystemExit(f'The --place option can only be used with a single image, not {items}') raise SystemExit(f'The --place option can only be used with a single image, not {items}')
sys.stdout.buffer.write(b'\0337') # save cursor sys.stdout.buffer.write(b'\0337') # save cursor
url_pat = re.compile(r'(?:https?|ftp)://', flags=re.I) url_pat = re.compile(r'(?:https?|ftp)://', flags=re.I)
def hold_if_needed(exit_code_or_msg: Union[int, str]) -> None:
if cli_opts.hold:
if isinstance(exit_code_or_msg, str):
print(exit_code_or_msg, file=sys.stderr, flush=True)
exit_code_or_msg = 1
with open(os.ctermid()) as tty, raw_mode(tty.fileno()):
tty.buffer.read(1)
raise SystemExit(exit_code_or_msg)
for item in items: for item in items:
try: try:
process_single_item(item, cli_opts, parsed_opts, url_pat) process_single_item(item, cli_opts, parsed_opts, url_pat)
except NoImageMagick as e: except NoImageMagick as e:
raise SystemExit(str(e)) hold_if_needed(str(e))
except OutdatedImageMagick as e: except OutdatedImageMagick as e:
print(e.detailed_error, file=sys.stderr) print(e.detailed_error, file=sys.stderr)
raise SystemExit(str(e)) hold_if_needed(str(e))
except ConvertFailed as e: except ConvertFailed as e:
raise SystemExit(str(e)) hold_if_needed(str(e))
except OpenFailed as e: except OpenFailed as e:
errors.append(e) errors.append(e)
if parsed_opts.place: if parsed_opts.place:
@@ -567,11 +605,8 @@ def main(args: List[str] = sys.argv) -> None:
if errors: if errors:
for err in errors: for err in errors:
print(err, file=sys.stderr) print(err, file=sys.stderr)
if cli_opts.hold: hold_if_needed(1 if errors else 0)
with open(os.ctermid()) as tty: raise SystemExit()
with raw_mode(tty.fileno()):
tty.buffer.read(1)
raise SystemExit(1 if errors else 0)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -85,9 +85,9 @@ def create_strut(
'-id', '-id',
str(int(win_id)), '-format', '_NET_WM_STRUT_PARTIAL', '32cccccccccccc', str(int(win_id)), '-format', '_NET_WM_STRUT_PARTIAL', '32cccccccccccc',
'-set', '_NET_WM_STRUT_PARTIAL', '-set', '_NET_WM_STRUT_PARTIAL',
'{left},{right},{top},{bottom},' f'{left},{right},{top},{bottom},'
'{left_start_y},{left_end_y},{right_start_y},{right_end_y},' f'{left_start_y},{left_end_y},{right_start_y},{right_end_y},'
'{top_start_x},{top_end_x},{bottom_start_x},{bottom_end_x}'.format(**locals()) f'{top_start_x},{top_end_x},{bottom_start_x},{bottom_end_x}'
) )

View File

@@ -245,7 +245,7 @@ def main(args: List[str] = sys.argv) -> None:
raise SystemExit(f'Unknown queries: {", ".join(extra)}') raise SystemExit(f'Unknown queries: {", ".join(extra)}')
for key, val in do_queries(queries, cli_opts).items(): for key, val in do_queries(queries, cli_opts).items():
print(key + ':', val) print(f'{key}:', val)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -52,7 +52,7 @@ class KeysHandler(Handler):
self.cmd.colored(etype + ' ', 'yellow') self.cmd.colored(etype + ' ', 'yellow')
self.cmd.styled(key_event.text, italic=True) self.cmd.styled(key_event.text, italic=True)
self.print() self.print()
rep = 'CSI ' + encode_key_event(key_event)[2:] rep = f'CSI {encode_key_event(key_event)[2:]}'
rep = rep.replace(';', ' ; ').replace(':', ' : ')[:-1] + ' ' + rep[-1] rep = rep.replace(';', ' ; ').replace(':', ' : ')[:-1] + ' ' + rep[-1]
self.cmd.styled(rep, fg='magenta') self.cmd.styled(rep, fg='magenta')
if (key_event.shifted_key or key_event.alternate_key): if (key_event.shifted_key or key_event.alternate_key):

View File

@@ -17,7 +17,7 @@ def print_key(raw: bytearray) -> None:
unix = '' unix = ''
for ch in raw: for ch in raw:
if ch < len(ctrl_keys): if ch < len(ctrl_keys):
unix += '^' + ctrl_keys[ch] unix += f'^{ctrl_keys[ch]}'
elif ch == 127: elif ch == 127:
unix += '^?' unix += '^?'
else: else:

View File

@@ -281,7 +281,7 @@ def complete_choices(ans: Completions, prefix: str, title: str, choices: Iterabl
if q.startswith(effective_prefix): if q.startswith(effective_prefix):
if comma_separated: if comma_separated:
tq = q tq = q
q = hidden_prefix + q + ',' q = f'{hidden_prefix}{q},'
word_transforms[q] = tq word_transforms[q] = tq
matches[q] = '' matches[q] = ''
ans.add_match_group(title, matches, trailing_space=not comma_separated, word_transforms=word_transforms) ans.add_match_group(title, matches, trailing_space=not comma_separated, word_transforms=word_transforms)

View File

@@ -131,7 +131,7 @@ def get_ssh_cli() -> Tuple[Set[str], Set[str]]:
other_ssh_args: Set[str] = set() other_ssh_args: Set[str] = set()
boolean_ssh_args: Set[str] = set() boolean_ssh_args: Set[str] = set()
for k, v in ssh_options().items(): for k, v in ssh_options().items():
k = '-' + k k = f'-{k}'
if v: if v:
other_ssh_args.add(k) other_ssh_args.add(k)
else: else:
@@ -213,7 +213,7 @@ class InvalidSSHArgs(ValueError):
def parse_ssh_args(args: List[str]) -> Tuple[List[str], List[str], bool]: def parse_ssh_args(args: List[str]) -> Tuple[List[str], List[str], bool]:
boolean_ssh_args, other_ssh_args = get_ssh_cli() boolean_ssh_args, other_ssh_args = get_ssh_cli()
passthrough_args = {'-' + x for x in 'Nnf'} passthrough_args = {f'-{x}' for x in 'Nnf'}
ssh_args = [] ssh_args = []
server_args: List[str] = [] server_args: List[str] = []
expecting_option_val = False expecting_option_val = False
@@ -230,7 +230,7 @@ def parse_ssh_args(args: List[str]) -> Tuple[List[str], List[str], bool]:
# could be a multi-character option # could be a multi-character option
all_args = argument[1:] all_args = argument[1:]
for i, arg in enumerate(all_args): for i, arg in enumerate(all_args):
arg = '-' + arg arg = f'-{arg}'
if arg in passthrough_args: if arg in passthrough_args:
passthrough = True passthrough = True
if arg in boolean_ssh_args: if arg in boolean_ssh_args:

View File

@@ -376,7 +376,7 @@ def fetch_themes(
needs_delete = False needs_delete = False
try: try:
with tempfile.NamedTemporaryFile(suffix='-' + os.path.basename(dest_path), dir=os.path.dirname(dest_path), delete=False) as f: with tempfile.NamedTemporaryFile(suffix=f'-{os.path.basename(dest_path)}', dir=os.path.dirname(dest_path), delete=False) as f:
needs_delete = True needs_delete = True
shutil.copyfileobj(res, f) shutil.copyfileobj(res, f)
f.flush() f.flush()
@@ -405,7 +405,7 @@ def theme_name_from_file_name(fname: str) -> str:
ans = ans.replace('_', ' ') ans = ans.replace('_', ' ')
def camel_case(m: 'Match[str]') -> str: def camel_case(m: 'Match[str]') -> str:
return str(m.group(1) + ' ' + m.group(2)) return f'{m.group(1)} {m.group(2)}'
ans = re.sub(r'([a-z])([A-Z])', camel_case, ans) ans = re.sub(r'([a-z])([A-Z])', camel_case, ans)
return ' '.join(x.capitalize() for x in filter(None, ans.split())) return ' '.join(x.capitalize() for x in filter(None, ans.split()))
@@ -522,10 +522,10 @@ class Theme:
def save_in_dir(self, dirpath: str) -> None: def save_in_dir(self, dirpath: str) -> None:
atomic_save(self.raw.encode('utf-8'), os.path.join(dirpath, f'{self.name}.conf')) atomic_save(self.raw.encode('utf-8'), os.path.join(dirpath, f'{self.name}.conf'))
def save_in_conf(self, confdir: str, reload_in: str) -> None: def save_in_conf(self, confdir: str, reload_in: str, config_file_name: str = 'kitty.conf') -> None:
os.makedirs(confdir, exist_ok=True) os.makedirs(confdir, exist_ok=True)
atomic_save(self.raw.encode('utf-8'), os.path.join(confdir, 'current-theme.conf')) atomic_save(self.raw.encode('utf-8'), os.path.join(confdir, 'current-theme.conf'))
confpath = os.path.realpath(os.path.join(confdir, 'kitty.conf')) confpath = os.path.realpath(os.path.join(confdir, config_file_name))
try: try:
with open(confpath) as f: with open(confpath) as f:
raw = f.read() raw = f.read()
@@ -533,7 +533,7 @@ class Theme:
raw = '' raw = ''
nraw = patch_conf(raw, self.name) nraw = patch_conf(raw, self.name)
if raw: if raw:
with open(confpath + '.bak', 'w') as f: with open(f'{confpath}.bak', 'w') as f:
f.write(raw) f.write(raw)
atomic_save(nraw.encode('utf-8'), confpath) atomic_save(nraw.encode('utf-8'), confpath)
if reload_in == 'parent': if reload_in == 'parent':

View File

@@ -37,7 +37,7 @@ def limit_length(text: str, limit: int = 32) -> str:
x = truncate_point_for_length(text, limit - 1) x = truncate_point_for_length(text, limit - 1)
if x >= len(text): if x >= len(text):
return text return text
return text[:x] + '' return f'{text[:x]}'
class State(Enum): class State(Enum):
@@ -332,7 +332,7 @@ class ThemesHandler(Handler):
for line, width, is_current in self.themes_list.lines(num_rows): for line, width, is_current in self.themes_list.lines(num_rows):
num_rows -= 1 num_rows -= 1
if is_current: if is_current:
line = line.replace(MARK_AFTER, '\033[' + color_code('green') + 'm') line = line.replace(MARK_AFTER, f'\033[{color_code("green")}m')
self.cmd.styled('>' if is_current else ' ', fg='green') self.cmd.styled('>' if is_current else ' ', fg='green')
self.cmd.styled(line, bold=is_current, fg='green' if is_current else None) self.cmd.styled(line, bold=is_current, fg='green' if is_current else None)
self.cmd.move_cursor_by(mw - width, 'right') self.cmd.move_cursor_by(mw - width, 'right')
@@ -433,7 +433,7 @@ class ThemesHandler(Handler):
def draw_accepting_screen(self) -> None: def draw_accepting_screen(self) -> None:
name = self.themes_list.current_theme.name name = self.themes_list.current_theme.name
name = styled(name, bold=True, fg="green") name = styled(name, bold=True, fg="green")
kc = styled('kitty.conf', italic=True) kc = styled(self.cli_opts.config_file_name, italic=True)
def ac(x: str) -> str: def ac(x: str) -> str:
return styled(x, fg='red') return styled(x, fg='red')
@@ -465,7 +465,7 @@ class ThemesHandler(Handler):
self.quit_on_next_key_release = 0 self.quit_on_next_key_release = 0
return return
if key_event.matches_text('m'): if key_event.matches_text('m'):
self.themes_list.current_theme.save_in_conf(config_dir, self.cli_opts.reload_in) self.themes_list.current_theme.save_in_conf(config_dir, self.cli_opts.reload_in, self.cli_opts.config_file_name)
self.update_recent() self.update_recent()
self.quit_on_next_key_release = 0 self.quit_on_next_key_release = 0
return return
@@ -536,6 +536,13 @@ type=bool-set
default=false default=false
When running non-interactively, dump the specified theme to STDOUT When running non-interactively, dump the specified theme to STDOUT
instead of changing kitty.conf. instead of changing kitty.conf.
--config-file-name
default=kitty.conf
The name or path to the config file to edit. Relative paths are interpreted
with respect to the kitty config directory. By default the kitty config file,
kitty.conf is edited.
'''.format '''.format
@@ -559,7 +566,7 @@ def non_interactive(cli_opts: ThemesCLIOptions, theme_name: str) -> None:
if cli_opts.dump_theme: if cli_opts.dump_theme:
print(theme.raw) print(theme.raw)
return return
theme.save_in_conf(config_dir, cli_opts.reload_in) theme.save_in_conf(config_dir, cli_opts.reload_in, cli_opts.config_file_name)
def main(args: List[str]) -> None: def main(args: List[str]) -> None:

View File

@@ -155,12 +155,12 @@ def develop() -> None:
import sys import sys
src = sys.argv[-1] src = sys.argv[-1]
sig_loader = LoadSignature() sig_loader = LoadSignature()
with open(src + '.sig', 'wb') as f: with open(f'{src}.sig', 'wb') as f:
for chunk in signature_of_file(src): for chunk in signature_of_file(src):
sig_loader.add_chunk(chunk) sig_loader.add_chunk(chunk)
f.write(chunk) f.write(chunk)
sig_loader.commit() sig_loader.commit()
with open(src + '.delta', 'wb') as f, PatchFile(src, src + '.output') as patcher: with open(f'{src}.delta', 'wb') as f, PatchFile(src, f'{src}.output') as patcher:
for chunk in delta_for_file(src, sig_loader.signature): for chunk in delta_for_file(src, sig_loader.signature):
f.write(chunk) f.write(chunk)
patcher.write(chunk) patcher.write(chunk)

View File

@@ -45,7 +45,7 @@ def render_path_in_width(path: str, width: int) -> str:
if wcswidth(path) <= width: if wcswidth(path) <= width:
return path return path
x = truncate_point_for_length(path, width - 1) x = truncate_point_for_length(path, width - 1)
return path[:x] + '' return f'{path[:x]}'
def render_seconds(val: float) -> str: def render_seconds(val: float) -> str:

View File

@@ -331,7 +331,7 @@ class Dircolors:
# change .xyz to *.xyz # change .xyz to *.xyz
yield '*' + pair[0], pair[1] yield '*' + pair[0], pair[1]
return ':'.join('%s=%s' % pair for pair in gen_pairs()) return ':'.join('{}={}'.format(*pair) for pair in gen_pairs())
def _format_code(self, text: str, code: str) -> str: def _format_code(self, text: str, code: str) -> str:
val = self.codes.get(code) val = self.codes.get(code)

View File

@@ -189,7 +189,9 @@ def render_image(
m: ImageData, m: ImageData,
available_width: int, available_height: int, available_width: int, available_height: int,
scale_up: bool, scale_up: bool,
only_first_frame: bool = False only_first_frame: bool = False,
remove_alpha: str = '',
flip: bool = False, flop: bool = False,
) -> RenderedImage: ) -> RenderedImage:
import tempfile import tempfile
has_multiple_frames = len(m) > 1 has_multiple_frames = len(m) > 1
@@ -202,7 +204,15 @@ def render_image(
if exe is None: if exe is None:
raise OSError('Failed to find the ImageMagick convert executable, make sure it is present in PATH') raise OSError('Failed to find the ImageMagick convert executable, make sure it is present in PATH')
cmd = [exe] cmd = [exe]
cmd += ['-background', 'none', '--', path] if remove_alpha:
cmd += ['-background', remove_alpha, '-alpha', 'remove']
else:
cmd += ['-background', 'none']
if flip:
cmd.append('-flip')
if flop:
cmd.append('-flop')
cmd += ['--', path]
if only_first_frame and has_multiple_frames: if only_first_frame and has_multiple_frames:
cmd[-1] += '[0]' cmd[-1] += '[0]'
cmd.append('-auto-orient') cmd.append('-auto-orient')
@@ -287,12 +297,15 @@ def render_as_single_image(
path: str, m: ImageData, path: str, m: ImageData,
available_width: int, available_height: int, available_width: int, available_height: int,
scale_up: bool, scale_up: bool,
tdir: Optional[str] = None tdir: Optional[str] = None,
remove_alpha: str = '', flip: bool = False, flop: bool = False,
) -> Tuple[str, int, int]: ) -> Tuple[str, int, int]:
import tempfile import tempfile
fd, output = tempfile.mkstemp(prefix='icat-', suffix=f'.{m.mode}', dir=tdir) fd, output = tempfile.mkstemp(prefix='icat-', suffix=f'.{m.mode}', dir=tdir)
os.close(fd) os.close(fd)
result = render_image(path, output, m, available_width, available_height, scale_up, only_first_frame=True) result = render_image(
path, output, m, available_width, available_height, scale_up,
only_first_frame=True, remove_alpha=remove_alpha, flip=flip, flop=flop)
os.rename(result.frames[0].path, output) os.rename(result.frames[0].path, output)
return output, result.width, result.height return output, result.width, result.height

View File

@@ -5,13 +5,13 @@ import sys
from contextlib import contextmanager from contextlib import contextmanager
from enum import Enum, auto from enum import Enum, auto
from functools import wraps from functools import wraps
from typing import ( from typing import IO, Any, Callable, Dict, Generator, Optional, TypeVar, Union
IO, Any, Callable, Dict, Generator, Optional, TypeVar, Union
)
from kitty.fast_data_types import Color from kitty.fast_data_types import Color
from kitty.rgb import color_as_sharp, to_color from kitty.rgb import color_as_sharp, to_color
from kitty.typing import GraphicsCommandType, HandlerType, ScreenSize from kitty.typing import (
GraphicsCommandType, HandlerType, ScreenSize, UnderlineLiteral
)
from .operations_stub import CMD from .operations_stub import CMD
@@ -163,14 +163,11 @@ def set_scrolling_region(screen_size: Optional['ScreenSize'] = None, top: Option
@cmd @cmd
def scroll_screen(amt: int = 1) -> str: def scroll_screen(amt: int = 1) -> str:
return '\033[' + str(abs(amt)) + ('T' if amt < 0 else 'S') return f'\033[{abs(amt)}{"T" if amt < 0 else "S"}'
STANDARD_COLORS = {name: i for i, name in enumerate( STANDARD_COLORS = {'black': 0, 'red': 1, 'green': 2, 'yellow': 3, 'blue': 4, 'magenta': 5, 'cyan': 6, 'gray': 7, 'white': 7}
'black red green yellow blue magenta cyan gray'.split())} UNDERLINE_STYLES = {'straight': 1, 'double': 2, 'curly': 3, 'dotted': 4, 'dashed': 5}
STANDARD_COLORS['white'] = STANDARD_COLORS['gray']
UNDERLINE_STYLES = {name: i + 1 for i, name in enumerate(
'straight double curly'.split())}
ColorSpec = Union[int, str, Color] ColorSpec = Union[int, str, Color]
@@ -217,7 +214,7 @@ def styled(
bg_intense: bool = False, bg_intense: bool = False,
italic: Optional[bool] = None, italic: Optional[bool] = None,
bold: Optional[bool] = None, bold: Optional[bool] = None,
underline: Optional[str] = None, underline: Optional[UnderlineLiteral] = None,
underline_color: Optional[ColorSpec] = None, underline_color: Optional[ColorSpec] = None,
reverse: Optional[bool] = None, reverse: Optional[bool] = None,
dim: Optional[bool] = None, dim: Optional[bool] = None,
@@ -468,7 +465,7 @@ def as_type_stub() -> str:
for name, func in all_cmds.items(): for name, func in all_cmds.items():
args = ', '.join(func_sig(func)) args = ', '.join(func_sig(func))
if args: if args:
args = ', ' + args args = f', {args}'
methods.append(f' def {name}(self{args}) -> str: pass') methods.append(f' def {name}(self{args}) -> str: pass')
ans += ['', '', 'class CMD:'] + methods ans += ['', '', 'class CMD:'] + methods

View File

@@ -192,7 +192,7 @@ class Table:
if w < 2: if w < 2:
text += ' ' * (2 - w) text += ' ' * (2 - w)
if len(desc) > space_for_desc: if len(desc) > space_for_desc:
text += desc[:space_for_desc - 1] + '' text += f'{desc[:space_for_desc - 1]}'
else: else:
text += desc text += desc
extra = space_for_desc - len(desc) extra = space_for_desc - len(desc)

107
kitty/boss.py Executable file → Normal file
View File

@@ -39,7 +39,7 @@ from .fast_data_types import (
set_application_quit_request, set_background_image, set_boss, set_application_quit_request, set_background_image, set_boss,
set_clipboard_string, set_in_sequence_mode, set_options, set_clipboard_string, set_in_sequence_mode, set_options,
set_os_window_size, set_os_window_title, thread_write, toggle_fullscreen, set_os_window_size, set_os_window_title, thread_write, toggle_fullscreen,
toggle_maximized toggle_maximized, toggle_secure_input
) )
from .key_encoding import get_name_to_functional_number_map from .key_encoding import get_name_to_functional_number_map
from .keys import get_shortcut, shortcut_matches from .keys import get_shortcut, shortcut_matches
@@ -653,18 +653,13 @@ class Boss:
msg = _('Are you sure you want to close this window?') msg = _('Are you sure you want to close this window?')
if window.has_running_program: if window.has_running_program:
msg += ' ' + _('It is running a program.') msg += ' ' + _('It is running a program.')
self._run_kitten( self.confirm(msg, self.handle_close_window_confirmation, window.id, window=window)
'ask', ['--type=yesno', '--message', msg],
window=window,
custom_callback=partial(self.handle_close_window_confirmation, window.id)
)
else: else:
self.mark_window_for_close(window) self.mark_window_for_close(window)
def handle_close_window_confirmation(self, window_id: int, data: Dict[str, Any], *a: Any) -> None: def handle_close_window_confirmation(self, allowed: bool, window_id: int) -> None:
if data['response'] != 'y': if allowed:
return self.mark_window_for_close(window_id)
self.mark_window_for_close(window_id)
@ac('tab', 'Close the current tab') @ac('tab', 'Close the current tab')
def close_tab(self, tab: Optional[Tab] = None) -> None: def close_tab(self, tab: Optional[Tab] = None) -> None:
@@ -672,13 +667,35 @@ class Boss:
if tab: if tab:
self.confirm_tab_close(tab) self.confirm_tab_close(tab)
def confirm(self, msg: str, callback: Callable[..., None], *args: Any, def confirm(
window: Optional[Window] = None, confirm_on_cancel: bool = False) -> None: self, msg: str, # can contain newlines and ANSI formatting
callback: Callable[..., None], # called with True or False and *args
*args: Any, # passed to the callback function
window: Optional[Window] = None, # the window associated with the confirmation
confirm_on_cancel: bool = False, # on closing window
confirm_on_accept: bool = True, # on pressing enter
) -> None:
def callback_(res: Dict[str, Any], x: int, boss: Boss) -> None: def callback_(res: Dict[str, Any], x: int, boss: Boss) -> None:
callback(res.get('response') == 'y', *args) callback(res.get('response') == 'y', *args)
self._run_kitten('ask', ['--type=yesno', '--message', msg], self._run_kitten('ask', ['--type=yesno', '--message', msg, '--default', 'y' if confirm_on_accept else 'n'],
window=window, custom_callback=callback_, default_data={'response': 'y' if confirm_on_cancel else 'n'}) window=window, custom_callback=callback_, default_data={'response': 'y' if confirm_on_cancel else 'n'})
def choose(
self, msg: str, # can contain newlines and ANSI formatting
callback: Callable[..., None], # called with the choice or empty string when aborted
*choices: str, # The choices, see the help for the ask kitten for format of a choice
window: Optional[Window] = None, # the window associated with the confirmation
default: str = '', # the default choice when the user presses Enter
) -> None:
def callback_(res: Dict[str, Any], x: int, boss: Boss) -> None:
callback(res.get('response') or '')
cmd = ['--type=choices', '--message', msg]
if default:
cmd += ['-d', default]
for c in choices:
cmd += ['-c', c]
self._run_kitten('ask', cmd, window=window, custom_callback=callback_, default_data={'response': ''})
def confirm_tab_close(self, tab: Tab) -> None: def confirm_tab_close(self, tab: Tab) -> None:
x = get_options().confirm_os_window_close x = get_options().confirm_os_window_close
num = tab.number_of_windows_with_running_programs if x < 0 else len(tab) num = tab.number_of_windows_with_running_programs if x < 0 else len(tab)
@@ -721,6 +738,10 @@ class Boss:
def toggle_maximized(self, os_window_id: int = 0) -> None: def toggle_maximized(self, os_window_id: int = 0) -> None:
toggle_maximized(os_window_id) toggle_maximized(os_window_id)
@ac('misc', 'Toggle macOS secure keyboard entry')
def toggle_macos_secure_keyboard_entry(self) -> None:
toggle_secure_input()
def start(self, first_os_window_id: int) -> None: def start(self, first_os_window_id: int) -> None:
if not getattr(self, 'io_thread_started', False): if not getattr(self, 'io_thread_started', False):
self.child_monitor.start() self.child_monitor.start()
@@ -1414,12 +1435,14 @@ class Boss:
def set_tab_title(self) -> None: def set_tab_title(self) -> None:
tab = self.active_tab tab = self.active_tab
if tab: if tab:
args = ['--name=tab-title', '--message', _('Enter the new title for this tab below.'), 'do_set_tab_title', str(tab.id)] args = [
'--name=tab-title', '--message', _('Enter the new title for this tab below.'),
'--default', tab.name or tab.title, 'do_set_tab_title', str(tab.id)]
self._run_kitten('ask', args) self._run_kitten('ask', args)
def do_set_tab_title(self, title: str, tab_id: int) -> None: def do_set_tab_title(self, title: str, tab_id: int) -> None:
tm = self.active_tab_manager tm = self.active_tab_manager
if tm is not None and title: if tm is not None:
tab_id = int(tab_id) tab_id = int(tab_id)
for tab in tm.tabs: for tab in tm.tabs:
if tab.id == tab_id: if tab.id == tab_id:
@@ -2072,9 +2095,10 @@ class Boss:
def choose_entry( def choose_entry(
self, title: str, entries: Iterable[Tuple[Union[_T, str, None], str]], self, title: str, entries: Iterable[Tuple[Union[_T, str, None], str]],
callback: Callable[[Union[_T, str, None]], None] callback: Callable[[Union[_T, str, None]], None],
subtitle: str = ''
) -> Optional[Window]: ) -> Optional[Window]:
lines = [title, ''] lines = [title, subtitle, '']
idx_map: List[Union[_T, str]] = [] idx_map: List[Union[_T, str]] = []
ans: Union[str, _T, None] = None ans: Union[str, _T, None] = None
fmt = ': {1}' fmt = ': {1}'
@@ -2110,8 +2134,19 @@ class Boss:
if tab.id == ans: if tab.id == ans:
self.set_active_tab(tab) self.set_active_tab(tab)
def format_tab_title(tab: Tab) -> str:
w = 'windows' if tab.num_window_groups > 1 else 'window'
return f'{tab.title} [{tab.num_window_groups} {w}]'
ct = self.active_tab ct = self.active_tab
self.choose_entry('Choose a tab to switch to', ((t.id, t.title) for t in self.all_tabs if t is not ct), chosen) st = ''
if ct is not None:
st = f'Current tab: {format_tab_title(ct)}'
self.choose_entry(
'Choose a tab to switch to',
((t.id, format_tab_title(t)) for t in self.all_tabs if t is not ct),
chosen, subtitle=st
)
@ac('win', ''' @ac('win', '''
Detach a window, moving it to another tab or OS Window Detach a window, moving it to another tab or OS Window
@@ -2200,19 +2235,31 @@ class Boss:
if path == ":cocoa::application launched::": if path == ":cocoa::application launched::":
self.cocoa_application_launched = True self.cocoa_application_launched = True
return return
from .open_actions import actions_for_launch
def new_os_window() -> None: from .launch import force_window_launch
self.new_os_window(path) actions = list(actions_for_launch(path))
if self.cocoa_application_launched or not self.os_window_map:
return new_os_window()
tab = self.active_tab tab = self.active_tab
if tab is None: if tab is not None:
return new_os_window() w = tab.active_window
w = tab.active_window else:
self.new_window(path) w = None
if w is not None: needs_window_replaced = not self.cocoa_application_launched or not self.os_window_map and w is not None and w.id == 1
tab.remove_window(w)
def clear_initial_window() -> None:
if needs_window_replaced and tab is not None and w is not None:
tab.remove_window(w)
if not actions:
with force_window_launch(needs_window_replaced):
self.launch(kitty_exe(), '+runpy', f'print("The file:", {path!r}, "is of unknown type, cannot open it.");'
'from kitty.utils import hold_till_enter; hold_till_enter(); raise SystemExit(1)')
clear_initial_window()
else:
with force_window_launch(needs_window_replaced):
self.dispatch_action(actions.pop(0))
clear_initial_window()
if actions:
self.drain_actions(actions)
@ac('debug', 'Show the effective configuration kitty is running with') @ac('debug', 'Show the effective configuration kitty is running with')
def debug_config(self) -> None: def debug_config(self) -> None:

View File

@@ -79,8 +79,8 @@ const uint COLOR_MASK = uint(0x4000);
const uint ZERO = uint(0); const uint ZERO = uint(0);
const uint ONE = uint(1); const uint ONE = uint(1);
const uint TWO = uint(2); const uint TWO = uint(2);
const uint THREE = uint(3);
const uint FOUR = uint(4); const uint FOUR = uint(4);
const uint SEVEN = uint(7);
vec3 color_to_vec(uint c) { vec3 color_to_vec(uint c) {
uint r, g, b; uint r, g, b;
@@ -193,7 +193,7 @@ void main() {
foreground = choose_color(float(is_selected & ONE), selection_color, foreground); foreground = choose_color(float(is_selected & ONE), selection_color, foreground);
decoration_fg = choose_color(float(is_selected & ONE), selection_color, decoration_fg); decoration_fg = choose_color(float(is_selected & ONE), selection_color, decoration_fg);
// Underline and strike through (rendered via sprites) // Underline and strike through (rendered via sprites)
underline_pos = choose_color(in_url, to_sprite_pos(pos, url_style, ZERO, ZERO), to_sprite_pos(pos, (text_attrs >> DECORATION_SHIFT) & THREE, ZERO, ZERO)); underline_pos = choose_color(in_url, to_sprite_pos(pos, url_style, ZERO, ZERO), to_sprite_pos(pos, (text_attrs >> DECORATION_SHIFT) & SEVEN, ZERO, ZERO));
strike_pos = to_sprite_pos(pos, ((text_attrs >> STRIKE_SHIFT) & ONE) * FOUR, ZERO, ZERO); strike_pos = to_sprite_pos(pos, ((text_attrs >> STRIKE_SHIFT) & ONE) * FOUR, ZERO, ZERO);
// Cursor // Cursor

View File

@@ -1039,6 +1039,7 @@ process_cocoa_pending_actions(void) {
if (cocoa_pending_actions[CLOSE_WINDOW]) { call_boss(close_window, NULL); } if (cocoa_pending_actions[CLOSE_WINDOW]) { call_boss(close_window, NULL); }
if (cocoa_pending_actions[RESET_TERMINAL]) { call_boss(clear_terminal, "sO", "reset", Py_True ); } if (cocoa_pending_actions[RESET_TERMINAL]) { call_boss(clear_terminal, "sO", "reset", Py_True ); }
if (cocoa_pending_actions[RELOAD_CONFIG]) { call_boss(load_config_file, NULL); } if (cocoa_pending_actions[RELOAD_CONFIG]) { call_boss(load_config_file, NULL); }
if (cocoa_pending_actions[TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY]) { call_boss(toggle_macos_secure_keyboard_entry, NULL); }
if (cocoa_pending_actions_data.wd) { if (cocoa_pending_actions_data.wd) {
if (cocoa_pending_actions[NEW_OS_WINDOW_WITH_WD]) { call_boss(new_os_window_with_wd, "s", cocoa_pending_actions_data.wd); } if (cocoa_pending_actions[NEW_OS_WINDOW_WITH_WD]) { call_boss(new_os_window_with_wd, "s", cocoa_pending_actions_data.wd); }
if (cocoa_pending_actions[NEW_TAB_WITH_WD]) { call_boss(new_tab_with_wd, "s", cocoa_pending_actions_data.wd); } if (cocoa_pending_actions[NEW_TAB_WITH_WD]) { call_boss(new_tab_with_wd, "s", cocoa_pending_actions_data.wd); }

View File

@@ -62,7 +62,7 @@ else:
except Exception: except Exception:
continue continue
try: try:
with open('/proc/' + x + '/stat', 'rb') as f: with open(f'/proc/{x}/stat', 'rb') as f:
raw = f.read().decode('utf-8') raw = f.read().decode('utf-8')
except OSError: except OSError:
continue continue
@@ -268,7 +268,7 @@ class Child:
# https://github.com/kovidgoyal/kitty/issues/1870 # https://github.com/kovidgoyal/kitty/issues/1870
# xterm, urxvt, konsole and gnome-terminal do not do it in my # xterm, urxvt, konsole and gnome-terminal do not do it in my
# testing. # testing.
argv[0] = ('-' + exe.split('/')[-1]) argv[0] = (f'-{exe.split("/")[-1]}')
exe = which(exe) or exe exe = which(exe) or exe
pid = fast_data_types.spawn(exe, self.cwd, tuple(argv), env, master, slave, stdin_read_fd, stdin_write_fd, ready_read_fd, ready_write_fd) pid = fast_data_types.spawn(exe, self.cwd, tuple(argv), env, master, slave, stdin_read_fd, stdin_write_fd, ready_read_fd, ready_write_fd)
os.close(slave) os.close(slave)

View File

@@ -82,7 +82,7 @@ def generate_stub() -> None:
for cmd_name in all_command_names(): for cmd_name in all_command_names():
cmd = command_for_name(cmd_name) cmd = command_for_name(cmd_name)
if cmd.options_spec: if cmd.options_spec:
do(cmd.options_spec, cmd.__class__.__name__ + 'RCOptions') do(cmd.options_spec, f'{cmd.__class__.__name__}RCOptions')
save_type_stub(text, __file__) save_type_stub(text, __file__)

View File

@@ -12,8 +12,8 @@ from contextlib import suppress
from typing import Any from typing import Any
CSI = '\033[' CSI = '\x1b['
OSC = '\033]' OSC = '\x1b]'
def write(x: str) -> None: def write(x: str) -> None:
@@ -42,11 +42,11 @@ def screen_alternate_keypad_mode() -> None:
def screen_cursor_position(y: int, x: int) -> None: def screen_cursor_position(y: int, x: int) -> None:
write(CSI + f'{y};{x}H') write(f'{CSI}{y};{x}H')
def screen_cursor_forward(amt: int) -> None: def screen_cursor_forward(amt: int) -> None:
write(CSI + '%sC' % amt) write(f'{CSI}{amt}C')
def screen_save_cursor() -> None: def screen_save_cursor() -> None:
@@ -58,105 +58,105 @@ def screen_restore_cursor() -> None:
def screen_cursor_back1(amt: int) -> None: def screen_cursor_back1(amt: int) -> None:
write(CSI + '%sD' % amt) write(f'{CSI}{amt}D')
def screen_save_modes() -> None: def screen_save_modes() -> None:
write(CSI + '?s') write(f'{CSI}?s')
def screen_restore_modes() -> None: def screen_restore_modes() -> None:
write(CSI + '?r') write(f'{CSI}?r')
def screen_designate_charset(which: int, to: int) -> None: def screen_designate_charset(which: int, to: int) -> None:
w = '()'[int(which)] w = '()'[int(which)]
t = chr(int(to)) t = chr(int(to))
write('\033' + w + t) write(f'\x1b{w}{t}')
def select_graphic_rendition(*a: int) -> None: def select_graphic_rendition(*a: int) -> None:
write(CSI + '%sm' % ';'.join(map(str, a))) write(f'{CSI}{";".join(map(str, a))}m')
def screen_cursor_to_column(c: int) -> None: def screen_cursor_to_column(c: int) -> None:
write(CSI + '%dG' % c) write(f'{CSI}{c}G')
def screen_cursor_to_line(ln: int) -> None: def screen_cursor_to_line(ln: int) -> None:
write(CSI + '%dd' % ln) write(f'{CSI}{ln}d')
def screen_set_mode(x: int, private: bool) -> None: def screen_set_mode(x: int, private: bool) -> None:
write(CSI + ('?' if private else '') + str(x) + 'h') write(f'{CSI}{"?" if private else ""}{x}h')
def screen_save_mode(x: int, private: bool) -> None: def screen_save_mode(x: int, private: bool) -> None:
write(CSI + ('?' if private else '') + str(x) + 's') write(f'{CSI}{"?" if private else ""}{x}s')
def screen_reset_mode(x: int, private: bool) -> None: def screen_reset_mode(x: int, private: bool) -> None:
write(CSI + ('?' if private else '') + str(x) + 'l') write(f'{CSI}{"?" if private else ""}{x}l')
def screen_restore_mode(x: int, private: bool) -> None: def screen_restore_mode(x: int, private: bool) -> None:
write(CSI + ('?' if private else '') + str(x) + 'r') write(f'{CSI}{"?" if private else ""}{x}r')
def screen_set_margins(t: int, b: int) -> None: def screen_set_margins(t: int, b: int) -> None:
write(CSI + '%d;%dr' % (t, b)) write(f'{CSI}{t};{b}r')
def screen_indexn(n: int) -> None: def screen_indexn(n: int) -> None:
write(CSI + '%dS' % n) write(f'{CSI}{n}S')
def screen_delete_characters(count: int) -> None: def screen_delete_characters(count: int) -> None:
write(CSI + '%dP' % count) write(f'{CSI}{count}P')
def screen_push_colors(which: int) -> None: def screen_push_colors(which: int) -> None:
write(CSI + '%d#P' % which) write(f'{CSI}{which}#P')
def screen_pop_colors(which: int) -> None: def screen_pop_colors(which: int) -> None:
write(CSI + '%d#Q' % which) write(f'{CSI}{which}#Q')
def screen_report_colors() -> None: def screen_report_colors() -> None:
write(CSI + '#R') write(f'{CSI}#R')
def screen_repeat_character(num: int) -> None: def screen_repeat_character(num: int) -> None:
write(CSI + '%db' % num) write(f'{CSI}{num}b')
def screen_insert_characters(count: int) -> None: def screen_insert_characters(count: int) -> None:
write(CSI + '%d@' % count) write(f'{CSI}{count}@')
def screen_scroll(count: int) -> None: def screen_scroll(count: int) -> None:
write(CSI + '%dS' % count) write(f'{CSI}{count}S')
def screen_erase_in_display(how: int, private: bool) -> None: def screen_erase_in_display(how: int, private: bool) -> None:
write(CSI + ('?' if private else '') + str(how) + 'J') write(f'{CSI}{"?" if private else ""}{how}J')
def screen_erase_in_line(how: int, private: bool) -> None: def screen_erase_in_line(how: int, private: bool) -> None:
write(CSI + ('?' if private else '') + str(how) + 'K') write(f'{CSI}{"?" if private else ""}{how}K')
def screen_delete_lines(num: int) -> None: def screen_delete_lines(num: int) -> None:
write(CSI + str(num) + 'M') write(f'{CSI}{num}M')
def screen_cursor_up2(count: int) -> None: def screen_cursor_up2(count: int) -> None:
write(CSI + '%dA' % count) write(f'{CSI}{count}A')
def screen_cursor_down(count: int) -> None: def screen_cursor_down(count: int) -> None:
write(CSI + '%dB' % count) write(f'{CSI}{count}B')
def screen_carriage_return() -> None: def screen_carriage_return() -> None:
@@ -176,11 +176,11 @@ def screen_backspace() -> None:
def screen_set_cursor(mode: int, secondary: int) -> None: def screen_set_cursor(mode: int, secondary: int) -> None:
write(CSI + '%d q' % secondary) write(f'{CSI}{secondary} q')
def screen_insert_lines(num: int) -> None: def screen_insert_lines(num: int) -> None:
write(CSI + '%dL' % num) write(f'{CSI}{num}L')
def draw(*a: str) -> None: def draw(*a: str) -> None:
@@ -188,7 +188,7 @@ def draw(*a: str) -> None:
def screen_manipulate_title_stack(op: int, which: int) -> None: def screen_manipulate_title_stack(op: int, which: int) -> None:
write(CSI + '%d;%dt' % (op, which)) write(f'{CSI}{op};{which}t')
def report_device_attributes(mode: int, char: int) -> None: def report_device_attributes(mode: int, char: int) -> None:
@@ -197,21 +197,22 @@ def report_device_attributes(mode: int, char: int) -> None:
x += chr(char) x += chr(char)
if mode: if mode:
x += str(mode) x += str(mode)
write(CSI + x + 'c') write(f'{CSI}{x}c')
def screen_decsace(mode: int) -> None: def screen_decsace(mode: int) -> None:
write(CSI + str(mode) + '*x') write(f'{CSI}{mode}*x')
def screen_set_8bit_controls(mode: int) -> None: def screen_set_8bit_controls(mode: int) -> None:
write('\x1b ' + ('G' if mode else 'F')) write(f'\x1b {"G" if mode else "F"}')
def write_osc(code: int, string: str = '') -> None: def write_osc(code: int, string: str = '') -> None:
if string: if string:
string = ';' + string write(f'{OSC}{code};{string}\x07')
write(OSC + str(code) + string + '\x07') else:
write(f'{OSC}{code}\x07')
set_dynamic_color = set_color_table_color = process_cwd_notification = write_osc set_dynamic_color = set_color_table_color = process_cwd_notification = write_osc
@@ -235,7 +236,7 @@ def clipboard_control(payload: str) -> None:
clipboard_control_pending += data.lstrip(';') clipboard_control_pending += data.lstrip(';')
payload = clipboard_control_pending payload = clipboard_control_pending
clipboard_control_pending = '' clipboard_control_pending = ''
write(OSC + payload + '\x07') write(f'{OSC}{payload}\x07')
def replay(raw: str) -> None: def replay(raw: str) -> None:

View File

@@ -9,6 +9,7 @@
#include "state.h" #include "state.h"
#include "cleanup.h" #include "cleanup.h"
#include "monotonic.h" #include "monotonic.h"
#include <Carbon/Carbon.h>
#include <Cocoa/Cocoa.h> #include <Cocoa/Cocoa.h>
#ifndef KITTY_USE_DEPRECATED_MACOS_NOTIFICATION_API #ifndef KITTY_USE_DEPRECATED_MACOS_NOTIFICATION_API
#include <UserNotifications/UserNotifications.h> #include <UserNotifications/UserNotifications.h>
@@ -19,6 +20,11 @@
#include <crt_externs.h> #include <crt_externs.h>
#include <objc/runtime.h> #include <objc/runtime.h>
#if (MAC_OS_X_VERSION_MAX_ALLOWED < 101300)
#define NSControlStateValueOn NSOnState
#define NSControlStateValueOff NSOffState
#define NSControlStateValueMixed NSMixedState
#endif
#if (MAC_OS_X_VERSION_MAX_ALLOWED < 101200) #if (MAC_OS_X_VERSION_MAX_ALLOWED < 101200)
#define NSWindowStyleMaskResizable NSResizableWindowMask #define NSWindowStyleMaskResizable NSResizableWindowMask
#define NSEventModifierFlagOption NSAlternateKeyMask #define NSEventModifierFlagOption NSAlternateKeyMask
@@ -75,6 +81,137 @@ find_app_name(void) {
return @"kitty"; return @"kitty";
} }
#define debug_key(...) if (OPT(debug_keyboard)) { fprintf(stderr, __VA_ARGS__); fflush(stderr); }
// SecureKeyboardEntryController {{{
@interface SecureKeyboardEntryController : NSObject
@property (nonatomic, readonly) BOOL isDesired;
@property (nonatomic, readonly, getter=isEnabled) BOOL enabled;
+ (instancetype)sharedInstance;
- (void)toggle;
- (void)update;
@end
@implementation SecureKeyboardEntryController {
int _count;
BOOL _desired;
}
+ (instancetype)sharedInstance {
static id instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (instancetype)init {
self = [super init];
if (self) {
_desired = false;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidResignActive:)
name:NSApplicationDidResignActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidBecomeActive:)
name:NSApplicationDidBecomeActiveNotification
object:nil];
if ([NSApp isActive]) {
[self update];
}
}
return self;
}
#pragma mark - API
- (void)toggle {
// Set _desired to the opposite of the current state.
_desired = !_desired;
debug_key("SecureKeyboardEntry: toggle called. Setting desired to %d ", _desired);
// Try to set the system's state of secure input to the desired state.
[self update];
}
- (BOOL)isEnabled {
return !!IsSecureEventInputEnabled();
}
- (BOOL)isDesired {
return _desired;
}
#pragma mark - Notifications
- (void)applicationDidResignActive:(NSNotification *)notification {
(void)notification;
if (_count > 0) {
debug_key("SecureKeyboardEntry: Application resigning active.");
[self update];
}
}
- (void)applicationDidBecomeActive:(NSNotification *)notification {
(void)notification;
if (self.isDesired) {
debug_key("SecureKeyboardEntry: Application became active.");
[self update];
}
}
#pragma mark - Private
- (BOOL)allowed {
return [NSApp isActive];
}
- (void)update {
debug_key("Update secure keyboard entry. desired=%d active=%d\n",
(int)self.isDesired, (int)[NSApp isActive]);
const BOOL secure = self.isDesired && [self allowed];
if (secure && _count > 0) {
debug_key("Want to turn on secure input but it's already on\n");
return;
}
if (!secure && _count == 0) {
debug_key("Want to turn off secure input but it's already off\n");
return;
}
debug_key("Before: IsSecureEventInputEnabled returns %d ", (int)self.isEnabled);
if (secure) {
OSErr err = EnableSecureEventInput();
debug_key("EnableSecureEventInput err=%d ", (int)err);
if (err) {
debug_key("EnableSecureEventInput failed with error %d ", (int)err);
} else {
_count += 1;
}
} else {
OSErr err = DisableSecureEventInput();
debug_key("DisableSecureEventInput err=%d ", (int)err);
if (err) {
debug_key("DisableSecureEventInput failed with error %d ", (int)err);
} else {
_count -= 1;
}
}
debug_key("After: IsSecureEventInputEnabled returns %d\n", (int)self.isEnabled);
}
@end
// }}}
@interface GlobalMenuTarget : NSObject @interface GlobalMenuTarget : NSObject
+ (GlobalMenuTarget *) shared_instance; + (GlobalMenuTarget *) shared_instance;
@end @end
@@ -95,12 +232,20 @@ PENDING(new_window, NEW_WINDOW)
PENDING(close_window, CLOSE_WINDOW) PENDING(close_window, CLOSE_WINDOW)
PENDING(reset_terminal, RESET_TERMINAL) PENDING(reset_terminal, RESET_TERMINAL)
PENDING(reload_config, RELOAD_CONFIG) PENDING(reload_config, RELOAD_CONFIG)
PENDING(toggle_macos_secure_keyboard_entry, TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY)
- (void)open_kitty_website_url:(id)sender { - (void)open_kitty_website_url:(id)sender {
(void)sender; (void)sender;
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://sw.kovidgoyal.net/kitty/"]]; [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://sw.kovidgoyal.net/kitty/"]];
} }
- (BOOL)validateMenuItem:(NSMenuItem *)item {
if (item.action == @selector(toggle_macos_secure_keyboard_entry:)) {
item.state = [SecureKeyboardEntryController sharedInstance].isDesired ? NSControlStateValueOn : NSControlStateValueOff;
}
return YES;
}
#undef PENDING #undef PENDING
+ (GlobalMenuTarget *) shared_instance + (GlobalMenuTarget *) shared_instance
@@ -108,8 +253,11 @@ PENDING(reload_config, RELOAD_CONFIG)
static GlobalMenuTarget *sharedGlobalMenuTarget = nil; static GlobalMenuTarget *sharedGlobalMenuTarget = nil;
@synchronized(self) @synchronized(self)
{ {
if (!sharedGlobalMenuTarget) if (!sharedGlobalMenuTarget) {
sharedGlobalMenuTarget = [[GlobalMenuTarget alloc] init]; sharedGlobalMenuTarget = [[GlobalMenuTarget alloc] init];
SecureKeyboardEntryController *k = [SecureKeyboardEntryController sharedInstance];
if (!k.isDesired && [[NSUserDefaults standardUserDefaults] boolForKey:@"SecureKeyboardEntry"]) [k toggle];
}
return sharedGlobalMenuTarget; return sharedGlobalMenuTarget;
} }
} }
@@ -123,6 +271,7 @@ typedef struct {
typedef struct { typedef struct {
GlobalShortcut new_os_window, close_os_window, close_tab, edit_config_file, reload_config; GlobalShortcut new_os_window, close_os_window, close_tab, edit_config_file, reload_config;
GlobalShortcut previous_tab, next_tab, new_tab, new_window, close_window, reset_terminal; GlobalShortcut previous_tab, next_tab, new_tab, new_window, close_window, reset_terminal;
GlobalShortcut toggle_macos_secure_keyboard_entry;
} GlobalShortcuts; } GlobalShortcuts;
static GlobalShortcuts global_shortcuts; static GlobalShortcuts global_shortcuts;
@@ -137,6 +286,7 @@ cocoa_set_global_shortcut(PyObject *self UNUSED, PyObject *args) {
Q(new_os_window); else Q(close_os_window); else Q(close_tab); else Q(edit_config_file); Q(new_os_window); else Q(close_os_window); else Q(close_tab); else Q(edit_config_file);
else Q(new_tab); else Q(next_tab); else Q(previous_tab); else Q(new_tab); else Q(next_tab); else Q(previous_tab);
else Q(new_window); else Q(close_window); else Q(reset_terminal); else Q(reload_config); else Q(new_window); else Q(close_window); else Q(reset_terminal); else Q(reload_config);
else Q(toggle_macos_secure_keyboard_entry);
#undef Q #undef Q
if (gs == NULL) { PyErr_SetString(PyExc_KeyError, "Unknown shortcut name"); return NULL; } if (gs == NULL) { PyErr_SetString(PyExc_KeyError, "Unknown shortcut name"); return NULL; }
int cocoa_mods; int cocoa_mods;
@@ -229,6 +379,7 @@ cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
- (void)userNotificationCenter:(UNUserNotificationCenter *)center - (void)userNotificationCenter:(UNUserNotificationCenter *)center
willPresentNotification:(UNNotification *)notification willPresentNotification:(UNNotification *)notification
withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler { withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
(void)(center); (void)notification;
UNNotificationPresentationOptions options = UNNotificationPresentationOptionSound; UNNotificationPresentationOptions options = UNNotificationPresentationOptionSound;
if (@available(macOS 11.0, *)) options |= UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner; if (@available(macOS 11.0, *)) options |= UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner;
else options |= (1 << 2); // UNNotificationPresentationOptionAlert avoid deprecated warning else options |= (1 << 2); // UNNotificationPresentationOptionAlert avoid deprecated warning
@@ -419,10 +570,7 @@ cocoa_create_global_menu(void) {
keyEquivalent:@""]; keyEquivalent:@""];
[appMenu addItem:[NSMenuItem separatorItem]]; [appMenu addItem:[NSMenuItem separatorItem]];
[[appMenu addItemWithTitle:@"Secure Keyboard Entry" MENU_ITEM(appMenu, @"Secure Keyboard Entry", toggle_macos_secure_keyboard_entry);
action:@selector(toggleSecureInput:)
keyEquivalent:@"s"]
setKeyEquivalentModifierMask:NSEventModifierFlagOption | NSEventModifierFlagCommand];
[appMenu addItem:[NSMenuItem separatorItem]]; [appMenu addItem:[NSMenuItem separatorItem]];
[appMenu addItemWithTitle:[NSString stringWithFormat:@"Quit %@", app_name] [appMenu addItemWithTitle:[NSString stringWithFormat:@"Quit %@", app_name]
@@ -547,6 +695,13 @@ cocoa_alt_option_key_pressed(NSUInteger flags) {
return (q & flags) == q; return (q & flags) == q;
} }
void
cocoa_toggle_secure_keyboard_entry(void) {
SecureKeyboardEntryController *k = [SecureKeyboardEntryController sharedInstance];
[k toggle];
[[NSUserDefaults standardUserDefaults] setBool:k.isDesired forKey:@"SecureKeyboardEntry"];
}
void void
cocoa_focus_window(void *w) { cocoa_focus_window(void *w) {
NSWindow *window = (NSWindow*)w; NSWindow *window = (NSWindow*)w;
@@ -711,10 +866,17 @@ static PyMethodDef module_methods[] = {
{NULL, NULL, 0, NULL} /* Sentinel */ {NULL, NULL, 0, NULL} /* Sentinel */
}; };
static void
uncaughtExceptionHandler(NSException *exception) {
log_error("Unhandled exception in Cocoa: %s", [[exception description] UTF8String]);
log_error("Stack trace:\n%s", [[exception.callStackSymbols description] UTF8String]);
}
bool bool
init_cocoa(PyObject *module) { init_cocoa(PyObject *module) {
memset(&global_shortcuts, 0, sizeof(global_shortcuts)); memset(&global_shortcuts, 0, sizeof(global_shortcuts));
if (PyModule_AddFunctions(module, module_methods) != 0) return false; if (PyModule_AddFunctions(module, module_methods) != 0) return false;
register_at_exit_cleanup_func(COCOA_CLEANUP_FUNC, cleanup); register_at_exit_cleanup_func(COCOA_CLEANUP_FUNC, cleanup);
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
return true; return true;
} }

View File

@@ -270,7 +270,7 @@ def zsh_output_serializer(ans: Completions) -> str:
yield ans yield ans
for description, matches in ans.match_groups.items(): for description, matches in ans.match_groups.items():
cmd = ['compadd', '-U', '-J', shlex.quote(description), '-X', shlex.quote('%B' + description + '%b')] cmd = ['compadd', '-U', '-J', shlex.quote(description), '-X', shlex.quote(f'%B{description}%b')]
if not matches.trailing_space: if not matches.trailing_space:
cmd += ['-S', '""'] cmd += ['-S', '""']
if matches.is_files: if matches.is_files:
@@ -350,10 +350,10 @@ def fish2_output_serializer(ans: Completions) -> str:
def completions_for_first_word(ans: Completions, prefix: str, entry_points: Iterable[str], namespaced_entry_points: Iterable[str]) -> None: def completions_for_first_word(ans: Completions, prefix: str, entry_points: Iterable[str], namespaced_entry_points: Iterable[str]) -> None:
cmds = ['@' + c for c in remote_control_command_names()] cmds = [f'@{c}' for c in remote_control_command_names()]
ans.add_match_group('Entry points', { ans.add_match_group('Entry points', {
k: '' for k in k: '' for k in
list(entry_points) + cmds + ['+' + k for k in namespaced_entry_points] list(entry_points) + cmds + [f'+{k}' for k in namespaced_entry_points]
if not prefix or k.startswith(prefix) if not prefix or k.startswith(prefix)
}) })
if prefix: if prefix:
@@ -443,7 +443,7 @@ def complete_alias_map(
long_opt = option_map.get(parts[0]) long_opt = option_map.get(parts[0])
if long_opt is not None and complete_args is not None: if long_opt is not None and complete_args is not None:
complete_args(ans, long_opt, parts[1], Delegate()) complete_args(ans, long_opt, parts[1], Delegate())
ans.add_prefix(parts[0] + '=') ans.add_prefix(f'{parts[0]}=')
return return
opt = option_map.get(w) opt = option_map.get(w)
if w is last_word and not new_word: if w is last_word and not new_word:
@@ -682,7 +682,7 @@ def find_completions(words: Sequence[str], new_word: bool, entry_points: Iterabl
if words[0].startswith('@'): if words[0].startswith('@'):
if len(words) == 1 and not new_word: if len(words) == 1 and not new_word:
prefix = words[0] prefix = words[0]
ans.add_match_group('Remote control commands', {'@' + c: '' for c in remote_control_command_names() if c.startswith(prefix)}) ans.add_match_group('Remote control commands', {f'@{c}': '' for c in remote_control_command_names() if c.startswith(prefix)})
else: else:
complete_remote_command(ans, words[0][1:], words[1:], new_word) complete_remote_command(ans, words[0][1:], words[1:], new_word)
if words[0] == '+': if words[0] == '+':

View File

@@ -39,17 +39,16 @@ def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
imports: Set[Tuple[str, str]] = set() imports: Set[Tuple[str, str]] = set()
tc_imports: Set[Tuple[str, str]] = set() tc_imports: Set[Tuple[str, str]] = set()
def type_name(x: type) -> str:
ans = x.__name__
if x.__module__ and x.__module__ != 'builtins':
imports.add((x.__module__, x.__name__))
return ans
def option_type_as_str(x: Any) -> str: def option_type_as_str(x: Any) -> str:
if hasattr(x, '__name__'): needs_import = False
return type_name(x) if type(x) is type:
ans = repr(x) ans = x.__name__
ans = ans.replace('NoneType', 'None') needs_import = True
else:
ans = repr(x)
ans = ans.replace('NoneType', 'None')
if needs_import and getattr(x, '__module__', None) and x.__module__ not in ('builtins', 'typing'):
imports.add((x.__module__, x.__name__))
return ans return ans
def option_type_data(option: Union[Option, MultiOption]) -> Tuple[Callable[[Any], Any], str]: def option_type_data(option: Union[Option, MultiOption]) -> Tuple[Callable[[Any], Any], str]:
@@ -418,9 +417,9 @@ def generate_c_conversion(loc: str, ctypes: List[Option]) -> str:
def write_output(loc: str, defn: Definition) -> None: def write_output(loc: str, defn: Definition) -> None:
cls, tc = generate_class(defn, loc) cls, tc = generate_class(defn, loc)
with open(os.path.join(*loc.split('.'), 'options', 'types.py'), 'w') as f: with open(os.path.join(*loc.split('.'), 'options', 'types.py'), 'w') as f:
f.write(cls + '\n') f.write(f'{cls}\n')
with open(os.path.join(*loc.split('.'), 'options', 'parse.py'), 'w') as f: with open(os.path.join(*loc.split('.'), 'options', 'parse.py'), 'w') as f:
f.write(tc + '\n') f.write(f'{tc}\n')
ctypes = [] ctypes = []
for opt in defn.root_group.iter_all_non_groups(): for opt in defn.root_group.iter_all_non_groups():
if isinstance(opt, Option) and opt.ctype: if isinstance(opt, Option) and opt.ctype:
@@ -428,7 +427,7 @@ def write_output(loc: str, defn: Definition) -> None:
if ctypes: if ctypes:
c = generate_c_conversion(loc, ctypes) c = generate_c_conversion(loc, ctypes)
with open(os.path.join(*loc.split('.'), 'options', 'to-c-generated.h'), 'w') as f: with open(os.path.join(*loc.split('.'), 'options', 'to-c-generated.h'), 'w') as f:
f.write(c + '\n') f.write(f'{c}\n')
def main() -> None: def main() -> None:
@@ -454,6 +453,6 @@ def main() -> None:
loc = package_name loc = package_name
cls, tc = generate_class(defn, loc) cls, tc = generate_class(defn, loc)
with open(os.path.join(os.path.dirname(path), 'kitten_options_types.py'), 'w') as f: with open(os.path.join(os.path.dirname(path), 'kitten_options_types.py'), 'w') as f:
f.write(cls + '\n') f.write(f'{cls}\n')
with open(os.path.join(os.path.dirname(path), 'kitten_options_parse.py'), 'w') as f: with open(os.path.join(os.path.dirname(path), 'kitten_options_parse.py'), 'w') as f:
f.write(tc + '\n') f.write(f'{tc}\n')

View File

@@ -37,8 +37,8 @@ def expand_opt_references(conf_name: str, text: str) -> str:
def expand(m: 'Match[str]') -> str: def expand(m: 'Match[str]') -> str:
ref = m.group(1) ref = m.group(1)
if '<' not in ref and '.' not in ref: if '<' not in ref and '.' not in ref:
full_ref = conf_name + ref # full ref
return f':opt:`{ref} <{full_ref}>`' return f':opt:`{ref} <{conf_name}{ref}>`'
return str(m.group()) return str(m.group())
return re.sub(r':opt:`(.+?)`', expand, text) return re.sub(r':opt:`(.+?)`', expand, text)
@@ -214,7 +214,7 @@ class Option:
if not self.documented: if not self.documented:
return ans return ans
mopts = [self] + option_group mopts = [self] + option_group
a('.. opt:: ' + ', '.join(conf_name + '.' + mo.name for mo in mopts)) a('.. opt:: ' + ', '.join(f'{conf_name}.{mo.name}' for mo in mopts))
a('.. code-block:: conf') a('.. code-block:: conf')
a('') a('')
sz = max(len(x.name) for x in mopts) sz = max(len(x.name) for x in mopts)
@@ -330,7 +330,7 @@ class Mapping:
raise ValueError(f'The shortcut for {self.name} has no short_text') raise ValueError(f'The shortcut for {self.name} has no short_text')
sc_text = f'{conf_name}.{self.short_text}' sc_text = f'{conf_name}.{self.short_text}'
shortcut_slugs[f'{conf_name}.{self.name}'] = (sc_text, self.key_text.replace('kitty_mod', kitty_mod)) shortcut_slugs[f'{conf_name}.{self.name}'] = (sc_text, self.key_text.replace('kitty_mod', kitty_mod))
a('.. shortcut:: ' + sc_text) a(f'.. shortcut:: {sc_text}')
block_started = False block_started = False
for sc in [self] + action_group: for sc in [self] + action_group:
if sc.add_to_default and sc.documented: if sc.add_to_default and sc.documented:
@@ -534,7 +534,7 @@ class Group:
ans[i] = ' '.join(parts) ans[i] = ' '.join(parts)
if commented: if commented:
ans = [x if x.startswith('#') or not x.strip() else ('# ' + x) for x in ans] ans = [x if x.startswith('#') or not x.strip() else (f'# {x}') for x in ans]
return ans return ans

View File

@@ -72,9 +72,10 @@ class ToCmdline:
def __exit__(self, *a: Any) -> None: def __exit__(self, *a: Any) -> None:
self.override_env = None self.override_env = None
def filter_env_vars(self, *a: str) -> 'ToCmdline': def filter_env_vars(self, *a: str, **override: str) -> 'ToCmdline':
remove = frozenset(a) remove = frozenset(a)
self.override_env = {k: v for k, v in os.environ.items() if k not in remove} self.override_env = {k: v for k, v in os.environ.items() if k not in remove}
self.override_env.update(override)
return self return self
def __call__(self, x: str) -> List[str]: def __call__(self, x: str) -> List[str]:
@@ -169,24 +170,30 @@ def parse_line(
log_error(f'Ignoring invalid config line: {line}') log_error(f'Ignoring invalid config line: {line}')
return return
key, val = m.groups() key, val = m.groups()
if key == 'include': if key in ('include', 'globinclude'):
val = os.path.expandvars(os.path.expanduser(val.strip())) val = os.path.expandvars(os.path.expanduser(val.strip()))
if not os.path.isabs(val): if key == 'globinclude':
val = os.path.join(base_path_for_includes, val) from pathlib import Path
try: vals = tuple(map(lambda x: str(os.fspath(x)), Path(base_path_for_includes).glob(val)))
with open(val, encoding='utf-8', errors='replace') as include: else:
with currently_parsing.set_file(val): if not os.path.isabs(val):
_parse(include, parse_conf_item, ans, accumulate_bad_lines) val = os.path.join(base_path_for_includes, val)
except FileNotFoundError: vals = (val,)
log_error( for val in vals:
'Could not find included config file: {}, ignoring'. try:
format(val) with open(val, encoding='utf-8', errors='replace') as include:
) with currently_parsing.set_file(val):
except OSError: _parse(include, parse_conf_item, ans, accumulate_bad_lines)
log_error( except FileNotFoundError:
'Could not read from included config file: {}, ignoring'. log_error(
format(val) 'Could not find included config file: {}, ignoring'.
) format(val)
)
except OSError:
log_error(
'Could not read from included config file: {}, ignoring'.
format(val)
)
return return
if not parse_conf_item(key, val, ans): if not parse_conf_item(key, val, ans):
log_error(f'Ignoring unknown config key: {key}') log_error(f'Ignoring unknown config key: {key}')
@@ -316,8 +323,7 @@ def parse_kittens_func_args(action: str, args_funcs: Dict[str, KeyFunc[Tuple[str
parser = args_funcs[func] parser = args_funcs[func]
except KeyError as e: except KeyError as e:
raise KeyError( raise KeyError(
'Unknown action: {}. Check if map action: ' f'Unknown action: {func}. Check if map action: {action} is valid'
'{} is valid'.format(func, action)
) from e ) from e
try: try:

View File

@@ -49,13 +49,12 @@ def atomic_save(data: bytes, path: str) -> None:
except FileNotFoundError: except FileNotFoundError:
pass pass
except Exception as err: except Exception as err:
log_error('Failed to delete temp file {} for atomic save with error: {}'.format( log_error(f'Failed to delete temp file {p} for atomic save with error: {err}')
p, err))
@contextmanager @contextmanager
def cached_values_for(name: str) -> Generator[Dict[str, Any], None, None]: def cached_values_for(name: str) -> Generator[Dict[str, Any], None, None]:
cached_path = os.path.join(cache_dir(), name + '.json') cached_path = os.path.join(cache_dir(), f'{name}.json')
cached_values: Dict[str, Any] = {} cached_values: Dict[str, Any] = {}
try: try:
with open(cached_path, 'rb') as f: with open(cached_path, 'rb') as f:
@@ -63,8 +62,7 @@ def cached_values_for(name: str) -> Generator[Dict[str, Any], None, None]:
except FileNotFoundError: except FileNotFoundError:
pass pass
except Exception as err: except Exception as err:
log_error('Failed to load cached in {} values with error: {}'.format( log_error(f'Failed to load cached in {name} values with error: {err}')
name, err))
yield cached_values yield cached_values
@@ -72,8 +70,7 @@ def cached_values_for(name: str) -> Generator[Dict[str, Any], None, None]:
data = json.dumps(cached_values).encode('utf-8') data = json.dumps(cached_values).encode('utf-8')
atomic_save(data, cached_path) atomic_save(data, cached_path)
except Exception as err: except Exception as err:
log_error('Failed to save cached values with error: {}'.format( log_error(f'Failed to save cached values with error: {err}')
err))
def commented_out_default_config() -> str: def commented_out_default_config() -> str:

View File

@@ -22,7 +22,7 @@ class Version(NamedTuple):
appname: str = 'kitty' appname: str = 'kitty'
kitty_face = '🐱' kitty_face = '🐱'
version: Version = Version(0, 24, 1) version: Version = Version(0, 24, 2)
str_version: str = '.'.join(map(str, version)) str_version: str = '.'.join(map(str, version))
_plat = sys.platform.lower() _plat = sys.platform.lower()
is_macos: bool = 'darwin' in _plat is_macos: bool = 'darwin' in _plat

View File

@@ -90,7 +90,7 @@ START_ALLOW_CASE_RANGE
case 3: case 3:
self->italic = true; break; self->italic = true; break;
case 4: case 4:
if (i < count) { self->decoration = MIN(3, params[i]); i++; } if (i < count) { self->decoration = MIN(5, params[i]); i++; }
else self->decoration = 1; else self->decoration = 1;
break; break;
case 7: case 7:
@@ -161,7 +161,7 @@ apply_sgr_to_cells(GPUCell *first_cell, unsigned int cell_count, int *params, un
S(italic, true); S(italic, true);
case 4: { case 4: {
uint8_t val = 1; uint8_t val = 1;
if (i < count) { val = MIN(3, params[i]); i++; } if (i < count) { val = MIN(5, params[i]); i++; }
S(decoration, val); S(decoration, val);
} }
case 7: case 7:

View File

@@ -142,7 +142,7 @@ typedef struct {
typedef union CellAttrs { typedef union CellAttrs {
struct { struct {
uint16_t width : 2; uint16_t width : 2;
uint16_t decoration : 2; uint16_t decoration : 3;
uint16_t bold : 1; uint16_t bold : 1;
uint16_t italic : 1; uint16_t italic : 1;
uint16_t reverse : 1; uint16_t reverse : 1;

View File

@@ -10,7 +10,7 @@ import time
from contextlib import suppress from contextlib import suppress
from functools import partial from functools import partial
from pprint import pformat from pprint import pformat
from typing import IO, Callable, Dict, Iterable, Iterator, Optional, Set, Tuple from typing import IO, Callable, Dict, Iterator, Optional, Set, TypeVar
from kittens.tui.operations import colored, styled from kittens.tui.operations import colored, styled
@@ -20,12 +20,13 @@ from .constants import (
) )
from .fast_data_types import Color, num_users from .fast_data_types import Color, num_users
from .options.types import Options as KittyOpts, defaults from .options.types import Options as KittyOpts, defaults
from .options.utils import MouseMap from .options.utils import SequenceMap
from .rgb import color_as_sharp from .rgb import color_as_sharp
from .types import MouseEvent, SingleKey from .types import MouseEvent, Shortcut
from .typing import SequenceMap
ShortcutMap = Dict[Tuple[SingleKey, ...], str] AnyEvent = TypeVar('AnyEvent', MouseEvent, Shortcut)
Print = Callable[..., None]
ShortcutMap = Dict[Shortcut, str]
def green(x: str) -> str: def green(x: str) -> str:
@@ -40,81 +41,41 @@ def title(x: str) -> str:
return colored(x, 'blue', intense=True) return colored(x, 'blue', intense=True)
def mod_to_names(mods: int) -> Iterator[str]: def print_event(ev: AnyEvent, defn: str, print: Print) -> None:
from .fast_data_types import ( print(f'\t{ev.human_repr}{defn}')
GLFW_MOD_ALT, GLFW_MOD_CAPS_LOCK, GLFW_MOD_CONTROL, GLFW_MOD_HYPER,
GLFW_MOD_META, GLFW_MOD_NUM_LOCK, GLFW_MOD_SHIFT, GLFW_MOD_SUPER
)
modmap = {'shift': GLFW_MOD_SHIFT, 'alt': GLFW_MOD_ALT, 'ctrl': GLFW_MOD_CONTROL, ('cmd' if is_macos else 'super'): GLFW_MOD_SUPER,
'hyper': GLFW_MOD_HYPER, 'meta': GLFW_MOD_META, 'num_lock': GLFW_MOD_NUM_LOCK, 'caps_lock': GLFW_MOD_CAPS_LOCK}
for name, val in modmap.items():
if mods & val:
yield name
def print_shortcut(key_sequence: Iterable[SingleKey], defn: str, print: Callable[..., None]) -> None: def print_mapping_changes(defns: Dict[AnyEvent, str], changes: Set[AnyEvent], text: str, print: Print) -> None:
from .fast_data_types import glfw_get_key_name
keys = []
for key_spec in key_sequence:
names = []
mods, is_native, key = key_spec
names = list(mod_to_names(mods))
if key:
kname = glfw_get_key_name(0, key) if is_native else glfw_get_key_name(key, 0)
names.append(kname or f'{key}')
keys.append('+'.join(names))
print('\t' + ' > '.join(keys), defn)
def print_shortcut_changes(defns: ShortcutMap, text: str, changes: Set[Tuple[SingleKey, ...]], print: Callable[..., None]) -> None:
if changes: if changes:
print(title(text)) print(title(text))
for k in sorted(changes): for k in sorted(changes):
print_shortcut(k, defns[k], print) print_event(k, defns[k], print)
def compare_keymaps(final: ShortcutMap, initial: ShortcutMap, print: Callable[..., None]) -> None: def compare_maps(final: Dict[AnyEvent, str], initial: Dict[AnyEvent, str], print: Print) -> None:
is_mouse = False
for k in initial:
if isinstance(k, MouseEvent):
is_mouse = True
break
added = set(final) - set(initial) added = set(final) - set(initial)
removed = set(initial) - set(final) removed = set(initial) - set(final)
changed = {k for k in set(final) & set(initial) if final[k] != initial[k]} changed = {k for k in set(final) & set(initial) if final[k] != initial[k]}
print_shortcut_changes(final, 'Added shortcuts:', added, print) which = 'mouse actions' if is_mouse else 'shortcuts'
print_shortcut_changes(initial, 'Removed shortcuts:', removed, print) print_mapping_changes(final, added, f'Added {which}:', print)
print_shortcut_changes(final, 'Changed shortcuts:', changed, print) print_mapping_changes(initial, removed, f'Removed {which}:', print)
print_mapping_changes(final, changed, f'Changed {which}:', print)
def flatten_sequence_map(m: SequenceMap) -> ShortcutMap: def flatten_sequence_map(m: SequenceMap) -> ShortcutMap:
ans = {} ans = {}
for key_spec, rest_map in m.items(): for key_spec, rest_map in m.items():
for r, action in rest_map.items(): for r, action in rest_map.items():
ans[(key_spec,) + (r)] = action ans[Shortcut((key_spec,) + (r))] = action
return ans return ans
def compare_mousemaps(final: MouseMap, initial: MouseMap, print: Callable[..., None]) -> None: def compare_opts(opts: KittyOpts, print: Print) -> None:
added = set(final) - set(initial)
removed = set(initial) - set(final)
changed = {k for k in set(final) & set(initial) if final[k] != initial[k]}
def print_mouse_action(trigger: MouseEvent, defn: str) -> None:
names = list(mod_to_names(trigger.mods)) + [f'b{trigger.button+1}']
when = {-1: 'repeat', 1: 'press', 2: 'doublepress', 3: 'triplepress'}.get(trigger.repeat_count, trigger.repeat_count)
grabbed = 'grabbed' if trigger.grabbed else 'ungrabbed'
print('\t', '+'.join(names), when, grabbed, defn)
def print_changes(defns: MouseMap, changes: Set[MouseEvent], text: str) -> None:
if changes:
print(title(text))
for k in sorted(changes):
print_mouse_action(k, defns[k])
print_changes(final, added, 'Added mouse actions:')
print_changes(initial, removed, 'Removed mouse actions:')
print_changes(final, changed, 'Changed mouse actions:')
def compare_opts(opts: KittyOpts, print: Callable[..., None]) -> None:
from .config import load_config from .config import load_config
print() print()
print('Config options different from defaults:') print('Config options different from defaults:')
@@ -130,7 +91,7 @@ def compare_opts(opts: KittyOpts, print: Callable[..., None]) -> None:
for f in changed_opts: for f in changed_opts:
val = getattr(opts, f) val = getattr(opts, f)
if isinstance(val, dict): if isinstance(val, dict):
print(f'{title(f)}:') print(title(f'{f}:'))
if f == 'symbol_map': if f == 'symbol_map':
for k in sorted(val): for k in sorted(val):
print(f'\tU+{k[0]:04x} - U+{k[1]:04x}{val[k]}') print(f'\tU+{k[0]:04x} - U+{k[1]:04x}{val[k]}')
@@ -143,14 +104,14 @@ def compare_opts(opts: KittyOpts, print: Callable[..., None]) -> None:
else: else:
print(fmt.format(f), str(getattr(opts, f))) print(fmt.format(f), str(getattr(opts, f)))
compare_mousemaps(opts.mousemap, default_opts.mousemap, print) compare_maps(opts.mousemap, default_opts.mousemap, print)
final_, initial_ = opts.keymap, default_opts.keymap final_, initial_ = opts.keymap, default_opts.keymap
final: ShortcutMap = {(k,): v for k, v in final_.items()} final: ShortcutMap = {Shortcut((k,)): v for k, v in final_.items()}
initial: ShortcutMap = {(k,): v for k, v in initial_.items()} initial: ShortcutMap = {Shortcut((k,)): v for k, v in initial_.items()}
final_s, initial_s = map(flatten_sequence_map, (opts.sequence_map, default_opts.sequence_map)) final_s, initial_s = map(flatten_sequence_map, (opts.sequence_map, default_opts.sequence_map))
final.update(final_s) final.update(final_s)
initial.update(initial_s) initial.update(initial_s)
compare_keymaps(final, initial, print) compare_maps(final, initial, print)
if colors: if colors:
print(f'{title("Colors")}:', end='\n\t') print(f'{title("Colors")}:', end='\n\t')
print('\n\t'.join(sorted(colors))) print('\n\t'.join(sorted(colors)))
@@ -239,7 +200,7 @@ def debug_config(opts: KittyOpts) -> str:
with open('/etc/lsb-release', encoding='utf-8', errors='replace') as f: with open('/etc/lsb-release', encoding='utf-8', errors='replace') as f:
p(f.read().strip()) p(f.read().strip())
if not is_macos: if not is_macos:
p('Running under:' + green('Wayland' if is_wayland() else 'X11')) p('Running under:', green('Wayland' if is_wayland() else 'X11'))
p(green('Frozen:'), 'True' if getattr(sys, 'frozen', False) else 'False') p(green('Frozen:'), 'True' if getattr(sys, 'frozen', False) else 'False')
p(green('Paths:')) p(green('Paths:'))
p(yellow(' kitty:'), os.path.realpath(kitty_exe())) p(yellow(' kitty:'), os.path.realpath(kitty_exe()))
@@ -253,4 +214,7 @@ def debug_config(opts: KittyOpts) -> str:
p(green('Loaded config overrides:')) p(green('Loaded config overrides:'))
p(' ', '\n '.join(opts.config_overrides)) p(' ', '\n '.join(opts.config_overrides))
compare_opts(opts, p) compare_opts(opts, p)
p()
p(green('Environment variable names seen by the kitty process:'))
p('\t' + '\n\t'.join(sorted(os.environ)))
return out.getvalue() return out.getvalue()

View File

@@ -60,9 +60,12 @@ copy_with_sendfile(int infd, int outfd, off_t in_pos, size_t len, FastFileCopyBu
off_t r = in_pos; off_t r = in_pos;
ssize_t n = sendfile(outfd, infd, &r, len); ssize_t n = sendfile(outfd, infd, &r, len);
if (n < 0) { if (n < 0) {
if (errno != EAGAIN) return false; if (errno == EAGAIN) continue;
if (errno == ENOSYS || errno == EPERM) return copy_with_buffer(infd, outfd, in_pos, len, fcb); if (errno == ENOSYS || // No kernel support
continue; errno == EPERM ||
errno == EINVAL) // ZFS for some reason
return copy_with_buffer(infd, outfd, in_pos, len, fcb);
return false;
} }
if (n == 0) { if (n == 0) {
// happens if input file is truncated // happens if input file is truncated
@@ -83,9 +86,15 @@ copy_with_file_range(int infd, int outfd, off_t in_pos, size_t len, FastFileCopy
off64_t r = in_pos; off64_t r = in_pos;
ssize_t n = copy_file_range(infd, &r, outfd, NULL, len, 0); ssize_t n = copy_file_range(infd, &r, outfd, NULL, len, 0);
if (n < 0) { if (n < 0) {
if (errno != EAGAIN) return false; if (errno == EAGAIN) continue;
if (errno == ENOSYS || errno == EPERM) return copy_with_sendfile(infd, outfd, in_pos, len, fcb); if (errno == ENOSYS || // Linux < 4.5
continue; errno == EPERM || // Possibly Docker
errno == EINVAL || // ZFS for some reason
errno == EIO || // CIFS
errno == EOPNOTSUPP || // NFS
errno == EXDEV) // Prior to Linux 5.3, it was not possible to copy_file_range across file systems
return copy_with_sendfile(infd, outfd, in_pos, len, fcb);
return false;
} }
if (n == 0) { if (n == 0) {
// happens if input file is truncated // happens if input file is truncated

View File

@@ -14,7 +14,7 @@ typedef struct FastFileCopyBuffer {
} FastFileCopyBuffer; } FastFileCopyBuffer;
static inline void static inline void
free_fast_file_copy_buffer(FastFileCopyBuffer *fcb) { free(fcb->buf); } free_fast_file_copy_buffer(FastFileCopyBuffer *fcb) { free(fcb->buf); fcb->buf = NULL; }
#define FREE_FCB_AFTER_FUNCTION __attribute__ ((__cleanup__(free_fast_file_copy_buffer))) #define FREE_FCB_AFTER_FUNCTION __attribute__ ((__cleanup__(free_fast_file_copy_buffer)))
#define AutoFreeFastFileCopyBuffer FREE_FCB_AFTER_FUNCTION FastFileCopyBuffer #define AutoFreeFastFileCopyBuffer FREE_FCB_AFTER_FUNCTION FastFileCopyBuffer

View File

@@ -764,6 +764,10 @@ def focus_os_window(os_window_id: int, also_raise: bool = True) -> bool:
pass pass
def toggle_secure_input() -> None:
pass
def start_profiler(path: str) -> None: def start_profiler(path: str) -> None:
pass pass
@@ -1339,5 +1343,5 @@ def set_os_window_title(os_window_id: int, title: str) -> None:
pass pass
def update_ime_position_for_window(window_id: int, force: bool = False) -> bool: def update_ime_position_for_window(window_id: int, force: bool = False, lost_focus: bool = False) -> bool:
pass pass

View File

@@ -7,7 +7,7 @@
# #
import math import math
from functools import partial as p, wraps from functools import partial as p, wraps, lru_cache
from itertools import repeat from itertools import repeat
from typing import ( from typing import (
Any, Callable, Dict, Iterable, Iterator, List, MutableSequence, Optional, Any, Callable, Dict, Iterable, Iterator, List, MutableSequence, Optional,
@@ -754,19 +754,32 @@ def eight_block(buf: BufType, width: int, height: int, level: int = 1, which: Tu
eight_bar(buf, width, height, level, x, horizontal) eight_bar(buf, width, height, level, x, horizontal)
@lru_cache(maxsize=64)
def distribute_dots(available_space: int, num_of_dots: int) -> Tuple[Tuple[int, ...], int]:
dot_size = max(1, available_space // (2 * num_of_dots))
extra = available_space - 2 * num_of_dots * dot_size
gaps = list(repeat(dot_size, num_of_dots))
if extra > 0:
idx = 0
while extra > 0:
gaps[idx] += 1
idx = (idx + 1) % len(gaps)
extra -= 1
gaps[0] //= 2
summed_gaps = tuple(sum(gaps[:i + 1]) for i in range(len(gaps)))
return summed_gaps, dot_size
def braille_dot(buf: BufType, width: int, height: int, col: int, row: int) -> None: def braille_dot(buf: BufType, width: int, height: int, col: int, row: int) -> None:
dot_height = max(1, height // 8) x_gaps, dot_width = distribute_dots(width, 2)
dot_width = max(1, width // 4) y_gaps, dot_height = distribute_dots(height, 4)
top_margin = (height - 7 * dot_height) // 2 x_start = x_gaps[col] + col * dot_width
left_margin = (width - 3 * dot_width) // 2 y_start = y_gaps[row] + row * dot_height
x_start = left_margin + (col * 2 * dot_width) if y_start < height and x_start < width:
y_start = top_margin + (row * 2 * dot_height)
if y_start < height:
for y in range(y_start, min(height, y_start + dot_height)): for y in range(y_start, min(height, y_start + dot_height)):
if x_start < width: offset = y * width
offset = y * width for x in range(x_start, min(width, x_start + dot_width)):
for x in range(x_start, min(width, x_start + dot_width)): buf[offset + x] = 255
buf[offset + x] = 255
def braille(buf: BufType, width: int, height: int, which: int = 0) -> None: def braille(buf: BufType, width: int, height: int, which: int = 0) -> None:

View File

@@ -29,7 +29,7 @@ def create_font_map(all_fonts: Iterable[CoreTextFont]) -> FontMap:
ps = (x['postscript_name'] or '').lower() ps = (x['postscript_name'] or '').lower()
ans['family_map'].setdefault(f, []).append(x) ans['family_map'].setdefault(f, []).append(x)
ans['ps_map'].setdefault(ps, []).append(x) ans['ps_map'].setdefault(ps, []).append(x)
ans['full_map'].setdefault(f + ' ' + s, []).append(x) ans['full_map'].setdefault(f'{f} {s}', []).append(x)
return ans return ans
@@ -45,7 +45,7 @@ def list_fonts() -> Generator[ListedFont, None, None]:
for fd in coretext_all_fonts(): for fd in coretext_all_fonts():
f = fd['family'] f = fd['family']
if f: if f:
fn = (f + ' ' + (fd['style'] or '')).strip() fn = f'{f} {fd.get("style", "")}'.strip()
is_mono = bool(fd['monospace']) is_mono = bool(fd['monospace'])
yield {'family': f, 'full_name': fn, 'postscript_name': fd['postscript_name'] or '', 'is_monospace': is_mono} yield {'family': f, 'full_name': fn, 'postscript_name': fd['postscript_name'] or '', 'is_monospace': is_mono}

View File

@@ -58,7 +58,7 @@ def list_fonts() -> Generator[ListedFont, None, None]:
if fn_: if fn_:
fn = str(fn_) fn = str(fn_)
else: else:
fn = (f + ' ' + str(fd.get('style', ''))).strip() fn = f'{f} {fd.get("style", "")}'.strip()
is_mono = fd.get('spacing') in ('MONO', 'DUAL') is_mono = fd.get('spacing') in ('MONO', 'DUAL')
yield {'family': f, 'full_name': fn, 'postscript_name': str(fd.get('postscript_name', '')), 'is_monospace': is_mono} yield {'family': f, 'full_name': fn, 'postscript_name': str(fd.get('postscript_name', '')), 'is_monospace': is_mono}

View File

@@ -28,13 +28,13 @@ def main(argv: Sequence[str]) -> None:
groups = create_family_groups() groups = create_family_groups()
for k in sorted(groups, key=lambda x: x.lower()): for k in sorted(groups, key=lambda x: x.lower()):
if isatty: if isatty:
print('\033[1;32m' + k + '\033[m') print(f'\033[1;32m{k}\033[m')
else: else:
print(k) print(k)
for f in sorted(groups[k], key=lambda x: x['full_name'].lower()): for f in sorted(groups[k], key=lambda x: x['full_name'].lower()):
p = f['full_name'] p = f['full_name']
if isatty: if isatty:
p = '\033[3m' + p + '\033[m' p = f'\033[3m{p}\033[m'
if psnames: if psnames:
p += ' ({})'.format(f['postscript_name']) p += ' ({})'.format(f['postscript_name'])
print(' ', p) print(' ', p)

View File

@@ -17,7 +17,7 @@ from kitty.fast_data_types import (
test_render_line, test_shape test_render_line, test_shape
) )
from kitty.fonts.box_drawing import ( from kitty.fonts.box_drawing import (
BufType, render_box_char, render_missing_glyph BufType, distribute_dots, render_box_char, render_missing_glyph
) )
from kitty.options.types import Options, defaults from kitty.options.types import Options, defaults
from kitty.typing import CoreTextFont, FontConfigPattern from kitty.typing import CoreTextFont, FontConfigPattern
@@ -279,6 +279,23 @@ def add_curl(buf: CBufType, cell_width: int, position: int, thickness: int, cell
add_intensity(x, y1 + t, 255) add_intensity(x, y1 + t, 255)
def add_dots(buf: CBufType, cell_width: int, position: int, thickness: int, cell_height: int) -> None:
spacing, size = distribute_dots(cell_width, cell_width // (2 * thickness))
y = 1 + position - thickness // 2
for i in range(y, min(y + thickness, cell_height)):
for j, s in enumerate(spacing):
buf[cell_width * i + j * size + s: cell_width * i + (j + 1) * size + s] = [255] * size
def add_dashes(buf: CBufType, cell_width: int, position: int, thickness: int, cell_height: int) -> None:
halfspace_width = cell_width // 4
y = 1 + position - thickness // 2
for i in range(y, min(y + thickness, cell_height)):
buf[cell_width * i:cell_width * i + (cell_width - 3 * halfspace_width)] = [255] * (cell_width - 3 * halfspace_width)
buf[cell_width * i + 3 * halfspace_width:cell_width * (i + 1)] = [255] * (cell_width - 3 * halfspace_width)
def render_special( def render_special(
underline: int = 0, underline: int = 0,
strikethrough: bool = False, strikethrough: bool = False,
@@ -306,14 +323,13 @@ def render_special(
try: try:
f(ans, cell_width, *a) f(ans, cell_width, *a)
except Exception as e: except Exception as e:
log_error('Failed to render {} at cell_width={} and cell_height={} with error: {}'.format( log_error(f'Failed to render {f.__name__} at cell_width={cell_width} and cell_height={cell_height} with error: {e}')
f.__name__, cell_width, cell_height, e))
if underline: if underline:
t = underline_thickness t = underline_thickness
if underline > 1: if underline > 1:
t = max(1, min(cell_height - underline_position - 1, t)) t = max(1, min(cell_height - underline_position - 1, t))
dl([add_line, add_line, add_dline, add_curl][underline], underline_position, t, cell_height) dl([add_line, add_line, add_dline, add_curl, add_dots, add_dashes][underline], underline_position, t, cell_height)
if strikethrough: if strikethrough:
dl(add_line, strikethrough_position, strikethrough_thickness, cell_height) dl(add_line, strikethrough_position, strikethrough_thickness, cell_height)
@@ -384,7 +400,7 @@ def prerender_function(
render_cursor, cursor_beam_thickness=cursor_beam_thickness, render_cursor, cursor_beam_thickness=cursor_beam_thickness,
cursor_underline_thickness=cursor_underline_thickness, cell_width=cell_width, cursor_underline_thickness=cursor_underline_thickness, cell_width=cell_width,
cell_height=cell_height, dpi_x=dpi_x, dpi_y=dpi_y) cell_height=cell_height, dpi_x=dpi_x, dpi_y=dpi_y)
cells = f(1), f(2), f(3), f(0, True), f(missing=True), c(1), c(2), c(3) cells = f(1), f(2), f(3), f(4), f(5), f(0, strikethrough=True), f(missing=True), c(1), c(2), c(3)
return tuple(map(ctypes.addressof, cells)), cells return tuple(map(ctypes.addressof, cells)), cells
@@ -499,7 +515,7 @@ def test_fallback_font(qtext: Optional[str] = None, bold: bool = False, italic:
try: try:
print(text, f) print(text, f)
except UnicodeEncodeError: except UnicodeEncodeError:
sys.stdout.buffer.write((text + ' %s\n' % f).encode('utf-8')) sys.stdout.buffer.write(f'{text} {f}\n'.encode('utf-8'))
def showcase() -> None: def showcase() -> None:

6
kitty/glfw-wrapper.c generated
View File

@@ -249,6 +249,12 @@ load_glfw(const char* path) {
*(void **) (&glfwPostEmptyEvent_impl) = dlsym(handle, "glfwPostEmptyEvent"); *(void **) (&glfwPostEmptyEvent_impl) = dlsym(handle, "glfwPostEmptyEvent");
if (glfwPostEmptyEvent_impl == NULL) fail("Failed to load glfw function glfwPostEmptyEvent with error: %s", dlerror()); if (glfwPostEmptyEvent_impl == NULL) fail("Failed to load glfw function glfwPostEmptyEvent with error: %s", dlerror());
*(void **) (&glfwGetIgnoreOSKeyboardProcessing_impl) = dlsym(handle, "glfwGetIgnoreOSKeyboardProcessing");
if (glfwGetIgnoreOSKeyboardProcessing_impl == NULL) fail("Failed to load glfw function glfwGetIgnoreOSKeyboardProcessing with error: %s", dlerror());
*(void **) (&glfwSetIgnoreOSKeyboardProcessing_impl) = dlsym(handle, "glfwSetIgnoreOSKeyboardProcessing");
if (glfwSetIgnoreOSKeyboardProcessing_impl == NULL) fail("Failed to load glfw function glfwSetIgnoreOSKeyboardProcessing with error: %s", dlerror());
*(void **) (&glfwGetInputMode_impl) = dlsym(handle, "glfwGetInputMode"); *(void **) (&glfwGetInputMode_impl) = dlsym(handle, "glfwGetInputMode");
if (glfwGetInputMode_impl == NULL) fail("Failed to load glfw function glfwGetInputMode with error: %s", dlerror()); if (glfwGetInputMode_impl == NULL) fail("Failed to load glfw function glfwGetInputMode with error: %s", dlerror());

8
kitty/glfw-wrapper.h generated
View File

@@ -1916,6 +1916,14 @@ typedef void (*glfwPostEmptyEvent_func)(void);
GFW_EXTERN glfwPostEmptyEvent_func glfwPostEmptyEvent_impl; GFW_EXTERN glfwPostEmptyEvent_func glfwPostEmptyEvent_impl;
#define glfwPostEmptyEvent glfwPostEmptyEvent_impl #define glfwPostEmptyEvent glfwPostEmptyEvent_impl
typedef bool (*glfwGetIgnoreOSKeyboardProcessing_func)(void);
GFW_EXTERN glfwGetIgnoreOSKeyboardProcessing_func glfwGetIgnoreOSKeyboardProcessing_impl;
#define glfwGetIgnoreOSKeyboardProcessing glfwGetIgnoreOSKeyboardProcessing_impl
typedef void (*glfwSetIgnoreOSKeyboardProcessing_func)(bool);
GFW_EXTERN glfwSetIgnoreOSKeyboardProcessing_func glfwSetIgnoreOSKeyboardProcessing_impl;
#define glfwSetIgnoreOSKeyboardProcessing glfwSetIgnoreOSKeyboardProcessing_impl
typedef int (*glfwGetInputMode_func)(GLFWwindow*, int); typedef int (*glfwGetInputMode_func)(GLFWwindow*, int);
GFW_EXTERN glfwGetInputMode_func glfwGetInputMode_impl; GFW_EXTERN glfwGetInputMode_func glfwGetInputMode_impl;
#define glfwGetInputMode glfwGetInputMode_impl #define glfwGetInputMode glfwGetInputMode_impl

View File

@@ -24,6 +24,7 @@ extern void cocoa_set_activation_policy(bool);
extern void cocoa_set_titlebar_appearance(void *w, unsigned int theme); extern void cocoa_set_titlebar_appearance(void *w, unsigned int theme);
extern void cocoa_set_titlebar_color(void *w, color_type color); extern void cocoa_set_titlebar_color(void *w, color_type color);
extern bool cocoa_alt_option_key_pressed(unsigned long); extern bool cocoa_alt_option_key_pressed(unsigned long);
extern void cocoa_toggle_secure_keyboard_entry(void);
extern size_t cocoa_get_workspace_ids(void *w, size_t *workspace_ids, size_t array_sz); extern size_t cocoa_get_workspace_ids(void *w, size_t *workspace_ids, size_t array_sz);
extern monotonic_t cocoa_cursor_blink_interval(void); extern monotonic_t cocoa_cursor_blink_interval(void);
@@ -1181,6 +1182,14 @@ glfw_window_hint(PyObject UNUSED *self, PyObject *args) {
// }}} // }}}
static PyObject*
toggle_secure_input(PYNOARG) {
#ifdef __APPLE__
cocoa_toggle_secure_keyboard_entry();
#endif
Py_RETURN_NONE;
}
static PyObject* static PyObject*
get_clipboard_string(PYNOARG) { get_clipboard_string(PYNOARG) {
OSWindow *w = current_os_window(); OSWindow *w = current_os_window();
@@ -1555,6 +1564,11 @@ strip_csi(PyObject *self UNUSED, PyObject *src) {
return PyUnicode_FromString(buf); return PyUnicode_FromString(buf);
} }
void
set_ignore_os_keyboard_processing(bool enabled) {
glfwSetIgnoreOSKeyboardProcessing(enabled);
}
// Boilerplate {{{ // Boilerplate {{{
static PyMethodDef module_methods[] = { static PyMethodDef module_methods[] = {
@@ -1562,6 +1576,7 @@ static PyMethodDef module_methods[] = {
{"create_os_window", (PyCFunction)(void (*) (void))(create_os_window), METH_VARARGS | METH_KEYWORDS, NULL}, {"create_os_window", (PyCFunction)(void (*) (void))(create_os_window), METH_VARARGS | METH_KEYWORDS, NULL},
METHODB(set_default_window_icon, METH_VARARGS), METHODB(set_default_window_icon, METH_VARARGS),
METHODB(get_clipboard_string, METH_NOARGS), METHODB(get_clipboard_string, METH_NOARGS),
METHODB(toggle_secure_input, METH_NOARGS),
METHODB(get_content_scale_for_window, METH_NOARGS), METHODB(get_content_scale_for_window, METH_NOARGS),
METHODB(ring_bell, METH_NOARGS), METHODB(ring_bell, METH_NOARGS),
METHODB(set_clipboard_string, METH_VARARGS), METHODB(set_clipboard_string, METH_VARARGS),

View File

@@ -58,7 +58,7 @@ def guess_type(path: str, allow_filesystem_access: bool = False) -> Optional[str
ext = path.rpartition('.')[-1].lower() ext = path.rpartition('.')[-1].lower()
mt = known_extensions.get(ext) mt = known_extensions.get(ext)
if mt in text_mimes: if mt in text_mimes:
mt = 'text/' + mt.split('/', 1)[-1] mt = f'text/{mt.split("/", 1)[-1]}'
if not mt and is_rc_file(path): if not mt and is_rc_file(path):
mt = 'text/plain' mt = 'text/plain'
if not mt and is_folder(path): if not mt and is_folder(path):

View File

@@ -219,7 +219,6 @@ encode_function_key(const KeyEvent *ev, char *output) {
case GLFW_FKEY_PAGE_UP: S(5, '~'); case GLFW_FKEY_PAGE_UP: S(5, '~');
case GLFW_FKEY_PAGE_DOWN: S(6, '~'); case GLFW_FKEY_PAGE_DOWN: S(6, '~');
case GLFW_FKEY_HOME: S(1, 'H'); case GLFW_FKEY_HOME: S(1, 'H');
case GLFW_FKEY_KP_BEGIN: S(1, 'E');
case GLFW_FKEY_END: S(1, 'F'); case GLFW_FKEY_END: S(1, 'F');
case GLFW_FKEY_F1: S(1, 'P'); case GLFW_FKEY_F1: S(1, 'P');
case GLFW_FKEY_F2: S(1, 'Q'); case GLFW_FKEY_F2: S(1, 'Q');
@@ -233,7 +232,12 @@ encode_function_key(const KeyEvent *ev, char *output) {
case GLFW_FKEY_F10: S(21, '~'); case GLFW_FKEY_F10: S(21, '~');
case GLFW_FKEY_F11: S(23, '~'); case GLFW_FKEY_F11: S(23, '~');
case GLFW_FKEY_F12: S(24, '~'); case GLFW_FKEY_F12: S(24, '~');
case GLFW_FKEY_KP_BEGIN: S(1, 'E');
/* end special numbers */ /* end special numbers */
case GLFW_FKEY_MENU:
// use the same encoding as xterm for this key in legacy mode (F16)
if (legacy_mode) { S(29, '~'); }
break;
default: break; default: break;
} }
#undef S #undef S

2
kitty/key_encoding.py generated
View File

@@ -181,7 +181,7 @@ class EventType(IntEnum):
@lru_cache(maxsize=128) @lru_cache(maxsize=128)
def parse_shortcut(spec: str) -> ParsedShortcut: def parse_shortcut(spec: str) -> ParsedShortcut:
if spec.endswith('+'): if spec.endswith('+'):
spec = spec[:-1] + 'plus' spec = f'{spec[:-1]}plus'
parts = spec.split('+') parts = spec.split('+')
key_name = parts[-1] key_name = parts[-1]
key_name = functional_key_name_aliases.get(key_name.upper(), key_name) key_name = functional_key_name_aliases.get(key_name.upper(), key_name)

View File

@@ -63,7 +63,7 @@ else:
import ctypes import ctypes
for suffix in ('.0', ''): for suffix in ('.0', ''):
with suppress(Exception): with suppress(Exception):
lib = ctypes.CDLL('libxkbcommon.so' + suffix) lib = ctypes.CDLL(f'libxkbcommon.so{suffix}')
break break
else: else:
from ctypes.util import find_library from ctypes.util import find_library

View File

@@ -79,6 +79,13 @@ active_window(void) {
return NULL; return NULL;
} }
void
update_ime_focus(OSWindow *osw, bool focused) {
if (!osw || !osw->handle) return;
GLFWIMEUpdateEvent ev = { .focused = focused, .type = GLFW_IME_UPDATE_FOCUS };
glfwUpdateIMEState(osw->handle, &ev);
}
void void
update_ime_position(Window* w, Screen *screen) { update_ime_position(Window* w, Screen *screen) {
unsigned int cell_width = global_state.callback_os_window->fonts_data->cell_width, cell_height = global_state.callback_os_window->fonts_data->cell_height; unsigned int cell_width = global_state.callback_os_window->fonts_data->cell_width, cell_height = global_state.callback_os_window->fonts_data->cell_height;

View File

@@ -8,6 +8,7 @@ from .boss import Boss
from .child import Child from .child import Child
from .cli import parse_args from .cli import parse_args
from .cli_stub import LaunchCLIOptions from .cli_stub import LaunchCLIOptions
from .constants import kitty_exe, shell_path
from .fast_data_types import patch_color_profiles, set_clipboard_string from .fast_data_types import patch_color_profiles, set_clipboard_string
from .options.utils import env as parse_env from .options.utils import env as parse_env
from .tabs import Tab from .tabs import Tab
@@ -71,6 +72,11 @@ Using :code:`name=` will set to empty string and just :code:`name` will
remove the environment variable. remove the environment variable.
--hold
type=bool-set
Keep the window open even after the command being executed exits.
--copy-colors --copy-colors
type=bool-set type=bool-set
Set the colors of the newly created window to be the same as the colors in the Set the colors of the newly created window to be the same as the colors in the
@@ -300,6 +306,28 @@ def apply_colors(window: Window, spec: Sequence[str]) -> None:
patch_color_profiles(colors, profiles, True) patch_color_profiles(colors, profiles, True)
class ForceWindowLaunch:
def __init__(self) -> None:
self.force = False
def __bool__(self) -> bool:
return self.force
def __call__(self, force: bool) -> 'ForceWindowLaunch':
self.force = force
return self
def __enter__(self) -> None:
pass
def __exit__(self, *a: object) -> None:
self.force = False
force_window_launch = ForceWindowLaunch()
def launch( def launch(
boss: Boss, boss: Boss,
opts: LaunchCLIOptions, opts: LaunchCLIOptions,
@@ -341,7 +369,7 @@ def launch(
if opts.stdin_add_formatting: if opts.stdin_add_formatting:
if q in ('@screen', '@screen_scrollback', '@alternate', '@alternate_scrollback', if q in ('@screen', '@screen_scrollback', '@alternate', '@alternate_scrollback',
'@first_cmd_output_on_screen', '@last_cmd_output', '@last_visited_cmd_output'): '@first_cmd_output_on_screen', '@last_cmd_output', '@last_visited_cmd_output'):
q = '@ansi_' + q[1:] q = f'@ansi_{q[1:]}'
if opts.stdin_add_line_wrap_markers: if opts.stdin_add_line_wrap_markers:
q += '_wrap' q += '_wrap'
penv, stdin = boss.process_stdin_source(window=active, stdin=q, copy_pipe_data=pipe_data) penv, stdin = boss.process_stdin_source(window=active, stdin=q, copy_pipe_data=pipe_data)
@@ -383,6 +411,8 @@ def launch(
if exe: if exe:
final_cmd[0] = exe final_cmd[0] = exe
kw['cmd'] = final_cmd kw['cmd'] = final_cmd
if force_window_launch and opts.type not in ('background', 'clipboard', 'primary'):
opts.type = 'window'
if opts.type == 'overlay' and active: if opts.type == 'overlay' and active:
kw['overlay_for'] = active.id kw['overlay_for'] = active.id
if opts.type == 'background': if opts.type == 'background':
@@ -398,6 +428,9 @@ def launch(
else: else:
set_primary_selection(stdin) set_primary_selection(stdin)
else: else:
if opts.hold:
cmd = kw['cmd'] or [shell_path]
kw['cmd'] = [kitty_exe(), '+hold'] + cmd
if force_target_tab: if force_target_tab:
tab = target_tab tab = target_tab
else: else:

View File

@@ -217,7 +217,7 @@ class Layout:
self.blank_rects: List[Rect] = [] self.blank_rects: List[Rect] = []
self.layout_opts = self.parse_layout_opts(layout_opts) self.layout_opts = self.parse_layout_opts(layout_opts)
assert self.name is not None assert self.name is not None
self.full_name = self.name + ((':' + layout_opts) if layout_opts else '') self.full_name = f'{self.name}:{layout_opts}' if layout_opts else self.name
self.remove_all_biases() self.remove_all_biases()
def bias_increment_for_cell(self, is_horizontal: bool) -> float: def bias_increment_for_cell(self, is_horizontal: bool) -> float:

View File

@@ -123,8 +123,12 @@ class Vertical(Layout):
assert wg is not None assert wg is not None
groups = tuple(all_windows.iter_all_layoutable_groups()) groups = tuple(all_windows.iter_all_layoutable_groups())
idx = groups.index(wg) idx = groups.index(wg)
before = [] if wg is groups[0] else [groups[idx-1].id] lg = len(groups)
after = [] if wg is groups[-1] else [groups[idx+1].id] if lg > 1:
before = [groups[(idx - 1 + lg) % lg].id]
after = [groups[(idx + 1) % lg].id]
else:
before, after = [], []
if self.main_is_horizontal: if self.main_is_horizontal:
return {'left': before, 'right': after, 'top': [], 'bottom': []} return {'left': before, 'right': after, 'top': [], 'bottom': []}
return {'top': before, 'bottom': after, 'left': [], 'right': []} return {'top': before, 'bottom': after, 'left': [], 'right': []}

View File

@@ -811,23 +811,22 @@ mark_text_in_line(PyObject *marker, Line *line) {
PyObject* PyObject*
as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, ANSIBuf *ansibuf) { as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, ANSIBuf *ansibuf) {
#define APPEND(x) { PyObject* retval = PyObject_CallFunctionObjArgs(callback, x, NULL); if (!retval) return NULL; Py_DECREF(retval); }
#define APPEND_AND_DECREF(x) { if (x == NULL) { if (PyErr_Occurred()) return NULL; Py_RETURN_NONE; } PyObject* retval = PyObject_CallFunctionObjArgs(callback, x, NULL); Py_CLEAR(x); if (!retval) return NULL; Py_DECREF(retval); }
PyObject *callback; PyObject *callback;
int as_ansi = 0, insert_wrap_markers = 0; int as_ansi = 0, insert_wrap_markers = 0;
if (!PyArg_ParseTuple(args, "O|pp", &callback, &as_ansi, &insert_wrap_markers)) return NULL; if (!PyArg_ParseTuple(args, "O|pp", &callback, &as_ansi, &insert_wrap_markers)) return NULL;
PyObject *ret = NULL, *t = NULL; PyObject *t = NULL;
PyObject *nl = PyUnicode_FromString("\n"); DECREF_AFTER_FUNCTION PyObject *nl = PyUnicode_FromString("\n");
PyObject *cr = PyUnicode_FromString("\r"); DECREF_AFTER_FUNCTION PyObject *cr = PyUnicode_FromString("\r");
PyObject *sgr_reset = PyUnicode_FromString("\x1b[m"); DECREF_AFTER_FUNCTION PyObject *sgr_reset = PyUnicode_FromString("\x1b[m");
if (nl == NULL || cr == NULL || sgr_reset == NULL) goto end; if (nl == NULL || cr == NULL || sgr_reset == NULL) return NULL;
const GPUCell *prev_cell = NULL; const GPUCell *prev_cell = NULL;
ansibuf->active_hyperlink_id = 0; ansibuf->active_hyperlink_id = 0;
for (index_type y = 0; y < lines; y++) { for (index_type y = 0; y < lines; y++) {
Line *line = get_line(container, y); Line *line = get_line(container, y);
if (!line->attrs.continued && y > 0) { if (!line) { if (PyErr_Occurred()) return NULL; break; }
ret = PyObject_CallFunctionObjArgs(callback, nl, NULL); if (!line->attrs.continued && y > 0) APPEND(nl);
if (ret == NULL) goto end;
Py_CLEAR(ret);
}
if (as_ansi) { if (as_ansi) {
// less has a bug where it resets colors when it sees a \r, so work // 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 // around it by resetting SGR at the start of every line. This is
@@ -837,36 +836,21 @@ as_text_generic(PyObject *args, void *container, get_line_func get_line, index_t
prev_cell = NULL; prev_cell = NULL;
line_as_ansi(line, ansibuf, &prev_cell); line_as_ansi(line, ansibuf, &prev_cell);
t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, ansibuf->buf, ansibuf->len); t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, ansibuf->buf, ansibuf->len);
if (t && ansibuf->len > 0) { if (t && ansibuf->len > 0) APPEND(sgr_reset);
ret = PyObject_CallFunctionObjArgs(callback, sgr_reset, NULL);
if (ret == NULL) goto end;
Py_CLEAR(ret);
}
} else { } else {
t = line_as_unicode(line, false); t = line_as_unicode(line, false);
} }
if (t == NULL) goto end; APPEND_AND_DECREF(t);
ret = PyObject_CallFunctionObjArgs(callback, t, NULL); if (insert_wrap_markers) APPEND(cr);
Py_DECREF(t); if (ret == NULL) goto end; Py_DECREF(ret);
if (insert_wrap_markers) {
ret = PyObject_CallFunctionObjArgs(callback, cr, NULL);
if (ret == NULL) goto end;
Py_CLEAR(ret);
}
} }
if (ansibuf->active_hyperlink_id) { if (ansibuf->active_hyperlink_id) {
ansibuf->active_hyperlink_id = 0; ansibuf->active_hyperlink_id = 0;
t = PyUnicode_FromString("\x1b]8;;\x1b\\"); t = PyUnicode_FromString("\x1b]8;;\x1b\\");
if (t) { APPEND_AND_DECREF(t);
ret = PyObject_CallFunctionObjArgs(callback, t, NULL);
Py_CLEAR(t);
Py_CLEAR(ret);
}
} }
end:
Py_CLEAR(nl); Py_CLEAR(cr); Py_CLEAR(sgr_reset);
if (PyErr_Occurred()) return NULL;
Py_RETURN_NONE; Py_RETURN_NONE;
#undef APPEND
#undef APPEND_AND_DECREF
} }
// Boilerplate {{{ // Boilerplate {{{

View File

@@ -44,7 +44,7 @@ def set_custom_ibeam_cursor() -> None:
data = f.read() data = f.read()
rgba_data, width, height = load_png_data(data) rgba_data, width, height = load_png_data(data)
c2x = os.path.splitext(beam_cursor_data_file) c2x = os.path.splitext(beam_cursor_data_file)
with open(c2x[0] + '@2x' + c2x[1], 'rb') as f: with open(f'{c2x[0]}@2x{c2x[1]}', 'rb') as f:
data = f.read() data = f.read()
rgba_data2, width2, height2 = load_png_data(data) rgba_data2, width2, height2 = load_png_data(data)
images = (rgba_data, width, height), (rgba_data2, width2, height2) images = (rgba_data, width, height), (rgba_data2, width2, height2)
@@ -138,7 +138,7 @@ def get_macos_shortcut_for(
def set_x11_window_icon() -> None: def set_x11_window_icon() -> None:
# max icon size on X11 64bits is 128x128 # max icon size on X11 64bits is 128x128
path, ext = os.path.splitext(logo_png_file) path, ext = os.path.splitext(logo_png_file)
set_default_window_icon(path + '-128' + ext) set_default_window_icon(f'{path}-128{ext}')
def _run_app(opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = ()) -> None: def _run_app(opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = ()) -> None:
@@ -151,7 +151,7 @@ def _run_app(opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = ())
func_map[parts].append(k) func_map[parts].append(k)
for ac in ('new_os_window', 'close_os_window', 'close_tab', 'edit_config_file', 'previous_tab', for ac in ('new_os_window', 'close_os_window', 'close_tab', 'edit_config_file', 'previous_tab',
'next_tab', 'new_tab', 'new_window', 'close_window'): 'next_tab', 'new_tab', 'new_window', 'close_window', 'toggle_macos_secure_keyboard_entry'):
val = get_macos_shortcut_for(func_map, ac) val = get_macos_shortcut_for(func_map, ac)
if val is not None: if val is not None:
global_shortcuts[ac] = val global_shortcuts[ac] = val
@@ -220,7 +220,7 @@ def ensure_macos_locale() -> None:
lang = 'en_US' lang = 'en_US'
else: else:
log_error(f'Could not set LANG Cocoa returns language as: {lang}') log_error(f'Could not set LANG Cocoa returns language as: {lang}')
os.environ['LANG'] = lang + '.UTF-8' os.environ['LANG'] = f'{lang}.UTF-8'
@contextmanager @contextmanager

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