Compare commits

..

289 Commits

Author SHA1 Message Date
Kovid Goyal
90ed5959de version 0.24.4 2022-03-03 10:54:01 +05:30
Kovid Goyal
d999cc4143 Merge branch 'patch-1' of https://github.com/rqel/kitty 2022-03-03 08:03:25 +05:30
Kovid Goyal
6c1d8d5c63 Add instructions on where to get the NERD font 2022-03-03 08:01:36 +05:30
rqel
a316242a4b Update symbol_map unicode codepoint range for Symbols Nerd Font
Signed-off-by: rqel <rqel@pm.me>
2022-03-02 18:19:13 -08:00
Kovid Goyal
a4ae090c37 commit surface even for out of order configures 2022-03-03 07:18:04 +05:30
Kovid Goyal
abdcc64053 Merge branch 'always-commit' of https://github.com/Nefsen402/kitty 2022-03-03 07:16:50 +05:30
Alexander Orzechowski
ac407d42de wayland: always commit surface on configure 2022-03-02 14:43:49 -05:00
rqel
19c4a3f0a8 Add U+E0CA
Signed-off-by: rqel <rqel@pm.me>
2022-03-02 07:07:05 -08:00
rqel
3afd96d421 Update symbol_map
Signed-off-by: rqel <rqel@pm.me>
2022-03-02 06:37:18 -08:00
Kovid Goyal
f421666a27 Nicer error message when importing a kitten fails 2022-03-02 08:26:06 +05:30
Kovid Goyal
af6baa33be Ignore out of order surface configure events 2022-03-01 07:59:17 +05:30
Kovid Goyal
1096cbe236 Use an anonymous struct 2022-03-01 07:46:45 +05:30
Kovid Goyal
998be9b90c Merge branch 'wayland-fix-configure' of https://github.com/Nefsen402/kitty 2022-03-01 07:45:24 +05:30
Alexander Orzechowski
854cb8f27e wayland: handle resize requests after acknowledging the configure
The wayland protocol requires that clients commit a new buffer sometime
after `xdg_surface_ack_configure`.
2022-02-28 19:42:11 -05:00
Alexander Orzechowski
e359094cff wayland: move window state 2022-02-28 19:42:11 -05:00
Kovid Goyal
db57230987 Cleanup changelog 2022-02-28 20:47:08 +05:30
Kovid Goyal
4de3cecbbe Update changelog for previous PR
Fixes #4763
2022-02-28 20:21:06 +05:30
Kovid Goyal
c37a8bd3b1 Merge branch 'fix-read-scr-size' of https://github.com/page-down/kitty 2022-02-28 20:19:44 +05:30
Kovid Goyal
e103b280fd Add test for HISTFILE 2022-02-28 20:09:54 +05:30
pagedown
2235ea67e2 Fix ScreenSizeGetter 2022-02-28 22:39:19 +08:00
Kovid Goyal
1f6a4f7bd4 ... 2022-02-28 20:07:57 +05:30
Kovid Goyal
8c7ffc90f3 Fix the default bash HISTFILE changing to .sh_history instead of .bash_history
Fixes #4765
2022-02-28 20:06:48 +05:30
Kovid Goyal
29d85833f1 Merge branch 'ksi-fish' of https://github.com/page-down/kitty 2022-02-28 17:50:05 +05:30
pagedown
74c56d69ac Also fall back to check the last known working version of fish 2022-02-28 19:26:20 +08:00
Kovid Goyal
cf520646a9 Merge branch 'feat-launch-url-err' of https://github.com/page-down/kitty 2022-02-28 13:12:14 +05:30
pagedown
da5213346a show_error kitten: Use green color for closing tips 2022-02-28 15:02:16 +08:00
pagedown
bdcb98eafc Use show_error kitten to display launch URL error messages 2022-02-28 15:02:05 +08:00
Kovid Goyal
ef637cd7d3 ... 2022-02-28 11:28:42 +05:30
Kovid Goyal
dbbabd73c5 Repeat hold message on ctrl+c/d instead of completely ignoring or ending hold 2022-02-28 11:17:00 +05:30
Kovid Goyal
436ef0074a DRYer 2022-02-28 11:08:32 +05:30
Kovid Goyal
0904dec810 Fix exception on ctrl+c exit of program under --hold 2022-02-28 11:02:06 +05:30
Kovid Goyal
0aa07ead7e Bundle wayland client libraries in the binary build
The version of libwayland kitty builds against is now 1.20 and this
introduces the new wl_proxy_marshal_flags function. The usual suspect
distros have too old libwayland-client so we need to bundle.

Fixes #4760
2022-02-28 10:27:44 +05:30
Kovid Goyal
d87cac87ff version 0.24.3 2022-02-28 08:00:18 +05:30
Kovid Goyal
7e8c96896f Do a shutdown and rebuild 2022-02-28 07:21:46 +05:30
Kovid Goyal
2619db0e58 Cleanup previous PR 2022-02-27 21:31:39 +05:30
Kovid Goyal
ee3632b85d Merge branch 'ksi-zsh' of https://github.com/page-down/kitty 2022-02-27 21:31:31 +05:30
pagedown
1c88c13fc2 Shell integration: Try to get ZDOTDIR from /etc/zshenv when no RC files 2022-02-27 23:25:26 +08:00
Kovid Goyal
7f476eb924 Merge branch 'test-ksi-zsh' of https://github.com/page-down/kitty 2022-02-27 18:30:13 +05:30
pagedown
2d4f7e3446 Disable system startup files in zsh integration test with --noglobalrcs 2022-02-27 20:21:05 +08:00
Kovid Goyal
165c1240a9 Make the test a little more robust 2022-02-27 11:04:12 +05:30
Kovid Goyal
ef5c66ee17 Dont bother with new escape code for setting title
Since we are only sending the last command, it cannot have CSI
sequences in it anyway except in extremely contrived situations, so just
strip all ctrl chars out.
2022-02-27 10:55:02 +05:30
Kovid Goyal
33abd31d1a ... 2022-02-27 10:48:52 +05:30
Kovid Goyal
f91463a494 Also add test for handling of control chars in zsh title reporting 2022-02-27 10:47:23 +05:30
Kovid Goyal
30146007d3 Avoid extra tr invocation 2022-02-27 10:23:04 +05:30
Kovid Goyal
d53f8f24c4 Fix #4757
Still have to do the fix for zsh/fish
2022-02-27 10:20:19 +05:30
Kovid Goyal
f9621b1e11 Merge branch 'fix-ksi-fish' of https://github.com/page-down/kitty 2022-02-27 09:32:37 +05:30
pagedown
e2f16ff624 Add fish pipestatus integration tests and changelog entries 2022-02-27 00:27:31 +08:00
pagedown
121778e5c6 Remove unnecessary function checks 2022-02-27 00:08:36 +08:00
pagedown
ef32488890 Shell integration: Fix fish pipestatus being overwritten 2022-02-27 00:07:46 +08:00
Kovid Goyal
9d349d618a Fix #4743 2022-02-26 14:42:21 +05:30
Kovid Goyal
9f0a4f43b8 Add some video reviews to press mentions 2022-02-26 09:54:16 +05:30
Kovid Goyal
a8826f0d02 When dropping URLs/files onto kitty at a shell prompt insert them appropriately quoted and space separated 2022-02-25 10:30:15 +05:30
Kovid Goyal
6689d312a3 macOS: Paste multiple URLs separated by newlines
Fixes #4734
2022-02-25 10:17:36 +05:30
Kovid Goyal
2d7cb25b20 Fix #4739
I hope, since I cannot reproduce.
2022-02-25 05:50:42 +05:30
Kovid Goyal
da10eaca00 Merge branch 'docs' of https://github.com/page-down/kitty 2022-02-24 19:19:12 +05:30
Kovid Goyal
6d3995d4ac Merge branch 'feat-eof-exit' of https://github.com/page-down/kitty 2022-02-24 19:18:52 +05:30
pagedown
e3adf8c6bf show_error kitten: Exit at EOF 2022-02-24 21:13:57 +08:00
pagedown
b8e522484d ... 2022-02-24 20:50:06 +08:00
Kovid Goyal
ef4240e196 Add Python bytes 272 to press mentions 2022-02-24 18:01:55 +05:30
Kovid Goyal
8fa4a48ae5 Merge branch 'ksi-fish' of https://github.com/page-down/kitty 2022-02-24 17:59:41 +05:30
pagedown
4f3da2bc00 Output warnings for outdated versions of fish 2022-02-24 20:25:48 +08:00
pagedown
7b8c2c846f Shell integration: Check fish version and exit on outdated versions 2022-02-24 18:43:46 +08:00
Kovid Goyal
b212fd5bcd Dont actually need a thread for PTY 2022-02-24 10:05:13 +05:30
Kovid Goyal
0a297f4656 proper ordering during shutdown 2022-02-24 00:27:09 +05:30
Kovid Goyal
fa397a1d24 PTY: Write to child in separate thread 2022-02-24 00:26:08 +05:30
Kovid Goyal
0610daaec7 Remove echo dance 2022-02-24 00:08:17 +05:30
Kovid Goyal
8278e2b88d Dont fail test in no PATH in environ 2022-02-23 23:27:24 +05:30
Kovid Goyal
cb4a9d89cf Cleanup linenum matching and add some tests 2022-02-23 23:06:19 +05:30
Kovid Goyal
7800c598f6 hints kitten: Fix a regression that broke recognition of path:linenumber:colnumber
Fixes #4675
2022-02-23 22:38:54 +05:30
Kovid Goyal
fc651b72ab Merge branch 'refactor' of https://github.com/page-down/kitty 2022-02-23 19:52:11 +05:30
pagedown
a9a7912372 ... 2022-02-23 21:37:41 +08:00
pagedown
43f435c334 Avoid shadow the global variable name 2022-02-23 21:30:05 +08:00
pagedown
693fc0f0c7 CI: Disable codeql auto install python dependencies 2022-02-23 21:28:29 +08:00
Kovid Goyal
12e011c481 Merge branch 'test-ksi-fish' of https://github.com/page-down/kitty 2022-02-23 18:31:13 +05:30
Kovid Goyal
668783ba1c Use a version check when skipping bash test 2022-02-23 18:27:20 +05:30
pagedown
c18ebef702 ... 2022-02-23 20:49:32 +08:00
Kovid Goyal
03f9f29ce4 DRYer 2022-02-23 18:14:40 +05:30
Kovid Goyal
f62acab715 Install fish on macOS CI 2022-02-23 18:07:06 +05:30
Kovid Goyal
28ab9bfa2a Merge branch 'test-ksi-fish' of https://github.com/page-down/kitty 2022-02-23 18:02:53 +05:30
pagedown
9c05481f14 ... 2022-02-23 20:05:30 +08:00
pagedown
8ca92e0536 Disable fish integration on CI since the version is outdated and broken 2022-02-23 19:58:09 +08:00
pagedown
c0f6201ac3 Add fish shell integration tests 2022-02-23 19:55:19 +08:00
Kovid Goyal
27f3a5e16a Fix previous PR breaking bash integration 2022-02-23 12:30:02 +05:30
Kovid Goyal
de10dfe91b Merge branch 'ksi-bash' of https://github.com/page-down/kitty 2022-02-23 12:22:00 +05:30
pagedown
444a690a86 Remove the extra comment line 2022-02-23 14:37:45 +08:00
pagedown
3d7b3f7d72 Use the official project name of Bash 2022-02-23 14:21:13 +08:00
pagedown
741ef7f115 Remove new lines with bash 2022-02-23 14:09:13 +08:00
pagedown
e0c84c7176 Use bash builtin 2022-02-23 14:01:47 +08:00
pagedown
393425e3d1 Remove trailing spaces 2022-02-23 13:59:39 +08:00
Kovid Goyal
fc9645832d Add tests for --noprofile, --rcfile, --norc as well 2022-02-23 10:42:25 +05:30
Kovid Goyal
c47f41cfc0 Add tests for bash startup file sourcing 2022-02-23 08:35:24 +05:30
Kovid Goyal
08221489fd Cleanup previous PR 2022-02-23 07:59:41 +05:30
Kovid Goyal
3c6766ff06 Merge branch '4721-lig-check-fallback' of https://github.com/nickwynja/kitty 2022-02-23 07:57:36 +05:30
Kovid Goyal
e28aae620a Add tests for reset of cursor shape when running commands 2022-02-23 07:48:42 +05:30
Nick Wynja
c240e7deaf Also check ### to confirm spacer strategy
relates to #4721
2022-02-22 14:59:19 -05:00
Kovid Goyal
31a5965b01 Splits layout: A new value for :option:launch --location to auto-select the split axis when splitting existing windows. 2022-02-22 22:37:26 +05:30
Kovid Goyal
7a2a849a97 Add a note that shell integration should be removed from bashrc 2022-02-22 21:45:31 +05:30
Kovid Goyal
2c96e49566 ... 2022-02-22 21:40:03 +05:30
Kovid Goyal
e7846f916a Cleanup how it works for bash 2022-02-22 21:37:50 +05:30
Kovid Goyal
d4f4d56f94 DRYer 2022-02-22 21:32:33 +05:30
Kovid Goyal
0108037076 Dont leak the ENV environ var we use to load our integration script into the shell 2022-02-22 21:29:32 +05:30
Kovid Goyal
88091b4ab3 BASH integration: No longer modify .bashrc to load shell integration
I think I have things setup robustly so that the shell integration
is loaded transparently via env vars and the normal bash startup files
are sourced, in the same way that vanilla bash does it. Let's hope I
haven't overlooked something.
2022-02-22 21:24:51 +05:30
Kovid Goyal
4487462b0d bash assumes ECHO is enabled for the tty 2022-02-22 20:43:58 +05:30
Kovid Goyal
dac9b07f16 bash shell integration: Handle both literal and escaped newlines in PS1 2022-02-22 18:30:20 +05:30
Kovid Goyal
b59212696a Skip testing bash on macOS as it is too old 2022-02-22 14:53:46 +05:30
Kovid Goyal
7fe1376e34 DRYer 2022-02-22 14:52:54 +05:30
Kovid Goyal
e25b90c1b6 Add bash integration tests 2022-02-22 12:23:19 +05:30
Kovid Goyal
cb32a0b8fc Handle remote print callback in tests 2022-02-22 11:47:44 +05:30
Kovid Goyal
05617f7dca strip kitty markup from ps1 before checking for newlines 2022-02-22 11:40:09 +05:30
Kovid Goyal
2d2f4b9ba9 Multi-line processing of bash prompts should only be done when the prompt actually contains a newline 2022-02-21 22:54:10 +05:30
Kovid Goyal
65c7ecbc30 Test prompt drawing after screen is shrunk 2022-02-21 21:17:52 +05:30
Kovid Goyal
da5e37620e Also test changing of title when running command 2022-02-21 21:17:52 +05:30
Kovid Goyal
261057396c Fix zsh integration test on CI
Also, add various other robustness improvements to the test
2022-02-21 21:17:41 +05:30
Kovid Goyal
a43f610555 Better error msg when cursor is not changed 2022-02-21 18:23:35 +05:30
Kovid Goyal
45ae52e5d0 Fix assert ordering 2022-02-21 18:07:54 +05:30
Kovid Goyal
a565443d4a Saner tempdir removal 2022-02-21 18:05:36 +05:30
Kovid Goyal
081d6a3f16 Install various shells ON CI so we can test with them 2022-02-21 18:00:23 +05:30
Kovid Goyal
c9cc832875 Add basic tests for zsh shell integration 2022-02-21 17:57:25 +05:30
Kovid Goyal
595698d8e9 Remove dirs in HOME from PATH wen running tests 2022-02-21 16:57:11 +05:30
Kovid Goyal
b28d94ac97 Always use kitty as the python interpreter to run in the tests 2022-02-21 14:45:12 +05:30
Kovid Goyal
b9684879e7 Try to fix tests failing on CI 2022-02-21 14:24:08 +05:30
Kovid Goyal
63f974531b Framework for testing with external programs via a PTY 2022-02-21 14:08:10 +05:30
Kovid Goyal
a559210923 Retry macOS builds as well on nightly
They seem to be failing with some regularity
2022-02-21 11:18:21 +05:30
Kovid Goyal
8d36fb9edc Fix copying of selection after selection has been scrolled off history buffer raising an error
Fixes #4713
2022-02-20 13:07:31 +05:30
Kovid Goyal
f652b23169 Retry ca certs download on macOS 2022-02-20 11:09:58 +05:30
Kovid Goyal
d50a2ea288 Note that Konsole also supports the kitty graphics protocol 2022-02-20 10:59:26 +05:30
Kovid Goyal
80fc3a1faa Fix incorrect function used to get tuple size 2022-02-19 15:29:59 +05:30
Kovid Goyal
9a04405ad2 Cache the bell sound in the canberra server since it is a frequently played sound 2022-02-19 13:07:44 +05:30
Kovid Goyal
fdc9835587 Cleanup previous PR 2022-02-18 13:44:25 +05:30
Kovid Goyal
702bb2cd06 Merge branch 'fix-macos-cur-update' of https://github.com/page-down/kitty 2022-02-18 13:40:05 +05:30
pagedown
58e8609c1a macOS: Ensure the cursor is updated once
macOS will set the cursor to arrow after milliseconds after the switch
desktop animation ends. So the cursor that was updated immediately after
the focus will be changed again. This also affects toggling fullscreen.
2022-02-18 13:52:19 +08:00
pagedown
69c5c49094 Disable cursor rects 2022-02-18 13:52:00 +08:00
pagedown
ff8de7607a Also handles the case where toggleFullScreen does not get called 2022-02-18 13:50:46 +08:00
Kovid Goyal
cfd0872cea Merge branch 'feat-macos-fullscreen' of https://github.com/page-down/kitty 2022-02-17 17:41:00 +05:30
pagedown
7007f2e7fc macOS: Allow to customize global menu full screen shortcut 2022-02-17 16:31:46 +08:00
pagedown
6d0721341b Fix phantom window appears when executing full screen in transition 2022-02-17 15:47:03 +08:00
Kovid Goyal
7a156d5ef3 Also fix --hold not working if program to be executed is not found 2022-02-17 11:16:08 +05:30
Kovid Goyal
185c3320a4 Fix --hold not working with some programs that cause keyboard interrupt to be passed to python 2022-02-17 11:12:33 +05:30
Kovid Goyal
1c9cf32735 Add a few more important env vars to debug output 2022-02-17 07:45:36 +05:30
Kovid Goyal
6bfb6da0ad Make the regex for sgr stripping tighter 2022-02-15 22:33:51 +05:30
Kovid Goyal
56a5738018 Remove unused code 2022-02-15 22:22:07 +05:30
Kovid Goyal
919667129f hints kitten: Avoid double scan of input text 2022-02-15 22:21:31 +05:30
Kovid Goyal
e9f49a3292 Make ANSI sanitizer pattern re-useable 2022-02-15 22:14:14 +05:30
Kovid Goyal
711f8b024e Properly sanitize pagerhist when returning as plain text 2022-02-15 22:11:57 +05:30
Kovid Goyal
53716c084b hints kitten: Strip prompt mark escape codes 2022-02-15 22:04:10 +05:30
Kovid Goyal
2b495bcf9d Merge branch 'docs' of https://github.com/page-down/kitty 2022-02-15 15:26:50 +05:30
pagedown
d10812c6b0 Docs: Fix GitHub discussion link in commented config 2022-02-15 17:42:33 +08:00
Kovid Goyal
80202d2679 Fix show_last_command_output not working when the output is stored partially in the scrollback pager history buffer
Fixes #4435
2022-02-15 13:04:02 +05:30
Kovid Goyal
5d120a2f36 Output prompt marking when serializing to ANSI 2022-02-15 08:57:17 +05:30
Kovid Goyal
cc11326baa Linux: Do not rescale fallback fonts to match the main font cell height
Fixes #4707
2022-02-15 07:51:15 +05:30
Kovid Goyal
f63dbc0ebd clamp min_lines when mouse is grabbed 2022-02-15 07:24:12 +05:30
Kovid Goyal
88630731dc Cleanup previous PR 2022-02-14 20:21:17 +05:30
Kovid Goyal
3e2a8d80d7 Merge branch 'feat-mouse-scroll-min' of https://github.com/page-down/kitty 2022-02-14 20:12:00 +05:30
Kovid Goyal
55626e5a41 Fix symbol_map in gen-config.py 2022-02-14 19:56:41 +05:30
pagedown
7ff07f7620 Add changelog entry 2022-02-14 22:07:01 +08:00
pagedown
d2f522277c Add wheel_scroll_min_lines to set the minimum scrolling lines 2022-02-14 21:53:02 +08:00
Kovid Goyal
0248edbdb9 xkb debug events should be printed to stderr 2022-02-14 15:03:31 +05:30
Kovid Goyal
d24248e93b Merge branch 'master' of https://github.com/ichigozero/kitty 2022-02-14 14:03:45 +05:30
Gary Sentosa
dcd7890a0c Show tab name otherwise tab title for select_tab 2022-02-14 15:48:08 +09:00
Kovid Goyal
f219b10c30 Changelog entry for previous PR 2022-02-14 07:19:54 +05:30
Kovid Goyal
cc039afc1e Merge branch 'glfw-wl-touchpad-scale' of https://github.com/xsrvmy/kitty 2022-02-14 07:18:49 +05:30
Rui Ming (Max) Xiong
d2288d8f83 Scale touchpad input by display scale on wayland 2022-02-13 20:47:02 -05:00
Kovid Goyal
4b4f904aac Dont use global variables for the axis discrete counts 2022-02-13 20:56:55 +05:30
Kovid Goyal
e06b774174 Avoid double scan of output to look for escape codes 2022-02-13 17:33:13 +05:30
Kovid Goyal
8d4772f804 Update bundled pygments
Fixes #4702
2022-02-13 17:10:55 +05:30
Kovid Goyal
3be00bd712 Reduce radius for detection of double click 2022-02-13 15:51:59 +05:30
Kovid Goyal
a0e4449fb2 Ensure prefixchar is written even for empty lines 2022-02-13 15:49:46 +05:30
Kovid Goyal
ebfc19def5 Only add an SGR reset at the end if there is atleast one escape code in the ansi output 2022-02-13 15:14:32 +05:30
Kovid Goyal
a4abe26d32 Fix copy ansi to clipboard ignoring newlines 2022-02-13 15:09:14 +05:30
Kovid Goyal
5f8cb22d02 Merge branch 'fix-mouse-extra-btn' of https://github.com/page-down/kitty 2022-02-13 14:53:40 +05:30
pagedown
cafc3973ec mouse_demo: Add mouse scroll wheel buttons 2022-02-13 16:45:48 +08:00
pagedown
436a57a61e mouse_demo: Fix mouse extra button bit mask 2022-02-13 16:36:51 +08:00
Kovid Goyal
da5111a267 Cleanup mouse debug input reporting 2022-02-13 11:14:41 +05:30
Kovid Goyal
ee5cf90684 Cleanup previous PR and add changelog entry for it
Fixes #4694
2022-02-13 10:53:24 +05:30
Kovid Goyal
108974f4f7 Merge branch 'pointer-resolution' of https://github.com/xsrvmy/kitty 2022-02-13 10:45:17 +05:30
Rui Ming (Max) Xiong
79fa9f1c95 Use counters for discrete flags instead 2022-02-12 17:45:28 -05:00
Rui Ming (Max) Xiong
40bf12af7a Check for discrete mouse events on wayland 2022-02-12 02:12:59 -05:00
Kovid Goyal
263d121f3e Rescale symbol_map faces for the desired cell height
Matches treatment of fallback faces. See #4670
2022-02-12 07:57:03 +05:30
Kovid Goyal
9222d002f7 Merge branch 'docs' of https://github.com/page-down/kitty 2022-02-11 20:46:06 +05:30
pagedown
9a6aab034d Add link for packaging status 2022-02-11 23:05:12 +08:00
Kovid Goyal
b931b06941 Add mouse co-ords to debug output for move events 2022-02-11 15:09:43 +05:30
Kovid Goyal
90dc9b6fe6 ... 2022-02-11 15:00:11 +05:30
Kovid Goyal
275ede6f0a Fix a fast click, move mouse, click sequence causing the first click event to be discarded
Fix #4603
2022-02-11 14:46:37 +05:30
Kovid Goyal
9fd4f8e5c2 Merge branch 'feat-hints-qm' of https://github.com/page-down/kitty 2022-02-11 13:05:34 +05:30
Kovid Goyal
01b4654461 A new option narrow_symbols to turn off opportunistic wide rendering of private use codepoints 2022-02-11 13:04:44 +05:30
pagedown
d40e6a9ece hints kitten: Match and strip Unicode single and double quotes 2022-02-11 14:59:35 +08:00
Kovid Goyal
b2317e0f12 Merge branch 'fix-macos-shortcuts' of https://github.com/page-down/kitty 2022-02-11 12:14:55 +05:30
pagedown
444d9bd341 macOS: Ignore when the global shortcut is disabled 2022-02-11 14:23:57 +08:00
Kovid Goyal
8fbe96744b ... 2022-02-11 10:50:33 +05:30
Kovid Goyal
1c48ec7196 Allow passing a "reset" argument to resize_window 2022-02-11 08:16:17 +05:30
Kovid Goyal
c4b3bbd057 Update changelog for previous PR
Fixes #4657
2022-02-11 06:55:53 +05:30
Kovid Goyal
502ed94f31 Merge branch 'fix-macos-shortcuts' of https://github.com/page-down/kitty 2022-02-11 06:54:31 +05:30
Kovid Goyal
18ce091bfa Fix symbol/PUA glyphs loaded via symbol_map instead of as fallbacks not using following spaces to render larger versions
Fixes #4670
2022-02-11 06:36:54 +05:30
Kovid Goyal
60f675758f Fix touch_scroll_multiplier also taking effect in terminal programs such as vim that handle mouse events themselves
Fixes #4680
2022-02-11 06:22:00 +05:30
Kovid Goyal
9bfa4553a8 Shell integration: bash: Fix rendering of multiline prompts with more than two lines
Fixes #4681
2022-02-11 06:08:34 +05:30
pagedown
358f30ca7a No longer need to fall back and check the default shortcuts 2022-02-11 02:39:50 +08:00
pagedown
e32785831b macOS: Add missing global shortcuts that will be enabled by default 2022-02-11 02:19:27 +08:00
Kovid Goyal
728eceb620 Add implementation for focus moving hardcoded shortcuts 2022-02-10 18:56:47 +05:30
Kovid Goyal
57f591d1ce Simplify default global shortcut lookup
Now if a shortcut is present in the plist, it will not be considered for
default matching
2022-02-10 17:47:11 +05:30
Kovid Goyal
e4397a1c73 Framework for Apple's default global shortcuts 2022-02-10 14:15:21 +05:30
Kovid Goyal
1170cf474f A new action copy_ansi_to_clipboard to copy the current selection with ANSI formatting codes
Fixes #4665
2022-02-10 12:20:19 +05:30
Kovid Goyal
ce8b0cf748 Allow using line_as_ansi with ranges 2022-02-09 21:40:33 +05:30
Kovid Goyal
696f371aa4 Merge branch 'ci' of https://github.com/page-down/kitty 2022-02-09 19:55:05 +05:30
pagedown
8ebd514251 Update Brewfile format 2022-02-09 21:35:22 +08:00
Kovid Goyal
d3bc6001d8 Merge branch 'ksi-fish' of https://github.com/page-down/kitty 2022-02-09 10:58:25 +05:30
pagedown
f5415ca824 Restore XDG_DATA_DIRS right after fish integration script is loaded 2022-02-09 13:06:38 +08:00
Kovid Goyal
0dbe27438d Ensure CSI are stripped from menbar title as well 2022-02-09 08:07:18 +05:30
Kovid Goyal
7448789951 Improve CWD detection when there are multiple foreground processes in the TTY process group 2022-02-08 20:30:00 +05:30
Kovid Goyal
dc6138d286 Add a note about why we check _ksi_prompt_command 2022-02-08 19:52:02 +05:30
Kovid Goyal
7457f637a2 bash shell integration: Dont call _ksi_prompt_command from PROMPT_COMMAND unlessit is set 2022-02-08 19:48:30 +05:30
Kovid Goyal
c9da734c0e Show the output from failed ssh commands 2022-02-08 10:46:34 +05:30
Kovid Goyal
141c814d72 Merge branch 'docs' of https://github.com/page-down/kitty 2022-02-08 05:35:36 +05:30
Kovid Goyal
bdb98cf210 Count chars not bytes when truncating menubar title 2022-02-07 22:37:58 +05:30
pagedown
7cc3d5907f Docs: Prefer namespaced launch points 2022-02-08 01:07:23 +08:00
Kovid Goyal
3c709a28f7 ... 2022-02-07 22:08:28 +05:30
Kovid Goyal
e9ea7b13b6 macOS: Add an option to control the max length of the window title displayed in the global menubar
Fixes #2132
2022-02-07 21:57:36 +05:30
Kovid Goyal
ac16880eec Linux: Fix fontconfig alias not being used if the aliased font is dual spaced instead of monospaced
Fixes #4649
2022-02-07 21:02:47 +05:30
Kovid Goyal
c54bdd921a Allow pressing esc to exit --hold as well 2022-02-07 18:14:54 +05:30
Kovid Goyal
fc17528337 Get --hold to work even when stdin is redirected 2022-02-07 15:39:21 +05:30
Kovid Goyal
9b2db6ec53 Another alternate icon 2022-02-07 14:19:04 +05:30
Kovid Goyal
0fcfaa2a98 Report errors correctly when launching multiple URLs 2022-02-07 14:07:17 +05:30
Kovid Goyal
ba97a728f2 Reduce max remembered window size of 50K pixels 2022-02-07 12:19:32 +05:30
Kovid Goyal
a3b046fdcd Merge branch 'fix-macos-new-tab' of https://github.com/page-down/kitty 2022-02-07 12:07:17 +05:30
pagedown
ddfda3efde Fix launching a new tab when there is no OS window 2022-02-07 13:55:43 +08:00
Kovid Goyal
a2269cb66e Fix launch command not creating a new window if no OS window is present 2022-02-07 11:14:29 +05:30
Kovid Goyal
1358f00969 Raise an exception for empty url schemes 2022-02-06 21:10:41 +05:30
Kovid Goyal
eedc849c45 Cleanup previous PR 2022-02-06 20:49:40 +05:30
Kovid Goyal
89679d07ae Merge branch 'feat-macos-handle-url' of https://github.com/page-down/kitty 2022-02-06 20:29:16 +05:30
Kovid Goyal
01d866f482 Also add openable mimetypes to kitty-open.desktop 2022-02-06 20:27:50 +05:30
Kovid Goyal
73b0312dcb Merge branch 'minor-tweaks' of https://github.com/page-down/kitty 2022-02-06 20:14:26 +05:30
Kovid Goyal
f9d9fe6db4 Shell integration: bash: Dont fail if an existing PROMPT_COMMAND ends with a semi-colon
Fixes #4645
2022-02-06 19:02:39 +05:30
pagedown
e31ca68875 macOS: Add a way to set kitty as the default handler for the URL schemes 2022-02-06 19:51:39 +08:00
pagedown
8ae273ee3a Simplify reassembling for the command 2022-02-06 19:24:02 +08:00
pagedown
73a197fcde Add kitty-launcher.desktop to handle URL scheme for Linux 2022-02-06 19:12:57 +08:00
pagedown
648bff02b5 Only remove the initial window if no startup session is configured 2022-02-06 18:58:52 +08:00
pagedown
7740bc138b Add changelog entry for previous PR 2022-02-06 18:58:40 +08:00
pagedown
f5337096d5 Rename LAUNCH_URL to LAUNCH_URLS 2022-02-06 18:57:40 +08:00
Kovid Goyal
dddff91fad Linux: Fix rendering of emoji when using scalable fonts such as Segoe UI Emoji 2022-02-06 13:31:11 +05:30
Kovid Goyal
f047678711 More robust initial window replacement when launching URLs 2022-02-06 07:11:57 +05:30
Kovid Goyal
aa8b23395f Merge branch 'feat-macos-open-with' of https://github.com/page-down/kitty 2022-02-06 06:53:36 +05:30
Kovid Goyal
7c36c19de0 Cleanup previous PR 2022-02-06 06:31:56 +05:30
Kovid Goyal
49183ced3b Merge branch 'fix-launch-cmd-args' of https://github.com/page-down/kitty 2022-02-06 06:31:22 +05:30
pagedown
007e9697c4 macOS: Add Open with kitty Service
There are still some file types that cannot be matched.
Add a standalone Open With service.
Users can configure shortcut for this service in System Preferences.
2022-02-05 23:20:19 +08:00
pagedown
9989edbe42 macOS: Add archive file type to info plist
List kitty in the Open With menu for archive files.
E.g. compressed files or disk image files
2022-02-05 23:19:33 +08:00
Kovid Goyal
9742e2ec48 Make mypy happy 2022-02-05 19:53:16 +05:30
pagedown
b47711e23a Fix kitty_exe() being executed when the script doesn't have shebang 2022-02-05 20:28:06 +08:00
pagedown
3dfdb3ac89 Fix missing args when running a script without exec perms via launch 2022-02-05 20:28:06 +08:00
Kovid Goyal
4d3e2a07d1 Cleanup previous PR 2022-02-05 17:45:46 +05:30
Kovid Goyal
a42eb3e643 Merge branch 'feat-lines-on-scr' of https://github.com/page-down/kitty
Fixes #4462
2022-02-05 17:44:21 +05:30
Kovid Goyal
39c77a9486 Allow running the launch actions by invoking the kitty executable with +open 2022-02-05 17:39:48 +05:30
pagedown
e22546c56a Add arguments to get the first and last line of text for launch command 2022-02-05 20:02:24 +08:00
Kovid Goyal
ced61096df Add some docs for the previous PR 2022-02-05 12:57:15 +05:30
Kovid Goyal
c0ea127810 Merge branch 'feat-macos-open-url' of https://github.com/page-down/kitty 2022-02-05 12:46:39 +05:30
Kovid Goyal
29b1e3fa46 Sanity check for main() args 2022-02-05 11:56:18 +05:30
Kovid Goyal
b2faa0d9f7 Fix #4637 2022-02-05 11:35:40 +05:30
pagedown
ddb9b67de3 Restrict custom launch actions to be prefixed with kitty:///launch/
Preserve the kitty URL scheme for further use.
2022-02-05 13:23:41 +08:00
pagedown
ffbc533565 Add ftps, ircs, kitty, sftp, and ssh to url_prefixes 2022-02-05 13:10:21 +08:00
pagedown
206e490491 macOS: Add common URL schemes to info plist 2022-02-05 13:09:13 +08:00
pagedown
135066f38c Add default launch action for ssh links 2022-02-05 13:05:58 +08:00
pagedown
51d591e177 macOS: Allow opening URLs with kitty 2022-02-05 13:05:41 +08:00
pagedown
80a62c8d71 Add preamble to generated file and fix gen-config.py filename 2022-02-05 13:05:13 +08:00
Kovid Goyal
90b54c5f7f Get build-dep working again for changes to bypy 2022-02-05 10:27:47 +05:30
Kovid Goyal
99c81e6858 Bump bundled version of wayland 2022-02-05 07:56:40 +05:30
Kovid Goyal
8fcb6278d7 Cleanup previous PR 2022-02-04 20:38:32 +05:30
Kovid Goyal
89e0abd41d Merge branch 'feat-reverse-scroll' of https://github.com/page-down/kitty 2022-02-04 20:34:37 +05:30
pagedown
a1a0c9ab80 Add new mappable action scroll_prompt_to_bottom 2022-02-04 22:52:06 +08:00
Kovid Goyal
9ab2a38d5c ... 2022-02-04 18:26:36 +05:30
Kovid Goyal
b5676a53ee Use a special role for actions documentation 2022-02-04 18:24:39 +05:30
Kovid Goyal
94898a8758 fix incorrect use of :ac: 2022-02-04 17:54:32 +05:30
Kovid Goyal
97a6a11066 Capitalization of menu action 2022-02-04 16:12:46 +05:30
Kovid Goyal
9fe22a5c27 Do a single pass over the cell vertex shader to set the defines 2022-02-04 14:34:41 +05:30
Kovid Goyal
f9b35c71d7 Another place to use NUM_UNDERLINE_STYLES 2022-02-04 14:16:31 +05:30
Kovid Goyal
6b47f6f769 Make adding new underline styles easier in the future 2022-02-04 14:05:19 +05:30
Kovid Goyal
37d8483728 Fix a regression in the previous release that broke strikethrough 2022-02-04 13:37:38 +05:30
Kovid Goyal
4776a9e785 When moving text into scrollback upto cursor actually move up to the prompt the cursor is at 2022-02-04 13:19:24 +05:30
Kovid Goyal
50bc5b0302 macOS: Add the default shortcut cmd+k to clear the terminal screen and scrollback up to the cursor
Fixes #4625
2022-02-04 11:58:52 +05:30
Kovid Goyal
7d496f20a1 Add changelog entry for previous PR 2022-02-04 11:43:57 +05:30
Kovid Goyal
42fbd0a1af A new action to clear scrollback+screen upto the line with the cursor 2022-02-04 11:42:42 +05:30
Kovid Goyal
8960bfebc0 ... 2022-02-04 11:41:45 +05:30
Kovid Goyal
b1209c1e7a Be explicit about python3 2022-02-04 11:19:51 +05:30
Kovid Goyal
365583efd7 Clarify SI docs a little
Fixes #4626
2022-02-04 10:51:41 +05:30
Kovid Goyal
926c2d71ee Merge branch 'fix-macos-ime' of https://github.com/page-down/kitty
Fixes #4621
2022-02-04 10:46:43 +05:30
pagedown
ce57c747cc macOS: Fix select next input source not working
For global shortcut to switch the next input source in the macOS system
preferences, final confirmation is only given when all modifier keys are
released.
2022-02-04 12:51:01 +08:00
Kovid Goyal
3e9129655a ... 2022-02-03 21:00:23 +05:30
Kovid Goyal
12f41f30b3 Only show important env vars in debug output
Also show their values since these are not sensitive
2022-02-03 20:43:23 +05:30
Kovid Goyal
1e7edd0218 ... 2022-02-03 20:14:52 +05:30
Kovid Goyal
2fd8ef389e Fix use-python not working reliably with SSH kitten 2022-02-03 19:58:10 +05:30
Kovid Goyal
53589c3954 ssh kitten: Fix location of generated terminfo files on NetBSD
Fixes #4622
2022-02-03 19:51:37 +05:30
Kovid Goyal
34a0218f35 Fix a regression in the previous release that broke active_tab_foreground
Fixes #4620
2022-02-03 18:12:42 +05:30
98 changed files with 2554 additions and 834 deletions

View File

@@ -30,13 +30,17 @@ def install_deps():
print('Installing kitty dependencies...')
sys.stdout.flush()
if is_macos:
items = (x.strip() for x in open('Brewfile').readlines() if not x.startswith('#'))
run('brew', 'install', *items)
items = (x.split()[1].strip('"') for x in open('Brewfile').readlines() if x.strip().startswith('brew '))
run('brew', 'install', 'fish', *items)
else:
run('sudo apt-get update')
run('sudo apt-get install -y libgl1-mesa-dev libxi-dev libxrandr-dev libxinerama-dev ca-certificates'
' libxcursor-dev libxcb-xkb-dev libdbus-1-dev libxkbcommon-dev libharfbuzz-dev libx11-xcb-dev'
' libpng-dev liblcms2-dev libfontconfig-dev libxkbcommon-x11-dev libcanberra-dev librsync-dev uuid-dev')
' libxcursor-dev libxcb-xkb-dev libdbus-1-dev libxkbcommon-dev libharfbuzz-dev libx11-xcb-dev zsh'
' libpng-dev liblcms2-dev libfontconfig-dev libxkbcommon-x11-dev libcanberra-dev librsync-dev uuid-dev'
' zsh bash dash')
# for some reason these directories are world writable which causes zsh
# compinit to break
run('sudo chmod -R og-w /usr/share/zsh')
if is_bundle:
install_bundle()
else:

View File

@@ -27,6 +27,7 @@ jobs:
uses: github/codeql-action/init@v1
with:
languages: python, c
setup-python-dependencies: false
- name: Build kitty
run: python3 .github/workflows/ci.py build

View File

@@ -1,7 +1,7 @@
pkg-config
zlib
librsync
python
imagemagick
harfbuzz
sphinx-doc
brew "pkg-config"
brew "zlib"
brew "librsync"
brew "python"
brew "imagemagick"
brew "harfbuzz"
brew "sphinx-doc"

View File

@@ -11,4 +11,4 @@ https://www.reddit.com/r/KittyTerminal[Reddit community]
Packaging status in various repositories:
image:https://repology.org/badge/vertical-allrepos/kitty.svg[https://repology.org/project/kitty/versions]
image:https://repology.org/badge/vertical-allrepos/kitty.svg["Packaging status", link="https://repology.org/project/kitty/versions"]

View File

@@ -31,7 +31,15 @@ def runpy(args: List[str]) -> None:
def hold(args: List[str]) -> None:
import subprocess
ret = subprocess.Popen(args[1:]).wait()
ret = 1
try:
ret = subprocess.Popen(args[1:]).wait()
except KeyboardInterrupt:
pass
except FileNotFoundError:
print(f'Could not find {args[1]!r} to execute', file=sys.stderr)
except Exception as e:
print(e, file=sys.stderr)
from kitty.utils import hold_till_enter
hold_till_enter()
raise SystemExit(ret)
@@ -42,6 +50,13 @@ def complete(args: List[str]) -> None:
complete_main(args[1:], entry_points, namespaced_entry_points)
def open_urls(args: List[str]) -> None:
setattr(sys, 'cmdline_args_for_open', True)
sys.argv = ['kitty'] + args[1:]
from kitty.main import main as kitty_main
kitty_main()
def launch(args: List[str]) -> None:
import runpy
sys.argv = args[1:]
@@ -129,6 +144,7 @@ namespaced_entry_points['hold'] = hold
namespaced_entry_points['complete'] = complete
namespaced_entry_points['runpy'] = runpy
namespaced_entry_points['launch'] = launch
namespaced_entry_points['open'] = open_urls
namespaced_entry_points['kitten'] = run_kitten
namespaced_entry_points['edit-config'] = edit_config_file
namespaced_entry_points['shebang'] = shebang

View File

@@ -35,7 +35,8 @@ def binary_includes():
'expat', 'sqlite3', 'ffi', 'z', 'lzma', 'png16', 'lcms2', 'crypt',
'iconv', 'pcre', 'graphite2', 'glib-2.0', 'freetype', 'rsync',
'harfbuzz', 'xkbcommon', 'xkbcommon-x11',
'ncursesw', 'readline', 'brotlicommon', 'brotlienc', 'brotlidec'
'ncursesw', 'readline', 'brotlicommon', 'brotlienc', 'brotlidec',
'wayland-client', 'wayland-cursor',
))) + (
get_dll_path('bz2', 2), get_dll_path('ssl', 2), get_dll_path('crypto', 2),
get_dll_path(f'python{py_ver}', 2),

View File

@@ -185,7 +185,16 @@ class Freeze(object):
def add_ca_certs(self):
print('\nDownloading CA certs...')
from urllib.request import urlopen
cdata = urlopen(kitty_constants['cacerts_url']).read()
cdata = None
for i in range(5):
try:
cdata = urlopen(kitty_constants['cacerts_url']).read()
break
except Exception as e:
print(f'Downloading CA certs failed with error: {e}, retrying...')
if cdata is None:
raise SystemExit('Downloading C certs failed, giving up')
dest = join(self.contents_dir, 'Resources', 'cacert.pem')
with open(dest, 'wb') as f:
f.write(cdata)

View File

@@ -166,8 +166,8 @@
{
"name": "pygments",
"unix": {
"filename": "Pygments-2.7.4.tar.gz",
"hash": "sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337",
"filename": "Pygments-2.11.2.tar.gz",
"hash": "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a",
"urls": ["pypi"]
}
},
@@ -273,8 +273,8 @@
"name": "wayland",
"os": "linux",
"unix": {
"filename": "wayland-1.19.0.tar.xz",
"hash": "sha256:baccd902300d354581cd5ad3cc49daa4921d55fb416a5883e218750fef166d15",
"filename": "wayland-1.20.0.tar.xz",
"hash": "sha256:b8a034154c7059772e0fdbd27dbfcda6c732df29cae56a82274f6ec5d7cd8725",
"urls": ["https://wayland.freedesktop.org/releases/{filename}"]
}
},
@@ -283,8 +283,8 @@
"name": "wayland-protocols",
"os": "linux",
"unix": {
"filename": "wayland-protocols-1.20.tar.xz",
"hash": "sha256:9782b7a1a863d82d7c92478497d13c758f52e7da4f197aa16443f73de77e4de7",
"filename": "wayland-protocols-1.25.tar.xz",
"hash": "sha256:f1ff0f7199d0a0da337217dd8c99979967808dc37731a1e759e822b75b571460",
"urls": ["https://wayland.freedesktop.org/releases/{filename}"]
}
}

View File

@@ -53,8 +53,10 @@ particular desktop, but it should work for most major desktop environments.
ln -s ~/.local/kitty.app/bin/kitty ~/.local/bin/
# Place the kitty.desktop file somewhere it can be found by the OS
cp ~/.local/kitty.app/share/applications/kitty.desktop ~/.local/share/applications/
# Update the path to the kitty icon in the kitty.desktop file
sed -i "s|Icon=kitty|Icon=/home/$USER/.local/kitty.app/share/icons/hicolor/256x256/apps/kitty.png|g" ~/.local/share/applications/kitty.desktop
# If you want to open text files and images in kitty via your file manager also add the kitty-open.desktop file
cp ~/.local/kitty.app/share/applications/kitty-open.desktop ~/.local/share/applications/
# Update the path to the kitty icon in the kitty.desktop file(s)
sed -i "s|Icon=kitty|Icon=/home/$USER/.local/kitty.app/share/icons/hicolor/256x256/apps/kitty.png|g" ~/.local/share/applications/kitty*.desktop
.. note::
If you use the venerable `stow <https://www.gnu.org/software/stow/>`_

View File

@@ -173,7 +173,7 @@ servers into which they ssh, without needing to install all of |kitty|.
``python -m pip -r docs/requirements.txt``.
This applies to creating packages for |kitty| for macOS package managers such as
brew or MacPorts as well.
Homebrew or MacPorts as well.
Changing defaults for packages
@@ -190,13 +190,11 @@ update-checking
shell-integration
|kitty| by default injects its :ref:`shell_integration` code into the user's
shell using environment variables or (for bash only) modifying
the user's :file:`~/.bashrc` file.
For a package, it might make more sense to distribute the shell
integration scripts into the system-wide shell vendor locations. The
shell integration files are found in the :file:`shell-integration`
directory. Copy them to the system wide shell vendor locations for each
shell, and use::
shell using environment variables. For a package, it might make more sense
to distribute the shell integration scripts into the system-wide shell
vendor locations. The shell integration files are found in the
:file:`shell-integration` directory. Copy them to the system wide shell
vendor locations for each shell, and use::
./setup.py linux-package --shell-integration=enabled\ no-rc

View File

@@ -75,6 +75,110 @@ command.
Detailed list of changes
-------------------------------------
0.24.4 [2022-03-03]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Shell integration: Fix the default Bash :code:`$HISTFILE` changing to :file:`~/.sh_history` instead of :file:`~/.bash_history` (:iss:`4765`)
- Linux binaries: Fix binaries not working on systems with older Wayland client libraries (:iss:`4760`)
- Fix a regression in the previous release that broke kittens launched with :code:`STDIN` not connected to a terminal (:iss:`4763`)
- Wayland: Fix surface configure events not being acknowledged before commit
the resized buffer (:pull:`4768`)
0.24.3 [2022-02-28]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Bash integration: No longer modify :file:`~/.bashrc` to load :ref:`shell integration <shell_integration>`.
It is recommended to remove the lines used to load the shell integration from :file:`~/.bashrc` as they are no-ops.
- macOS: Allow kitty to handle various URL types. Can be configured via
:ref:`launch_actions` (:pull:`4618`)
- macOS: Add a new service ``Open with kitty`` to open file types that are not
recognized by the system (:pull:`4641`)
- Splits layout: A new value for :option:`launch --location` to auto-select the split axis when splitting existing windows.
Wide windows are split side-by-side and tall windows are split one-above-the-other
- hints kitten: Fix a regression that broke recognition of path:linenumber:colnumber (:iss:`4675`)
- Fix a regression in the previous release that broke :opt:`active_tab_foreground` (:iss:`4620`)
- Fix :ac:`show_last_command_output` not working when the output is stored
partially in the scrollback pager history buffer (:iss:`4435`)
- When dropping URLs/files onto kitty at a shell prompt insert them appropriately quoted and space
separated (:iss:`4734`)
- Improve CWD detection when there are multiple foreground processes in the TTY process group
- A new option :opt:`narrow_symbols` to turn off opportunistic wide rendering of private use codepoints
- ssh kitten: Fix location of generated terminfo files on NetBSD (:iss:`4622`)
- A new action to clear the screen up to the line containing the cursor, see
:ac:`clear_terminal`
- A new action :ac:`copy_ansi_to_clipboard` to copy the current selection with ANSI formatting codes
(:iss:`4665`)
- Linux: Do not rescale fallback fonts to match the main font cell height, instead just
set the font size and let FreeType take care of it. This matches
rendering on macOS (:iss:`4707`)
- macOS: Fix a regression in the previous release that broke switching input
sources by keyboard (:iss:`4621`)
- macOS: Add the default shortcut :kbd:`cmd+k` to clear the terminal screen and
scrollback up to the cursor (:iss:`4625`)
- Fix a regression in the previous release that broke strikethrough (:disc:`4632`)
- A new action :ac:`scroll_prompt_to_bottom` to move the current prompt
to the bottom, filling in the window from the scrollback (:pull:`4634`)
- Add two special arguments ``@first-line-on-screen`` and ``@last-line-on-screen``
for the :doc:`launch <launch>` command to be used for pager positioning.
(:iss:`4462`)
- Linux: Fix rendering of emoji when using scalable fonts such as Segoe UI Emoji
- Shell integration: bash: Dont fail if an existing PROMPT_COMMAND ends with a semi-colon (:iss:`4645`)
- Shell integration: bash: Fix rendering of multiline prompts with more than two lines (:iss:`4681`)
- Shell integration: fish: Check fish version 3.3.0+ and exit on outdated versions (:pull:`4745`)
- Shell integration: fish: Fix pipestatus being overwritten (:pull:`4756`)
- Linux: Fix fontconfig alias not being used if the aliased font is dual spaced instead of monospaced (:iss:`4649`)
- macOS: Add an option :opt:`macos_menubar_title_max_length` to control the max length of the window title displayed in the global menubar (:iss:`2132`)
- Fix :opt:`touch_scroll_multiplier` also taking effect in terminal programs such as vim that handle mouse events themselves (:iss:`4680`)
- Fix symbol/PUA glyphs loaded via :opt:`symbol_map` instead of as fallbacks not using following spaces to render larger versions (:iss:`4670`)
- macOS: Fix regression in previous release that caused Apple's global shortcuts to not work if they had never been configured on a particular machine (:iss:`4657`)
- Fix a fast *click, move mouse, click* sequence causing the first click event to be discarded (:iss:`4603`)
- Wayland: Fix wheel mice with line based scrolling being incorrectly handled as high precision devices (:iss:`4694`)
- Wayland: Fix touchpads and high resolution wheels not scrolling at the same speed on monitors with different scales (:iss:`4703`)
- Add an option :opt:`wheel_scroll_min_lines` to set the minimum number of lines for mouse wheel scrolling when using a mouse with a wheel that generates very small offsets when slow scrolling (:pull:`4710`)
- macOS: Make the shortcut to toggle full screen configurable (:pull:`4714`)
- macOS: Fix the mouse cursor being set to arrow after switching desktops or toggling full screen (:pull:`4716`)
- Fix copying of selection after selection has been scrolled off history buffer raising an error (:iss:`4713`)
0.24.2 [2022-02-03]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -420,7 +524,7 @@ Detailed list of changes
0.22.0 [2021-07-26]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Add a new :ref:`action-toggle_layout` action to easily zoom/unzoom a window
- Add a new :ac:`toggle_layout` action to easily zoom/unzoom a window
- When right clicking to extend a selection, move the nearest selection
boundary rather than the end of the selection. To restore previous behavior

View File

@@ -399,6 +399,12 @@ def parse_shortcut_node(env: Any, sig: str, signode: Any) -> str:
return sig
def parse_action_node(env: Any, sig: str, signode: Any) -> str:
"""Transform an action description into RST nodes."""
signode += addnodes.desc_name(sig, sig)
return sig
def process_opt_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
conf_name, opt = target.partition('.')[::2]
if not opt:
@@ -407,6 +413,10 @@ def process_opt_link(env: Any, refnode: Any, has_explicit_title: bool, title: st
return title, opt_aliases.get(full_name, full_name)
def process_action_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
return title, target
def process_shortcut_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
conf_name, slug = target.partition('.')[::2]
if not slug:
@@ -444,6 +454,15 @@ def write_conf_docs(app: Any, all_kitten_names: Iterable[str]) -> None:
sc_role.process_link = process_shortcut_link
shortcut_slugs.clear()
app.add_object_type(
'action', 'ac',
indextemplate="pair: %s; Action",
parse_node=parse_action_node,
)
ac_role = app.registry.domain_roles['std']['ac']
ac_role.warn_dangling = True
ac_role.process_link = process_action_link
def generate_default_config(definition: Definition, name: str) -> None:
with open(f'generated/conf-{name}.rst', 'w', encoding='utf-8') as f:
print('.. highlight:: conf\n', file=f)

View File

@@ -6,21 +6,22 @@ Frequently Asked Questions
Some special symbols are rendered small/truncated in kitty?
-----------------------------------------------------------
The number of cells a unicode character takes up are controlled by the unicode
standard. All characters are rendered in a single cell unless the unicode
The number of cells a Unicode character takes up are controlled by the Unicode
standard. All characters are rendered in a single cell unless the Unicode
standard says they should be rendered in two cells. When a symbol does not fit,
it will either be rescaled to be smaller or truncated (depending on how much
extra space it needs). This is often different from other terminals which just
let the character overflow into neighboring cells, which is fine if the
neighboring cell is empty, but looks terrible if it is not.
Some programs, like powerline, vim with fancy gutter symbols/status-bar, etc.
misuse unicode characters from the private use area to represent symbols. Often
these symbols are square and should be rendered in two cells. However, since
private use area symbols all have their width set to one in the unicode
Some programs, like Powerline, vim with fancy gutter symbols/status-bar, etc.
use Unicode characters from the private use area to represent symbols. Often
these symbols are wide and should be rendered in two cells. However, since
private use area symbols all have their width set to one in the Unicode
standard, |kitty| renders them either smaller or truncated. The exception is if
these characters are followed by a space or empty cell in which case kitty
makes use of the extra cell to render them in two cells.
makes use of the extra cell to render them in two cells. This behavior can be
turned off for specific symbols using :opt:`narrow_symbols`.
Using a color theme with a background color does not work well in vim?
@@ -202,13 +203,14 @@ bitmapped fonts.
.. note::
If you are trying to use a font patched with NERD font symbols, dont do that
as patching destroys fonts. There is no need, simply install the standalone
NERD font, kitty should pick up symbols from it automatically, and you can
tell it to do so explicitly in case it doesnt with the :opt:`symbol_map`
directive::
NERD font (the file :file:`NerdFontsSymbolsOnly.zip` from the `NERD font
releases page <https://github.com/ryanoasis/nerd-fonts/releases>`__). kitty
should pick up symbols from it automatically, and you can tell it to do so
explicitly in case it doesnt with the :opt:`symbol_map` directive::
symbol_map U+23FB-U+23FE,U+2B58,U+E200-U+E2A9,U+E0A0-U+E0A3,U+E0B0-U+E0BF,U+E0C0-U+E0C8,U+E0CC-U+E0CF,U+E0D0-U+E0D2,U+E0D4,U+E700-U+E7C5,U+F000-U+F2E0,U+2665,U+26A1,U+F400-U+F4A8,U+F67C,U+E000-U+E00A,U+F300-U+F313,U+E5FA-U+E62B Symbols Nerd Font
symbol_map U+23FB-U+23FE,U+2665,U+26A1,U+2B58,U+E000-U+E00A,U+E0A0-U+E0A3,U+E0B0-U+E0C8,U+E0CA,U+E0CC-U+E0D2,U+E0D4,U+E200-U+E2A9,U+E300-U+E3E3,U+E5FA-U+E62F,U+E700-U+E7C5,U+F000-U+F2E0,U+F300-U+F31C,U+F400-U+F4A9,U+F500-U+F8FF Symbols Nerd Font
If your font is not listed in ``kitty list-fonts`` it means that it is not
If your font is not listed in ``kitty +list-fonts`` it means that it is not
monospace or is a bitmapped font. On Linux you can list all monospace fonts with::
fc-list : family spacing outline scalable | grep -e spacing=100 -e spacing=90 | grep -e outline=True | grep -e scalable=True
@@ -236,7 +238,7 @@ command to rebuild your fontconfig cache::
fc-cache -r
Then, the font will be available in ``kitty list-fonts``.
Then, the font will be available in ``kitty +list-fonts``.
How can I assign a single global shortcut to bring up the kitty terminal?
@@ -270,6 +272,10 @@ homepage:
:target: https://github.com/hristost/kitty-alternative-icon
:width: 256
.. image:: https://github.com/igrmk/whiskers/raw/main/whiskers.svg
:target: https://github.com/igrmk/whiskers
:width: 256
On macOS you can change the icon by following the steps:
#. Find :file:`kitty.app` in the Applications folder, select it and press :kbd:`⌘+i`

View File

@@ -46,6 +46,7 @@ Some programs and libraries that use the kitty graphics protocol:
Other terminals that have implemented the graphics protocol:
* `WezTerm <https://github.com/wez/wezterm/issues/986>`_
* `Konsole <https://invent.kde.org/utilities/konsole/-/merge_requests/594>`_
Getting the window size

View File

@@ -134,7 +134,7 @@ terminal program, you can tell the kittens system to run the
For example, here is a kitten that "zooms/unzooms" the current terminal window
by switching to the stack layout or back to the previous layout. This is
equivalent to the builtin :ref:`action-toggle_layout` action.
equivalent to the builtin :ac:`toggle_layout` action.
Create a file in the kitty config folder, :file:`~/.config/kitty/zoom_toggle.py`

View File

@@ -95,6 +95,12 @@ the command line:
``@cursor-y``
replaced by the current cursor y position with 1 being the topmost cell
``@first-line-on-screen``
replaced by the first line on screen. Can be used for pager positioning.
``@last-line-on-screen``
replaced by the last line on screen. Can be used for pager positioning.
For example::

View File

@@ -138,6 +138,11 @@ define a few extra key bindings in :file:`kitty.conf`::
# the two windows are placed side by side
map F6 launch --location=vsplit
# Create a new window splitting the space used by the existing one so that
# the two windows are placed side by side if the existing window is wide or
# one above the other if the existing window is tall.
map F4 launch --location=split
# Rotate the current split, chaging its split axis from vertical to
# horizontal or vice versa
map F7 layout_action rotate
@@ -248,6 +253,8 @@ action, for example::
map ctrl+right resize_window wider
map ctrl+up resize_window taller
map ctrl+down resize_window shorter 3
# reset all windows in the tab to default sizes
map ctrl+home resize_window reset
The ``resize_window`` action has a second, optional argument to control
the resizing increment (a positive integer that defaults to 1).

View File

@@ -102,14 +102,38 @@ lines. The various available criteria are:
``image-??.png``
.. _launch_actions:
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
On macOS you can use :guilabel:`Open With` in Finder or drag and drop files and
URLs onto the kitty dock icon to open them with kitty. The default actions are:
* Open text files in your editor and images using the icat kitten.
* Run shell scripts in a shell
* Open SSH urls using the ssh command
These actions can also be executed from the command line by running::
open -a kitty.app file_or_url ... (on macOS only)
or
kitty +open file_or_url ...
Since macOS lacks an official interface to set default URL scheme handlers,
kitty has a command you can use for it. The first argument for is the URL
scheme, and the second optional argument is the bundle id of the app, which
defaults to kitty, if not specified. For example:
.. code-block:: sh
# Set kitty as the handler for ssh:// URLs
kitty +runpy 'from kitty.fast_data_types import cocoa_set_url_handler; import sys; cocoa_set_url_handler(*sys.argv[1:]); print("OK")' ssh
# Set someapp as the handler for xyz:// URLs
kitty +runpy 'from kitty.fast_data_types import cocoa_set_url_handler; import sys; cocoa_set_url_handler(*sys.argv[1:]); print("OK")' xyz someapp.bundle.identifier
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
@@ -138,3 +162,7 @@ the :file:`open-actions.conf` file above. For example:
protocol file
mime image/*
action launch --type=os-window kitty +kitten icat --hold $FILE_PATH
# Open ssh URLs with ssh command
protocol ssh
action launch --type=os-window ssh $URL

View File

@@ -1,5 +1,18 @@
Press mentions of kitty
========================
`Console #88 <https://console.substack.com/p/console-88>`__
`Python Bytes 272 <https://youtu.be/8HKliSbA-gQ?t=815>`__ (Feb 2022)
A podcast demoing some of kitty's coolness
`Console #88 <https://console.substack.com/p/console-88>`__ (Jan 2022)
An interview with Kovid about kitty
Video reviews
--------------
`Review (Jan 2021) <https://www.youtube.com/watch?v=TTzP2zYJn2k>`__
A kitty review by distrotube
`Review (Dec 2020) <https://www.youtube.com/watch?v=KUMkLhFeBrI>`__
A kitty review/intro by TechHut

View File

@@ -48,11 +48,16 @@ off or it can be disabled entirely as well. The :opt:`shell_integration` option
takes a space separated list of keywords:
disabled
Turn off all shell integration
Turn off all shell integration. The shell's launch environment is not
modified and :envvar:`KITTY_SHELL_INTEGRATION` is not set. Useful for
:ref:`manual integration <manual_shell_integration>`.
no-rc
Do not modify the shell's launch environment to enable integration. Useful if you prefer
to :ref:`manually enable integration <manual_shell_integration>`.
to load the kitty shell integration code yourself, either as part of
:ref:`manually integration <manual_shell_integration>` or because you have
some other software that sets up shell integration.
This will still set the :envvar:`KITTY_SHELL_INTEGRATION` environment variable when kitty runs the shell.
no-cursor
Turn off changing of the text cursor to a bar when editing text
@@ -88,7 +93,7 @@ to be copied.
The feature to jump to previous prompts (
:sc:`scroll_to_previous_prompt` and :sc:`scroll_to_next_prompt`) and mouse
actions (:ref:`action-mouse_select_command_output` and :ref:`action-mouse_show_command_output`) can
actions (:ac:`mouse_select_command_output` and :ac:`mouse_show_command_output`) can
be integrated with browsing command output as well. For example, define the
following mapping in :file:`kitty.conf`:
@@ -137,14 +142,18 @@ different shells.
For fish, to make it automatically load the integration code provided by
kitty, the integration script directory path is prepended to the
:code:`XDG_DATA_DIRS` environment variable. This is only applied to the fish
``XDG_DATA_DIRS`` environment variable. This is only applied to the fish
process and will be cleaned up by the integration script after startup. No files
are added or modified.
.. tab:: bash
For bash, kitty adds a couple of lines to the bottom of :file:`~/.bashrc`
(in an atomic manner) to load the shell integration code.
For bash, kitty starts bash in POSIX mode, using the environment variable
``ENV`` to load the shell integrtion script. This prevents bash from
loading any startup files itself. The loading of the startup files is done
by the integration script, after disabling POSIX mode. From the
perspective of those scripts there should be no difference to running
vanilla bash.
Then, when launching the shell, kitty sets the environment variable

View File

@@ -358,32 +358,49 @@ static GLFWapplicationwillfinishlaunchingfun finish_launching_callback = NULL;
finish_launching_callback();
}
- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename {
(void)theApplication;
if (!filename || !_glfw.ns.file_open_callback) return NO;
const char *path = NULL;
- (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename {
(void)sender;
if (!filename || !_glfw.ns.url_open_callback) return NO;
const char *url = NULL;
@try {
path = [[NSFileManager defaultManager] fileSystemRepresentationWithPath: filename];
url = [[[NSURL fileURLWithPath:filename] absoluteString] UTF8String];
} @catch(NSException *exc) {
NSLog(@"Converting openFile filename: %@ failed with error: %@", filename, exc.reason);
return NO;
}
if (!path) return NO;
return _glfw.ns.file_open_callback(path);
if (!url) return NO;
return _glfw.ns.url_open_callback(url);
}
- (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames {
(void)sender;
if (!_glfw.ns.file_open_callback || !filenames) return;
if (!_glfw.ns.url_open_callback || !filenames) return;
for (id x in filenames) {
NSString *filename = x;
const char *path = NULL;
const char *url = NULL;
@try {
path = [[NSFileManager defaultManager] fileSystemRepresentationWithPath: filename];
url = [[[NSURL fileURLWithPath:filename] absoluteString] UTF8String];
} @catch(NSException *exc) {
NSLog(@"Converting openFiles filename: %@ failed with error: %@", filename, exc.reason);
}
if (path) _glfw.ns.file_open_callback(path);
if (url) _glfw.ns.url_open_callback(url);
}
}
// Remove openFile and openFiles when the minimum supported macOS version is 10.13
- (void)application:(NSApplication *)sender openURLs:(NSArray<NSURL *> *)urls
{
(void)sender;
if (!_glfw.ns.url_open_callback || !urls) return;
for (id x in urls) {
NSURL *ns_url = x;
const char *url = NULL;
@try {
url = [[ns_url absoluteString] UTF8String];
} @catch(NSException *exc) {
NSLog(@"Converting openURLs url: %@ failed with error: %@", ns_url, exc.reason);
}
if (url) _glfw.ns.url_open_callback(url);
}
}
@@ -391,7 +408,6 @@ static GLFWapplicationwillfinishlaunchingfun finish_launching_callback = NULL;
{
(void)notification;
[NSApp stop:nil];
if (_glfw.ns.file_open_callback) _glfw.ns.file_open_callback(":cocoa::application launched::");
CGDisplayRegisterReconfigurationCallback(display_reconfigured, NULL);
_glfwCocoaPostEmptyEvent();
@@ -563,7 +579,10 @@ is_shiftable_shortcut(int scv) {
static void
build_global_shortcuts_lookup(void) {
// dump these in a terminal with: defaults read com.apple.symbolichotkeys
NSMutableDictionary<NSString*, NSNumber*> *temp = [NSMutableDictionary dictionaryWithCapacity:128]; // will be autoreleased
NSMutableSet<NSNumber*> *temp_configured = [NSMutableSet setWithCapacity:128]; // will be autoreleased
NSMutableSet<NSNumber*> *temp_missing_value = [NSMutableSet setWithCapacity:128]; // will be autoreleased
NSDictionary *apple_settings = [[NSUserDefaults standardUserDefaults] persistentDomainForName:@"com.apple.symbolichotkeys"];
if (apple_settings) {
NSDictionary<NSString*, id> *symbolic_hotkeys = [apple_settings objectForKey:@"AppleSymbolicHotKeys"];
@@ -574,9 +593,14 @@ build_global_shortcuts_lookup(void) {
NSInteger sc = [key integerValue];
NSDictionary *sc_value = obj;
id enabled = [sc_value objectForKey:@"enabled"];
if (!enabled || ![enabled isKindOfClass:[NSNumber class]] || ![(NSNumber*)enabled boolValue]) continue;
if (!enabled || ![enabled isKindOfClass:[NSNumber class]]) continue;
[temp_configured addObject:@(sc)];
if (![enabled boolValue]) continue;
id v = [sc_value objectForKey:@"value"];
if (!v || ![v isKindOfClass:[NSDictionary class]]) continue;
if (!v || ![v isKindOfClass:[NSDictionary class]]) {
if ([enabled boolValue]) [temp_missing_value addObject:@(sc)];
continue;
}
NSDictionary *value = v;
id t = [value objectForKey:@"type"];
if (!t || ![t isKindOfClass:[NSString class]] || ![t isEqualToString:@"standard"]) continue;
@@ -601,6 +625,39 @@ build_global_shortcuts_lookup(void) {
}
}
}
// Add global shortcut definitions when the default enabled shortcut is not defined,
// or when the default enabled shortcut is not disabled and is missing a value.
// Here are the shortcuts that are enabled by default in the standard ANSI (US) layout.
// macOS provides separate configurations for some languages or keyboards.
// In general, the rules here will not take effect.
static char buf[64];
#define S(i, t, m, k) if ([temp_configured member:@(i)] == nil || [temp_missing_value member:@(i)] != nil) { \
snprintf(buf, sizeof(buf) - 1, #t":%lx:%ld", (unsigned long)m, (long)k); \
temp[@(buf)] = @(i); \
}
// launchpad & dock
S(kSHKTurnDockHidingOnOrOff, c, (NSEventModifierFlagOption | NSEventModifierFlagCommand), 'd'); // Opt, Cmd, D
// mission control
S(kSHKMissionControl, v, NSEventModifierFlagControl, 126); // Ctrl, Arrow Up
S(kSHKApplicationWindows, v, NSEventModifierFlagControl, 125); // Ctrl, Arrow Down
// keyboard
S(kSHKMoveFocusToTheMenuBar, v, NSEventModifierFlagControl, 120); // Ctrl, F2
S(kSHKMoveFocusToTheDock, v, NSEventModifierFlagControl, 99); // Ctrl, F3
S(kSHKMoveFocusToActiveOrNextWindow, v, NSEventModifierFlagControl, 118); // Ctrl, F4
S(kSHKMoveFocusToActiveOrNextWindow, v, (NSEventModifierFlagShift | NSEventModifierFlagControl), 118); // Shift, Ctrl, F4
S(kSHKMoveFocusToNextWindow, c, NSEventModifierFlagCommand, 96); // Cmd, `
S(kSHKMoveFocusToNextWindow, c, (NSEventModifierFlagShift | NSEventModifierFlagCommand), 96); // Shift, Cmd, `
S(kSHKMoveFocusToStatusMenus, v, NSEventModifierFlagControl, 100); // Ctrl, F8
// input sources
S(kSHKSelectThePreviousInputSource, c, NSEventModifierFlagControl, 32); // Ctrl, Space bar
S(kSHKSelectNextSourceInInputMenu, c, (NSEventModifierFlagControl | NSEventModifierFlagOption), 32); // Ctrl, Opt, Space bar
// spotlight
S(kSHKShowSpotlightSearch, c, NSEventModifierFlagCommand, 32); // Cmd, Space bar
S(kSHKShowFinderSearchWindow, c, (NSEventModifierFlagOption | NSEventModifierFlagCommand), 32); // Opt, Cmd, Space bar
#undef S
global_shortcuts = [[NSDictionary dictionaryWithDictionary:temp] retain];
/* NSLog(@"global_shortcuts: %@", global_shortcuts); */
}
@@ -610,11 +667,10 @@ is_active_apple_global_shortcut(NSEvent *event) {
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 (sc != nil) return [sc intValue]; \
if ([event.charactersIgnoringModifiers length] == 1) {
if (modifierFlags & NSEventModifierFlagShift) {
@@ -752,9 +808,11 @@ int _glfwPlatformInit(void)
[NSApp setDelegate:_glfw.ns.delegate];
static struct {
unsigned short virtual_key_code;
NSEventModifierFlags input_source_switch_modifiers;
NSTimeInterval timestamp;
} last_keydown_shortcut_event;
last_keydown_shortcut_event.virtual_key_code = 0xffff;
last_keydown_shortcut_event.input_source_switch_modifiers = 0;
NSEvent* (^keydown_block)(NSEvent*) = ^ NSEvent* (NSEvent* event)
{
@@ -765,6 +823,7 @@ int _glfwPlatformInit(void)
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.input_source_switch_modifiers = 0;
last_keydown_shortcut_event.timestamp = [event timestamp];
return nil;
}
@@ -773,6 +832,8 @@ int _glfwPlatformInit(void)
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];
// record the modifier keys if switching to the next input source
last_keydown_shortcut_event.input_source_switch_modifiers = (global_shortcut == kSHKSelectNextSourceInInputMenu) ? USEFUL_MODS([event modifierFlags]) : 0;
last_keydown_shortcut_event.timestamp = [event timestamp];
return event;
}
@@ -805,6 +866,12 @@ int _glfwPlatformInit(void)
debug_key("-------------- flags changed -----------------\n");
debug_key("%s\n", [[event description] UTF8String]);
last_keydown_shortcut_event.virtual_key_code = 0xffff;
// switching to the next input source is only confirmed when all modifier keys are released
if (last_keydown_shortcut_event.input_source_switch_modifiers) {
if (!([event modifierFlags] & last_keydown_shortcut_event.input_source_switch_modifiers))
last_keydown_shortcut_event.input_source_switch_modifiers = 0;
return event;
}
NSWindow *kw = [NSApp keyWindow];
if (kw && kw.contentView) [kw.contentView flagsChanged:event];
else debug_key("flagsChanged ignored as no keyWindow present\n");

View File

@@ -68,7 +68,7 @@ typedef void* CVDisplayLinkRef;
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int, unsigned long);
typedef bool (* GLFWapplicationshouldhandlereopenfun)(int);
typedef bool (* GLFWhandlefileopen)(const char*);
typedef bool (* GLFWhandleurlopen)(const char*);
typedef void (* GLFWapplicationwillfinishlaunchingfun)(void);
typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
typedef void (* GLFWcocoarenderframefun)(GLFWwindow*);
@@ -131,6 +131,7 @@ typedef struct _GLFWwindowNS
bool maximized;
bool retina;
bool in_traditional_fullscreen;
bool in_fullscreen_transition;
bool titlebar_hidden;
unsigned long pre_full_screen_style_mask;
@@ -153,6 +154,8 @@ typedef struct _GLFWwindowNS
// Whether a render frame has been requested for this window
bool renderFrameRequested;
GLFWcocoarenderframefun renderFrameCallback;
// update cursor after switching desktops with Mission Control
bool delayed_cursor_update_requested;
} _GLFWwindowNS;
typedef struct _GLFWDisplayLinkNS
@@ -199,8 +202,8 @@ typedef struct _GLFWlibraryNS
_GLFWDisplayLinkNS entries[256];
size_t count;
} displayLinks;
// the callback to handle file open events
GLFWhandlefileopen file_open_callback;
// the callback to handle url open events
GLFWhandleurlopen url_open_callback;
} _GLFWlibraryNS;

View File

@@ -582,6 +582,7 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
}
- (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow;
- (void)request_delayed_cursor_update:(id)sender;
@end
@@ -692,6 +693,9 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
_glfwPlatformGetCursorPos(window, &x, &y);
_glfwInputCursorPos(window, x, y);
}
// macOS will send a delayed event to update the cursor to arrow after switching desktops.
// So we need to delay and update the cursor once after that.
[self performSelector:@selector(request_delayed_cursor_update:) withObject:nil afterDelay:0.3];
}
- (void)windowDidResignKey:(NSNotification *)notification
@@ -722,6 +726,38 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
}
}
- (void)request_delayed_cursor_update:(id)sender
{
(void)sender;
if (window) window->ns.delayed_cursor_update_requested = true;
}
- (void)windowWillEnterFullScreen:(NSNotification *)notification
{
(void)notification;
if (window) window->ns.in_fullscreen_transition = true;
}
- (void)windowDidEnterFullScreen:(NSNotification *)notification
{
(void)notification;
if (window) window->ns.in_fullscreen_transition = false;
[self performSelector:@selector(request_delayed_cursor_update:) withObject:nil afterDelay:0.3];
}
- (void)windowWillExitFullScreen:(NSNotification *)notification
{
(void)notification;
if (window) window->ns.in_fullscreen_transition = true;
}
- (void)windowDidExitFullScreen:(NSNotification *)notification
{
(void)notification;
if (window) window->ns.in_fullscreen_transition = false;
[self performSelector:@selector(request_delayed_cursor_update:) withObject:nil afterDelay:0.3];
}
@end // }}}
// Text input context class for the GLFW content view {{{
@@ -904,6 +940,11 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
window->ns.cursorWarpDeltaX = 0;
window->ns.cursorWarpDeltaY = 0;
if (window->ns.delayed_cursor_update_requested) {
window->ns.delayed_cursor_update_requested = false;
if (cursorInContentArea(window)) updateCursorImage(window);
}
}
- (void)rightMouseDown:(NSEvent *)event
@@ -1003,6 +1044,8 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
- (void)updateTrackingAreas
{
if (window && [window->ns.object areCursorRectsEnabled])
[window->ns.object disableCursorRects];
if (trackingArea != nil)
{
[self removeTrackingArea:trackingArea];
@@ -1325,14 +1368,17 @@ is_ascii_control_char(char x) {
options:options];
if (!objs) return NO;
const NSUInteger count = [objs count];
NSMutableString *uri_list = [NSMutableString stringWithCapacity:4096]; // auto-released
if (count)
{
for (NSUInteger i = 0; i < count; i++)
{
id obj = objs[i];
if ([obj isKindOfClass:[NSURL class]]) {
const char *path = [obj fileSystemRepresentation];
_glfwInputDrop(window, "text/plain;charset=utf-8", path, strlen(path));
NSURL *url = (NSURL*)obj;
if ([uri_list length] > 0) [uri_list appendString:@("\n")];
if (url.fileURL) [uri_list appendFormat:@("file://%s"), url.fileSystemRepresentation];
else [uri_list appendString:url.absoluteString];
} else if ([obj isKindOfClass:[NSString class]]) {
const char *text = [obj UTF8String];
_glfwInputDrop(window, "text/plain;charset=utf-8", text, strlen(text));
@@ -1342,6 +1388,7 @@ is_ascii_control_char(char x) {
}
}
}
if ([uri_list length] > 0) _glfwInputDrop(window, "text/uri-list", uri_list.UTF8String, strlen(uri_list.UTF8String));
return YES;
}
@@ -1554,8 +1601,11 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
- (void)toggleFullScreen:(nullable id)sender
{
if (glfw_window && glfw_window->ns.toggleFullscreenCallback && glfw_window->ns.toggleFullscreenCallback((GLFWwindow*)glfw_window) == 1)
return;
if (glfw_window) {
if (glfw_window->ns.in_fullscreen_transition) return;
if (glfw_window->ns.toggleFullscreenCallback && glfw_window->ns.toggleFullscreenCallback((GLFWwindow*)glfw_window) == 1) return;
glfw_window->ns.in_fullscreen_transition = true;
}
// When resizeIncrements is set, Cocoa cannot restore the original window size after returning from fullscreen.
const NSSize original = [self resizeIncrements];
[self setResizeIncrements:NSMakeSize(1.0, 1.0)];
@@ -2642,10 +2692,10 @@ GLFWAPI GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow *hand
return previous;
}
GLFWAPI GLFWhandlefileopen glfwSetCocoaFileOpenCallback(GLFWhandlefileopen callback) {
GLFWAPI GLFWhandleurlopen glfwSetCocoaURLOpenCallback(GLFWhandleurlopen callback) {
_GLFW_REQUIRE_INIT_OR_RETURN(nil);
GLFWhandlefileopen prev = _glfw.ns.file_open_callback;
_glfw.ns.file_open_callback = callback;
GLFWhandleurlopen prev = _glfw.ns.url_open_callback;
_glfw.ns.url_open_callback = callback;
return prev;
}

2
glfw/dbus_glfw.c vendored
View File

@@ -263,7 +263,7 @@ call_method_with_msg(DBusConnection *conn, DBusMessage *msg, int timeout, dbus_p
static bool
call_method(DBusConnection *conn, const char *node, const char *path, const char *interface, const char *method, int timeout, dbus_pending_callback callback, void *user_data, va_list ap) {
if (!conn) return false;
if (!conn || !path) return false;
DBusMessage *msg = dbus_message_new_method_call(node, path, interface, method);
if (!msg) return false;
bool retval = false;

View File

@@ -233,7 +233,7 @@ def generate_wrappers(glfw_header: str) -> None:
void* glfwGetNSGLContext(GLFWwindow *window)
uint32_t glfwGetCocoaMonitor(GLFWmonitor* monitor)
GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow* window, GLFWcocoatextinputfilterfun callback)
GLFWhandlefileopen glfwSetCocoaFileOpenCallback(GLFWhandlefileopen callback)
GLFWhandleurlopen glfwSetCocoaURLOpenCallback(GLFWhandleurlopen callback)
GLFWcocoatogglefullscreenfun glfwSetCocoaToggleFullscreenIntercept(GLFWwindow *window, GLFWcocoatogglefullscreenfun callback)
GLFWapplicationshouldhandlereopenfun glfwSetApplicationShouldHandleReopen(GLFWapplicationshouldhandlereopenfun callback)
GLFWapplicationwillfinishlaunchingfun glfwSetApplicationWillFinishLaunching(GLFWapplicationwillfinishlaunchingfun callback)
@@ -277,7 +277,7 @@ const char *action_text, int32_t timeout, GLFWDBusnotificationcreatedfun callbac
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int,unsigned long);
typedef bool (* GLFWapplicationshouldhandlereopenfun)(int);
typedef bool (* GLFWhandlefileopen)(const char*);
typedef bool (* GLFWhandleurlopen)(const char*);
typedef void (* GLFWapplicationwillfinishlaunchingfun)(void);
typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
typedef void (* GLFWcocoarenderframefun)(GLFWwindow*);
@@ -292,6 +292,8 @@ const char* load_glfw(const char* path);
f.write(header)
code = '''
// generated by glfw.py DO NOT edit
#define GFW_EXTERN
#include "data-types.h"
#include "glfw-wrapper.h"

View File

@@ -263,11 +263,11 @@ static bool
create_shm_buffers(_GLFWwindow* window) {
const unsigned scale = window->wl.scale >= 1 ? window->wl.scale : 1;
const size_t vertical_width = decs.metrics.width, vertical_height = window->wl.height + decs.metrics.top;
const size_t horizontal_height = decs.metrics.width, horizontal_width = window->wl.width + 2 * decs.metrics.width;
const size_t vertical_width = decs.metrics.width, vertical_height = window->wl.current.height + decs.metrics.top;
const size_t horizontal_height = decs.metrics.width, horizontal_width = window->wl.current.width + 2 * decs.metrics.width;
decs.mapping.size = 0;
decs.mapping.size += init_buffer_pair(&decs.top.buffer, window->wl.width, decs.metrics.top, scale);
decs.mapping.size += init_buffer_pair(&decs.top.buffer, window->wl.current.width, decs.metrics.top, scale);
decs.mapping.size += init_buffer_pair(&decs.left.buffer, vertical_width, vertical_height, scale);
decs.mapping.size += init_buffer_pair(&decs.bottom.buffer, horizontal_width, horizontal_height, scale);
decs.mapping.size += init_buffer_pair(&decs.right.buffer, vertical_width, vertical_height, scale);
@@ -347,8 +347,8 @@ ensure_csd_resources(_GLFWwindow *window) {
const bool is_focused = window->id == _glfw.focusedWindowId;
const bool focus_changed = is_focused != decs.for_window_state.focused;
const bool size_changed = (
decs.for_window_state.width != window->wl.width ||
decs.for_window_state.height != window->wl.height ||
decs.for_window_state.width != window->wl.current.width ||
decs.for_window_state.height != window->wl.current.height ||
decs.for_window_state.scale != window->wl.scale ||
!decs.mapping.data
);
@@ -368,11 +368,11 @@ ensure_csd_resources(_GLFWwindow *window) {
if (!decs.left.surface) create_csd_surfaces(window, &decs.left);
position_csd_surface(&decs.left, x, y, scale);
x = -decs.metrics.width; y = window->wl.height;
x = -decs.metrics.width; y = window->wl.current.height;
if (!decs.bottom.surface) create_csd_surfaces(window, &decs.bottom);
position_csd_surface(&decs.bottom, x, y, scale);
x = window->wl.width; y = -decs.metrics.top;
x = window->wl.current.width; y = -decs.metrics.top;
if (!decs.right.surface) create_csd_surfaces(window, &decs.right);
position_csd_surface(&decs.right, x, y, scale);
@@ -382,8 +382,8 @@ ensure_csd_resources(_GLFWwindow *window) {
damage_csd(bottom, is_focused ? decs.bottom.buffer.front : decs.bottom.buffer.back);
damage_csd(right, is_focused ? decs.right.buffer.front : decs.right.buffer.back);
decs.for_window_state.width = window->wl.width;
decs.for_window_state.height = window->wl.height;
decs.for_window_state.width = window->wl.current.width;
decs.for_window_state.height = window->wl.current.height;
decs.for_window_state.scale = window->wl.scale;
decs.for_window_state.focused = is_focused;
return true;
@@ -409,7 +409,7 @@ change_csd_title(_GLFWwindow *window) {
void
set_csd_window_geometry(_GLFWwindow *window, int32_t *width, int32_t *height) {
bool has_csd = window->decorated && !window->wl.decorations.serverSide && window->wl.decorations.left.surface && !(window->wl.toplevel_states & TOPLEVEL_STATE_FULLSCREEN);
bool has_csd = window->decorated && !window->wl.decorations.serverSide && window->wl.decorations.left.surface && !(window->wl.current.toplevel_states & TOPLEVEL_STATE_FULLSCREEN);
bool size_specified_by_compositor = *width > 0 && *height > 0;
if (!size_specified_by_compositor) {
*width = window->wl.user_requested_content_size.width;

79
glfw/wl_init.c vendored
View File

@@ -206,7 +206,7 @@ static void pointerHandleMotion(void* data UNUSED,
case BOTTOM_DECORATION:
if (x < window->wl.decorations.metrics.width)
cursorShape = GLFW_SW_RESIZE_CURSOR;
else if (x > window->wl.width + window->wl.decorations.metrics.width)
else if (x > window->wl.current.width + window->wl.decorations.metrics.width)
cursorShape = GLFW_SE_RESIZE_CURSOR;
else
cursorShape = GLFW_VRESIZE_CURSOR;
@@ -243,7 +243,7 @@ static void pointerHandleButton(void* data UNUSED,
window->wl.decorations.last_click_on_top_decoration_at = monotonic();
if (window->wl.decorations.last_click_on_top_decoration_at - last_click_at <= _glfwPlatformGetDoubleClickInterval(window)) {
window->wl.decorations.last_click_on_top_decoration_at = 0;
if (window->wl.toplevel_states & TOPLEVEL_STATE_MAXIMIZED)
if (window->wl.current.toplevel_states & TOPLEVEL_STATE_MAXIMIZED)
xdg_toplevel_unset_maximized(window->wl.xdg.toplevel);
else
xdg_toplevel_set_maximized(window->wl.xdg.toplevel);
@@ -274,7 +274,7 @@ static void pointerHandleButton(void* data UNUSED,
case BOTTOM_DECORATION:
if (x < window->wl.decorations.metrics.width)
edges = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT;
else if (x > window->wl.width + window->wl.decorations.metrics.width)
else if (x > window->wl.current.width + window->wl.decorations.metrics.width)
edges = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT;
else
edges = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM;
@@ -331,20 +331,83 @@ static void pointerHandleAxis(void* data UNUSED,
assert(axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL ||
axis == WL_POINTER_AXIS_VERTICAL_SCROLL);
if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL)
x = -wl_fixed_to_double(value);
else if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL)
y = -wl_fixed_to_double(value);
if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) {
if (window->wl.axis_discrete_count.x) {
window->wl.axis_discrete_count.x--;
return;
}
x = -wl_fixed_to_double(value) * (window->wl.scale);
}
else if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) {
if (window->wl.axis_discrete_count.y) {
window->wl.axis_discrete_count.y--;
return;
}
y = -wl_fixed_to_double(value) * (window->wl.scale);
}
_glfwInputScroll(window, x, y, 1, _glfw.wl.xkb.states.modifiers);
}
static void pointerHandleFrame(void* data UNUSED,
struct wl_pointer* pointer UNUSED)
{
_GLFWwindow* window = _glfw.wl.pointerFocus;
if (window) {
window->wl.axis_discrete_count.x = 0;
window->wl.axis_discrete_count.y = 0;
}
}
static void pointerHandleAxisSource(void* data UNUSED,
struct wl_pointer* pointer UNUSED,
uint32_t source UNUSED)
{
}
static void pointerHandleAxisStop(void *data UNUSED,
struct wl_pointer *wl_pointer UNUSED,
uint32_t time UNUSED,
uint32_t axis UNUSED)
{
}
static void pointerHandleAxisDiscrete(void *data UNUSED,
struct wl_pointer *wl_pointer UNUSED,
uint32_t axis,
int32_t discrete)
{
_GLFWwindow* window = _glfw.wl.pointerFocus;
double x = 0.0, y = 0.0;
if (!window)
return;
assert(axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL ||
axis == WL_POINTER_AXIS_VERTICAL_SCROLL);
if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) {
x = -discrete;
window->wl.axis_discrete_count.x++;
}
else if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) {
y = -discrete;
window->wl.axis_discrete_count.y++;
}
_glfwInputScroll(window, x, y, 0, _glfw.wl.xkb.states.modifiers);
}
static const struct wl_pointer_listener pointerListener = {
pointerHandleEnter,
pointerHandleLeave,
pointerHandleMotion,
pointerHandleButton,
pointerHandleAxis,
pointerHandleFrame,
pointerHandleAxisSource,
pointerHandleAxisStop,
pointerHandleAxisDiscrete,
};
static void keyboardHandleKeymap(void* data UNUSED,
@@ -571,7 +634,7 @@ static void registryHandleGlobal(void* data UNUSED,
{
if (!_glfw.wl.seat)
{
_glfw.wl.seatVersion = min(4, version);
_glfw.wl.seatVersion = min(5, version);
_glfw.wl.seat =
wl_registry_bind(registry, name, &wl_seat_interface,
_glfw.wl.seatVersion);

12
glfw/wl_platform.h vendored
View File

@@ -132,7 +132,6 @@ static const WaylandWindowState TOPLEVEL_STATE_DOCKED = TOPLEVEL_STATE_MAXIMIZED
//
typedef struct _GLFWwindowWayland
{
int width, height;
bool visible;
bool hovered;
bool transparent;
@@ -210,9 +209,18 @@ typedef struct _GLFWwindowWayland
int32_t width, height;
} user_requested_content_size;
uint32_t toplevel_states;
bool maximize_on_first_show;
// counters for ignoring axis events following axis_discrete events in the
// same frame along the same axis
struct {
unsigned int x, y;
} axis_discrete_count;
struct {
int width, height;
uint32_t toplevel_states;
bool set;
} current, pending;
} _GLFWwindowWayland;
typedef enum _GLFWWaylandOfferType

76
glfw/wl_window.c vendored
View File

@@ -206,7 +206,7 @@ static void setOpaqueRegion(_GLFWwindow* window)
if (!region)
return;
wl_region_add(region, 0, 0, window->wl.width, window->wl.height);
wl_region_add(region, 0, 0, window->wl.current.width, window->wl.current.height);
wl_surface_set_opaque_region(window->wl.surface, region);
wl_surface_commit(window->wl.surface);
wl_region_destroy(region);
@@ -216,9 +216,9 @@ static void setOpaqueRegion(_GLFWwindow* window)
static void
resizeFramebuffer(_GLFWwindow* window) {
int scale = window->wl.scale;
int scaledWidth = window->wl.width * scale;
int scaledHeight = window->wl.height * scale;
debug("Resizing framebuffer to: %dx%d at scale: %d\n", window->wl.width, window->wl.height, scale);
int scaledWidth = window->wl.current.width * scale;
int scaledHeight = window->wl.current.height * scale;
debug("Resizing framebuffer to: %dx%d at scale: %d\n", window->wl.current.width, window->wl.current.height, scale);
wl_egl_window_resize(window->wl.native, scaledWidth, scaledHeight, 0, 0);
if (!window->wl.transparent) setOpaqueRegion(window);
_glfwInputFramebufferSize(window, scaledWidth, scaledHeight);
@@ -236,12 +236,12 @@ clipboard_mime(void) {
static void
dispatchChangesAfterConfigure(_GLFWwindow *window, int32_t width, int32_t height) {
bool size_changed = width != window->wl.width || height != window->wl.height;
bool size_changed = width != window->wl.current.width || height != window->wl.current.height;
bool scale_changed = checkScaleChange(window);
if (size_changed) {
_glfwInputWindowSize(window, width, height);
window->wl.width = width; window->wl.height = height;
window->wl.current.width = width; window->wl.current.height = height;
resizeFramebuffer(window);
}
@@ -274,7 +274,7 @@ xdgDecorationHandleConfigure(void* data,
debug("XDG decoration configure event received: has_server_side_decorations: %d\n", has_server_side_decorations);
if (has_server_side_decorations == window->wl.decorations.serverSide) return;
window->wl.decorations.serverSide = has_server_side_decorations;
int width = window->wl.width, height = window->wl.height;
int width = window->wl.current.width, height = window->wl.current.height;
if (window->wl.decorations.serverSide) {
free_csd_surfaces(window);
height += window->wl.decorations.metrics.visible_titlebar_height;
@@ -285,7 +285,7 @@ xdgDecorationHandleConfigure(void* data,
dispatchChangesAfterConfigure(window, width, height);
ensure_csd_resources(window);
wl_surface_commit(window->wl.surface);
debug("final window content size: %dx%d\n", window->wl.width, window->wl.height);
debug("final window content size: %dx%d\n", window->wl.current.width, window->wl.current.height);
inform_compositor_of_window_geometry(window, "configure-decorations");
}
@@ -384,8 +384,8 @@ static bool createSurface(_GLFWwindow* window,
if (!window->wl.native)
return false;
window->wl.width = wndconfig->width;
window->wl.height = wndconfig->height;
window->wl.current.width = wndconfig->width;
window->wl.current.height = wndconfig->height;
window->wl.user_requested_content_size.width = wndconfig->width;
window->wl.user_requested_content_size.height = wndconfig->height;
window->wl.scale = 1;
@@ -416,7 +416,7 @@ static void setFullscreen(_GLFWwindow* window, _GLFWmonitor* monitor, bool on)
bool
_glfwPlatformIsFullscreen(_GLFWwindow *window, unsigned int flags UNUSED) {
return window->wl.toplevel_states & TOPLEVEL_STATE_FULLSCREEN;
return window->wl.current.toplevel_states & TOPLEVEL_STATE_FULLSCREEN;
}
bool
@@ -458,7 +458,7 @@ xdgToplevelHandleConfigure(void* data,
if (new_states & TOPLEVEL_STATE_RESIZING) {
if (width) window->wl.user_requested_content_size.width = width;
if (height) window->wl.user_requested_content_size.height = height;
if (!(window->wl.toplevel_states & TOPLEVEL_STATE_RESIZING)) _glfwInputLiveResize(window, true);
if (!(window->wl.current.toplevel_states & TOPLEVEL_STATE_RESIZING)) _glfwInputLiveResize(window, true);
}
if (width != 0 && height != 0)
{
@@ -475,16 +475,11 @@ xdgToplevelHandleConfigure(void* data,
}
}
}
bool live_resize_done = !(new_states & TOPLEVEL_STATE_RESIZING) && (window->wl.toplevel_states & TOPLEVEL_STATE_RESIZING);
window->wl.toplevel_states = new_states;
set_csd_window_geometry(window, &width, &height);
dispatchChangesAfterConfigure(window, width, height);
debug("final window content size: %dx%d\n", window->wl.width, window->wl.height);
_glfwInputWindowFocus(window, window->wl.toplevel_states & TOPLEVEL_STATE_ACTIVATED);
ensure_csd_resources(window);
wl_surface_commit(window->wl.surface);
inform_compositor_of_window_geometry(window, "configure");
if (live_resize_done) _glfwInputLiveResize(window, false);
window->wl.pending.toplevel_states = new_states;
window->wl.pending.width = width;
window->wl.pending.height = height;
window->wl.pending.set = true;
}
static void xdgToplevelHandleClose(void* data,
@@ -499,11 +494,34 @@ static const struct xdg_toplevel_listener xdgToplevelListener = {
xdgToplevelHandleClose
};
static void xdgSurfaceHandleConfigure(void* data UNUSED,
static void xdgSurfaceHandleConfigure(void* data,
struct xdg_surface* surface,
uint32_t serial)
{
_GLFWwindow* window = data;
xdg_surface_ack_configure(surface, serial);
if (window->wl.pending.set) {
uint32_t new_states = window->wl.pending.toplevel_states;
int width = window->wl.pending.width;
int height = window->wl.pending.height;
window->wl.pending.set = false;
if (new_states != window->wl.current.toplevel_states ||
width != window->wl.current.width ||
height != window->wl.current.height) {
bool live_resize_done = !(new_states & TOPLEVEL_STATE_RESIZING) && (window->wl.current.toplevel_states & TOPLEVEL_STATE_RESIZING);
window->wl.current.toplevel_states = new_states;
set_csd_window_geometry(window, &width, &height);
dispatchChangesAfterConfigure(window, width, height);
debug("final window content size: %dx%d\n", window->wl.current.width, window->wl.current.height);
_glfwInputWindowFocus(window, window->wl.current.toplevel_states & TOPLEVEL_STATE_ACTIVATED);
ensure_csd_resources(window);
inform_compositor_of_window_geometry(window, "configure");
if (live_resize_done) _glfwInputLiveResize(window, false);
}
}
wl_surface_commit(window->wl.surface);
}
static const struct xdg_surface_listener xdgSurfaceListener = {
@@ -886,19 +904,19 @@ void _glfwPlatformSetWindowPos(_GLFWwindow* window UNUSED, int xpos UNUSED, int
void _glfwPlatformGetWindowSize(_GLFWwindow* window, int* width, int* height)
{
if (width)
*width = window->wl.width;
*width = window->wl.current.width;
if (height)
*height = window->wl.height;
*height = window->wl.current.height;
}
void _glfwPlatformSetWindowSize(_GLFWwindow* window, int width, int height)
{
if (width != window->wl.width || height != window->wl.height) {
if (width != window->wl.current.width || height != window->wl.current.height) {
window->wl.user_requested_content_size.width = width;
window->wl.user_requested_content_size.height = height;
int32_t w = 0, h = 0;
set_csd_window_geometry(window, &w, &h);
window->wl.width = w; window->wl.height = h;
window->wl.current.width = w; window->wl.current.height = h;
resizeFramebuffer(window);
ensure_csd_resources(window);
wl_surface_commit(window->wl.surface);
@@ -991,7 +1009,7 @@ void _glfwPlatformRestoreWindow(_GLFWwindow* window)
{
if (window->monitor)
xdg_toplevel_unset_fullscreen(window->wl.xdg.toplevel);
if (window->wl.toplevel_states & TOPLEVEL_STATE_MAXIMIZED)
if (window->wl.current.toplevel_states & TOPLEVEL_STATE_MAXIMIZED)
xdg_toplevel_unset_maximized(window->wl.xdg.toplevel);
// There is no way to unset minimized, or even to know if we are
// minimized, so there is nothing to do in this case.
@@ -1092,7 +1110,7 @@ int _glfwPlatformWindowVisible(_GLFWwindow* window)
int _glfwPlatformWindowMaximized(_GLFWwindow* window)
{
return window->wl.toplevel_states & TOPLEVEL_STATE_MAXIMIZED;
return window->wl.current.toplevel_states & TOPLEVEL_STATE_MAXIMIZED;
}
int _glfwPlatformWindowHovered(_GLFWwindow* window)

2
glfw/xkb_glfw.c vendored
View File

@@ -33,7 +33,7 @@
#include <X11/XKBlib.h>
#endif
#define debug(...) if (_glfw.hints.init.debugKeyboard) printf(__VA_ARGS__);
#define debug(...) if (_glfw.hints.init.debugKeyboard) fprintf(stderr, __VA_ARGS__);
#ifdef XKB_HAS_NO_UTF32
#include "xkb-compat-shim.h"

View File

@@ -20,7 +20,8 @@ from kitty.fast_data_types import get_options, set_clipboard_string
from kitty.key_encoding import KeyEvent
from kitty.typing import BossType, KittyCommonOpts
from kitty.utils import (
ScreenSize, resolve_custom_file, screen_size_function, set_primary_selection
ScreenSize, kitty_ansi_sanitizer_pat, resolve_custom_file,
screen_size_function, set_primary_selection
)
from ..tui.handler import Handler, result_handler
@@ -230,7 +231,7 @@ def regex_finditer(pat: 'Pattern[str]', minimum_match_length: int, text: str) ->
yield s, e, m.groupdict()
closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>', '*': '*', '"': '"', "'": "'"}
closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>', '*': '*', '"': '"', "'": "'", "": "", "": ""}
opening_brackets = ''.join(closing_bracket_map)
PostprocessorFunc = Callable[[str, int, int], Tuple[int, int]]
postprocessor_map: Dict[str, PostprocessorFunc] = {}
@@ -288,11 +289,12 @@ def quotes(text: str, s: int, e: int) -> Tuple[int, int]:
# Remove matching quotes
if s < e <= len(text):
before = text[s]
if before in '\'"':
if text[e-1] == before:
if before in '\'"“‘':
q = closing_bracket_map[before]
if text[e-1] == q:
s += 1
e -= 1
elif text[e:e+1] == before:
elif text[e:e+1] == q:
s += 1
return s, e
@@ -430,11 +432,7 @@ def load_custom_processor(customize_processing: str) -> Any:
return runpy.run_path(custom_path, run_name='__main__')
def remove_sgr(text: str) -> str:
return re.sub(r'\x1b\[.*?m', '', text)
def process_hyperlinks(text: str) -> Tuple[str, Tuple[Mark, ...]]:
def process_escape_codes(text: str) -> Tuple[str, Tuple[Mark, ...]]:
hyperlinks: List[Mark] = []
removed_size = idx = 0
active_hyperlink_url: Optional[str] = None
@@ -457,6 +455,9 @@ def process_hyperlinks(text: str) -> Tuple[str, Tuple[Mark, ...]]:
def process_hyperlink(m: 're.Match[str]') -> str:
nonlocal removed_size, active_hyperlink_url, active_hyperlink_id, active_hyperlink_start_offset
raw = m.group()
if not raw.startswith('\x1b]8'):
removed_size += len(raw)
return ''
start = m.start() - removed_size
removed_size += len(raw)
if active_hyperlink_url is not None:
@@ -474,7 +475,7 @@ def process_hyperlinks(text: str) -> Tuple[str, Tuple[Mark, ...]]:
return ''
text = re.sub(r'\x1b\]8.+?\x1b\\', process_hyperlink, text)
text = kitty_ansi_sanitizer_pat().sub(process_hyperlink, text)
if active_hyperlink_url is not None:
add_hyperlink(len(text))
return text, tuple(hyperlinks)
@@ -482,8 +483,8 @@ def process_hyperlinks(text: str) -> Tuple[str, Tuple[Mark, ...]]:
def run(args: HintsCLIOptions, text: str, extra_cli_args: Sequence[str] = ()) -> Optional[Dict[str, Any]]:
try:
text = parse_input(remove_sgr(text))
text, hyperlinks = process_hyperlinks(text)
text = parse_input(text)
text, hyperlinks = process_escape_codes(text)
pattern, post_processors = functions_for(args)
if args.type == 'linenum':
args.customize_processing = '::linenum::'
@@ -701,14 +702,26 @@ def main(args: List[str]) -> Optional[Dict[str, Any]]:
return None
def linenum_handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: BossType, extra_cli_args: Sequence[str], *a: Any) -> None:
def linenum_process_result(data: Dict[str, Any]) -> Tuple[str, int]:
pat = re.compile(r':(\d+)$')
for m, g in zip(data['match'], data['groupdicts']):
if m:
path, line = g['path'], g['line']
path = os.path.expanduser(path.split(':')[-1])
line = int(line)
break
else:
# look for trailers on path of the for :number
while True:
m = pat.search(path)
if m is None:
break
line = m.group(1)
path = path[:-len(m.group())]
return os.path.expanduser(path), int(line)
return '', -1
def linenum_handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: BossType, extra_cli_args: Sequence[str], *a: Any) -> None:
path, line = linenum_process_result(data)
if not path:
return
cmd = [x.format(path=path, line=line) for x in extra_cli_args or ('vim', '+{line}', '{path}')]

View File

@@ -102,7 +102,7 @@ detecting image display support.
--print-window-size
type=bool-set
Print out the window size as :italic:`widthxheight` (in pixels) and quit. This is a
convenience method to query the window size if using kitty icat from a
convenience method to query the window size if using :code:`kitty +kitten icat` from a
scripting language that cannot make termios calls.

View File

@@ -122,10 +122,17 @@ class ControlMaster:
cmd.extend(['-i', conn_data.identity_file])
self.batch_cmd_prefix = cmd + ['-o', 'BatchMode=yes']
def check_call(self, cmd: List[str]) -> None:
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL)
stdout = p.communicate()[0]
if p.wait() != 0:
out = stdout.decode('utf-8', 'replace')
raise Exception(f'The ssh command: {shlex.join(cmd)} failed with exit code {p.returncode} and output: {out}')
def __enter__(self) -> 'ControlMaster':
subprocess.check_call(
self.check_call(
self.cmd_prefix + ['-o', 'ControlMaster=auto', '-fN', self.conn_data.hostname])
subprocess.check_call(
self.check_call(
self.batch_cmd_prefix + ['-O', 'check', self.conn_data.hostname])
if not self.dest:
self.tdir = tempfile.mkdtemp()

View File

@@ -9,10 +9,10 @@ from contextlib import contextmanager
from functools import partial
from typing import TYPE_CHECKING, Any, Dict, FrozenSet, Generator, List, cast
from kitty.constants import list_kitty_resources
from kitty.types import run_once
from kitty.utils import resolve_abs_or_config_path
aliases = {'url_hints': 'hints'}
if TYPE_CHECKING:
from kitty.conf.types import Definition
@@ -85,7 +85,7 @@ def launch(args: List[str]) -> None:
del args[:2]
args = [kitten] + args
os.environ['KITTY_CONFIG_DIRECTORY'] = config_dir
from kittens.tui.operations import clear_screen, reset_mode, Mode
from kittens.tui.operations import Mode, clear_screen, reset_mode
set_debug(kitten)
m = import_kitten_main_module(config_dir, kitten)
try:
@@ -116,11 +116,9 @@ def run_kitten(kitten: str, run_name: str = '__main__') -> None:
original_kitten_name = kitten
kitten = resolved_kitten(kitten)
set_debug(kitten)
try:
if kitten in all_kitten_names():
runpy.run_module(f'kittens.{kitten}.main', run_name=run_name)
return
except ImportError:
pass
# Look for a custom kitten
if not kitten.endswith('.py'):
kitten += '.py'
@@ -137,7 +135,6 @@ def run_kitten(kitten: str, run_name: str = '__main__') -> None:
@run_once
def all_kitten_names() -> FrozenSet[str]:
from kitty.constants import list_kitty_resources
ans = []
for name in list_kitty_resources('kittens'):
if '__' not in name and '.' not in name and name != 'tui':

View File

@@ -23,16 +23,17 @@ def real_main(args: List[str]) -> None:
cli_opts, items = parse_args(args[1:], OPTIONS, '', msg, 'hints', result_class=ErrorCLIOptions)
error_message = sys.stdin.buffer.read().decode('utf-8')
sys.stdin = open(os.ctermid())
print(styled(cli_opts.title, fg_intense=True, fg='red', bold=True))
print()
if cli_opts.title:
print(styled(cli_opts.title, fg_intense=True, fg='red', bold=True))
print()
print(error_message)
print()
input('Press Enter to close.')
input('\x1b[1;32mPress Enter to close\x1b[m')
def main(args: List[str]) -> None:
try:
with suppress(KeyboardInterrupt):
with suppress(KeyboardInterrupt, EOFError):
real_main(args)
except Exception:
import traceback

View File

@@ -20,12 +20,17 @@ cat >$tmp << 'TERMEOF'
TERMINFO
TERMEOF
tic_out=$(tic -x -o $HOME/.terminfo $tmp 2>&1)
tname=.terminfo
if [ -e "/usr/share/misc/terminfo.cdb" ]; then
# NetBSD requires this see https://github.com/kovidgoyal/kitty/issues/4622
tname=".terminfo.cdb"
fi
tic_out=$(tic -x -o $HOME/$tname $tmp 2>&1)
rc=$?
rm $tmp
if [ "$rc" != "0" ]; then echo "$tic_out"; exit 1; fi
if [ -z "$USER" ]; then export USER=$(whoami); fi
export TERMINFO="$HOME/.terminfo"
export TERMINFO="$HOME/$tname"
login_shell=""
python=""
@@ -109,8 +114,12 @@ import subprocess, os, sys, pwd, binascii, json
# macOS ships with an ancient version of tic that cannot read from stdin, so we
# create a temp file for it
with NamedTemporaryFile() as tmp:
tname = '.terminfo'
if os.path.exists('/usr/share/misc/terminfo.cdb'):
tname += '.cdb'
tmp.write(binascii.unhexlify('{terminfo}'))
p = subprocess.Popen(['tic', '-x', '-o', os.path.expanduser('~/.terminfo'), tmp.name], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
tmp.flush()
p = subprocess.Popen(['tic', '-x', '-o', os.path.expanduser('~/' + tname), tmp.name], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
if p.wait() != 0:
getattr(sys.stderr, 'buffer', sys.stderr).write(stdout + stderr)

View File

@@ -36,7 +36,7 @@ def patch_conf(raw: str, theme_name: str) -> str:
nraw = raw + addition
# comment out all existing color definitions
color_conf_items = ( # {{{
# generated by gen_config.py do not EDIT
# generated by gen-config.py DO NOT edit
# ALL_COLORS_START
'active_border_color',
'active_tab_background',

View File

@@ -110,14 +110,16 @@ class TermManager:
class MouseButton(IntFlag):
NONE, LEFT, MIDDLE, RIGHT, FOURTH, FIFTH = 0, 1, 2, 4, 8, 16
WHEEL_UP, WHEEL_DOWN = -1, -2
bmap = {0: MouseButton.LEFT, 1: MouseButton.MIDDLE, 2: MouseButton.RIGHT}
MOTION_INDICATOR = 1 << 5
EXTRA_BUTTON_INDICATOR = 1 << 6
SHIFT_INDICATOR = 1 << 2
ALT_INDICATOR = 1 << 3
CTRL_INDICATOR = 1 << 4
MOTION_INDICATOR = 1 << 5
SCROLL_BUTTON_INDICATOR = 1 << 6
EXTRA_BUTTON_INDICATOR = 1 << 7
class EventType(Enum):
@@ -149,7 +151,9 @@ def decode_sgr_mouse(text: str, screen_size: ScreenSize) -> MouseEvent:
buttons: MouseButton = MouseButton.NONE
cb3 = cb & 3
if cb3 != 3:
if cb & EXTRA_BUTTON_INDICATOR:
if cb & SCROLL_BUTTON_INDICATOR:
buttons = MouseButton.WHEEL_DOWN if cb3 & 1 else MouseButton.WHEEL_UP
elif cb & EXTRA_BUTTON_INDICATOR:
buttons |= MouseButton.FIFTH if cb3 & 1 else MouseButton.FOURTH
else:
buttons |= bmap[cb3]

View File

@@ -99,10 +99,7 @@ def as_rst() -> str:
for action in allg[group]:
a('')
a(f'.. _action-{action.name}:')
a('')
a(action.name)
a('+' * len(action.name))
a(f'.. action:: {action.name}')
a('')
a(action.short_help)
a('')

View File

@@ -5,12 +5,14 @@ import atexit
import json
import os
import re
import sys
from contextlib import suppress
from functools import partial
from gettext import gettext as _
from time import monotonic
from typing import (
Any, Callable, Container, Dict, Iterable, Iterator, List, Optional, Tuple,
Union
Any, Callable, Container, Dict, Iterable, Iterator, List, Optional,
Sequence, Tuple, Union
)
from weakref import WeakValueDictionary
@@ -229,7 +231,6 @@ class Boss:
global_shortcuts: Dict[str, SingleKey]
):
set_layout_options(opts)
self.cocoa_application_launched = False
self.clipboard_buffers: Dict[str, str] = {}
self.update_check_process: Optional['PopenType[bytes]'] = None
self.window_id_map: WeakValueDictionary[int, Window] = WeakValueDictionary()
@@ -273,9 +274,9 @@ class Boss:
for sc in self.global_shortcuts.values():
self.keymap.pop(sc, None)
def startup_first_child(self, os_window_id: Optional[int]) -> None:
startup_sessions = create_sessions(get_options(), self.args, default_session=get_options().startup_session)
for startup_session in startup_sessions:
def startup_first_child(self, os_window_id: Optional[int], startup_sessions: Sequence[Optional[Session]] = ()) -> None:
si = startup_sessions or create_sessions(get_options(), self.args, default_session=get_options().startup_session)
for startup_session in si:
self.add_os_window(startup_session, os_window_id=os_window_id)
os_window_id = None
if self.args.start_as != 'normal':
@@ -546,6 +547,10 @@ class Boss:
from .cli_stub import CLIOptions
startup_id = data.get('startup_id')
args, rest = parse_args(data['args'][1:], result_class=CLIOptions)
cmdline_args_for_open = data.get('cmdline_args_for_open')
if cmdline_args_for_open:
self.launch_urls(*cmdline_args_for_open, no_replace_window=True)
return None
args.args = rest
opts = create_opts(args)
if args.session == '-':
@@ -746,7 +751,14 @@ class Boss:
if not getattr(self, 'io_thread_started', False):
self.child_monitor.start()
self.io_thread_started = True
self.startup_first_child(first_os_window_id)
urls: List[str] = getattr(sys, 'cmdline_args_for_open', [])
sess = create_sessions(get_options(), self.args, special_window=SpecialWindow([kitty_exe(), '+runpy', 'input()']))
if urls:
delattr(sys, 'cmdline_args_for_open')
self.startup_first_child(first_os_window_id, startup_sessions=tuple(sess))
self.launch_urls(*urls)
else:
self.startup_first_child(first_os_window_id)
if get_options().update_check_interval > 0 and not hasattr(self, 'update_check_started'):
from .update_check import run_update_check
@@ -779,7 +791,8 @@ class Boss:
map kitty_mod+f11 clear_terminal scrollback active
# Scroll the contents of the screen into the scrollback
map kitty_mod+f12 clear_terminal scroll active
# Clear everything up to the line with the cursor
map kitty_mod+f9 clear_terminal to_cursor active
''')
def clear_terminal(self, action: str, only_active: bool) -> None:
if only_active:
@@ -792,8 +805,10 @@ class Boss:
reset = action == 'reset'
how = 3 if action == 'scrollback' else 2
for w in windows:
if action == 'scroll':
w.screen.scroll_until_cursor()
if action in ('to_cursor', 'scroll'):
w.screen.scroll_until_cursor_prompt()
if action == 'to_cursor':
w.screen.clear_scrollback()
continue
w.screen.cursor.x = w.screen.cursor.y = 0
if reset:
@@ -1206,7 +1221,12 @@ class Boss:
if w is not None:
text = data.decode('utf-8', 'replace')
if mime == 'text/uri-list':
text = '\n'.join(parse_uri_list(text))
urls = parse_uri_list(text)
if w.at_prompt:
import shlex
text = ' '.join(map(shlex.quote, urls))
else:
text = '\n'.join(urls)
w.paste(text)
@ac('win', 'Focus the nth OS window')
@@ -1811,6 +1831,8 @@ class Boss:
self._create_tab(list(args), cwd_from=cwd_from)
def new_tab_with_wd(self, wd: str) -> None:
if not self.os_window_map:
self.add_os_window()
special_window = SpecialWindow(None, cwd=wd)
self._new_tab(special_window)
@@ -2136,7 +2158,7 @@ class Boss:
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}]'
return f'{tab.name or tab.title} [{tab.num_window_groups} {w}]'
ct = self.active_tab
st = ''
@@ -2208,8 +2230,6 @@ class Boss:
# Can be called with kitty -o "map f1 send_test_notification"
def send_test_notification(self) -> None:
from time import monotonic
from .notify import notify
now = monotonic()
ident = f'test-notify-{now}'
@@ -2231,30 +2251,44 @@ class Boss:
output = '\n'.join(f'{k}={v}' for k, v in os.environ.items())
self.display_scrollback(w, output, title=_('Current kitty env vars'), report_cursor=False)
def open_file(self, path: str) -> None:
if path == ":cocoa::application launched::":
self.cocoa_application_launched = True
return
from .open_actions import actions_for_launch
def launch_urls(self, *urls: str, no_replace_window: bool = False) -> None:
from .launch import force_window_launch
actions = list(actions_for_launch(path))
from .open_actions import actions_for_launch
actions: List[KeyAction] = []
failures = []
for url in urls:
uactions = tuple(actions_for_launch(url))
if uactions:
actions.extend(uactions)
else:
failures.append(url)
tab = self.active_tab
if tab is not None:
w = tab.active_window
else:
w = None
needs_window_replaced = not self.cocoa_application_launched or not self.os_window_map and w is not None and w.id == 1
needs_window_replaced = False
if not no_replace_window and not get_options().startup_session:
if w is not None and w.id == 1 and monotonic() - w.started_at < 2 and len(tuple(self.all_windows)) == 1:
# first window, soon after startup replace it
needs_window_replaced = True
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)')
if failures:
from kittens.tui.operations import styled
spec = '\n '.join(styled(u, fg='red') for u in failures)
bdata = f"Unknown URL type, cannot open:\n\n {spec}".encode('utf-8')
special_window = SpecialWindow([kitty_exe(), '+kitten', 'show_error', '--title', 'Open URL Error'], bdata, 'Open URL Error')
if needs_window_replaced and tab is not None:
tab.new_special_window(special_window)
else:
self._new_os_window(special_window)
clear_initial_window()
else:
needs_window_replaced = False
if actions:
with force_window_launch(needs_window_replaced):
self.dispatch_action(actions.pop(0))
clear_initial_window()

View File

@@ -1,5 +1,5 @@
#version GLSL_VERSION
#define WHICH_PROGRAM
#define {WHICH_PROGRAM}
#define NOT_TRANSPARENT
#if defined(SIMPLE) || defined(BACKGROUND) || defined(SPECIAL)

View File

@@ -1,5 +1,5 @@
#version GLSL_VERSION
#define WHICH_PROGRAM
#define {WHICH_PROGRAM}
#define NOT_TRANSPARENT
#define DECORATION_SHIFT {DECORATION_SHIFT}
#define REVERSE_SHIFT {REVERSE_SHIFT}
@@ -79,8 +79,8 @@ const uint COLOR_MASK = uint(0x4000);
const uint ZERO = uint(0);
const uint ONE = uint(1);
const uint TWO = uint(2);
const uint FOUR = uint(4);
const uint SEVEN = uint(7);
const uint STRIKE_SPRITE_INDEX = uint({STRIKE_SPRITE_INDEX});
const uint DECORATION_MASK = uint({DECORATION_MASK});
vec3 color_to_vec(uint c) {
uint r, g, b;
@@ -193,8 +193,8 @@ void main() {
foreground = choose_color(float(is_selected & ONE), selection_color, foreground);
decoration_fg = choose_color(float(is_selected & ONE), selection_color, decoration_fg);
// 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) & SEVEN, ZERO, ZERO));
strike_pos = to_sprite_pos(pos, ((text_attrs >> STRIKE_SHIFT) & ONE) * FOUR, ZERO, ZERO);
underline_pos = choose_color(in_url, to_sprite_pos(pos, url_style, ZERO, ZERO), to_sprite_pos(pos, (text_attrs >> DECORATION_SHIFT) & DECORATION_MASK, ZERO, ZERO));
strike_pos = to_sprite_pos(pos, ((text_attrs >> STRIKE_SHIFT) & ONE) * STRIKE_SPRITE_INDEX, ZERO, ZERO);
// Cursor
cursor_color_vec = vec4(color_to_vec(cursor_bg), 1.0);

View File

@@ -553,8 +553,6 @@ pyset_iutf8(ChildMonitor *self, PyObject *args) {
#undef INCREF_CHILD
#undef DECREF_CHILD
extern void cocoa_update_menu_bar_title(PyObject*);
static bool
cursor_needs_render(Window *w) {
return w->cursor_visible_at_last_render != w->render_data.screen->cursor_render_info.is_visible || w->last_cursor_x != w->render_data.screen->cursor_render_info.x || w->last_cursor_y != w->render_data.screen->cursor_render_info.y || w->last_cursor_shape != w->render_data.screen->cursor_render_info.shape;
@@ -592,7 +590,7 @@ change_menubar_title(PyObject *title UNUSED) {
static PyObject *current_title = NULL;
if (title != current_title) {
current_title = title;
if (title && OPT(macos_show_window_title_in) & MENUBAR) cocoa_update_menu_bar_title(title);
if (title && OPT(macos_show_window_title_in) & MENUBAR) update_menu_bar_title(title);
}
#endif
}
@@ -1001,21 +999,21 @@ static bool cocoa_pending_actions[NUM_COCOA_PENDING_ACTIONS] = {0};
static bool has_cocoa_pending_actions = false;
typedef struct {
char* wd;
char **open_files;
size_t open_files_count;
size_t open_files_capacity;
char **open_urls;
size_t open_urls_count;
size_t open_urls_capacity;
} CocoaPendingActionsData;
static CocoaPendingActionsData cocoa_pending_actions_data = {0};
void
set_cocoa_pending_action(CocoaPendingAction action, const char *wd) {
if (wd) {
if (action == OPEN_FILE) {
ensure_space_for(&cocoa_pending_actions_data, open_files, char*, cocoa_pending_actions_data.open_files_count + 8, open_files_capacity, 8, true);
cocoa_pending_actions_data.open_files[cocoa_pending_actions_data.open_files_count++] = strdup(wd);
set_cocoa_pending_action(CocoaPendingAction action, const char *data) {
if (data) {
if (action == LAUNCH_URLS) {
ensure_space_for(&cocoa_pending_actions_data, open_urls, char*, cocoa_pending_actions_data.open_urls_count + 8, open_urls_capacity, 8, true);
cocoa_pending_actions_data.open_urls[cocoa_pending_actions_data.open_urls_count++] = strdup(data);
} else {
if (cocoa_pending_actions_data.wd) free(cocoa_pending_actions_data.wd);
cocoa_pending_actions_data.wd = strdup(wd);
cocoa_pending_actions_data.wd = strdup(data);
}
}
cocoa_pending_actions[action] = true;
@@ -1038,23 +1036,25 @@ process_cocoa_pending_actions(void) {
if (cocoa_pending_actions[NEW_WINDOW]) { call_boss(new_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[CLEAR_TERMINAL_AND_SCROLLBACK]) { call_boss(clear_terminal, "sO", "to_cursor", Py_True ); }
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[TOGGLE_FULLSCREEN]) { call_boss(toggle_fullscreen, NULL); }
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_TAB_WITH_WD]) { call_boss(new_tab_with_wd, "s", cocoa_pending_actions_data.wd); }
free(cocoa_pending_actions_data.wd);
cocoa_pending_actions_data.wd = NULL;
}
if (cocoa_pending_actions_data.open_files_count) {
for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.open_files_count; cpa++) {
if (cocoa_pending_actions_data.open_files[cpa]) {
call_boss(open_file, "s", cocoa_pending_actions_data.open_files[cpa]);
free(cocoa_pending_actions_data.open_files[cpa]);
cocoa_pending_actions_data.open_files[cpa] = NULL;
if (cocoa_pending_actions_data.open_urls_count) {
for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.open_urls_count; cpa++) {
if (cocoa_pending_actions_data.open_urls[cpa]) {
call_boss(launch_urls, "s", cocoa_pending_actions_data.open_urls[cpa]);
free(cocoa_pending_actions_data.open_urls[cpa]);
cocoa_pending_actions_data.open_urls[cpa] = NULL;
}
}
cocoa_pending_actions_data.open_files_count = 0;
cocoa_pending_actions_data.open_urls_count = 0;
}
memset(cocoa_pending_actions, 0, sizeof(cocoa_pending_actions));
has_cocoa_pending_actions = false;
@@ -1114,11 +1114,11 @@ main_loop(ChildMonitor *self, PyObject *a UNUSED) {
run_main_loop(process_global_state, self);
#ifdef __APPLE__
if (cocoa_pending_actions_data.wd) { free(cocoa_pending_actions_data.wd); cocoa_pending_actions_data.wd = NULL; }
if (cocoa_pending_actions_data.open_files) {
for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.open_files_count; cpa++) {
if (cocoa_pending_actions_data.open_files[cpa]) free(cocoa_pending_actions_data.open_files[cpa]);
if (cocoa_pending_actions_data.open_urls) {
for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.open_urls_count; cpa++) {
if (cocoa_pending_actions_data.open_urls[cpa]) free(cocoa_pending_actions_data.open_urls[cpa]);
}
free(cocoa_pending_actions_data.open_files); cocoa_pending_actions_data.open_files = NULL;
free(cocoa_pending_actions_data.open_urls); cocoa_pending_actions_data.open_urls = NULL;
}
#endif
if (PyErr_Occurred()) return NULL;

View File

@@ -196,7 +196,7 @@ class Child:
allow_remote_control: bool = False
):
self.allow_remote_control = allow_remote_control
self.argv = argv
self.argv = list(argv)
if cwd_from is not None:
try:
cwd = cwd_of_process(cwd_from)
@@ -233,7 +233,7 @@ class Child:
opts = fast_data_types.get_options()
if 'disabled' not in opts.shell_integration:
from .shell_integration import modify_shell_environ
modify_shell_environ(self.argv[0], opts, env)
modify_shell_environ(opts, env, self.argv)
env = {k: v for k, v in env.items() if v is not DELETE_ENV_VAR}
return env
@@ -344,8 +344,18 @@ class Child:
assert self.child_fd is not None
pgrp = os.tcgetpgrp(self.child_fd)
foreground_processes = processes_in_group(pgrp) if pgrp >= 0 else []
if len(foreground_processes) == 1:
return foreground_processes[0]
if foreground_processes:
# there is no easy way that I know of to know which process is the
# foreground process in this group from the users perspective,
# so we assume the one with the highest PID is as that is most
# likely to be the newest process. This situation can happen
# for example with a shell script such as:
# #!/bin/bash
# cd /tmp
# vim
# With this script , the foreground process group will contain
# both the bash instance running the script and vim.
return max(foreground_processes)
return self.pid
@property

View File

@@ -231,8 +231,10 @@ PENDING(previous_tab, PREVIOUS_TAB)
PENDING(new_window, NEW_WINDOW)
PENDING(close_window, CLOSE_WINDOW)
PENDING(reset_terminal, RESET_TERMINAL)
PENDING(clear_terminal_and_scrollback, CLEAR_TERMINAL_AND_SCROLLBACK)
PENDING(reload_config, RELOAD_CONFIG)
PENDING(toggle_macos_secure_keyboard_entry, TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY)
PENDING(toggle_fullscreen, TOGGLE_FULLSCREEN)
- (void)open_kitty_website_url:(id)sender {
(void)sender;
@@ -242,6 +244,8 @@ PENDING(toggle_macos_secure_keyboard_entry, TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY)
- (BOOL)validateMenuItem:(NSMenuItem *)item {
if (item.action == @selector(toggle_macos_secure_keyboard_entry:)) {
item.state = [SecureKeyboardEntryController sharedInstance].isDesired ? NSControlStateValueOn : NSControlStateValueOff;
} else if (item.action == @selector(toggle_fullscreen:)) {
item.title = ([NSApp currentSystemPresentationOptions] & NSApplicationPresentationFullScreen) ? @"Exit Full Screen" : @"Enter Full Screen";
}
return YES;
}
@@ -270,8 +274,8 @@ typedef struct {
} GlobalShortcut;
typedef struct {
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 toggle_macos_secure_keyboard_entry;
GlobalShortcut previous_tab, next_tab, new_tab, new_window, close_window, reset_terminal, clear_terminal_and_scrollback;
GlobalShortcut toggle_macos_secure_keyboard_entry, toggle_fullscreen;
} GlobalShortcuts;
static GlobalShortcuts global_shortcuts;
@@ -285,8 +289,8 @@ cocoa_set_global_shortcut(PyObject *self UNUSED, PyObject *args) {
#define Q(x) if (strcmp(name, #x) == 0) gs = &global_shortcuts.x
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_window); else Q(close_window); else Q(reset_terminal); else Q(reload_config);
else Q(toggle_macos_secure_keyboard_entry);
else Q(new_window); else Q(close_window); else Q(reset_terminal); else Q(clear_terminal_and_scrollback); else Q(reload_config);
else Q(toggle_macos_secure_keyboard_entry); else Q(toggle_fullscreen);
#undef Q
if (gs == NULL) { PyErr_SetString(PyExc_KeyError, "Unknown shortcut name"); return NULL; }
int cocoa_mods;
@@ -519,6 +523,19 @@ cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
return YES;
}
- (BOOL)openFileURLs:(NSPasteboard*)pasteboard
userData:(NSString *) UNUSED userData error:(NSError **) UNUSED error {
NSDictionary *options = @{ NSPasteboardURLReadingFileURLsOnlyKey: @YES };
NSArray *urlArray = [pasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]] options:options];
for (NSURL *url in urlArray) {
NSString *path = [url path];
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
set_cocoa_pending_action(LAUNCH_URLS, [[[NSURL fileURLWithPath:path] absoluteString] UTF8String]);
}
}
return YES;
}
@end
// global menu {{{
@@ -593,6 +610,7 @@ cocoa_create_global_menu(void) {
MENU_ITEM(shellMenu, @"Close Window", close_window);
[shellMenu addItem:[NSMenuItem separatorItem]];
MENU_ITEM(shellMenu, @"Reset", reset_terminal);
MENU_ITEM(shellMenu, @"Clear to Cursor Line", clear_terminal_and_scrollback);
[shellMenu release];
NSMenuItem* windowMenuItem =
@@ -621,10 +639,7 @@ cocoa_create_global_menu(void) {
keyEquivalent:@""] setTarget:global_menu_target];
[windowMenu addItem:[NSMenuItem separatorItem]];
[[windowMenu addItemWithTitle:@"Enter Full Screen"
action:@selector(toggleFullScreen:)
keyEquivalent:@"f"]
setKeyEquivalentModifierMask:NSEventModifierFlagControl | NSEventModifierFlagCommand];
MENU_ITEM(windowMenu, @"Enter Full Screen", toggle_fullscreen);
[NSApp setWindowsMenu:windowMenu];
[windowMenu release];
@@ -656,7 +671,16 @@ cocoa_create_global_menu(void) {
void
cocoa_update_menu_bar_title(PyObject *pytitle) {
NSString *title = @(PyUnicode_AsUTF8(pytitle));
NSString *title = nil;
if (OPT(macos_menubar_title_max_length) > 0 && PyUnicode_GetLength(pytitle) > OPT(macos_menubar_title_max_length)) {
static char fmt[64];
snprintf(fmt, sizeof(fmt), "%%%ld.%ldU%%s", OPT(macos_menubar_title_max_length), OPT(macos_menubar_title_max_length));
DECREF_AFTER_FUNCTION PyObject *st = PyUnicode_FromFormat(fmt, pytitle, "…");
if (st) title = @(PyUnicode_AsUTF8(st));
} else {
title = @(PyUnicode_AsUTF8(pytitle));
}
if (!title) return;
NSMenu *bar = [NSApp mainMenu];
if (title_menu != NULL) {
[bar removeItem:title_menu];
@@ -814,6 +838,33 @@ cocoa_set_titlebar_color(void *w, color_type titlebar_color)
} // autoreleasepool
}
static PyObject*
cocoa_set_url_handler(PyObject UNUSED *self, PyObject *args) {
@autoreleasepool {
const char *url_scheme = NULL, *bundle_id = NULL;
if (!PyArg_ParseTuple(args, "s|z", &url_scheme, &bundle_id)) return NULL;
if (!url_scheme || url_scheme[0] == '\0') {
PyErr_SetString(PyExc_TypeError, "Empty url scheme");
return NULL;
}
NSString *scheme = [NSString stringWithUTF8String:url_scheme];
NSString *identifier = @"";
if (!bundle_id) {
identifier = [[NSBundle mainBundle] bundleIdentifier];
if (!identifier || identifier.length == 0) identifier = @"net.kovidgoyal.kitty";
} else if (bundle_id[0] != '\0') {
identifier = [NSString stringWithUTF8String:bundle_id];
}
// This API has been marked as deprecated. It will need to be replaced when a new approach is available.
OSStatus err = LSSetDefaultHandlerForURLScheme((CFStringRef)scheme, (CFStringRef)identifier);
if (err == noErr) Py_RETURN_NONE;
PyErr_Format(PyExc_OSError, "Failed to set default handler with error code: %d", err);
return NULL;
} // autoreleasepool
}
static NSSound *beep_sound = nil;
static void
@@ -863,6 +914,7 @@ static PyMethodDef module_methods[] = {
{"cocoa_set_global_shortcut", (PyCFunction)cocoa_set_global_shortcut, METH_VARARGS, ""},
{"cocoa_send_notification", (PyCFunction)cocoa_send_notification, METH_VARARGS, ""},
{"cocoa_set_notification_activated_callback", (PyCFunction)set_notification_activated_callback, METH_O, ""},
{"cocoa_set_url_handler", (PyCFunction)cocoa_set_url_handler, METH_VARARGS, ""},
{NULL, NULL, 0, NULL} /* Sentinel */
};

View File

@@ -120,6 +120,8 @@ def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
tc_imports.add((func.__module__, func.__name__))
defval = repr(func(option.defval_as_string))
if len(defval) > 100:
defval += ' # noqa'
if option.macos_defval is not unset:
md = repr(func(option.macos_defval))
defval = f'{md} if is_macos else {defval}'

View File

@@ -53,6 +53,7 @@ def ref_map() -> Dict[str, str]:
'sessions': f'{website_url("overview")}#startup-sessions',
'functional': f'{website_url("keyboard-protocol")}#functional-key-definitions',
'shell_integration': website_url("shell-integration"),
'github_discussions': 'https://github.com/kovidgoyal/kitty/discussions',
}
for actions in get_all_actions().values():
for ac in actions:
@@ -66,6 +67,14 @@ def remove_markup(text: str) -> str:
if m.group(1) == 'ref':
q = m.group(2).split('<')[-1].rstrip('>')
return ref_map()[q]
if m.group(1) == 'ac':
q = m.group(2).split('<')[-1].rstrip('>')
return ref_map()[f'action-{q}']
if m.group(1) == 'disc':
parts = m.group(2).split('<')
t = parts[0].strip()
q = parts[-1].rstrip('>')
return f'{t} {ref_map()["github_discussions"]}/{q}'
return str(m.group(2))
return re.sub(r':([a-zA-Z0-9]+):`(.+?)`', sub, text, flags=re.DOTALL)

View File

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

View File

@@ -302,6 +302,8 @@ PyInit_fast_data_types(void) {
s(STRIKETHROUGH, strike); s(DIM, dim); s(DECORATION, decoration);
#undef s
PyModule_AddIntConstant(m, "MARK_MASK", MARK_MASK);
PyModule_AddIntConstant(m, "DECORATION_MASK", DECORATION_MASK);
PyModule_AddIntConstant(m, "NUM_UNDERLINE_STYLES", NUM_UNDERLINE_STYLES);
PyModule_AddStringMacro(m, ERROR_PREFIX);
#ifdef KITTY_VCS_REV
PyModule_AddStringMacro(m, KITTY_VCS_REV);

View File

@@ -154,6 +154,8 @@ typedef union CellAttrs {
} CellAttrs;
#define MARK_MASK (3u)
#define WIDTH_MASK (3u)
#define DECORATION_MASK (7u)
#define NUM_UNDERLINE_STYLES (5u)
#define SGR_MASK (~(((CellAttrs){.width=WIDTH_MASK, .mark=MARK_MASK}).val))
typedef struct {

View File

@@ -215,6 +215,19 @@ def debug_config(opts: KittyOpts) -> str:
p(' ', '\n '.join(opts.config_overrides))
compare_opts(opts, p)
p()
p(green('Environment variable names seen by the kitty process:'))
p('\t' + '\n\t'.join(sorted(os.environ)))
p(green('Important environment variables seen by the kitty process:'))
def penv(k: str) -> None:
v = os.environ.get(k)
if v is not None:
p('\t' + k.ljust(35), styled(v, dim=True))
for k in (
'PATH LANG KITTY_CONFIG_DIRECTORY KITTY_CACHE_DIRECTORY VISUAL EDITOR SHELL'
' GLFW_IM_MODULE KITTY_WAYLAND_DETECT_MODIFIERS DISPLAY WAYLAND_DISPLAY USER XCURSOR_SIZE'
).split():
penv(k)
for k in os.environ:
if k.startswith('LC_') or k.startswith('XDG_'):
penv(k)
return out.getvalue()

View File

@@ -154,6 +154,7 @@ play_canberra_sound(const char *which_sound, const char *event_id, bool is_path,
which_type, which_sound,
"event.description", event_id,
"media.role", media_role,
"canberra.cache-control", "permanent",
NULL
);
}

View File

@@ -256,6 +256,8 @@ GRAPHICS_PREMULT_PROGRAM: int
GRAPHICS_PROGRAM: int
MARK: int
MARK_MASK: int
DECORATION_MASK: int
NUM_UNDERLINE_STYLES: int
OSC: int
FILE_TRANSFER_CODE: int
REVERSE: int
@@ -736,6 +738,10 @@ def cocoa_get_lang() -> Optional[str]:
pass
def cocoa_set_url_handler(url_scheme: str, bundle_id: Optional[str]) -> None:
pass
def locale_is_valid(name: str) -> bool:
pass
@@ -961,7 +967,8 @@ def set_font_data(
descriptor_for_idx: Callable[[int], Tuple[FontObject, bool, bool]],
bold: int, italic: int, bold_italic: int, num_symbol_fonts: int,
symbol_maps: Tuple[Tuple[int, int, int], ...], font_sz_in_pts: float,
font_feature_settings: Dict[str, Tuple[FontFeature, ...]]
font_feature_settings: Dict[str, Tuple[FontFeature, ...]],
narrow_symbols: Tuple[Tuple[int, int, int], ...],
) -> None:
pass
@@ -980,7 +987,7 @@ class HistoryBuf:
def as_text(self, callback: Callable[[str], None], as_ansi: bool, insert_wrap_markers: bool) -> None:
pass
def pagerhist_as_text(self) -> str:
def pagerhist_as_text(self, upto_output_start: bool = False) -> str:
pass
def pagerhist_as_bytes(self) -> bytes:
@@ -1049,6 +1056,9 @@ class Screen:
def line(self, num: int) -> Line:
pass
def visual_line(self, num: int) -> Line:
pass
def draw(self, text: str) -> None:
pass
@@ -1076,7 +1086,7 @@ class Screen:
def has_selection(self) -> bool:
pass
def text_for_selection(self) -> Tuple[str, ...]:
def text_for_selection(self, ansi: bool, strip_trailing_spaces: bool) -> Tuple[str, ...]:
pass
def is_rectangle_select(self) -> bool:
@@ -1103,6 +1113,9 @@ class Screen:
def reverse_scroll(self, amt: int, fill_from_scrollback: bool = False) -> bool:
pass
def scroll_prompt_to_bottom(self) -> None:
pass
def clear_selection(self) -> None:
pass
@@ -1124,10 +1137,10 @@ class Screen:
as_text_non_visual = as_text
as_text_alternate = as_text
def cmd_output(self, which: int, callback: Callable[[str], None], as_ansi: bool, insert_wrap_markers: bool) -> None:
def cmd_output(self, which: int, callback: Callable[[str], None], as_ansi: bool, insert_wrap_markers: bool) -> bool:
pass
def scroll_until_cursor(self) -> None:
def scroll_until_cursor_prompt(self) -> None:
pass
def reset(self) -> None:
@@ -1136,6 +1149,9 @@ class Screen:
def erase_in_display(self, how: int = 0, private: bool = False) -> None:
pass
def clear_scrollback(self) -> None:
pass
def focus_changed(self, focused: bool) -> bool:
pass

View File

@@ -13,7 +13,7 @@
#include "charsets.h"
#include "glyph-cache.h"
#define MISSING_GLYPH 4
#define MISSING_GLYPH (NUM_UNDERLINE_STYLES + 2)
#define MAX_NUM_EXTRA_GLYPHS_PUA 4u
typedef void (*send_sprite_to_gpu_func)(FONTS_DATA_HANDLE fg, unsigned int, unsigned int, unsigned int, pixel*);
@@ -50,8 +50,8 @@ typedef struct {
size_t font_idx;
} SymbolMap;
static SymbolMap *symbol_maps = NULL;
static size_t num_symbol_maps = 0;
static SymbolMap *symbol_maps = NULL, *narrow_symbols = NULL;
static size_t num_symbol_maps = 0, num_narrow_symbols = 0;
typedef enum { SPACER_STRATEGY_UNKNOWN, SPACERS_BEFORE, SPACERS_AFTER, SPACERS_IOSEVKA } SpacerStrategy;
@@ -492,8 +492,8 @@ in_symbol_maps(FontGroup *fg, char_type ch) {
// - BOX_FONT
// - an index in the fonts list
static ssize_t
font_for_cell(FontGroup *fg, CPUCell *cpu_cell, GPUCell *gpu_cell, bool *is_fallback_font, bool *is_emoji_presentation) {
*is_fallback_font = false;
font_for_cell(FontGroup *fg, CPUCell *cpu_cell, GPUCell *gpu_cell, bool *is_main_font, bool *is_emoji_presentation) {
*is_main_font = false;
*is_emoji_presentation = false;
START_ALLOW_CASE_RANGE
ssize_t ans;
@@ -510,6 +510,7 @@ START_ALLOW_CASE_RANGE
case 0x1fba0 ... 0x1fbae:
return BOX_FONT;
default:
*is_emoji_presentation = has_emoji_presentation(cpu_cell, gpu_cell);
ans = in_symbol_maps(fg, cpu_cell->ch);
if (ans > -1) return ans;
switch(gpu_cell->attrs.bold | (gpu_cell->attrs.italic << 1)) {
@@ -523,9 +524,7 @@ START_ALLOW_CASE_RANGE
ans = fg->bi_font_idx; break;
}
if (ans < 0) ans = fg->medium_font_idx;
*is_emoji_presentation = has_emoji_presentation(cpu_cell, gpu_cell);
if (!*is_emoji_presentation && has_cell_text(fg->fonts + ans, cpu_cell)) return ans;
*is_fallback_font = true;
if (!*is_emoji_presentation && has_cell_text(fg->fonts + ans, cpu_cell)) { *is_main_font = true; return ans; }
return fallback_font(fg, cpu_cell, gpu_cell);
}
END_ALLOW_CASE_RANGE
@@ -852,6 +851,19 @@ detect_spacer_strategy(hb_font_t *hbf, Font *font) {
}
}
}
// If spacer_strategy is still default, check ### glyph to to confirm strategy
// https://github.com/kovidgoyal/kitty/issues/4721
if (font->spacer_strategy == SPACERS_BEFORE) {
cpu_cells[0].ch = '#'; cpu_cells[1].ch = '#'; cpu_cells[2].ch = '#';
shape(cpu_cells, gpu_cells, arraysz(cpu_cells), hbf, font, false);
if (G(num_glyphs) > 1) {
glyph_index glyph_id = G(info)[G(num_glyphs) - 1].codepoint;
bool is_special = is_special_glyph(glyph_id, font, &G(current_cell_data));
bool is_empty = is_special && is_empty_glyph(glyph_id, font);
if (is_empty) font->spacer_strategy = SPACERS_AFTER;
}
}
}
static LigatureType
@@ -963,7 +975,7 @@ group_normal(Font *font, hb_font_t *hbf) {
*
* We rely on the cluster numbers from harfbuzz to tell us how many unicode codepoints a glyph corresponds to.
* Then we check if the glyph is a ligature glyph (is_special_glyph) and if it is an empty glyph.
* We detect if the font uses EMPTY glyphs before or after ligature glyphs (1. or 3. above) by checking what it does for ===.
* We detect if the font uses EMPTY glyphs before or after ligature glyphs (1. or 3. above) by checking what it does for === and ###.
* Finally we look at the glyph name. These five datapoints give us enough information to satisfy the constraint above,
* for a wide variety of fonts.
*/
@@ -1220,6 +1232,17 @@ is_non_emoji_dingbat(char_type ch) {
return false;
}
static unsigned int
cell_cap_for_codepoint(const char_type cp) {
unsigned int ans = UINT_MAX;
for (size_t i = 0; i < num_narrow_symbols; i++) {
SymbolMap *sm = narrow_symbols + i;
if (sm->left <= cp && cp <= sm->right) ans = sm->font_idx;
}
return ans;
}
void
render_line(FONTS_DATA_HANDLE fg_, Line *line, index_type lnum, Cursor *cursor, DisableLigature disable_ligature_strategy) {
#define RENDER if (run_font_idx != NO_FONT && i > first_cell_in_run) { \
@@ -1237,12 +1260,12 @@ render_line(FONTS_DATA_HANDLE fg_, Line *line, index_type lnum, Cursor *cursor,
if (prev_width == 2) { prev_width = 0; continue; }
CPUCell *cpu_cell = line->cpu_cells + i;
GPUCell *gpu_cell = line->gpu_cells + i;
bool is_fallback_font, is_emoji_presentation;
ssize_t cell_font_idx = font_for_cell(fg, cpu_cell, gpu_cell, &is_fallback_font, &is_emoji_presentation);
bool is_main_font, is_emoji_presentation;
ssize_t cell_font_idx = font_for_cell(fg, cpu_cell, gpu_cell, &is_main_font, &is_emoji_presentation);
if (
cell_font_idx != MISSING_FONT &&
((is_fallback_font && !is_emoji_presentation && is_symbol(cpu_cell->ch)) || (cell_font_idx != BOX_FONT && (is_private_use(cpu_cell->ch))) || is_non_emoji_dingbat(cpu_cell->ch))
((!is_main_font && !is_emoji_presentation && is_symbol(cpu_cell->ch)) || (cell_font_idx != BOX_FONT && (is_private_use(cpu_cell->ch))) || is_non_emoji_dingbat(cpu_cell->ch))
) {
unsigned int desired_cells = 1;
if (cell_font_idx > 0) {
@@ -1252,6 +1275,7 @@ render_line(FONTS_DATA_HANDLE fg_, Line *line, index_type lnum, Cursor *cursor,
int width = get_glyph_width(font->face, glyph_id);
desired_cells = (unsigned int)ceilf((float)width / fg->cell_width);
}
desired_cells = MIN(desired_cells, cell_cap_for_codepoint(cpu_cell->ch));
unsigned int num_spaces = 0;
while (
@@ -1304,6 +1328,7 @@ render_simple_text(FONTS_DATA_HANDLE fg_, const char *text) {
static void
clear_symbol_maps(void) {
if (symbol_maps) { free(symbol_maps); symbol_maps = NULL; num_symbol_maps = 0; }
if (narrow_symbols) { free(narrow_symbols); narrow_symbols = NULL; num_narrow_symbols = 0; }
}
typedef struct {
@@ -1312,26 +1337,33 @@ typedef struct {
DescriptorIndices descriptor_indices = {0};
static PyObject*
set_font_data(PyObject UNUSED *m, PyObject *args) {
PyObject *sm;
Py_CLEAR(box_drawing_function); Py_CLEAR(prerender_function); Py_CLEAR(descriptor_for_idx); Py_CLEAR(font_feature_settings);
if (!PyArg_ParseTuple(args, "OOOIIIIO!dO",
&box_drawing_function, &prerender_function, &descriptor_for_idx,
&descriptor_indices.bold, &descriptor_indices.italic, &descriptor_indices.bi, &descriptor_indices.num_symbol_fonts,
&PyTuple_Type, &sm, &OPT(font_size), &font_feature_settings)) return NULL;
Py_INCREF(box_drawing_function); Py_INCREF(prerender_function); Py_INCREF(descriptor_for_idx); Py_INCREF(font_feature_settings);
free_font_groups();
clear_symbol_maps();
num_symbol_maps = PyTuple_GET_SIZE(sm);
symbol_maps = calloc(num_symbol_maps, sizeof(SymbolMap));
if (symbol_maps == NULL) return PyErr_NoMemory();
for (size_t s = 0; s < num_symbol_maps; s++) {
static bool
set_symbol_maps(SymbolMap **maps, size_t *num, const PyObject *sm) {
*num = PyTuple_GET_SIZE(sm);
*maps = calloc(*num, sizeof(SymbolMap));
if (*maps == NULL) { PyErr_NoMemory(); return false; }
for (size_t s = 0; s < *num; s++) {
unsigned int left, right, font_idx;
SymbolMap *x = symbol_maps + s;
SymbolMap *x = *maps + s;
if (!PyArg_ParseTuple(PyTuple_GET_ITEM(sm, s), "III", &left, &right, &font_idx)) return NULL;
x->left = left; x->right = right; x->font_idx = font_idx;
}
return true;
}
static PyObject*
set_font_data(PyObject UNUSED *m, PyObject *args) {
PyObject *sm, *ns;
Py_CLEAR(box_drawing_function); Py_CLEAR(prerender_function); Py_CLEAR(descriptor_for_idx); Py_CLEAR(font_feature_settings);
if (!PyArg_ParseTuple(args, "OOOIIIIO!dOO!",
&box_drawing_function, &prerender_function, &descriptor_for_idx,
&descriptor_indices.bold, &descriptor_indices.italic, &descriptor_indices.bi, &descriptor_indices.num_symbol_fonts,
&PyTuple_Type, &sm, &OPT(font_size), &font_feature_settings, &PyTuple_Type, &ns)) return NULL;
Py_INCREF(box_drawing_function); Py_INCREF(prerender_function); Py_INCREF(descriptor_for_idx); Py_INCREF(font_feature_settings);
free_font_groups();
clear_symbol_maps();
set_symbol_maps(&symbol_maps, &num_symbol_maps, sm);
set_symbol_maps(&narrow_symbols, &num_narrow_symbols, ns);
Py_RETURN_NONE;
}
@@ -1398,6 +1430,11 @@ initialize_font_group(FontGroup *fg) {
}
#undef I
calc_cell_metrics(fg);
// rescale the symbol_map faces for the desired cell height, this is how fallback fonts are sized as well
for (size_t i = 0; i < descriptor_indices.num_symbol_fonts; i++) {
Font *font = fg->fonts + i + fg->first_symbol_font_idx;
set_size_for_face(font->face, fg->cell_height, true, (FONTS_DATA_HANDLE)fg);
}
}

View File

@@ -115,8 +115,13 @@ def find_best_match(family: str, bold: bool = False, italic: bool = False, monos
return candidates[0]
# Use fc-match to see if we can find a monospaced font that matches family
for spacing in (FC_MONO, FC_DUAL):
possibility = fc_match(family, False, False, spacing)
# When aliases are defined, spacing can cause the incorrect font to be
# returned, so check with and without spacing and use the one that matches.
mono_possibility = fc_match(family, False, False, FC_MONO)
dual_possibility = fc_match(family, False, False, FC_DUAL)
any_possibility = fc_match(family, False, False, 0)
tries = (dual_possibility, mono_possibility) if any_possibility == dual_possibility else (mono_possibility, dual_possibility)
for possibility in tries:
for key, map_key in (('postscript_name', 'ps_map'), ('full_name', 'full_map'), ('family', 'family_map')):
val: Optional[str] = cast(Optional[str], possibility.get(key))
if val:

View File

@@ -14,13 +14,14 @@ from kitty.constants import is_macos
from kitty.fast_data_types import (
Screen, create_test_font_group, get_fallback_font, set_font_data,
set_options, set_send_sprite_to_gpu, sprite_map_set_limits,
test_render_line, test_shape
test_render_line, test_shape, NUM_UNDERLINE_STYLES
)
from kitty.fonts.box_drawing import (
BufType, distribute_dots, render_box_char, render_missing_glyph
)
from kitty.options.types import Options, defaults
from kitty.typing import CoreTextFont, FontConfigPattern
from kitty.types import _T
from kitty.utils import log_error
if is_macos:
@@ -50,10 +51,9 @@ def font_for_family(family: str) -> Tuple[FontObject, bool, bool]:
return font_for_family_fontconfig(family)
Range = Tuple[Tuple[int, int], str]
def merge_ranges(a: Range, b: Range, priority_map: Dict[Tuple[int, int], int]) -> Generator[Range, None, None]:
def merge_ranges(
a: Tuple[Tuple[int, int], _T], b: Tuple[Tuple[int, int], _T], priority_map: Dict[Tuple[int, int], int]
) -> Generator[Tuple[Tuple[int, int], _T], None, None]:
a_start, a_end = a[0]
b_start, b_end = b[0]
a_val, b_val = a[1], b[1]
@@ -95,7 +95,7 @@ def merge_ranges(a: Range, b: Range, priority_map: Dict[Tuple[int, int], int]) -
after_range = ((b_end + 1, a_end), a_val)
after_range_prio = a_prio
# check if the before, mid and after ranges can be coalesced
ranges: List[Range] = []
ranges: List[Tuple[Tuple[int, int], _T]] = []
priorities: List[int] = []
for rq, prio in ((before_range, before_range_prio), (mid_range, mid_range_prio), (after_range, after_range_prio)):
if rq is None:
@@ -117,7 +117,7 @@ def merge_ranges(a: Range, b: Range, priority_map: Dict[Tuple[int, int], int]) -
yield from ranges
def coalesce_symbol_maps(maps: Dict[Tuple[int, int], str]) -> Dict[Tuple[int, int], str]:
def coalesce_symbol_maps(maps: Dict[Tuple[int, int], _T]) -> Dict[Tuple[int, int], _T]:
if not maps:
return maps
priority_map = {r: i for i, r in enumerate(maps.keys())}
@@ -155,6 +155,10 @@ def create_symbol_map(opts: Options) -> Tuple[Tuple[int, int, int], ...]:
return sm
def create_narrow_symbols(opts: Options) -> Tuple[Tuple[int, int, int], ...]:
return tuple((a, b, v) for (a, b), v in coalesce_symbol_maps(opts.narrow_symbols).items())
def descriptor_for_idx(idx: int) -> Tuple[FontObject, bool, bool]:
return current_faces[idx]
@@ -193,6 +197,7 @@ def set_font_family(opts: Optional[Options] = None, override_font_size: Optional
current_faces.append((font_map[k], 'b' in k, 'i' in k))
before = len(current_faces)
sm = create_symbol_map(opts)
ns = create_narrow_symbols(opts)
num_symbol_fonts = len(current_faces) - before
font_features = {}
for face, _, _ in current_faces:
@@ -203,7 +208,7 @@ def set_font_family(opts: Optional[Options] = None, override_font_size: Optional
set_font_data(
render_box_drawing, prerender_function, descriptor_for_idx,
indices['bold'], indices['italic'], indices['bi'], num_symbol_fonts,
sm, sz, font_features
sm, sz, font_features, ns
)
@@ -400,8 +405,15 @@ def prerender_function(
render_cursor, cursor_beam_thickness=cursor_beam_thickness,
cursor_underline_thickness=cursor_underline_thickness, cell_width=cell_width,
cell_height=cell_height, dpi_x=dpi_x, dpi_y=dpi_y)
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
# If you change the mapping of these cells you will need to change
# NUM_UNDERLINE_STYLES and BEAM_IDX in shader.c and STRIKE_SPRITE_INDEX in
# window.py and MISSING_GLYPH in font.c
cells = list(map(f, range(1, NUM_UNDERLINE_STYLES + 1))) # underline sprites
cells.append(f(0, strikethrough=True)) # strikethrough sprite
cells.append(f(missing=True)) # missing glyph
cells.extend((c(1), c(2), c(3))) # cursor glyphs
tcells = tuple(cells)
return tuple(map(ctypes.addressof, tcells)), tcells
def render_box_drawing(codepoint: int, cell_width: int, cell_height: int, dpi: float) -> Tuple[int, CBufType]:

View File

@@ -143,11 +143,6 @@ static bool
set_font_size(Face *self, FT_F26Dot6 char_width, FT_F26Dot6 char_height, FT_UInt xdpi, FT_UInt ydpi, unsigned int desired_height, unsigned int cell_height) {
int error = FT_Set_Char_Size(self->face, 0, char_height, xdpi, ydpi);
if (!error) {
unsigned int ch = calc_cell_height(self, false);
if (desired_height && ch != desired_height) {
FT_F26Dot6 h = (FT_F26Dot6)floor((double)char_height * (double)desired_height / (double) ch);
return set_font_size(self, 0, h, xdpi, ydpi, 0, cell_height);
}
self->char_width = char_width; self->char_height = char_height; self->xdpi = xdpi; self->ydpi = ydpi;
if (self->harfbuzz_font != NULL) hb_ft_font_changed(self->harfbuzz_font);
} else {
@@ -522,19 +517,20 @@ detect_right_edge(ProcessedBitmap *ans) {
static bool
render_color_bitmap(Face *self, int glyph_id, ProcessedBitmap *ans, unsigned int cell_width, unsigned int cell_height, unsigned int num_cells, unsigned int baseline UNUSED) {
unsigned short best = 0, diff = USHRT_MAX;
const short limit = self->face->num_fixed_sizes;
for (short i = 0; i < limit; i++) {
unsigned short w = self->face->available_sizes[i].width;
unsigned short d = w > (unsigned short)cell_width ? w - (unsigned short)cell_width : (unsigned short)cell_width - w;
if (d < diff) {
diff = d;
best = i;
if (self->face->num_fixed_sizes > 0) {
const short limit = self->face->num_fixed_sizes;
for (short i = 0; i < limit; i++) {
unsigned short w = self->face->available_sizes[i].width;
unsigned short d = w > (unsigned short)cell_width ? w - (unsigned short)cell_width : (unsigned short)cell_width - w;
if (d < diff) {
diff = d;
best = i;
}
}
}
FT_Error error = FT_Select_Size(self->face, best);
if (error) { set_freetype_error("Failed to set char size for non-scalable font, with error:", error); return false; }
if (!load_glyph(self, glyph_id, FT_LOAD_COLOR)) return false;
FT_Set_Char_Size(self->face, 0, self->char_height, self->xdpi, self->ydpi);
FT_Error error = FT_Select_Size(self->face, best);
if (error) { set_freetype_error("Failed to set char size for non-scalable font, with error:", error); return false; }
} else FT_Set_Char_Size(self->face, 0, self->char_height, self->xdpi, self->ydpi);
if (!load_glyph(self, glyph_id, FT_LOAD_COLOR | FT_LOAD_RENDER)) return false;
FT_Bitmap *bitmap = &self->face->glyph->bitmap;
if (bitmap->pixel_mode != FT_PIXEL_MODE_BGRA) return false;
ans->buf = bitmap->buffer;

View File

@@ -134,7 +134,7 @@ choose_bitmap_size(FT_Face face, FT_UInt desired_height) {
static void
set_pixel_size(RenderCtx *ctx, Face *face, FT_UInt sz, bool get_metrics UNUSED) {
if (sz != face->pixel_size) {
if (FT_HAS_COLOR(face->freetype)) sz = choose_bitmap_size(face->freetype, font_units_to_pixels_y(main_face.freetype, main_face.freetype->height));
if (face->freetype->num_fixed_sizes > 0 && FT_HAS_COLOR(face->freetype)) choose_bitmap_size(face->freetype, font_units_to_pixels_y(main_face.freetype, main_face.freetype->height));
else FT_Set_Pixel_Sizes(face->freetype, sz, sz);
hb_ft_font_changed(face->hb);
hb_ft_font_set_load_flags(face->hb, get_load_flags(face->hinting, face->hintstyle, FT_LOAD_DEFAULT));
@@ -304,7 +304,7 @@ render_run(RenderCtx *ctx, RenderState *rs) {
hb_glyph_info_t *info = hb_buffer_get_glyph_infos(hb_buffer, NULL);
hb_glyph_position_t *positions = hb_buffer_get_glyph_positions(hb_buffer, NULL);
int baseline = font_units_to_pixels_y(face, face->ascender);
int load_flags = get_load_flags(rs->current_face->hinting, rs->current_face->hintstyle, has_color ? FT_LOAD_COLOR : FT_LOAD_RENDER);
int load_flags = get_load_flags(rs->current_face->hinting, rs->current_face->hintstyle, FT_LOAD_RENDER | (has_color ? FT_LOAD_COLOR : 0));
float pos = rs->x;
unsigned int limit = len;
for (unsigned int i = 0; i < len; i++) {

6
kitty/glfw-wrapper.c generated
View File

@@ -1,4 +1,6 @@
// generated by glfw.py DO NOT edit
#define GFW_EXTERN
#include "data-types.h"
#include "glfw-wrapper.h"
@@ -405,8 +407,8 @@ load_glfw(const char* path) {
*(void **) (&glfwSetCocoaTextInputFilter_impl) = dlsym(handle, "glfwSetCocoaTextInputFilter");
if (glfwSetCocoaTextInputFilter_impl == NULL) dlerror(); // clear error indicator
*(void **) (&glfwSetCocoaFileOpenCallback_impl) = dlsym(handle, "glfwSetCocoaFileOpenCallback");
if (glfwSetCocoaFileOpenCallback_impl == NULL) dlerror(); // clear error indicator
*(void **) (&glfwSetCocoaURLOpenCallback_impl) = dlsym(handle, "glfwSetCocoaURLOpenCallback");
if (glfwSetCocoaURLOpenCallback_impl == NULL) dlerror(); // clear error indicator
*(void **) (&glfwSetCocoaToggleFullscreenIntercept_impl) = dlsym(handle, "glfwSetCocoaToggleFullscreenIntercept");
if (glfwSetCocoaToggleFullscreenIntercept_impl == NULL) dlerror(); // clear error indicator

8
kitty/glfw-wrapper.h generated
View File

@@ -1597,7 +1597,7 @@ typedef struct GLFWgamepadstate
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int,unsigned long);
typedef bool (* GLFWapplicationshouldhandlereopenfun)(int);
typedef bool (* GLFWhandlefileopen)(const char*);
typedef bool (* GLFWhandleurlopen)(const char*);
typedef void (* GLFWapplicationwillfinishlaunchingfun)(void);
typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
typedef void (* GLFWcocoarenderframefun)(GLFWwindow*);
@@ -2124,9 +2124,9 @@ typedef GLFWcocoatextinputfilterfun (*glfwSetCocoaTextInputFilter_func)(GLFWwind
GFW_EXTERN glfwSetCocoaTextInputFilter_func glfwSetCocoaTextInputFilter_impl;
#define glfwSetCocoaTextInputFilter glfwSetCocoaTextInputFilter_impl
typedef GLFWhandlefileopen (*glfwSetCocoaFileOpenCallback_func)(GLFWhandlefileopen);
GFW_EXTERN glfwSetCocoaFileOpenCallback_func glfwSetCocoaFileOpenCallback_impl;
#define glfwSetCocoaFileOpenCallback glfwSetCocoaFileOpenCallback_impl
typedef GLFWhandleurlopen (*glfwSetCocoaURLOpenCallback_func)(GLFWhandleurlopen);
GFW_EXTERN glfwSetCocoaURLOpenCallback_func glfwSetCocoaURLOpenCallback_impl;
#define glfwSetCocoaURLOpenCallback glfwSetCocoaURLOpenCallback_impl
typedef GLFWcocoatogglefullscreenfun (*glfwSetCocoaToggleFullscreenIntercept_func)(GLFWwindow*, GLFWcocoatogglefullscreenfun);
GFW_EXTERN glfwSetCocoaToggleFullscreenIntercept_func glfwSetCocoaToggleFullscreenIntercept_impl;

View File

@@ -25,6 +25,7 @@ extern void cocoa_set_titlebar_appearance(void *w, unsigned int theme);
extern void cocoa_set_titlebar_color(void *w, color_type color);
extern bool cocoa_alt_option_key_pressed(unsigned long);
extern void cocoa_toggle_secure_keyboard_entry(void);
extern void cocoa_update_menu_bar_title(PyObject*);
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);
@@ -46,6 +47,43 @@ get_platform_dependent_config_values(void *glfw_window) {
}
}
static void
strip_csi_(const char *title, char *buf, size_t bufsz) {
enum { NORMAL, IN_ESC, IN_CSI} state = NORMAL;
char *dest = buf, *last = &buf[bufsz-1];
*dest = 0; *last = 0;
for (; *title && dest < last; title++) {
const char ch = *title;
switch (state) {
case NORMAL: {
if (ch == 0x1b) { state = IN_ESC; }
else *(dest++) = ch;
} break;
case IN_ESC: {
if (ch == '[') { state = IN_CSI; }
else { state = NORMAL; }
} break;
case IN_CSI: {
if (!(('0' <= ch && ch <= '9') || ch == ';' || ch == ':')) state = NORMAL;
} break;
}
}
*dest = 0;
}
void
update_menu_bar_title(PyObject *title UNUSED) {
#ifdef __APPLE__
static char buf[2048];
strip_csi_(PyUnicode_AsUTF8(title), buf, arraysz(buf));
DECREF_AFTER_FUNCTION PyObject *stitle = PyUnicode_FromString(buf);
cocoa_update_menu_bar_title(stitle);
#endif
}
void
request_tick_callback(void) {
glfwPostEmptyEvent();
@@ -429,8 +467,8 @@ static void get_window_dpi(GLFWwindow *w, double *x, double *y);
#ifdef __APPLE__
static bool
apple_file_open_callback(const char* filepath) {
set_cocoa_pending_action(OPEN_FILE, filepath);
apple_url_open_callback(const char* url) {
set_cocoa_pending_action(LAUNCH_URLS, url);
return true;
}
@@ -438,7 +476,9 @@ extern bool cocoa_render_line_of_text(const char *text, const color_type fg, con
bool
draw_window_title(OSWindow *window UNUSED, const char *text, color_type fg, color_type bg, uint8_t *output_buf, size_t width, size_t height) {
return cocoa_render_line_of_text(text, fg, bg, output_buf, width, height);
static char buf[2048];
strip_csi_(text, buf, arraysz(buf));
return cocoa_render_line_of_text(buf, fg, bg, output_buf, width, height);
}
extern uint8_t* render_single_ascii_char_as_mask(const char ch, size_t *result_width, size_t *result_height);
@@ -484,10 +524,12 @@ draw_text_callback(GLFWwindow *window, const char *text, uint32_t fg, uint32_t b
bool
draw_window_title(OSWindow *window, const char *text, color_type fg, color_type bg, uint8_t *output_buf, size_t width, size_t height) {
if (!ensure_csd_title_render_ctx()) return false;
static char buf[2048];
strip_csi_(text, buf, arraysz(buf));
unsigned px_sz = (unsigned)(window->fonts_data->font_sz_in_pts * window->fonts_data->logical_dpi_y / 72.);
px_sz = MIN(px_sz, 3 * height / 4);
#define RGB2BGR(x) (x & 0xFF000000) | ((x & 0xFF0000) >> 16) | (x & 0x00FF00) | ((x & 0x0000FF) << 16)
bool ok = render_single_line(csd_title_render_ctx, text, px_sz, RGB2BGR(fg), RGB2BGR(bg), output_buf, width, height, 0, 0, 0);
bool ok = render_single_line(csd_title_render_ctx, buf, px_sz, RGB2BGR(fg), RGB2BGR(bg), output_buf, width, height, 0, 0, 0);
#undef RGB2BGR
if (!ok && PyErr_Occurred()) PyErr_Print();
return ok;
@@ -1003,7 +1045,7 @@ glfw_init(PyObject UNUSED *self, PyObject *args) {
PyObject *ans = glfwInit(monotonic_start_time) ? Py_True: Py_False;
if (ans == Py_True) {
#ifdef __APPLE__
glfwSetCocoaFileOpenCallback(apple_file_open_callback);
glfwSetCocoaURLOpenCallback(apple_url_open_callback);
#else
glfwSetDrawTextFunction(draw_text_callback);
#endif
@@ -1286,31 +1328,6 @@ request_window_attention(id_type kitty_window_id, bool audio_bell) {
}
}
static void
strip_csi_(const char *title, char *buf, size_t bufsz) {
enum { NORMAL, IN_ESC, IN_CSI} state = NORMAL;
char *dest = buf, *last = &buf[bufsz-1];
*dest = 0; *last = 0;
for (; *title && dest < last; title++) {
const char ch = *title;
switch (state) {
case NORMAL: {
if (ch == 0x1b) { state = IN_ESC; }
else *(dest++) = ch;
} break;
case IN_ESC: {
if (ch == '[') { state = IN_CSI; }
else { state = NORMAL; }
} break;
case IN_CSI: {
if (!(('0' <= ch && ch <= '9') || ch == ';' || ch == ':')) state = NORMAL;
} break;
}
}
*dest = 0;
}
void
set_os_window_title(OSWindow *w, const char *title) {
static char buf[2048];

View File

@@ -242,7 +242,7 @@ pagerhist_push(HistoryBuf *self, ANSIBuf *as_ansi_buf) {
const GPUCell *prev_cell = NULL;
Line l = {.xnum=self->xnum};
init_line(self, self->start_of_data, &l);
line_as_ansi(&l, as_ansi_buf, &prev_cell);
line_as_ansi(&l, as_ansi_buf, &prev_cell, 0, l.xnum, 0);
if (ringbuf_bytes_used(ph->ringbuf) && !l.attrs.continued) pagerhist_write_bytes(ph, (const uint8_t*)"\n", 1);
pagerhist_write_bytes(ph, (const uint8_t*)"\x1b[m", 3);
if (pagerhist_write_ucs4(ph, as_ansi_buf->buf, as_ansi_buf->len)) pagerhist_write_bytes(ph, (const uint8_t*)"\r", 1);
@@ -324,7 +324,7 @@ as_ansi(HistoryBuf *self, PyObject *callback) {
if (i < self->count - 1) {
l.attrs.continued = attrptr(self, index_of(self, i + 1))->continued;
} else l.attrs.continued = false;
line_as_ansi(&l, &output, &prev_cell);
line_as_ansi(&l, &output, &prev_cell, 0, l.xnum, 0);
if (!l.attrs.continued) {
ensure_space_for(&output, buf, Py_UCS4, output.len + 1, capacity, 2048, false);
output.buf[output.len++] = 10; // 10 = \n
@@ -418,8 +418,21 @@ pagerhist_write(HistoryBuf *self, PyObject *what) {
Py_RETURN_NONE;
}
static const uint8_t*
reverse_find(const uint8_t *haystack, size_t haystack_sz, const uint8_t *needle) {
const size_t needle_sz = strlen((const char*)needle);
if (!needle_sz || needle_sz > haystack_sz) return NULL;
const uint8_t *p = haystack + haystack_sz - (needle_sz - 1);
while (--p >= haystack) {
if (*p == needle[0] && memcmp(p, needle, MIN(needle_sz, haystack_sz - (p - haystack))) == 0) return p;
}
return NULL;
}
static PyObject*
pagerhist_as_bytes(HistoryBuf *self, PyObject *args UNUSED) {
pagerhist_as_bytes(HistoryBuf *self, PyObject *args) {
int upto_output_start = 0;
if (!PyArg_ParseTuple(args, "|p", &upto_output_start)) return NULL;
#define ph self->pagerhist
if (!ph || !ringbuf_bytes_used(ph->ringbuf)) return PyBytes_FromStringAndSize("", 0);
pagerhist_ensure_start_is_valid_utf8(ph);
@@ -433,6 +446,13 @@ pagerhist_as_bytes(HistoryBuf *self, PyObject *args UNUSED) {
uint8_t *buf = (uint8_t*)PyBytes_AS_STRING(ans);
ringbuf_memcpy_from(buf, ph->ringbuf, sz);
if (!l.attrs.continued) buf[sz-1] = '\n';
if (upto_output_start) {
const uint8_t *p = reverse_find(buf, sz, (const uint8_t*)"\x1b]133;C\x1b\\");
if (p) {
PyObject *t = PyBytes_FromStringAndSize((const char*)p, sz - (p - buf));
Py_DECREF(ans); ans = t;
}
}
return ans;
#undef ph
}
@@ -501,8 +521,8 @@ static PyMethodDef methods[] = {
METHOD(as_ansi, METH_O)
METHODB(pagerhist_write, METH_O),
METHODB(pagerhist_rewrap, METH_O),
METHODB(pagerhist_as_text, METH_NOARGS),
METHODB(pagerhist_as_bytes, METH_NOARGS),
METHODB(pagerhist_as_text, METH_VARARGS),
METHODB(pagerhist_as_bytes, METH_VARARGS),
METHODB(as_text, METH_VARARGS),
METHOD(dirty_lines, METH_NOARGS)
METHOD(push, METH_VARARGS)

View File

@@ -11,7 +11,7 @@ from .cli_stub import LaunchCLIOptions
from .constants import kitty_exe, shell_path
from .fast_data_types import patch_color_profiles, set_clipboard_string
from .options.utils import env as parse_env
from .tabs import Tab
from .tabs import Tab, TabManager
from .types import run_once
from .utils import log_error, resolve_custom_file, set_primary_selection, which
from .window import Watchers, Window
@@ -102,16 +102,17 @@ kitty remote control feature with :code:`kitty @launch --copy-env`.
--location
type=choices
default=default
choices=first,after,before,neighbor,last,vsplit,hsplit,default
choices=first,after,before,neighbor,last,vsplit,hsplit,split,default
Where to place the newly created window when it is added to a tab which
already has existing windows in it. :code:`after` and :code:`before` place the new
window before or after the active window. :code:`neighbor` is a synonym for :code:`after`.
Also applies to creating a new tab, where the value of :code:`after`
will cause the new tab to be placed next to the current tab instead of at the end.
The values of :code:`vsplit` and :code:`hsplit` are only used by the :code:`splits`
layout and control if the new window is placed in a vertical or horizontal split
with the currently active window. The default is to place the window in a
layout dependent manner, typically, after the currently active window.
The values of :code:`vsplit`, :code:`hsplit` and :code:`split` are only used by the
:code:`splits` layout and control if the new window is placed in a vertical,
horizontal or automatic split with the currently active window. The default is
to place the window in a layout dependent manner, typically, after the
currently active window.
--allow-remote-control
@@ -227,25 +228,26 @@ def get_env(opts: LaunchCLIOptions, active_child: Optional[Child]) -> Dict[str,
def tab_for_window(boss: Boss, opts: LaunchCLIOptions, target_tab: Optional[Tab] = None) -> Optional[Tab]:
def create_tab(tm: Optional[TabManager] = None) -> Tab:
if tm is None:
oswid = boss.add_os_window(wclass=opts.os_window_class, wname=opts.os_window_name, override_title=opts.os_window_title or None)
tm = boss.os_window_map[oswid]
tab = tm.new_tab(empty_tab=True, location=opts.location)
if opts.tab_title:
tab.set_title(opts.tab_title)
return tab
if opts.type == 'tab':
if target_tab is not None:
tm = target_tab.tab_manager_ref() or boss.active_tab_manager
else:
tm = boss.active_tab_manager
if tm:
tab: Optional[Tab] = tm.new_tab(empty_tab=True, location=opts.location)
if opts.tab_title and tab is not None:
tab.set_title(opts.tab_title)
else:
tab = None
tab = create_tab(tm)
elif opts.type == 'os-window':
oswid = boss.add_os_window(wclass=opts.os_window_class, wname=opts.os_window_name, override_title=opts.os_window_title or None)
tm = boss.os_window_map[oswid]
tab = tm.new_tab(empty_tab=True)
if opts.tab_title and tab is not None:
tab.set_title(opts.tab_title)
tab = create_tab()
else:
tab = target_tab or boss.active_tab
tab = target_tab or boss.active_tab or create_tab()
return tab
@@ -397,7 +399,7 @@ def launch(
elif x == '@line-count':
if 'lines' in pipe_data:
x = str(pipe_data['lines'])
elif x in ('@cursor-x', '@cursor-y', '@scrolled-by'):
elif x in ('@cursor-x', '@cursor-y', '@scrolled-by', '@first-line-on-screen', '@last-line-on-screen'):
if active is not None:
screen = active.screen
if x == '@scrolled-by':
@@ -406,6 +408,10 @@ def launch(
x = str(screen.cursor.x + 1)
elif x == '@cursor-y':
x = str(screen.cursor.y + 1)
elif x == '@first-line-on-screen':
x = str(screen.visual_line(0) or '')
elif x == '@last-line-on-screen':
x = str(screen.visual_line(screen.lines - 1) or '')
final_cmd.append(x)
exe = which(final_cmd[0])
if exe:

View File

@@ -450,13 +450,12 @@ class Splits(Layout):
) -> None:
horizontal = self.default_axis_is_horizontal
after = True
if location is not None:
if location == 'vsplit':
horizontal = True
elif location == 'hsplit':
horizontal = False
if location in ('before', 'first'):
after = False
if location == 'vsplit':
horizontal = True
elif location == 'hsplit':
horizontal = False
elif location in ('before', 'first'):
after = False
aw = all_windows.active_window
if aw is not None:
ag = all_windows.active_group
@@ -464,6 +463,10 @@ class Splits(Layout):
group_id = ag.id
pair = self.pairs_root.pair_for_window(group_id)
if pair is not None:
if location == 'split':
wwidth = aw.geometry.right - aw.geometry.left
wheight = aw.geometry.bottom - aw.geometry.top
horizontal = wwidth >= wheight
target_group = all_windows.add_window(window, next_to=aw, before=not after)
pair.split_and_add(group_id, target_group.id, horizontal, after)
return

View File

@@ -413,7 +413,7 @@ as_ansi(LineBuf *self, PyObject *callback) {
ANSIBuf output = {0};
do {
init_line(self, (&l), self->line_map[ylimit]);
line_as_ansi(&l, &output, &prev_cell);
line_as_ansi(&l, &output, &prev_cell, 0, l.xnum, 0);
if (output.len) break;
ylimit--;
} while(ylimit > 0);
@@ -421,7 +421,7 @@ as_ansi(LineBuf *self, PyObject *callback) {
for(index_type i = 0; i <= ylimit; i++) {
l.attrs.continued = self->line_attrs[(i + 1 < self->ynum) ? i+1 : i].continued;
init_line(self, (&l), self->line_map[i]);
line_as_ansi(&l, &output, &prev_cell);
line_as_ansi(&l, &output, &prev_cell, 0, l.xnum, 0);
if (!l.attrs.continued) {
ensure_space_for(&output, buf, Py_UCS4, output.len + 1, capacity, 2048, false);
output.buf[output.len++] = 10; // 10 = \n

View File

@@ -9,6 +9,7 @@
#include "unicode-data.h"
#include "lineops.h"
#include "charsets.h"
#include "wcwidth-std.h"
extern PyTypeObject Cursor_Type;
@@ -300,24 +301,50 @@ write_hyperlink(hyperlink_id_type hid, ANSIBuf *output) {
#undef W
}
static void
write_mark(const char *mark, ANSIBuf *output) {
#define W(c) output->buf[output->len++] = c
W(0x1b); W(']'); W('1'); W('3'); W('3'); W(';');
for (size_t i = 0; mark[i] != 0 && i < 32; i++) W(mark[i]);
W(0x1b); W('\\');
#undef W
void
line_as_ansi(Line *self, ANSIBuf *output, const GPUCell** prev_cell) {
}
bool
line_as_ansi(Line *self, ANSIBuf *output, const GPUCell** prev_cell, index_type start_at, index_type stop_before, char_type prefix_char) {
#define ENSURE_SPACE(extra) ensure_space_for(output, buf, Py_UCS4, output->len + extra, capacity, 2048, false);
#define WRITE_SGR(val) { ENSURE_SPACE(128); write_sgr(val, output); }
#define WRITE_SGR(val) { ENSURE_SPACE(128); escape_code_written = true; write_sgr(val, output); }
#define WRITE_CH(val) { ENSURE_SPACE(1); output->buf[output->len++] = val; }
#define WRITE_HYPERLINK(val) { ENSURE_SPACE(2256); write_hyperlink(val, output); }
#define WRITE_HYPERLINK(val) { ENSURE_SPACE(2256); escape_code_written = true; write_hyperlink(val, output); }
#define WRITE_MARK(val) { ENSURE_SPACE(64); escape_code_written = true; write_mark(val, output); }
bool escape_code_written = false;
output->len = 0;
index_type limit = xlimit_for_line(self);
if (limit == 0) return;
index_type limit = MIN(stop_before, xlimit_for_line(self));
char_type previous_width = 0;
if (prefix_char) { WRITE_CH(prefix_char); previous_width = wcwidth_std(prefix_char); }
switch (self->attrs.prompt_kind) {
case UNKNOWN_PROMPT_KIND:
break;
case PROMPT_START:
WRITE_MARK("A");
break;
case SECONDARY_PROMPT:
WRITE_MARK("A;k=s");
break;
case OUTPUT_START:
WRITE_MARK("C");
break;
}
if (limit <= start_at) return escape_code_written;
static const GPUCell blank_cell = { 0 };
GPUCell *cell;
if (*prev_cell == NULL) *prev_cell = &blank_cell;
const CellAttrs mask_for_sgr = {.val=SGR_MASK};
for (index_type pos=0; pos < limit; pos++) {
for (index_type pos=start_at; pos < limit; pos++) {
char_type ch = self->cpu_cells[pos].ch;
if (ch == 0) {
if (previous_width == 2) { previous_width = 0; continue; }
@@ -352,12 +379,14 @@ line_as_ansi(Line *self, ANSIBuf *output, const GPUCell** prev_cell) {
}
previous_width = cell->attrs.width;
}
return escape_code_written;
#undef CMP_ATTRS
#undef CMP
#undef WRITE_SGR
#undef WRITE_CH
#undef ENSURE_SPACE
#undef WRITE_HYPERLINK
#undef WRITE_MARK
}
static PyObject*
@@ -365,7 +394,7 @@ as_ansi(Line* self, PyObject *a UNUSED) {
#define as_ansi_doc "Return the line's contents with ANSI (SGR) escape codes for formatting"
const GPUCell *prev_cell = NULL;
ANSIBuf output = {0};
line_as_ansi(self, &output, &prev_cell);
line_as_ansi(self, &output, &prev_cell, 0, self->xnum, 0);
PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len);
free(output.buf);
return ans;
@@ -834,7 +863,7 @@ as_text_generic(PyObject *args, void *container, get_line_func get_line, index_t
// get around to writing a nice pager kitten.
// see https://github.com/kovidgoyal/kitty/issues/2381
prev_cell = NULL;
line_as_ansi(line, ansibuf, &prev_cell);
line_as_ansi(line, ansibuf, &prev_cell, 0, line->xnum, 0);
t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, ansibuf->buf, ansibuf->len);
if (t && ansibuf->len > 0) APPEND(sgr_reset);
} else {

View File

@@ -84,7 +84,7 @@ void line_add_combining_char(Line *, uint32_t , unsigned int );
index_type line_url_start_at(Line *self, index_type x);
index_type line_url_end_at(Line *self, index_type x, bool, char_type, bool);
bool line_startswith_url_chars(Line*);
void line_as_ansi(Line *self, ANSIBuf *output, const GPUCell**) __attribute__((nonnull));
bool line_as_ansi(Line *self, ANSIBuf *output, const GPUCell**, index_type start_at, index_type stop_before, char_type prefix_char) __attribute__((nonnull));
unsigned int line_length(Line *self);
size_t cell_as_unicode(CPUCell *cell, bool include_cc, Py_UCS4 *buf, char_type);
size_t cell_as_unicode_for_fallback(CPUCell *cell, Py_UCS4 *buf);

View File

@@ -30,7 +30,6 @@ from .options.types import Options
from .options.utils import DELETE_ENV_VAR
from .os_window_size import initial_window_size_func
from .session import get_os_window_sizing_data
from .shell_integration import setup_shell_integration
from .types import SingleKey
from .utils import (
detach, expandvars, log_error, single_instance,
@@ -60,7 +59,7 @@ def talk_to_instance(args: CLIOptions) -> None:
stdin = ''
if args.session == '-':
stdin = sys.stdin.read()
data = {'cmd': 'new_instance', 'args': tuple(sys.argv),
data = {'cmd': 'new_instance', 'args': tuple(sys.argv), 'cmdline_args_for_open': getattr(sys, 'cmdline_args_for_open', []),
'startup_id': os.environ.get('DESKTOP_STARTUP_ID'),
'cwd': os.getcwd(), 'stdin': stdin}
notify_socket = None
@@ -151,13 +150,16 @@ def _run_app(opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = ())
func_map[parts].append(k)
for ac in ('new_os_window', 'close_os_window', 'close_tab', 'edit_config_file', 'previous_tab',
'next_tab', 'new_tab', 'new_window', 'close_window', 'toggle_macos_secure_keyboard_entry'):
'next_tab', 'new_tab', 'new_window', 'close_window', 'toggle_macos_secure_keyboard_entry', 'toggle_fullscreen'):
val = get_macos_shortcut_for(func_map, ac)
if val is not None:
global_shortcuts[ac] = val
val = get_macos_shortcut_for(func_map, 'clear_terminal reset active', lookup_name='reset_terminal')
if val is not None:
global_shortcuts['reset_terminal'] = val
val = get_macos_shortcut_for(func_map, 'clear_terminal to_cursor active', lookup_name='clear_terminal_and_scrollback')
if val is not None:
global_shortcuts['clear_terminal_and_scrollback'] = val
val = get_macos_shortcut_for(func_map, 'load_config_file', lookup_name='reload_config')
if val is not None:
global_shortcuts['reload_config'] = val
@@ -319,7 +321,6 @@ def setup_environment(opts: Options, cli_opts: CLIOptions) -> None:
os.environ['KITTY_LISTEN_ON'] = cli_opts.listen_on
env = opts.env.copy()
ensure_kitty_in_path()
setup_shell_integration(opts, env)
kitty_path = shutil.which('kitty')
if kitty_path:
child_path = env.get('PATH')
@@ -364,8 +365,21 @@ def _main() -> None:
cwd_ok = False
if not cwd_ok:
os.chdir(os.path.expanduser('~'))
cli_opts, rest = parse_args(args=args, result_class=CLIOptions)
cli_opts.args = rest
if getattr(sys, 'cmdline_args_for_open', False):
usage: Optional[str] = 'file_or_url ...'
appname: Optional[str] = 'kitty +open'
msg: Optional[str] = (
'Run kitty and open the specified files or URLs in it, using launch-actions.conf. For details'
' see https://sw.kovidgoyal.net/kitty/open_actions/#scripting-the-opening-of-files-with-kitty-on-macos'
'\n\nAll the normal kitty options can be used.')
else:
usage = msg = appname = None
cli_opts, rest = parse_args(args=args, result_class=CLIOptions, usage=usage, message=msg, appname=appname)
if getattr(sys, 'cmdline_args_for_open', False):
setattr(sys, 'cmdline_args_for_open', rest)
cli_opts.args = []
else:
cli_opts.args = rest
if cli_opts.detach:
if cli_opts.session == '-':
from .session import PreReadSession

View File

@@ -141,6 +141,13 @@ static bool
dispatch_mouse_event(Window *w, int button, int count, int modifiers, bool grabbed) {
bool handled = false;
if (w->render_data.screen && w->render_data.screen->callbacks != Py_None) {
PyObject *callback_ret = PyObject_CallMethod(w->render_data.screen->callbacks, "on_mouse_event", "{si si si sO}",
"button", button, "repeat_count", count, "mods", modifiers, "grabbed", grabbed ? Py_True : Py_False);
if (callback_ret == NULL) PyErr_Print();
else {
handled = callback_ret == Py_True;
Py_DECREF(callback_ret);
}
if (OPT(debug_keyboard)) {
const char *evname = "move";
switch(count) {
@@ -162,14 +169,7 @@ dispatch_mouse_event(Window *w, int button, int count, int modifiers, bool grabb
case GLFW_MOUSE_BUTTON_7: bname = "b7"; break;
case GLFW_MOUSE_BUTTON_8: bname = "b8"; break;
}
debug("\x1b[33mon_mouse_input\x1b[m: %s button: %s %sgrabbed: %d\n", evname, bname, format_mods(modifiers), grabbed);
}
PyObject *callback_ret = PyObject_CallMethod(w->render_data.screen->callbacks, "on_mouse_event", "{si si si sO}",
"button", button, "repeat_count", count, "mods", modifiers, "grabbed", grabbed ? Py_True : Py_False);
if (callback_ret == NULL) PyErr_Print();
else {
handled = callback_ret == Py_True;
Py_DECREF(callback_ret);
debug("\x1b[33mon_mouse_input\x1b[m: %s button: %s %sgrabbed: %d handled_in_kitty: %d\n", evname, bname, format_mods(modifiers), grabbed, handled);
}
}
return handled;
@@ -377,18 +377,22 @@ clear_click_queue(Window *w, int button) {
#define N(n) (q->clicks[q->length - n])
static double
radius_for_multiclick(void) {
return 0.5 * (global_state.callback_os_window ? global_state.callback_os_window->fonts_data->cell_height : 8);
}
static bool
release_is_click(Window *w, int button) {
ClickQueue *q = &w->click_queues[button];
double click_allowed_radius = 1.2 * (global_state.callback_os_window ? global_state.callback_os_window->fonts_data->cell_height : 20);
release_is_click(const Window *w, int button) {
const ClickQueue *q = &w->click_queues[button];
monotonic_t now = monotonic();
return (q->length > 0 && distance(N(1).x, N(1).y, MAX(0, w->mouse_pos.global_x), MAX(0, w->mouse_pos.global_y)) <= click_allowed_radius && now - N(1).at < OPT(click_interval));
return (q->length > 0 && distance(N(1).x, N(1).y, MAX(0, w->mouse_pos.global_x), MAX(0, w->mouse_pos.global_y)) <= radius_for_multiclick() && now - N(1).at < OPT(click_interval));
}
static unsigned
multi_click_count(Window *w, int button) {
ClickQueue *q = &w->click_queues[button];
double multi_click_allowed_radius = 1.2 * (global_state.callback_os_window ? global_state.callback_os_window->fonts_data->cell_height : 20);
multi_click_count(const Window *w, int button) {
const ClickQueue *q = &w->click_queues[button];
double multi_click_allowed_radius = radius_for_multiclick();
if (q->length > 2) {
// possible triple-click
if (
@@ -414,7 +418,8 @@ add_press(Window *w, int button, int modifiers) {
ClickQueue *q = &w->click_queues[button];
if (q->length == CLICK_QUEUE_SZ) { memmove(q->clicks, q->clicks + 1, sizeof(Click) * (CLICK_QUEUE_SZ - 1)); q->length--; }
monotonic_t now = monotonic();
N(0).at = now; N(0).button = button; N(0).modifiers = modifiers; N(0).x = MAX(0, w->mouse_pos.global_x); N(0).y = MAX(0, w->mouse_pos.global_y);
static unsigned long num = 0;
N(0).at = now; N(0).button = button; N(0).modifiers = modifiers; N(0).x = MAX(0, w->mouse_pos.global_x); N(0).y = MAX(0, w->mouse_pos.global_y); N(0).num = ++num;
q->length++;
Screen *screen = w->render_data.screen;
int count = multi_click_count(w, button);
@@ -459,6 +464,8 @@ typedef struct PendingClick {
bool grabbed;
monotonic_t at;
MousePosition mouse_pos;
unsigned long press_num;
double radius_for_multiclick;
} PendingClick;
static void
@@ -467,14 +474,24 @@ free_pending_click(id_type timer_id UNUSED, void *pc) { free(pc); }
void
send_pending_click_to_window(Window *w, void *data) {
PendingClick *pc = (PendingClick*)data;
ClickQueue *q = &w->click_queues[pc->button];
// only send click if no presses have happened since the release that triggered the click
if (q->length && q->clicks[q->length - 1].at <= pc->at) {
const ClickQueue *q = &w->click_queues[pc->button];
// only send click if no presses have happened since the release that triggered
// the click or if the subsequent press is too far or too late for a double click
if (!q->length) return;
#define press(n) q->clicks[q->length - n]
if (
press(1).at <= pc->at || // latest press is before click release
(q->length > 1 && press(2).num == pc->press_num && ( // penultimate press is the press that belongs to this click
press(1).at - press(2).at > OPT(click_interval) || // too long between the presses for it to be a double click
distance(press(1).x, press(1).y, press(2).x, press(2).y) > pc->radius_for_multiclick // presses are too far apart
))
) {
MousePosition current_pos = w->mouse_pos;
w->mouse_pos = pc->mouse_pos;
dispatch_mouse_event(w, pc->button, pc->count, pc->modifiers, pc->grabbed);
w->mouse_pos = current_pos;
}
#undef press
}
static void
@@ -484,6 +501,8 @@ dispatch_possible_click(Window *w, int button, int modifiers) {
if (release_is_click(w, button)) {
PendingClick *pc = calloc(sizeof(PendingClick), 1);
if (pc) {
const ClickQueue *q = &w->click_queues[button];
pc->press_num = q->length ? q->clicks[q->length - 1].num : 0;
pc->window_id = w->id;
pc->mouse_pos = w->mouse_pos;
pc->at = monotonic();
@@ -491,6 +510,7 @@ dispatch_possible_click(Window *w, int button, int modifiers) {
pc->count = count == 2 ? -3 : -2;
pc->modifiers = modifiers;
pc->grabbed = screen->modes.mouse_tracking_mode != 0;
pc->radius_for_multiclick = radius_for_multiclick();
add_main_loop_timer(OPT(click_interval), false, send_pending_click_to_window_id, pc, free_pending_click);
}
}
@@ -701,7 +721,10 @@ mouse_event(const int button, int modifiers, int action) {
bool in_tab_bar;
unsigned int window_idx = 0;
Window *w = NULL;
debug("%s mouse_button: %d %s", action == GLFW_RELEASE ? "\x1b[32mRelease\x1b[m" : (button < 0 ? "\x1b[36mMove\x1b[m" : "\x1b[31mPress\x1b[m"), button, format_mods(modifiers));
if (OPT(debug_keyboard)) {
if (button < 0) { debug("%s x: %.1f y: %.1f ", "\x1b[36mMove\x1b[m", global_state.callback_os_window->mouse_x, global_state.callback_os_window->mouse_y); }
else { debug("%s mouse_button: %d %s", action == GLFW_RELEASE ? "\x1b[32mRelease\x1b[m" : "\x1b[31mPress\x1b[m", button, format_mods(modifiers)); }
}
if (global_state.redirect_mouse_handling) {
w = window_for_event(&window_idx, &in_tab_bar);
call_boss(mouse_event, "OK iiii dd",
@@ -760,9 +783,9 @@ mouse_event(const int button, int modifiers, int action) {
clamp_to_window = true;
Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab;
for (window_idx = 0; window_idx < t->num_windows && t->windows[window_idx].id != w->id; window_idx++);
debug("sent to child as drag end\n");
handle_button_event(w, button, modifiers, window_idx);
clamp_to_window = false;
debug("sent to child as drag end\n");
return;
}
}
@@ -851,11 +874,10 @@ scroll_event(double UNUSED xoffset, double yoffset, int flags, int modifiers) {
int s;
bool is_high_resolution = flags & 1;
// scale the scroll by the multiplier unless the mouse is grabbed. If the mouse is grabbed only change direction.
#define SCALE_SCROLL(which) { double scale = OPT(which); if (screen->modes.mouse_tracking_mode) scale /= fabs(scale); yoffset *= scale; }
if (is_high_resolution) {
yoffset *= OPT(touch_scroll_multiplier);
if (yoffset * screen->pending_scroll_pixels < 0) {
screen->pending_scroll_pixels = 0; // change of direction
}
SCALE_SCROLL(touch_scroll_multiplier);
double pixels = screen->pending_scroll_pixels + yoffset;
if (fabs(pixels) < global_state.callback_os_window->fonts_data->cell_height) {
screen->pending_scroll_pixels = pixels;
@@ -864,20 +886,20 @@ scroll_event(double UNUSED xoffset, double yoffset, int flags, int modifiers) {
s = (int)round(pixels) / (int)global_state.callback_os_window->fonts_data->cell_height;
screen->pending_scroll_pixels = pixels - s * (int) global_state.callback_os_window->fonts_data->cell_height;
} else {
if (!screen->modes.mouse_tracking_mode) {
// Dont use multiplier if we are sending events to the application
yoffset *= OPT(wheel_scroll_multiplier);
} else if (OPT(wheel_scroll_multiplier) < 0) {
// ensure that changing scroll direction still works, even though
// we are not using wheel_scroll_multiplier
yoffset *= -1;
}
SCALE_SCROLL(wheel_scroll_multiplier);
s = (int) round(yoffset);
// apparently on cocoa some mice generate really small yoffset values
// when scrolling slowly https://github.com/kovidgoyal/kitty/issues/1238
if (s == 0 && yoffset != 0) s = yoffset > 0 ? 1 : -1;
if (yoffset != 0) {
const int min_lines = screen->modes.mouse_tracking_mode ? 1 : OPT(wheel_scroll_min_lines);
if (min_lines > 0 && abs(s) < min_lines) s = yoffset > 0 ? min_lines : -min_lines;
// Always add the minimum number of lines when it is negative
else if (min_lines < 0) s = yoffset > 0 ? s - min_lines : s + min_lines;
// apparently on cocoa some mice generate really small yoffset values
// when scrolling slowly https://github.com/kovidgoyal/kitty/issues/1238
if (s == 0) s = yoffset > 0 ? 1 : -1;
}
screen->pending_scroll_pixels = 0;
}
#undef SCALE_SCROLL
if (s == 0) return;
bool upwards = s > 0;
if (screen->modes.mouse_tracking_mode) {

View File

@@ -9,7 +9,7 @@ from contextlib import suppress
from typing import (
Any, Dict, Iterable, Iterator, List, NamedTuple, Optional, Tuple, cast
)
from urllib.parse import ParseResult, quote, unquote, urlparse
from urllib.parse import ParseResult, unquote, urlparse
from .conf.utils import KeyAction, to_cmdline_implementation
from .constants import config_dir
@@ -243,6 +243,10 @@ action launch --type=os-window $EDITOR $FILE_PATH
protocol file
mime image/*
action launch --type=os-window kitty +kitten icat --hold $FILE_PATH
# Open ssh URLs with ssh command
protocol ssh
action launch --type=os-window ssh $URL
'''.splitlines()))
@@ -254,8 +258,10 @@ def actions_for_url(url: str, actions_spec: Optional[str] = None) -> Iterator[Ke
yield from actions_for_url_from_list(url, actions)
def actions_for_launch(path: str) -> Iterator[KeyAction]:
url = f'file://{quote(path)}'
def actions_for_launch(url: str) -> Iterator[KeyAction]:
# Custom launch actions using kitty URL scheme needs to be prefixed with `kitty:///launch/`
if url.startswith('kitty://') and not url.startswith('kitty:///launch/'):
return
found = False
for action in actions_for_url_from_list(url, load_launch_actions()):
found = True

View File

@@ -33,7 +33,7 @@ and even specify special fonts for particular characters.
opt('font_family', 'monospace',
long_text='''
You can specify different fonts for the bold/italic/bold-italic variants.
To get a full list of supported fonts use the `kitty list-fonts` command.
To get a full list of supported fonts use the `kitty +list-fonts` command.
By default they are derived automatically, by the OSes font system. When
bold_font or bold_italic_font is set to :code:`auto` on macOS, the priority of
bold fonts is semi-bold, bold, heavy. Setting them manually is useful for font
@@ -124,6 +124,22 @@ Syntax is::
'''
)
opt('+narrow_symbols', 'U+E0A0-U+E0A3,U+E0C0-U+E0C7 1',
option_type='narrow_symbols',
add_to_default=False,
long_text='''
Usually, for Private Use Unicode characters and some symbol/dingbat characters,
if the character is followed by one or more spaces, kitty will use those extra cells
to render the character larger, if the character in the font has a wide aspect ratio.
Using this setting you can force kitty to restrict the specified code points to render in
the specified number of cells (defaulting to one cell).
Syntax is::
narrow_symbols codepoints Optionally the number of cells
'''
)
opt('disable_ligatures', 'never',
option_type='disable_ligatures', ctype='int',
long_text='''
@@ -166,11 +182,11 @@ disable a feature in the italic font but not in the regular font.
On Linux, these are read from the FontConfig database first and then this,
setting is applied, so they can be configured in a single, central place.
To get the PostScript name for a font, use :code:`kitty + list-fonts --psnames`:
To get the PostScript name for a font, use :code:`kitty +list-fonts --psnames`:
.. code-block:: sh
$ kitty + list-fonts --psnames | grep Fira
$ kitty +list-fonts --psnames | grep Fira
Fira Code
Fira Code Bold (FiraCode-Bold)
Fira Code Light (FiraCode-Light)
@@ -310,7 +326,7 @@ Separate scrollback history size, used only for browsing the scrollback buffer
(in MB). This separate buffer is not available for interactive scrolling but
will be piped to the pager program when viewing scrollback buffer in a separate
window. The current implementation stores the data in UTF-8, so approximatively
10000 lines per megabyte at 100 chars per line, for pure ASCII text, unformatted
10000 lines per megabyte at 100 chars per line, for pure ASCII, unformatted
text. A value of zero or less disables this feature. The maximum allowed size is
4GB. Note that on config reload if this
is changed it will only affect newly created windows, not existing ones.
@@ -327,7 +343,18 @@ opt('wheel_scroll_multiplier', '5.0',
long_text='''
Modify the amount scrolled by the mouse wheel. Note this is only used for low
precision scrolling devices, not for high precision scrolling on platforms such
as macOS and Wayland. Use negative numbers to change scroll direction.
as macOS and Wayland. Use negative numbers to change scroll direction. See also
:opt:`wheel_scroll_min_lines`.
'''
)
opt('wheel_scroll_min_lines', '1',
option_type='int', ctype='int',
long_text='''
The minimum number of lines scrolled by the mouse wheel. The :opt:`scroll multiplier <wheel_scroll_multiplier>`
only takes effect after it reaches this number. Note that this is only used for
low precision scrolling devices like wheel mice that scroll by very small amounts when using the wheel.
With a negative number, the minimum number of lines will always be added.
'''
)
@@ -377,7 +404,7 @@ The program with which to open URLs that are clicked on. The special value
'''
)
opt('url_prefixes', 'http https file ftp gemini irc gopher mailto news git',
opt('url_prefixes', 'file ftp ftps gemini git gopher http https irc ircs kitty mailto news sftp ssh',
option_type='url_prefixes', ctype='!url_prefixes',
long_text='''
The set of URL prefixes to look for when detecting a URL under the mouse cursor.
@@ -947,7 +974,7 @@ windows, via the quit action). Negative values are converted to positive ones,
however, with :ref:`shell_integration` enabled, using negative values means
windows sitting at a shell prompt are not counted, only windows where some
command is currently running. Note that if you want confirmation when closing
individual windows, you can map the :ref:`action-close_window_with_confirmation` action.
individual windows, you can map the :ac:`close_window_with_confirmation` action.
'''
)
egr() # }}}
@@ -995,10 +1022,11 @@ The tab bar style, can be one of:
:file:`tab_bar.py` in the kitty config directory. For examples of how to
write such a function, see the functions named :code:`draw_tab_with_*` in
kitty's source code: :file:`kitty/tab_bar.py`. See also
:disc:`this discussion <4447>` for examples from kitty users.
:disc:`this discussion <4447>`
for examples from kitty users.
:code:`hidden`
The tab bar is hidden. If you use this, you might want to create a
mapping for the :ref:`action-select_tab`
mapping for the :ac:`select_tab`
action which presents you with a list of tabs and allows for easy
switching to a tab.
'''
@@ -1064,7 +1092,7 @@ surround the text with quotes. See :opt:`tab_title_template` for how this is ren
'''
)
opt('tab_title_template', '"{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.default}{title}"',
opt('tab_title_template', '"{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.tab}{title}"',
option_type='tab_title_template',
long_text='''
A template to render the tab title. The default just renders the title with optional symbols for bell and activity.
@@ -1079,7 +1107,7 @@ is done by Python's string formatting machinery, so you can use, for instance,
:code:`{layout_name[:2].upper()}` to show only the first two letters of the
layout name, upper-cased. If you want to style the text, you can use styling
directives, for example:
:code:`{fmt.fg.red}red{fmt.fg.default}normal{fmt.bg._00FF00}green
:code:`{fmt.fg.red}red{fmt.fg.tab}normal{fmt.bg._00FF00}green
bg{fmt.bg.tab}`. Similarly, for bold and italic:
:code:`{fmt.bold}bold{fmt.nobold}normal{fmt.italic}italic{fmt.noitalic}`.
Note that for backward compatibility, if :code:`{bell_symbol}` or :code:`{activity_symbol}`
@@ -2829,9 +2857,14 @@ Show or hide the window title in the macOS window or menu-bar. A value of
the macOS window. A value of :code:`menubar` will show the title of the
currently active window in the macOS menu-bar, making use of otherwise wasted
space. :code:`all` will show the title everywhere and :code:`none` hides the
title in the window and the menu-bar.
'''
)
title in the window and the menu-bar. See :opt`macos_menubar_title_max_length`
for how to control the length of the title in the menu bar.
''')
opt('macos_menubar_title_max_length', '0', option_type='positive_int', ctype='int', long_text='''
The maximum number of characters from the window title to show in the global menubar.
Values less than one mean there is no maximum.
''')
opt('macos_custom_beam_cursor', 'no',
option_type='to_bool',
@@ -3539,6 +3572,10 @@ agr('shortcuts.misc', 'Miscellaneous')
map('Toggle fullscreen',
'toggle_fullscreen kitty_mod+f11 toggle_fullscreen',
)
map('Toggle fullscreen',
'toggle_fullscreen ctrl+cmd+f toggle_fullscreen',
only='macos',
)
map('Toggle maximized',
'toggle_maximized kitty_mod+f10 toggle_maximized',
@@ -3627,6 +3664,11 @@ map('Reset the terminal',
only='macos',
)
map('Clear up to cursor line',
'clear_terminal_and_scrollback cmd+k clear_terminal to_cursor active',
only='macos',
)
map('Reload kitty.conf',
'reload_config_file kitty_mod+f5 load_config_file',
long_text='''

23
kitty/options/parse.py generated
View File

@@ -11,12 +11,12 @@ from kitty.options.utils import (
clipboard_control, config_or_absolute_path, copy_on_select, cursor_text_color,
deprecated_hide_window_decorations_aliases, deprecated_macos_show_window_title_in_menubar_alias,
deprecated_send_text, disable_ligatures, edge_width, env, font_features, hide_window_decorations,
macos_option_as_alt, macos_titlebar_color, optional_edge_width, parse_map, parse_mouse_map,
resize_draw_strategy, scrollback_lines, scrollback_pager_history_size, shell_integration,
store_multiple, symbol_map, tab_activity_symbol, tab_bar_edge, tab_bar_margin_height,
tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator, tab_title_template, titlebar_color,
to_cursor_shape, to_font_size, to_layout_names, to_modifiers, url_prefixes, url_style,
visual_window_select_characters, window_border_width, window_size
macos_option_as_alt, macos_titlebar_color, narrow_symbols, optional_edge_width, parse_map,
parse_mouse_map, resize_draw_strategy, scrollback_lines, scrollback_pager_history_size,
shell_integration, store_multiple, symbol_map, tab_activity_symbol, tab_bar_edge,
tab_bar_margin_height, tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator,
tab_title_template, titlebar_color, to_cursor_shape, to_font_size, to_layout_names, to_modifiers,
url_prefixes, url_style, visual_window_select_characters, window_border_width, window_size
)
@@ -1029,6 +1029,9 @@ class Parser:
def macos_hide_from_tasks(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['macos_hide_from_tasks'] = to_bool(val)
def macos_menubar_title_max_length(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['macos_menubar_title_max_length'] = positive_int(val)
def macos_option_as_alt(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['macos_option_as_alt'] = macos_option_as_alt(val)
@@ -1076,6 +1079,10 @@ class Parser:
def mouse_hide_wait(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['mouse_hide_wait'] = float(val)
def narrow_symbols(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
for k, v in narrow_symbols(val):
ans["narrow_symbols"][k] = v
def open_url_with(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['open_url_with'] = to_cmdline(val)
@@ -1265,6 +1272,9 @@ class Parser:
def wayland_titlebar_color(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['wayland_titlebar_color'] = titlebar_color(val)
def wheel_scroll_min_lines(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['wheel_scroll_min_lines'] = int(val)
def wheel_scroll_multiplier(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['wheel_scroll_multiplier'] = float(val)
@@ -1328,6 +1338,7 @@ def create_result_dict() -> typing.Dict[str, typing.Any]:
'exe_search_path': {},
'font_features': {},
'kitten_alias': {},
'narrow_symbols': {},
'symbol_map': {},
'watcher': {},
'map': [],

View File

@@ -187,6 +187,19 @@ convert_from_opts_wheel_scroll_multiplier(PyObject *py_opts, Options *opts) {
Py_DECREF(ret);
}
static void
convert_from_python_wheel_scroll_min_lines(PyObject *val, Options *opts) {
opts->wheel_scroll_min_lines = PyLong_AsLong(val);
}
static void
convert_from_opts_wheel_scroll_min_lines(PyObject *py_opts, Options *opts) {
PyObject *ret = PyObject_GetAttrString(py_opts, "wheel_scroll_min_lines");
if (ret == NULL) return;
convert_from_python_wheel_scroll_min_lines(ret, opts);
Py_DECREF(ret);
}
static void
convert_from_python_touch_scroll_multiplier(PyObject *val, Options *opts) {
opts->touch_scroll_multiplier = PyFloat_AsDouble(val);
@@ -980,6 +993,19 @@ convert_from_opts_macos_show_window_title_in(PyObject *py_opts, Options *opts) {
Py_DECREF(ret);
}
static void
convert_from_python_macos_menubar_title_max_length(PyObject *val, Options *opts) {
opts->macos_menubar_title_max_length = PyLong_AsLong(val);
}
static void
convert_from_opts_macos_menubar_title_max_length(PyObject *py_opts, Options *opts) {
PyObject *ret = PyObject_GetAttrString(py_opts, "macos_menubar_title_max_length");
if (ret == NULL) return;
convert_from_python_macos_menubar_title_max_length(ret, opts);
Py_DECREF(ret);
}
static bool
convert_opts_from_python_opts(PyObject *py_opts, Options *opts) {
convert_from_opts_font_size(py_opts, opts);
@@ -1010,6 +1036,8 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) {
if (PyErr_Occurred()) return false;
convert_from_opts_wheel_scroll_multiplier(py_opts, opts);
if (PyErr_Occurred()) return false;
convert_from_opts_wheel_scroll_min_lines(py_opts, opts);
if (PyErr_Occurred()) return false;
convert_from_opts_touch_scroll_multiplier(py_opts, opts);
if (PyErr_Occurred()) return false;
convert_from_opts_mouse_hide_wait(py_opts, opts);
@@ -1132,5 +1160,7 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) {
if (PyErr_Occurred()) return false;
convert_from_opts_macos_show_window_title_in(py_opts, opts);
if (PyErr_Occurred()) return false;
convert_from_opts_macos_menubar_title_max_length(py_opts, opts);
if (PyErr_Occurred()) return false;
return true;
}

13
kitty/options/types.py generated
View File

@@ -370,6 +370,7 @@ option_names = ( # {{{
'listen_on',
'macos_custom_beam_cursor',
'macos_hide_from_tasks',
'macos_menubar_title_max_length',
'macos_option_as_alt',
'macos_quit_when_last_window_closed',
'macos_show_window_title_in',
@@ -386,6 +387,7 @@ option_names = ( # {{{
'mark3_foreground',
'mouse_hide_wait',
'mouse_map',
'narrow_symbols',
'open_url_with',
'placement_strategy',
'pointer_shape_when_dragging',
@@ -435,6 +437,7 @@ option_names = ( # {{{
'visual_window_select_characters',
'watcher',
'wayland_titlebar_color',
'wheel_scroll_min_lines',
'wheel_scroll_multiplier',
'window_alert_on_bell',
'window_border_width',
@@ -516,6 +519,7 @@ class Options:
listen_on: str = 'none'
macos_custom_beam_cursor: bool = False
macos_hide_from_tasks: bool = False
macos_menubar_title_max_length: int = 0
macos_option_as_alt: int = 0
macos_quit_when_last_window_closed: bool = False
macos_show_window_title_in: choices_for_macos_show_window_title_in = 'all'
@@ -565,18 +569,19 @@ class Options:
tab_powerline_style: choices_for_tab_powerline_style = 'angled'
tab_separator: str = ''
tab_switch_strategy: choices_for_tab_switch_strategy = 'previous'
tab_title_template: str = '{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.default}{title}'
tab_title_template: str = '{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.tab}{title}'
term: str = 'xterm-kitty'
touch_scroll_multiplier: float = 1.0
update_check_interval: float = 24.0
url_color: Color = Color(0, 135, 189)
url_excluded_characters: str = ''
url_prefixes: typing.Tuple[str, ...] = ('http', 'https', 'file', 'ftp', 'gemini', 'irc', 'gopher', 'mailto', 'news', 'git')
url_prefixes: typing.Tuple[str, ...] = ('file', 'ftp', 'ftps', 'gemini', 'git', 'gopher', 'http', 'https', 'irc', 'ircs', 'kitty', 'mailto', 'news', 'sftp', 'ssh') # noqa
url_style: int = 3
visual_bell_color: typing.Optional[kitty.fast_data_types.Color] = None
visual_bell_duration: float = 0
visual_window_select_characters: str = '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ'
wayland_titlebar_color: int = 0
wheel_scroll_min_lines: int = 1
wheel_scroll_multiplier: float = 5.0
window_alert_on_bell: bool = True
window_border_width: typing.Tuple[float, str] = (0.5, 'pt')
@@ -592,6 +597,7 @@ class Options:
exe_search_path: typing.Dict[str, str] = {}
font_features: typing.Dict[str, typing.Tuple[kitty.fonts.FontFeature, ...]] = {}
kitten_alias: typing.Dict[str, str] = {}
narrow_symbols: typing.Dict[typing.Tuple[int, int], int] = {}
symbol_map: typing.Dict[typing.Tuple[int, int], str] = {}
watcher: typing.Dict[str, str] = {}
map: typing.List[kitty.options.utils.KeyDefinition] = []
@@ -710,6 +716,7 @@ defaults.env = {}
defaults.exe_search_path = {}
defaults.font_features = {}
defaults.kitten_alias = {}
defaults.narrow_symbols = {}
defaults.symbol_map = {}
defaults.watcher = {}
defaults.map = [
@@ -898,10 +905,12 @@ if is_macos:
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=45), definition='change_font_size all -2.0')) # noqa
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=45), definition='change_font_size all -2.0')) # noqa
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=48), definition='change_font_size all 0')) # noqa
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=12, key=102), definition='toggle_fullscreen')) # noqa
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=10, key=115), definition='toggle_macos_secure_keyboard_entry')) # noqa
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=12, key=32), definition='kitten unicode_input')) # noqa
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=44), definition='edit_config_file')) # noqa
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=10, key=114), definition='clear_terminal reset active')) # noqa
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=107), definition='clear_terminal to_cursor active')) # noqa
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=12, key=44), definition='load_config_file')) # noqa
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=10, key=44), definition='debug_config')) # noqa
defaults.mouse_map = [

View File

@@ -163,9 +163,13 @@ def clear_terminal(func: str, rest: str) -> FuncArgsType:
vals = rest.strip().split(maxsplit=1)
if len(vals) != 2:
log_error('clear_terminal needs two arguments, using defaults')
args: List[Union[str, bool]] = ['reset', 'active']
args = ['reset', True]
else:
args = [vals[0].lower(), vals[1].lower() == 'active']
action = vals[0].lower()
if action not in ('reset', 'scroll', 'scrollback', 'clear', 'to_cursor',):
log_error(f'{action} is unknown for clear_terminal, using reset')
action = 'reset'
args = [action, vals[1].lower() == 'active']
return func, args
@@ -207,7 +211,7 @@ def resize_window(func: str, rest: str) -> FuncArgsType:
args = ['wider', 1]
else:
quality = vals[0].lower()
if quality not in ('taller', 'shorter', 'wider', 'narrower'):
if quality not in ('reset', 'taller', 'shorter', 'wider', 'narrower'):
log_error(f'Invalid quality specification: {quality}')
quality = 'wider'
increment = 1
@@ -814,19 +818,19 @@ def action_alias(val: str) -> Iterable[Tuple[str, str]]:
kitten_alias = action_alias
def symbol_map(val: str) -> Iterable[Tuple[Tuple[int, int], str]]:
def symbol_map_parser(val: str, min_size: int = 2) -> Iterable[Tuple[Tuple[int, int], str]]:
parts = val.split()
def abort() -> None:
log_error(f'Symbol map: {val} is invalid, ignoring')
if len(parts) < 2:
if len(parts) < min_size:
return abort()
family = ' '.join(parts[1:])
def to_chr(x: str) -> int:
if not x.startswith('U+'):
raise ValueError()
raise ValueError(f'{x} is not a unicode codepoint of the form U+number')
return int(x[2:], 16)
for x in parts[0].split(','):
@@ -841,6 +845,15 @@ def symbol_map(val: str) -> Iterable[Tuple[Tuple[int, int], str]]:
yield (a, b), family
def symbol_map(val: str) -> Iterable[Tuple[Tuple[int, int], str]]:
yield from symbol_map_parser(val)
def narrow_symbols(val: str) -> Iterable[Tuple[Tuple[int, int], int]]:
for x, y in symbol_map_parser(val, min_size=1):
yield x, int(y or 1)
def parse_key_action(action: str, action_type: str = 'map') -> KeyAction:
parts = action.strip().split(maxsplit=1)
func = parts[0]

View File

@@ -31,7 +31,7 @@ class WindowSizeData(NamedTuple):
def sanitize_window_size(x: Any) -> int:
ans = int(x)
return max(20, min(ans, 100000))
return max(20, min(ans, 50000))
def initial_window_size_func(opts: WindowSizeData, cached_values: Dict[str, Any]) -> Callable[[int, int, float, float, float, float], Tuple[int, int]]:

View File

@@ -43,8 +43,7 @@ requires :ref:`shell_integration` to be enabled.
--ansi
type=bool-set
By default, only plain text is returned. If you specify this flag, the text will
include the formatting escape codes for colors/bold/italic/etc. Note that when
getting the current selection, the result is always plain text.
include the formatting escape codes for colors/bold/italic/etc.
--add-cursor
@@ -83,7 +82,7 @@ Clear the selection in the matched window, if any
from kitty.window import CommandOutput
window = self.windows_for_match_payload(boss, window, payload_get)[0]
if payload_get('extent') == 'selection':
ans = window.text_for_selection()
ans = window.text_for_selection(as_ansi=payload_get('ansi'))
elif payload_get('extent') == 'first_cmd_output_on_screen':
ans = window.cmd_output(
CommandOutput.first_on_screen,

View File

@@ -1516,8 +1516,7 @@ int
screen_cursor_at_a_shell_prompt(const Screen *self) {
if (self->cursor->y >= self->lines || self->linebuf != self->main_linebuf || !screen_is_cursor_visible(self)) return -1;
for (index_type y=self->cursor->y + 1; y-- > 0; ) {
linebuf_init_line(self->linebuf, y);
switch(self->linebuf->line->attrs.prompt_kind) {
switch(self->linebuf->line_attrs[y].prompt_kind) {
case OUTPUT_START:
return -1;
case PROMPT_START:
@@ -1538,8 +1537,9 @@ screen_fake_move_cursor_to_position(Screen *self, index_type start_x, index_type
else { start = &b; end = &a; key = GLFW_FKEY_RIGHT; }
unsigned int count = 0;
for (unsigned y = start->y, x = start->x; y <= end->y; y++) {
for (unsigned y = start->y, x = start->x; y <= end->y && y < self->lines; y++) {
unsigned x_limit = y == end->y ? end->x : self->columns;
x_limit = MIN(x_limit, self->columns);
bool found_non_empty_cell = false;
while (x < x_limit) {
unsigned int w = linebuf_char_width_at(self->linebuf, x, y);
@@ -1616,6 +1616,15 @@ screen_erase_in_line(Screen *self, unsigned int how, bool private) {
}
}
static void
screen_clear_scrollback(Screen *self) {
historybuf_clear(self->historybuf);
if (self->scrolled_by != 0) {
self->scrolled_by = 0;
self->scroll_changed = true;
}
}
void
screen_erase_in_display(Screen *self, unsigned int how, bool private) {
/* Erases display in a specific way.
@@ -1662,11 +1671,7 @@ screen_erase_in_display(Screen *self, unsigned int how, bool private) {
if (how == 1) linebuf_clear_attrs_and_dirty(self->linebuf, self->cursor->y);
}
if (how == 3 && self->linebuf == self->main_linebuf) {
historybuf_clear(self->historybuf);
if (self->scrolled_by != 0) {
self->scrolled_by = 0;
self->scroll_changed = true;
}
screen_clear_scrollback(self);
}
}
@@ -1682,13 +1687,15 @@ screen_insert_lines(Screen *self, unsigned int count) {
}
}
void
screen_scroll_until_cursor(Screen *self) {
unsigned int num_lines_to_scroll = MIN(self->margin_bottom, self->cursor->y + 1);
index_type y = self->cursor->y;
static void
screen_scroll_until_cursor_prompt(Screen *self) {
int q = screen_cursor_at_a_shell_prompt(self);
unsigned int y = q > -1 ? (unsigned int)q : self->cursor->y;
unsigned int num_lines_to_scroll = MIN(self->margin_bottom, y);
unsigned int final_y = num_lines_to_scroll <= self->cursor->y ? self->cursor->y - num_lines_to_scroll : 0;
self->cursor->y = self->margin_bottom;
while (num_lines_to_scroll--) screen_index(self);
self->cursor->y = y;
self->cursor->y = final_y;
}
void
@@ -2328,6 +2335,7 @@ iteration_data(const Screen *self, const Selection *sel, IterationData *ans, int
ans->y += self->scrolled_by; ans->y_limit += self->scrolled_by;
}
ans->y = MAX(ans->y, min_y);
ans->y_limit = MAX(ans->y, ans->y_limit); // iteration is from y to y_limit
}
static XRange
@@ -2397,8 +2405,25 @@ screen_apply_selection(Screen *self, void *address, size_t size) {
self->url_ranges.last_rendered_count = self->url_ranges.count;
}
static index_type
limit_without_trailing_whitespace(const Line *line, index_type limit) {
if (!limit) return limit;
if (limit > line->xnum) limit = line->xnum;
while (limit > 0) {
CPUCell *cell = line->cpu_cells + limit - 1;
if (cell->cc_idx[0]) break;
switch(cell->ch) {
case ' ': case '\t': case '\n': case '\r': case 0: break;
default:
return limit;
}
limit--;
}
return limit;
}
static PyObject*
text_for_range(Screen *self, const Selection *sel, bool insert_newlines) {
text_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool strip_trailing_whitespace) {
IterationData idata;
iteration_data(self, sel, &idata, -self->historybuf->count, false);
int limit = MIN((int)self->lines, idata.y_limit);
@@ -2408,13 +2433,67 @@ text_for_range(Screen *self, const Selection *sel, bool insert_newlines) {
Line *line = range_line_(self, y);
XRange xr = xrange_for_iteration(&idata, y, line);
char leading_char = (i > 0 && insert_newlines && !line->attrs.continued) ? '\n' : 0;
PyObject *text = unicode_in_range(line, xr.x, xr.x_limit, true, leading_char, false);
index_type x_limit = xr.x_limit;
if (strip_trailing_whitespace) {
index_type new_limit = limit_without_trailing_whitespace(line, x_limit);
if (new_limit != x_limit) {
x_limit = new_limit;
if (!x_limit) {
PyObject *text = PyUnicode_FromString("\n");
if (text == NULL) { Py_DECREF(ans); return PyErr_NoMemory(); }
PyTuple_SET_ITEM(ans, i, text);
continue;
}
}
}
PyObject *text = unicode_in_range(line, xr.x, x_limit, true, leading_char, false);
if (text == NULL) { Py_DECREF(ans); return PyErr_NoMemory(); }
PyTuple_SET_ITEM(ans, i, text);
}
return ans;
}
static PyObject*
ansi_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool strip_trailing_whitespace) {
IterationData idata;
iteration_data(self, sel, &idata, -self->historybuf->count, false);
int limit = MIN((int)self->lines, idata.y_limit);
DECREF_AFTER_FUNCTION PyObject *ans = PyTuple_New(limit - idata.y + 1);
DECREF_AFTER_FUNCTION PyObject *nl = PyUnicode_FromString("\n");
if (!ans || !nl) return NULL;
ANSIBuf output = {0};
const GPUCell *prev_cell = NULL;
bool has_escape_codes = false;
for (int i = 0, y = idata.y; y < limit; y++, i++) {
Line *line = range_line_(self, y);
XRange xr = xrange_for_iteration(&idata, y, line);
output.len = 0;
char_type prefix_char = 0;
if (i > 0 && insert_newlines && !line->attrs.continued) prefix_char = '\n';
index_type x_limit = xr.x_limit;
if (strip_trailing_whitespace) {
index_type new_limit = limit_without_trailing_whitespace(line, x_limit);
if (new_limit != x_limit) {
x_limit = new_limit;
if (!x_limit) {
PyTuple_SET_ITEM(ans, i, nl);
continue;
}
}
}
if (line_as_ansi(line, &output, &prev_cell, xr.x, x_limit, prefix_char)) has_escape_codes = true;
PyObject *t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len);
if (!t) return NULL;
PyTuple_SET_ITEM(ans, i, t);
}
PyObject *t = PyUnicode_FromFormat("%s%s", has_escape_codes ? "\x1b[m" : "", output.active_hyperlink_id ? "\x1b]8;;\x1b\\" : "");
if (!t) return NULL;
PyTuple_SET_ITEM(ans, PyTuple_GET_SIZE(ans) - 1, t);
Py_INCREF(ans);
return ans;
}
static hyperlink_id_type
hyperlink_id_for_range(Screen *self, const Selection *sel) {
IterationData idata;
@@ -2431,7 +2510,7 @@ hyperlink_id_for_range(Screen *self, const Selection *sel) {
static PyObject*
extend_tuple(PyObject *a, PyObject *b) {
Py_ssize_t bs = PyBytes_GET_SIZE(b);
Py_ssize_t bs = PyTuple_GET_SIZE(b);
if (bs < 1) return a;
Py_ssize_t off = PyTuple_GET_SIZE(a);
if (_PyTuple_Resize(&a, off + bs) != 0) return NULL;
@@ -2450,7 +2529,7 @@ current_url_text(Screen *self, PyObject *args UNUSED) {
for (size_t i = 0; i < self->url_ranges.count; i++) {
Selection *s = self->url_ranges.items + i;
if (!is_selection_empty(s)) {
PyObject *temp = text_for_range(self, s, false);
PyObject *temp = text_for_range(self, s, false, false);
if (!temp) goto error;
PyObject *text = PyUnicode_Join(empty_string, temp);
Py_CLEAR(temp);
@@ -2659,6 +2738,7 @@ typedef struct OutputOffset {
Screen *screen;
int start;
unsigned num_lines;
bool reached_upper_limit;
} OutputOffset;
static Line*
@@ -2711,7 +2791,10 @@ find_cmd_output(Screen *self, OutputOffset *oo, index_type start_screen_y, unsig
}
y1--;
}
if (y1 < upward_limit) start = upward_limit;
if (y1 < upward_limit) {
oo->reached_upper_limit = true;
start = upward_limit;
}
found_output = true; found_prompt = true;
}
@@ -2771,8 +2854,12 @@ cmd_output(Screen *self, PyObject *args) {
PyErr_Format(PyExc_KeyError, "%u is not a valid type of command", which);
return NULL;
}
if (found) return as_text_generic(as_text_args, &oo, get_line_from_offset, oo.num_lines, &self->as_ansi_buf);
Py_RETURN_NONE;
if (found) {
DECREF_AFTER_FUNCTION PyObject *ret = as_text_generic(as_text_args, &oo, get_line_from_offset, oo.num_lines, &self->as_ansi_buf);
if (!ret) return NULL;
}
if (oo.reached_upper_limit && self->linebuf == self->main_linebuf && OPT(scrollback_pager_history_size) > 0) Py_RETURN_TRUE;
Py_RETURN_FALSE;
}
bool
@@ -2956,7 +3043,8 @@ is_using_alternate_linebuf(Screen *self, PyObject *a UNUSED) {
WRAP1E(cursor_back, 1, -1)
WRAP1B(erase_in_line, 0)
WRAP1B(erase_in_display, 0)
WRAP0(scroll_until_cursor)
WRAP0(scroll_until_cursor_prompt)
WRAP0(clear_scrollback)
#define MODE_GETSET(name, uname) \
static PyObject* name##_get(Screen *self, void UNUSED *closure) { PyObject *ans = self->modes.m##uname ? Py_True : Py_False; Py_INCREF(ans); return ans; } \
@@ -3082,10 +3170,10 @@ copy_colors_from(Screen *self, Screen *other) {
}
static PyObject*
text_for_selections(Screen *self, Selections *selections) {
text_for_selections(Screen *self, Selections *selections, bool ansi, bool strip_trailing_whitespace) {
PyObject *lines = NULL;
for (size_t i = 0; i < selections->count; i++) {
PyObject *temp = text_for_range(self, selections->items + i, true);
PyObject *temp = ansi ? ansi_for_range(self, selections->items +i, true, strip_trailing_whitespace) : text_for_range(self, selections->items + i, true, strip_trailing_whitespace);
if (temp) {
if (lines) {
lines = extend_tuple(lines, temp);
@@ -3099,13 +3187,17 @@ text_for_selections(Screen *self, Selections *selections) {
}
static PyObject*
text_for_selection(Screen *self, PyObject *a UNUSED) {
return text_for_selections(self, &self->selections);
text_for_selection(Screen *self, PyObject *args) {
int ansi = 0, strip_trailing_whitespace = 0;
if (!PyArg_ParseTuple(args, "|pp", &ansi, &strip_trailing_whitespace)) return NULL;
return text_for_selections(self, &self->selections, ansi, strip_trailing_whitespace);
}
static PyObject*
text_for_marked_url(Screen *self, PyObject *a UNUSED) {
return text_for_selections(self, &self->url_ranges);
text_for_marked_url(Screen *self, PyObject *args) {
int ansi = 0, strip_trailing_whitespace = 0;
if (!PyArg_ParseTuple(args, "|pp", &ansi, &strip_trailing_whitespace)) return NULL;
return text_for_selections(self, &self->url_ranges, ansi, strip_trailing_whitespace);
}
@@ -3711,6 +3803,33 @@ reverse_scroll(Screen *self, PyObject *args) {
Py_RETURN_NONE;
}
static PyObject*
scroll_prompt_to_bottom(Screen *self, PyObject *args UNUSED) {
if (self->linebuf != self->main_linebuf || !self->historybuf->count) Py_RETURN_NONE;
int q = screen_cursor_at_a_shell_prompt(self);
index_type limit_y = q > -1 ? (unsigned int)q : self->cursor->y;
index_type y = self->lines - 1;
// not before prompt or cursor line
while (y > limit_y) {
Line *line = checked_range_line(self, y);
if (!line || line_length(line)) break;
y--;
}
// don't scroll back beyond the history buffer range
unsigned int count = MIN(self->lines - (y + 1), self->historybuf->count);
if (count > 0) {
_reverse_scroll(self, count, true);
screen_cursor_down(self, count);
}
// always scroll to the bottom
if (self->scrolled_by != 0) {
self->scrolled_by = 0;
self->scroll_changed = true;
}
Py_RETURN_NONE;
}
static PyObject*
dump_lines_with_attrs(Screen *self, PyObject *accum) {
int y = (self->linebuf == self->main_linebuf) ? -self->historybuf->count : 0;
@@ -3777,11 +3896,13 @@ static PyMethodDef methods[] = {
MND(cursor_back, METH_VARARGS)
MND(erase_in_line, METH_VARARGS)
MND(erase_in_display, METH_VARARGS)
MND(scroll_until_cursor, METH_NOARGS)
MND(clear_scrollback, METH_NOARGS)
MND(scroll_until_cursor_prompt, METH_NOARGS)
MND(hyperlinks_as_list, METH_NOARGS)
MND(garbage_collect_hyperlink_pool, METH_NOARGS)
MND(hyperlink_for_id, METH_O)
MND(reverse_scroll, METH_VARARGS)
MND(scroll_prompt_to_bottom, METH_NOARGS)
METHOD(current_char_width, METH_NOARGS)
MND(insert_lines, METH_VARARGS)
MND(delete_lines, METH_VARARGS)
@@ -3817,8 +3938,8 @@ static PyMethodDef methods[] = {
MND(detect_url, METH_VARARGS)
MND(rescale_images, METH_NOARGS)
MND(current_key_encoding_flags, METH_NOARGS)
MND(text_for_selection, METH_NOARGS)
MND(text_for_marked_url, METH_NOARGS)
MND(text_for_selection, METH_VARARGS)
MND(text_for_marked_url, METH_VARARGS)
MND(is_rectangle_select, METH_NOARGS)
MND(scroll, METH_VARARGS)
MND(scroll_to_prompt, METH_VARARGS)

View File

@@ -314,7 +314,7 @@ cell_update_uniform_block(ssize_t vao_idx, Screen *screen, int uniform_buffer, c
}
rd->use_cell_for_selection_bg = IS_SPECIAL_COLOR(highlight_bg) ? 1. : 0.;
// Cursor position
enum { BLOCK_IDX = 0, BEAM_IDX = 8, UNDERLINE_IDX = 9, UNFOCUSED_IDX = 10 };
enum { BLOCK_IDX = 0, BEAM_IDX = NUM_UNDERLINE_STYLES + 3, UNDERLINE_IDX = NUM_UNDERLINE_STYLES + 4, UNFOCUSED_IDX = NUM_UNDERLINE_STYLES + 5 };
if (cursor->is_visible) {
rd->cursor_x = screen->cursor->x, rd->cursor_y = screen->cursor->y;
if (cursor->is_focused) {

View File

@@ -3,61 +3,16 @@
import os
import subprocess
from contextlib import suppress
from typing import Optional, Union, Dict
from typing import Dict, List, Optional
from .options.types import Options
from .config import atomic_save
from .constants import shell_integration_dir
from .utils import log_error, resolved_shell
posix_template = '''\
# BEGIN_KITTY_SHELL_INTEGRATION
if test -n "$KITTY_INSTALLATION_DIR" -a -e "$KITTY_INSTALLATION_DIR/{path}"; then source "$KITTY_INSTALLATION_DIR/{path}"; fi
# END_KITTY_SHELL_INTEGRATION\
'''
from .options.types import Options
from .utils import log_error, which
def atomic_write(path: str, data: Union[str, bytes]) -> None:
if isinstance(data, str):
data = data.encode('utf-8')
atomic_save(data, path)
def safe_read(path: str) -> str:
with suppress(FileNotFoundError):
with open(path) as f:
return f.read()
return ''
def setup_integration(shell_name: str, rc_path: str, template: str = posix_template) -> None:
import re
rc_path = os.path.realpath(rc_path)
rc = safe_read(rc_path)
integration = template.format(path=f"shell-integration/{shell_name}/kitty.{shell_name}")
newrc, num_subs = re.subn(
r'^# BEGIN_KITTY_SHELL_INTEGRATION.+?^# END_KITTY_SHELL_INTEGRATION',
integration, rc, flags=re.DOTALL | re.MULTILINE)
if num_subs < 1:
newrc = newrc.rstrip() + '\n\n' + integration
if newrc != rc:
atomic_write(rc_path, newrc)
def setup_zsh_integration(env: Dict[str, str]) -> None:
pass # this is handled in the zsh env modifier
def setup_bash_integration(env: Dict[str, str]) -> None:
setup_integration('bash', os.path.expanduser('~/.bashrc'))
def setup_fish_integration(env: Dict[str, str]) -> None:
pass # this is handled in the fish env modifier
def setup_fish_env(env: Dict[str, str]) -> None:
def setup_fish_env(env: Dict[str, str], argv: List[str]) -> None:
val = env.get('XDG_DATA_DIRS')
env['KITTY_FISH_XDG_DATA_DIR'] = shell_integration_dir
if not val:
@@ -68,13 +23,13 @@ def setup_fish_env(env: Dict[str, str]) -> None:
env['XDG_DATA_DIRS'] = os.pathsep.join(dirs)
def is_new_zsh_install(env: Dict[str, str]) -> bool:
def is_new_zsh_install(env: Dict[str, str], zdotdir: Optional[str]) -> bool:
# if ZDOTDIR is empty, zsh will read user rc files from /
# if there aren't any, it'll run zsh-newuser-install
# the latter will bail if there are rc files in $HOME
zdotdir = env.get('ZDOTDIR')
if not zdotdir:
zdotdir = os.path.expanduser('~')
zdotdir = env.get('HOME', os.path.expanduser('~'))
assert isinstance(zdotdir, str)
if zdotdir == '~':
return True
for q in ('.zshrc', '.zshenv', '.zprofile', '.zlogin'):
@@ -83,12 +38,25 @@ def is_new_zsh_install(env: Dict[str, str]) -> bool:
return True
def setup_zsh_env(env: Dict[str, str]) -> None:
if is_new_zsh_install(env):
# dont prevent zsh-newuser-install from running
# zsh-newuser-install never runs as root but we assume that it does
return
def get_zsh_zdotdir_from_global_zshenv(env: Dict[str, str], argv: List[str]) -> Optional[str]:
exe = which(argv[0], only_system=True) or 'zsh'
with suppress(Exception):
return subprocess.check_output([exe, '--norcs', '--interactive', '-c', 'echo -n $ZDOTDIR'], env=env).decode('utf-8')
return None
def setup_zsh_env(env: Dict[str, str], argv: List[str]) -> None:
zdotdir = env.get('ZDOTDIR')
if is_new_zsh_install(env, zdotdir):
if zdotdir is None:
# Try to get ZDOTDIR from /etc/zshenv, when all startup files are not present
zdotdir = get_zsh_zdotdir_from_global_zshenv(env, argv)
if zdotdir is None or is_new_zsh_install(env, zdotdir):
return
else:
# dont prevent zsh-newuser-install from running
# zsh-newuser-install never runs as root but we assume that it does
return
if zdotdir is not None:
env['KITTY_ORIG_ZDOTDIR'] = zdotdir
else:
@@ -98,48 +66,62 @@ def setup_zsh_env(env: Dict[str, str]) -> None:
env['ZDOTDIR'] = os.path.join(shell_integration_dir, 'zsh')
SUPPORTED_SHELLS = {
'zsh': setup_zsh_integration,
'bash': setup_bash_integration,
'fish': setup_fish_integration,
}
def setup_bash_env(env: Dict[str, str], argv: List[str]) -> None:
inject = {'1'}
posix_env = rcfile = ''
remove_args = set()
for i in range(1, len(argv)):
arg = argv[i]
if arg == '--posix':
inject.add('posix')
posix_env = env.get('ENV', '')
remove_args.add(i)
elif arg == '--norc':
inject.add('no-rc')
remove_args.add(i)
elif arg == '--noprofile':
inject.add('no-profile')
remove_args.add(i)
elif arg in ('--rcfile', '--init-file') and i + 1 < len(argv):
rcfile = argv[i+1]
remove_args |= {i, i+1}
env['ENV'] = os.path.join(shell_integration_dir, 'bash', 'kitty.bash')
env['KITTY_BASH_INJECT'] = ' '.join(inject)
if posix_env:
env['KITTY_BASH_POSIX_ENV'] = posix_env
if rcfile:
env['KITTY_BASH_RCFILE'] = rcfile
for i in sorted(remove_args, reverse=True):
del argv[i]
if 'HISTFILE' not in env and 'posix' not in inject:
# In POSIX mode the default history file is ~/.sh_history instead of ~/.bash_history
env['HISTFILE'] = os.path.expanduser('~/.bash_history')
env['KITTY_BASH_UNEXPORT_HISTFILE'] = '1'
argv.insert(1, '--posix')
ENV_MODIFIERS = {
'fish': setup_fish_env,
'zsh': setup_zsh_env,
'bash': setup_bash_env,
}
def get_supported_shell_name(path: str) -> Optional[str]:
name = os.path.basename(path).split('.')[0].lower()
name = name.replace('-', '')
if name in SUPPORTED_SHELLS:
return name
return None
name = os.path.basename(path)
if name.lower().endswith('.exe'):
name = name.rpartition('.')[0]
if name.startswith('-'):
name = name[1:]
return name if name in ENV_MODIFIERS else None
def shell_integration_allows_rc_modification(opts: Options) -> bool:
return not (opts.shell_integration & {'disabled', 'no-rc'})
def setup_shell_integration(opts: Options, env: Dict[str, str]) -> bool:
if not shell_integration_allows_rc_modification(opts):
return False
shell = get_supported_shell_name(resolved_shell(opts)[0])
if shell is None:
return False
func = SUPPORTED_SHELLS[shell]
try:
func(env)
except Exception:
import traceback
traceback.print_exc()
log_error(f'Failed to setup shell integration for: {shell}')
return False
return True
def modify_shell_environ(argv0: str, opts: Options, env: Dict[str, str]) -> None:
shell = get_supported_shell_name(argv0)
def modify_shell_environ(opts: Options, env: Dict[str, str], argv: List[str]) -> None:
shell = get_supported_shell_name(argv[0])
if shell is None or 'disabled' in opts.shell_integration:
return
env['KITTY_SHELL_INTEGRATION'] = ' '.join(opts.shell_integration)
@@ -148,7 +130,7 @@ def modify_shell_environ(argv0: str, opts: Options, env: Dict[str, str]) -> None
f = ENV_MODIFIERS.get(shell)
if f is not None:
try:
f(env)
f(env, argv)
except Exception:
import traceback
traceback.print_exc()

View File

@@ -24,6 +24,7 @@ typedef struct {
typedef struct {
monotonic_t visual_bell_duration, cursor_blink_interval, cursor_stop_blinking_after, mouse_hide_wait, click_interval;
double wheel_scroll_multiplier, touch_scroll_multiplier;
int wheel_scroll_min_lines;
bool enable_audio_bell;
CursorShape cursor_shape;
float cursor_beam_thickness;
@@ -80,6 +81,7 @@ typedef struct {
struct {
double outer, inner;
} tab_bar_margin_height;
long macos_menubar_title_max_length;
} Options;
typedef struct WindowLogoRenderData {
@@ -104,6 +106,7 @@ typedef struct {
monotonic_t at;
int button, modifiers;
double x, y;
unsigned long num;
} Click;
#define CLICK_QUEUE_SZ 3
@@ -298,12 +301,14 @@ typedef enum {
NEXT_TAB,
PREVIOUS_TAB,
DETACH_TAB,
OPEN_FILE,
LAUNCH_URLS,
NEW_WINDOW,
CLOSE_WINDOW,
RESET_TERMINAL,
CLEAR_TERMINAL_AND_SCROLLBACK,
RELOAD_CONFIG,
TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY,
TOGGLE_FULLSCREEN,
NUM_COCOA_PENDING_ACTIONS
} CocoaPendingAction;
@@ -339,3 +344,4 @@ void update_ime_focus(OSWindow* osw, bool focused);
void update_ime_position(Window* w, Screen *screen);
bool update_ime_position_for_window(id_type window_id, bool force, bool lost_focus);
void set_ignore_os_keyboard_processing(bool enabled);
void update_menu_bar_title(PyObject *title UNUSED);

View File

@@ -203,7 +203,7 @@ def draw_title(draw_data: DrawData, screen: Screen, tab: TabBarData, index: int)
if eval_locals['activity_symbol'] and not template_has_field(template, 'activity_symbol'):
prefix += '{activity_symbol}'
if prefix:
template = '{fmt.fg.red}' + prefix + '{fmt.fg.default}' + template
template = '{fmt.fg.red}' + prefix + '{fmt.fg.tab}' + template
try:
title = eval(compile_template(template), {'__builtins__': {}}, eval_locals)
except Exception as e:

View File

@@ -316,6 +316,9 @@ class Tab: # {{{
See :ref:`window_resizing` for details.
''')
def resize_window(self, quality: str, increment: int) -> None:
if quality == 'reset':
self.reset_window_sizes()
return
if increment < 1:
raise ValueError(increment)
is_horizontal = quality in ('wider', 'narrower')
@@ -383,12 +386,12 @@ class Tab: # {{{
import shlex
with suppress(OSError):
with open(old_exe) as f:
cmd = [kitty_exe(), '+hold']
if f.read(2) == '#!':
line = f.read(4096).splitlines()[0]
cmd += shlex.split(line) + [old_exe]
cmd[:0] = shlex.split(line)
else:
cmd += [resolved_shell(get_options())[0], cmd[0]]
cmd[:0] = [resolved_shell(get_options())[0]]
cmd[:0] = [kitty_exe(), '+hold']
fenv: Dict[str, str] = {}
if env:
fenv.update(env)

View File

@@ -9,7 +9,7 @@ import os
import re
import string
import sys
from contextlib import suppress
from contextlib import contextmanager, suppress
from functools import lru_cache
from time import monotonic
from typing import (
@@ -21,10 +21,10 @@ from .constants import (
appname, config_dir, is_macos, is_wayland, read_kitty_resource, shell_path,
supports_primary_selection
)
from .fast_data_types import Color, open_tty
from .rgb import to_color
from .types import run_once
from .typing import AddressFamily, PopenType, Socket, StartupCtx
from .fast_data_types import Color
if TYPE_CHECKING:
from .fast_data_types import OSWindowSize
@@ -61,6 +61,13 @@ def sgr_sanitizer_pat(for_splitting: bool = False) -> 're.Pattern[str]':
return re.compile(pat)
@run_once
def kitty_ansi_sanitizer_pat() -> 're.Pattern[str]':
# removes ANSI sequences generated by kitty's ANSI output routines. Not
# suitable for stripping general ANSI sequences
return re.compile(r'\x1b(?:\[[0-9;:]*?m|\].*?\x1b\\)')
def platform_window_id(os_window_id: int) -> Optional[int]:
if is_macos:
from .fast_data_types import cocoa_window_id
@@ -141,6 +148,19 @@ class ScreenSize(NamedTuple):
cell_height: int
def read_screen_size(fd: int = -1) -> ScreenSize:
import array
import fcntl
import termios
buf = array.array('H', [0, 0, 0, 0])
if fd < 0:
fd = sys.stdout.fileno()
fcntl.ioctl(fd, termios.TIOCGWINSZ, cast(bytearray, buf))
rows, cols, width, height = tuple(buf)
cell_width, cell_height = width // (cols or 1), height // (rows or 1)
return ScreenSize(rows, cols, width, height, cell_width, cell_height)
class ScreenSizeGetter:
changed = True
Size = ScreenSize
@@ -153,14 +173,7 @@ class ScreenSizeGetter:
def __call__(self) -> ScreenSize:
if self.changed:
import array
import fcntl
import termios
buf = array.array('H', [0, 0, 0, 0])
fcntl.ioctl(self.fd, termios.TIOCGWINSZ, cast(bytearray, buf))
rows, cols, width, height = tuple(buf)
cell_width, cell_height = width // (cols or 1), height // (rows or 1)
self.ans = ScreenSize(rows, cols, width, height, cell_width, cell_height)
self.ans = read_screen_size(self.fd)
self.changed = False
return cast(ScreenSize, self.ans)
@@ -459,7 +472,6 @@ class TTYIO:
self.read_with_timeout = read_with_timeout
def __enter__(self) -> 'TTYIO':
from .fast_data_types import open_tty
self.tty_fd, self.original_termios = open_tty(self.read_with_timeout)
return self
@@ -495,6 +507,21 @@ class TTYIO:
break
@contextmanager
def no_echo(fd: int = -1) -> Generator[None, None, None]:
import termios
if fd < 0:
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
new = termios.tcgetattr(fd)
new[3] = new[3] & ~termios.ECHO
try:
termios.tcsetattr(fd, termios.TCSADRAIN, new)
yield
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
def natsort_ints(iterable: Iterable[str]) -> List[str]:
def convert(text: str) -> Union[int, str]:
@@ -654,6 +681,7 @@ def which(name: str, only_system: bool = False) -> Optional[str]:
if os.sep in name:
return name
import shutil
from .fast_data_types import get_options
opts: Optional[Options] = None
with suppress(RuntimeError):
@@ -837,6 +865,7 @@ def is_kitty_gui_cmdline(*cmd: str) -> bool:
def reload_conf_in_all_kitties() -> None:
import signal
from kitty.child import cmdline_of_process
for pid in get_all_processes():
@@ -858,21 +887,22 @@ def sanitize_control_codes(text: str, replace_with: str = '') -> str:
def hold_till_enter() -> None:
import select
import termios
from kittens.tui.operations import init_state, set_cursor_visible
with suppress(BaseException):
print(
'\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()
fd, original_termios = open_tty()
msg = '\n\r\x1b[1;32mPress Enter or Esc to exit\x1b[m'
write_all(fd, msg)
write_all(fd, init_state(alternate_screen=False, kitty_keyboard_mode=False) + set_cursor_visible(False))
with no_echo(fd):
termios.tcdrain(fd)
while True:
rd = select.select([fd], [], [])[0]
if not rd:
break
q = os.read(fd, 1)
if q in b'\n\r\x1b':
break
if q in b'\x03\x04':
write_all(fd, msg)

View File

@@ -3,6 +3,7 @@
import json
import os
import re
import sys
import weakref
from collections import deque
@@ -23,17 +24,17 @@ from .constants import appname, is_macos, wakeup
from .fast_data_types import (
BGIMAGE_PROGRAM, BLIT_PROGRAM, CELL_BG_PROGRAM, CELL_FG_PROGRAM,
CELL_PROGRAM, CELL_SPECIAL_PROGRAM, CURSOR_BEAM, CURSOR_BLOCK,
CURSOR_UNDERLINE, DCS, DECORATION, DIM, GLFW_MOD_CONTROL,
CURSOR_UNDERLINE, DCS, DECORATION, DECORATION_MASK, DIM, GLFW_MOD_CONTROL,
GRAPHICS_ALPHA_MASK_PROGRAM, GRAPHICS_PREMULT_PROGRAM, GRAPHICS_PROGRAM,
MARK, MARK_MASK, NO_CURSOR_SHAPE, OSC, REVERSE, SCROLL_FULL, SCROLL_LINE,
SCROLL_PAGE, STRIKETHROUGH, TINT_PROGRAM, Color, KeyEvent, Screen,
add_timer, add_window, cell_size_for_window, click_mouse_cmd_output,
click_mouse_url, compile_program, current_os_window, encode_key_for_tty,
get_boss, get_click_interval, get_clipboard_string, get_options,
init_cell_program, mark_os_window_dirty, mouse_selection,
move_cursor_to_mouse_if_in_prompt, pt_to_px, set_clipboard_string,
set_titlebar_color, set_window_logo, set_window_padding,
set_window_render_data, update_ime_position_for_window,
MARK, MARK_MASK, NO_CURSOR_SHAPE, NUM_UNDERLINE_STYLES, OSC, REVERSE,
SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE, STRIKETHROUGH, TINT_PROGRAM, Color,
KeyEvent, Screen, add_timer, add_window, cell_size_for_window,
click_mouse_cmd_output, click_mouse_url, compile_program,
current_os_window, encode_key_for_tty, get_boss, get_click_interval,
get_clipboard_string, get_options, init_cell_program, mark_os_window_dirty,
mouse_selection, move_cursor_to_mouse_if_in_prompt, pt_to_px,
set_clipboard_string, set_titlebar_color, set_window_logo,
set_window_padding, set_window_render_data, update_ime_position_for_window,
update_window_title, update_window_visibility
)
from .keys import keyboard_mode_name, mod_mask
@@ -44,9 +45,9 @@ from .terminfo import get_capabilities
from .types import MouseEvent, WindowGeometry, ac
from .typing import BossType, ChildType, EdgeLiteral, TabType, TypedDict
from .utils import (
get_primary_selection, load_shaders, log_error, open_cmd, open_url,
parse_color_set, resolve_custom_file, sanitize_title,
set_primary_selection, sgr_sanitizer_pat
get_primary_selection, kitty_ansi_sanitizer_pat, load_shaders, log_error,
open_cmd, open_url, parse_color_set, resolve_custom_file, sanitize_title,
set_primary_selection
)
MatchPatternType = Union[Pattern[str], Tuple[Pattern[str], Optional[Pattern[str]]]]
@@ -56,6 +57,16 @@ if TYPE_CHECKING:
from .file_transmission import FileTransmission
def process_title_from_child(title: str, is_base64: bool) -> str:
if is_base64:
from base64 import standard_b64decode
try:
title = standard_b64decode(title).decode('utf-8', 'replace')
except Exception:
title = 'undecodeable title'
return sanitize_title(title)
class WindowDict(TypedDict):
id: int
is_focused: bool
@@ -156,6 +167,14 @@ def call_watchers(windowref: Callable[[], Optional['Window']], which: str, data:
add_timer(callback, 0, False)
def pagerhist(screen: Screen, as_ansi: bool = False, add_wrap_markers: bool = True, upto_output_start: bool = False) -> str:
pht = screen.historybuf.pagerhist_as_text(upto_output_start)
if pht and (not as_ansi or not add_wrap_markers):
sanitizer = text_sanitizer(as_ansi, add_wrap_markers)
pht = sanitizer(pht)
return pht
def as_text(
screen: Screen,
as_ansi: bool = False,
@@ -185,13 +204,8 @@ def as_text(
ctext += f'\x1b[{code} q'
if add_history:
h: List[str] = []
pht = screen.historybuf.pagerhist_as_text()
if pht:
h.append(pht)
if h and (not as_ansi or not add_wrap_markers):
sanitizer = text_sanitizer(as_ansi, add_wrap_markers)
h = list(map(sanitizer, h))
pht = pagerhist(screen, as_ansi, add_wrap_markers)
h: List[str] = [pht] if pht else []
screen.historybuf.as_text(h.append, as_ansi, add_wrap_markers)
if h:
if not screen.linebuf.is_continued(0):
@@ -208,6 +222,15 @@ def as_text(
return ans
def multi_replace(src: str, **replacements: Any) -> str:
r = {k: str(v) for k, v in replacements.items()}
def sub(m: 're.Match[str]') -> str:
return r.get(m.group(1), m.group(1))
return re.sub(r'\{([A-Z_]+)\}', sub, src)
class LoadShaderPrograms:
def __call__(self, semi_transparent: bool = False) -> None:
@@ -220,16 +243,19 @@ class LoadShaderPrograms:
'SPECIAL': CELL_SPECIAL_PROGRAM,
'FOREGROUND': CELL_FG_PROGRAM,
}.items():
vv, ff = v.replace('WHICH_PROGRAM', which), f.replace('WHICH_PROGRAM', which)
for gln, pyn in {
'REVERSE_SHIFT': REVERSE,
'STRIKE_SHIFT': STRIKETHROUGH,
'DIM_SHIFT': DIM,
'DECORATION_SHIFT': DECORATION,
'MARK_SHIFT': MARK,
'MARK_MASK': MARK_MASK,
}.items():
vv = vv.replace(f'{{{gln}}}', str(pyn), 1)
ff = f.replace('{WHICH_PROGRAM}', which)
vv = multi_replace(
v,
WHICH_PROGRAM=which,
REVERSE_SHIFT=REVERSE,
STRIKE_SHIFT=STRIKETHROUGH,
DIM_SHIFT=DIM,
DECORATION_SHIFT=DECORATION,
MARK_SHIFT=MARK,
MARK_MASK=MARK_MASK,
DECORATION_MASK=DECORATION_MASK,
STRIKE_SPRITE_INDEX=NUM_UNDERLINE_STYLES + 1,
)
if semi_transparent:
vv = vv.replace('#define NOT_TRANSPARENT', '#define TRANSPARENT')
ff = ff.replace('#define NOT_TRANSPARENT', '#define TRANSPARENT')
@@ -268,7 +294,7 @@ def setup_colors(screen: Screen, opts: Options) -> None:
def text_sanitizer(as_ansi: bool, add_wrap_markers: bool) -> Callable[[str], str]:
pat = sgr_sanitizer_pat()
pat = kitty_ansi_sanitizer_pat()
ansi, wrap_markers = not as_ansi, not add_wrap_markers
def remove_wrap_markers(line: str) -> str:
@@ -285,6 +311,23 @@ def text_sanitizer(as_ansi: bool, add_wrap_markers: bool) -> Callable[[str], str
return remove_wrap_markers
def cmd_output(screen: Screen, which: CommandOutput = CommandOutput.last_run, as_ansi: bool = False, add_wrap_markers: bool = False) -> str:
lines: List[str] = []
search_in_pager_hist = screen.cmd_output(which, lines.append, as_ansi, add_wrap_markers)
if search_in_pager_hist:
pht = pagerhist(screen, as_ansi, add_wrap_markers, True)
if pht:
lines.insert(0, pht)
return ''.join(lines)
def process_remote_print(msg: bytes) -> str:
from base64 import standard_b64decode
from .cli import green
text = standard_b64decode(msg).decode('utf-8', 'replace')
return text.replace('\x1b', green(r'\e')).replace('\a', green(r'\a')).replace('\0', green(r'\0'))
class EdgeWidths:
left: Optional[float]
top: Optional[float]
@@ -347,6 +390,7 @@ class Window:
else:
self.watchers = global_watchers().copy()
self.last_focused_at = 0.
self.started_at = monotonic()
self.current_mouse_event_button = 0
self.current_clipboard_read_ask: Optional[bool] = None
self.prev_osc99_cmd = NotificationCommand()
@@ -486,9 +530,13 @@ class Window:
def current_colors(self) -> Dict[str, Optional[int]]:
return self.screen.color_profile.as_dict()
@property
def at_prompt(self) -> bool:
return self.screen.cursor_at_prompt()
@property
def has_running_program(self) -> bool:
return not self.screen.cursor_at_prompt()
return not self.at_prompt
def matches(self, field: str, pat: MatchPatternType) -> bool:
if not pat:
@@ -696,8 +744,8 @@ class Window:
# Cancel IME composition after loses focus
update_ime_position_for_window(self.id, False, True)
def title_changed(self, new_title: Optional[str]) -> None:
self.child_title = sanitize_title(new_title or self.default_title)
def title_changed(self, new_title: Optional[str], is_base64: bool = False) -> None:
self.child_title = process_title_from_child(new_title or self.default_title, is_base64)
if self.override_title is None:
self.title_updated()
@@ -828,11 +876,7 @@ class Window:
get_boss().handle_remote_cmd(cmd, self)
def handle_remote_print(self, msg: bytes) -> None:
from base64 import standard_b64decode
from .cli import green
text = standard_b64decode(msg).decode('utf-8')
text = text.replace('\x1b', green(r'\e')).replace('\a', green(r'\a')).replace('\0', green(r'\0'))
text = process_remote_print(msg)
print(text, end='', file=sys.stderr)
sys.stderr.flush()
@@ -1012,12 +1056,10 @@ class Window:
self.show_cmd_output(CommandOutput.last_visited, 'Clicked command output')
# }}}
def text_for_selection(self) -> str:
lines = self.screen.text_for_selection()
def text_for_selection(self, as_ansi: bool = False) -> str:
sts = get_options().strip_trailing_spaces
if sts == 'always' or (
sts == 'smart' and not self.screen.is_rectangle_select()):
return ''.join((ln.rstrip() or '\n') for ln in lines)
strip_trailing_spaces = sts == 'always' or (sts == 'smart' and not self.screen.is_rectangle_select())
lines = self.screen.text_for_selection(as_ansi, strip_trailing_spaces)
return ''.join(lines)
def call_watchers(self, which: Iterable[Watcher], data: Dict[str, Any]) -> None:
@@ -1051,9 +1093,7 @@ class Window:
return as_text(self.screen, as_ansi, add_history, add_wrap_markers, alternate_screen, add_cursor)
def cmd_output(self, which: CommandOutput = CommandOutput.last_run, as_ansi: bool = False, add_wrap_markers: bool = False) -> str:
lines: List[str] = []
self.screen.cmd_output(which, lines.append, as_ansi, add_wrap_markers)
return ''.join(lines)
return cmd_output(self.screen, which, as_ansi, add_wrap_markers)
@property
def cwd_of_child(self) -> Optional[str]:
@@ -1147,6 +1187,12 @@ class Window:
if text:
set_clipboard_string(text)
@ac('cp', 'Copy the selected text from the active window to the clipboard with ANSI formatting codes')
def copy_ansi_to_clipboard(self) -> None:
text = self.text_for_selection(as_ansi=True)
if text:
set_clipboard_string(text)
def encoded_key(self, key_event: KeyEvent) -> bytes:
return encode_key_for_tty(
key=key_event.key, shifted_key=key_event.shifted_key, alternate_key=key_event.alternate_key,
@@ -1155,7 +1201,7 @@ class Window:
cursor_key_mode=self.screen.cursor_key_mode,
).encode('ascii')
@ac('cp', 'Copy the selected text from the active window to the clipboard, if no selection, send Ctrl-C')
@ac('cp', 'Copy the selected text from the active window to the clipboard, if no selection, send SIGINT (aka :kbd:`ctrl+c`)')
def copy_or_interrupt(self) -> None:
text = self.text_for_selection()
if text:
@@ -1163,7 +1209,7 @@ class Window:
else:
self.write_to_child(self.encoded_key(KeyEvent(key=ord('c'), mods=GLFW_MOD_CONTROL)))
@ac('cp', 'Copy the selected text from the active window to the clipboard and clear selection, if no selection, send Ctrl-C')
@ac('cp', 'Copy the selected text from the active window to the clipboard and clear selection, if no selection, send SIGINT (aka :kbd:`ctrl+c`)')
def copy_and_clear_or_interrupt(self) -> None:
self.copy_or_interrupt()
self.screen.clear_selection()
@@ -1228,6 +1274,10 @@ class Window:
if self.screen.is_main_linebuf():
self.screen.scroll_to_prompt(num_of_prompts)
@ac('sc', 'Scroll prompt to the bottom of the screen, filling in extra lines form the scrollback buffer')
def scroll_prompt_to_bottom(self) -> None:
self.screen.scroll_prompt_to_bottom()
@ac('mk', 'Toggle the current marker on/off')
def toggle_marker(self, ftype: str, spec: Union[str, Tuple[Tuple[int, str], ...]], flags: int) -> None:
from .marks import marker_from_spec

View File

@@ -1,29 +1,39 @@
#!/usr/bin/env python3
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import fcntl
import io
import os
import select
import shlex
import struct
import sys
import termios
import time
from pty import CHILD, fork
from unittest import TestCase
from kitty.config import finalize_keys, finalize_mouse_mappings
from kitty.fast_data_types import (
Cursor, HistoryBuf, LineBuf, Screen, set_options
Cursor, HistoryBuf, LineBuf, Screen, get_options, parse_bytes, set_options
)
from kitty.options.parse import merge_result_dicts
from kitty.options.types import Options, defaults
from kitty.types import MouseEvent
from kitty.utils import read_screen_size
from kitty.window import process_remote_print, process_title_from_child
class Callbacks:
def __init__(self, opts) -> None:
def __init__(self) -> None:
self.clear()
self.opts = opts
def write(self, data) -> None:
self.wtcbuf += data
def title_changed(self, data) -> None:
self.titlebuf += data
def title_changed(self, data, is_base64=False) -> None:
self.titlebuf.append(process_title_from_child(data, is_base64))
def icon_changed(self, data) -> None:
self.iconbuf += data
@@ -53,7 +63,8 @@ class Callbacks:
def clear(self) -> None:
self.wtcbuf = b''
self.iconbuf = self.titlebuf = self.colorbuf = self.ctbuf = ''
self.iconbuf = self.colorbuf = self.ctbuf = ''
self.titlebuf = []
self.iutf8 = True
self.notifications = []
self.open_urls = []
@@ -68,15 +79,20 @@ class Callbacks:
def on_mouse_event(self, event):
ev = MouseEvent(**event)
action_def = self.opts.mousemap.get(ev)
opts = get_options()
action_def = opts.mousemap.get(ev)
if not action_def:
return False
self.current_mouse_button = ev.button
for action in self.opts.alias_map.resolve_aliases(action_def, 'mouse_map'):
for action in opts.alias_map.resolve_aliases(action_def, 'mouse_map'):
getattr(self, action.func)(*action.args)
self.current_mouse_button = 0
return True
def handle_remote_print(self, msg):
text = process_remote_print(msg)
print(text, file=sys.__stderr__)
def filled_line_buf(ynum=5, xnum=5, cursor=Cursor()):
ans = LineBuf(ynum, xnum)
@@ -120,12 +136,20 @@ class BaseTest(TestCase):
set_options(options)
return options
def cmd_to_run_python_code(self, code):
from kitty.constants import kitty_exe
return [kitty_exe(), '+runpy', code]
def create_screen(self, cols=5, lines=5, scrollback=5, cell_width=10, cell_height=20, options=None):
opts = self.set_options(options)
c = Callbacks(opts)
self.set_options(options)
c = Callbacks()
s = Screen(c, lines, cols, scrollback, cell_width, cell_height, 0, c)
return s
def create_pty(self, argv, cols=80, lines=25, scrollback=100, cell_width=10, cell_height=20, options=None, cwd=None, env=None):
self.set_options(options)
return PTY(argv, lines, cols, scrollback, cell_width, cell_height, cwd, env)
def assertEqualAttributes(self, c1, c2):
x1, y1, c1.x, c1.y = c1.x, c1.y, 0, 0
x2, y2, c2.x, c2.y = c2.x, c2.y, 0, 0
@@ -133,3 +157,93 @@ class BaseTest(TestCase):
self.assertEqual(c1, c2)
finally:
c1.x, c1.y, c2.x, c2.y = x1, y1, x2, y2
class PTY:
def __init__(self, argv, rows=25, columns=80, scrollback=100, cell_width=10, cell_height=20, cwd=None, env=None):
if isinstance(argv, str):
argv = shlex.split(argv)
pid, self.master_fd = fork()
self.is_child = pid == CHILD
self.write_buf = b''
if self.is_child:
while read_screen_size().width != columns * cell_width:
time.sleep(0.01)
if cwd:
os.chdir(cwd)
os.execvpe(argv[0], argv, env or os.environ)
os.set_blocking(self.master_fd, False)
self.cell_width = cell_width
self.cell_height = cell_height
self.set_window_size(rows=rows, columns=columns)
self.callbacks = Callbacks()
self.screen = Screen(self.callbacks, rows, columns, scrollback, cell_width, cell_height, 0, self.callbacks)
self.received_bytes = b''
def __del__(self):
if not self.is_child:
fd = self.master_fd
del self.master_fd
os.close(fd)
def write_to_child(self, data):
if isinstance(data, str):
data = data.encode('utf-8')
self.write_buf += data
def send_cmd_to_child(self, cmd):
self.write_to_child(cmd + '\r')
def process_input_from_child(self, timeout=10):
rd, wd, err = select.select([self.master_fd], [self.master_fd] if self.write_buf else [], [self.master_fd], timeout)
if err:
raise OSError('master_fd is in error condition')
while wd:
try:
n = os.write(self.master_fd, self.write_buf)
except (BlockingIOError, OSError):
n = 0
if not n:
break
self.write_buf = self.write_buf[n:]
bytes_read = 0
while rd:
try:
data = os.read(self.master_fd, io.DEFAULT_BUFFER_SIZE)
except (BlockingIOError, OSError):
data = b''
if not data:
break
bytes_read += len(data)
self.received_bytes += data
parse_bytes(self.screen, data)
return bytes_read
def wait_till(self, q, timeout=10):
st = time.monotonic()
while not q() and time.monotonic() - st < timeout:
self.process_input_from_child(timeout=timeout - (time.monotonic() - st))
if not q():
raise TimeoutError(f'The condition was not met. Screen contents: \n {repr(self.screen_contents())}')
def set_window_size(self, rows=25, columns=80):
if hasattr(self, 'screen'):
self.screen.resize(rows, columns)
x_pixels = columns * self.cell_width
y_pixels = rows * self.cell_height
s = struct.pack('HHHH', rows, columns, x_pixels, y_pixels)
fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, s)
def screen_contents(self):
lines = []
for i in range(self.screen.lines):
x = str(self.screen.line(i))
if x:
lines.append(x)
return '\n'.join(lines)
def last_cmd_output(self, as_ansi=False, add_wrap_markers=False):
from kitty.window import cmd_output
return cmd_output(self.screen, as_ansi=as_ansi, add_wrap_markers=add_wrap_markers)

View File

@@ -1,18 +1,21 @@
#!/usr/bin/env python3
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import os
from . import BaseTest
class TestHints(BaseTest):
def test_url_hints(self):
from kittens.hints.main import parse_hints_args, functions_for, mark, convert_text
from kittens.hints.main import (
Mark, convert_text, functions_for, linenum_marks,
linenum_process_result, mark, parse_hints_args
)
args = parse_hints_args([])[0]
pattern, post_processors = functions_for(args)
def create_marks(text, cols=20):
def create_marks(text, cols=20, mark=mark):
text = convert_text(text, cols)
return tuple(mark(pattern, post_processors, text, args))
@@ -30,8 +33,27 @@ class TestHints(BaseTest):
t(f'`xyz <{u}>`_.', u)
t(f'<a href="{u}">moo', u)
def m(text, path, line, cols=20):
def adapt(pattern, postprocessors, text, *a):
return linenum_marks(text, args, Mark, ())
marks = create_marks(text, cols, mark=adapt)
data = {'groupdicts': [m.groupdict for m in marks], 'match': [m.text for m in marks]}
self.ae(linenum_process_result(data), (path, line))
args = parse_hints_args('--type=linenum'.split())[0]
m('file.c:23', 'file.c', 23)
m('file.c:23:32', 'file.c', 23)
m('file.cpp:23:1', 'file.cpp', 23)
m('a/file.c:23', 'a/file.c', 23)
m('a/file.c:23:32', 'a/file.c', 23)
m('~/file.c:23:32', os.path.expanduser('~/file.c'), 23)
def test_ip_hints(self):
from kittens.hints.main import parse_hints_args, functions_for, mark, convert_text
from kittens.hints.main import (
convert_text, functions_for, mark, parse_hints_args
)
args = parse_hints_args(['--type', 'ip'])[0]
pattern, post_processors = functions_for(args)

View File

@@ -213,18 +213,18 @@ class TestParser(BaseTest):
c = s.callbacks
pb('a\033]2;x\\ryz\x9cbcde', 'a', ('set_title', 'x\\ryz'), 'bcde')
self.ae(str(s.line(0)), 'abcde')
self.ae(c.titlebuf, 'x\\ryz')
self.ae(c.titlebuf, ['x\\ryz'])
c.clear()
pb('\033]\x07', ('set_title', ''), ('set_icon', ''))
self.ae(c.titlebuf, ''), self.ae(c.iconbuf, '')
self.ae(c.titlebuf, ['']), self.ae(c.iconbuf, '')
pb('\033]ab\x07', ('set_title', 'ab'), ('set_icon', 'ab'))
self.ae(c.titlebuf, 'ab'), self.ae(c.iconbuf, 'ab')
self.ae(c.titlebuf, ['', 'ab']), self.ae(c.iconbuf, 'ab')
c.clear()
pb('\033]2;;;;\x07', ('set_title', ';;;'))
self.ae(c.titlebuf, ';;;')
self.ae(c.titlebuf, [';;;'])
c.clear()
pb('\033]2;\x07', ('set_title', ''))
self.ae(c.titlebuf, '')
self.ae(c.titlebuf, [''])
pb('\033]110\x07', ('set_dynamic_color', 110, ''))
self.ae(c.colorbuf, '')
c.clear()

View File

@@ -5,6 +5,7 @@ from kitty.fast_data_types import (
DECAWM, DECCOLM, DECOM, IRM, Cursor, parse_bytes
)
from kitty.marks import marker_from_function, marker_from_regex
from kitty.window import pagerhist
from . import BaseTest
@@ -530,6 +531,28 @@ class TestScreen(BaseTest):
s.scroll(2, True)
self.ae(s.text_for_selection(), expected)
s.reset()
s.draw('ab cd')
s.start_selection(0, 0)
s.update_selection(1, 3)
self.ae(s.text_for_selection(), ('ab ', 'cd'))
self.ae(s.text_for_selection(False, True), ('ab', 'cd'))
s.reset()
s.draw('ab cd')
s.start_selection(0, 0)
s.update_selection(3, 4)
self.ae(s.text_for_selection(), ('ab ', ' ', 'cd'))
self.ae(s.text_for_selection(False, True), ('ab', '\n', 'cd'))
s.reset()
s.draw('a')
s.select_graphic_rendition(32)
s.draw('b')
s.select_graphic_rendition(39)
s.draw('c xy')
s.start_selection(0, 0)
s.update_selection(1, 3)
self.ae(s.text_for_selection(), ('abc ', 'xy'))
self.ae(s.text_for_selection(True), ('a\x1b[32mb\x1b[39mc ', 'xy', '\x1b[m'))
self.ae(s.text_for_selection(True, True), ('a\x1b[32mb\x1b[39mc', 'xy', '\x1b[m'))
def test_soft_hyphen(self):
s = self.create_screen()
@@ -974,9 +997,12 @@ class TestScreen(BaseTest):
self.assertTrue(s.scroll_to_prompt())
self.ae(str(s.visual_line(0)), '$ 1')
def lco():
def lco(as_ansi=False):
a = []
s.cmd_output(0, a.append)
if s.cmd_output(0, a.append, as_ansi):
pht = pagerhist(s, as_ansi=as_ansi, upto_output_start=True)
if pht:
a.insert(0, pht)
return ''.join(a)
def fco():
@@ -1003,6 +1029,7 @@ class TestScreen(BaseTest):
mark_prompt(), s.draw('$ 1')
self.ae(fco(), 'abcd\n12')
self.ae(lco(), 'abcd\n12')
self.ae(lco(as_ansi=True), '\x1b[m\x1b]133;C\x1b\\abcd\n\x1b[m12')
def draw_prompt(x):
mark_prompt(), s.draw(f'$ {x}'), s.carriage_return(), s.index()
@@ -1053,3 +1080,9 @@ class TestScreen(BaseTest):
self.ae(lvco(), '0\n1\n2')
s.scroll_to_prompt(1)
self.ae(lvco(), '0x\n1x')
# last command output from pager history
s = self.create_screen()
draw_prompt('p1')
draw_output(30)
self.ae(tuple(map(int, lco().split())), tuple(range(0, 30)))

View File

@@ -0,0 +1,329 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
import os
import shlex
import shutil
import subprocess
import tempfile
import unittest
from contextlib import contextmanager
from functools import lru_cache, partial
from kitty.constants import kitty_base_dir, terminfo_dir
from kitty.fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE
from kitty.shell_integration import (
setup_bash_env, setup_fish_env, setup_zsh_env
)
from . import BaseTest
@lru_cache()
def bash_ok():
v = shutil.which('bash')
if not v:
return False
o = subprocess.check_output([v, '-c', 'echo "${BASH_VERSION}"']).decode('utf-8').strip()
if not o or int(o[0]) < 5:
return False
return True
def basic_shell_env(home_dir):
ans = {
'PATH': os.environ.get('PATH', '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'),
'HOME': home_dir,
'TERM': 'xterm-kitty',
'TERMINFO': terminfo_dir,
'KITTY_SHELL_INTEGRATION': 'enabled',
'KITTY_INSTALLATION_DIR': kitty_base_dir,
'BASH_SILENCE_DEPRECATION_WARNING': '1',
}
for x in ('USER', 'LANG'):
if os.environ.get(x):
ans[x] = os.environ[x]
return ans
def safe_env_for_running_shell(argv, home_dir, rc='', shell='zsh'):
ans = basic_shell_env(home_dir)
if shell == 'zsh':
argv.insert(1, '--noglobalrcs')
ans['ZLE_RPROMPT_INDENT'] = '0'
with open(os.path.join(home_dir, '.zshrc'), 'w') as f:
print(rc + '\n', file=f)
setup_zsh_env(ans, argv)
elif shell == 'fish':
conf_dir = os.path.join(home_dir, '.config', 'fish')
os.makedirs(conf_dir, exist_ok=True)
# Avoid generating unneeded completion scripts
os.makedirs(os.path.join(home_dir, '.local', 'share', 'fish', 'generated_completions'), exist_ok=True)
with open(os.path.join(conf_dir, 'config.fish'), 'w') as f:
print(rc + '\n', file=f)
setup_fish_env(ans, argv)
elif shell == 'bash':
setup_bash_env(ans, argv)
ans['KITTY_BASH_INJECT'] += ' posix'
ans['KITTY_BASH_POSIX_ENV'] = os.path.join(home_dir, '.bashrc')
with open(ans['KITTY_BASH_POSIX_ENV'], 'w') as f:
# ensure LINES and COLUMNS are kept up to date
print('shopt -s checkwinsize', file=f)
if rc:
print(rc, file=f)
return ans
class ShellIntegration(BaseTest):
@contextmanager
def run_shell(self, shell='zsh', rc='', cmd='', setup_env=None):
home_dir = os.path.realpath(tempfile.mkdtemp())
cmd = cmd or shell
cmd = shlex.split(cmd.format(**locals()))
env = (setup_env or safe_env_for_running_shell)(cmd, home_dir, rc=rc, shell=shell)
try:
pty = self.create_pty(cmd, cwd=home_dir, env=env)
i = 10
while i > 0 and not pty.screen_contents().strip():
pty.process_input_from_child()
i -= 1
yield pty
finally:
if os.path.exists(home_dir):
shutil.rmtree(home_dir)
@unittest.skipUnless(shutil.which('zsh'), 'zsh not installed')
def test_zsh_integration(self):
ps1, rps1 = 'left>', '<right'
with self.run_shell(
rc=f'''
PS1="{ps1}"
RPS1="{rps1}"
''') as pty:
q = ps1 + ' ' * (pty.screen.columns - len(ps1) - len(rps1)) + rps1
try:
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
except TimeoutError as e:
raise AssertionError(f'Cursor was not changed to beam. Screen contents: {repr(pty.screen_contents())}') from e
self.ae(pty.screen_contents(), q)
self.ae(pty.callbacks.titlebuf[-1], '~')
pty.callbacks.clear()
pty.send_cmd_to_child('mkdir test && ls -a')
pty.wait_till(lambda: pty.screen_contents().count(rps1) == 2)
self.ae(pty.callbacks.titlebuf[-2:], ['mkdir test && ls -a', '~'])
q = '\n'.join(str(pty.screen.line(i)) for i in range(1, pty.screen.cursor.y))
self.ae(pty.last_cmd_output(), q)
# shrink the screen
pty.write_to_child(r'echo $COLUMNS')
pty.set_window_size(rows=20, columns=40)
q = ps1 + 'echo $COLUMNS' + ' ' * (40 - len(ps1) - len(rps1) - len('echo $COLUMNS')) + rps1
pty.process_input_from_child()
def redrawn():
q = pty.screen_contents()
return '$COLUMNS' in q and q.count(rps1) == 2 and q.count(ps1) == 2
pty.wait_till(redrawn)
self.ae(q, str(pty.screen.line(pty.screen.cursor.y)))
pty.write_to_child('\r')
pty.wait_till(lambda: pty.screen_contents().count(rps1) == 3)
self.ae('40', str(pty.screen.line(pty.screen.cursor.y - 1)))
self.ae(q, str(pty.screen.line(pty.screen.cursor.y - 2)))
pty.send_cmd_to_child('clear')
q = ps1 + ' ' * (pty.screen.columns - len(ps1) - len(rps1)) + rps1
pty.wait_till(lambda: pty.screen_contents() == q)
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
pty.send_cmd_to_child('cat')
pty.wait_till(lambda: pty.screen.cursor.shape == 0)
pty.write_to_child('\x04')
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
with self.run_shell(rc=f'''PS1="{ps1}"''') as pty:
pty.callbacks.clear()
pty.send_cmd_to_child('printf "%s\x16\a%s" "a" "b"')
pty.wait_till(lambda: 'ab' in pty.screen_contents())
self.assertIn('%s^G%s', pty.screen_contents())
@unittest.skipUnless(shutil.which('fish'), 'fish not installed')
def test_fish_integration(self):
fish_prompt, right_prompt = 'left>', '<right'
completions_dir = os.path.join(kitty_base_dir, 'shell-integration', 'fish', 'vendor_completions.d')
with self.run_shell(
shell='fish',
rc=f'''
set -g fish_greeting
function fish_prompt; echo -n "{fish_prompt}"; end
function fish_right_prompt; echo -n "{right_prompt}"; end
function _test_comp_path; contains "{completions_dir}" $fish_complete_path; and echo ok; end
function _set_key; set -g fish_key_bindings fish_$argv[1]_key_bindings; end
function _set_status_prompt; function fish_prompt; echo -n "$pipestatus $status {fish_prompt}"; end; end
''') as pty:
q = fish_prompt + ' ' * (pty.screen.columns - len(fish_prompt) - len(right_prompt)) + right_prompt
pty.wait_till(lambda: pty.screen_contents().count(right_prompt) == 1)
self.ae(pty.screen_contents(), q)
# XDG_DATA_DIRS
pty.send_cmd_to_child('set -q XDG_DATA_DIRS; or echo ok')
pty.wait_till(lambda: pty.screen_contents().count(right_prompt) == 2)
self.ae(str(pty.screen.line(1)), 'ok')
# completion and prompt marking
pty.send_cmd_to_child('clear')
pty.send_cmd_to_child('_test_comp_path')
pty.wait_till(lambda: pty.screen_contents().count(right_prompt) == 2)
q = '\n'.join(str(pty.screen.line(i)) for i in range(1, pty.screen.cursor.y))
self.ae(q, 'ok')
self.ae(pty.last_cmd_output(), q)
# resize and redraw (fish_handle_reflow)
pty.write_to_child(r'echo $COLUMNS')
pty.set_window_size(rows=20, columns=40)
q = fish_prompt + 'echo $COLUMNS' + ' ' * (40 - len(fish_prompt) - len(right_prompt) - len('echo $COLUMNS')) + right_prompt
pty.process_input_from_child()
def redrawn():
q = pty.screen_contents()
return '$COLUMNS' in q and q.count(right_prompt) == 2 and q.count(fish_prompt) == 2
pty.wait_till(redrawn)
self.ae(q, str(pty.screen.line(pty.screen.cursor.y)))
pty.write_to_child('\r')
pty.wait_till(lambda: pty.screen_contents().count(right_prompt) == 3)
self.ae('40', str(pty.screen.line(pty.screen.cursor.y - 1)))
self.ae(q, str(pty.screen.line(pty.screen.cursor.y - 2)))
# cursor shapes
pty.send_cmd_to_child('clear')
q = fish_prompt + ' ' * (pty.screen.columns - len(fish_prompt) - len(right_prompt)) + right_prompt
pty.wait_till(lambda: pty.screen_contents() == q)
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
pty.send_cmd_to_child('echo; cat')
pty.wait_till(lambda: pty.screen.cursor.shape == 0 and pty.screen.cursor.y > 1)
pty.write_to_child('\x04')
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
pty.send_cmd_to_child('_set_key vi')
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BLOCK)
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
pty.write_to_child('\x1b')
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BLOCK)
pty.write_to_child('r')
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_UNDERLINE)
pty.write_to_child('\x1b')
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BLOCK)
pty.write_to_child('i')
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
pty.send_cmd_to_child('_set_key default')
# pipestatus
pty.send_cmd_to_child('clear;false|true|false')
pty.send_cmd_to_child('echo $pipestatus $status')
pty.wait_till(lambda: pty.screen_contents().count(right_prompt) == 2)
self.ae(pty.last_cmd_output(), '1 0 1 1')
pty.send_cmd_to_child('_set_status_prompt')
pty.send_cmd_to_child('false|true|false')
pty.wait_till(lambda: pty.screen_contents().count(right_prompt) == 4)
self.assertTrue(str(pty.screen.line(pty.screen.cursor.y)).startswith(f'1 0 1 1 {fish_prompt}'))
pty.send_cmd_to_child('exit')
@unittest.skipUnless(bash_ok(), 'bash not installed or too old')
def test_bash_integration(self):
ps1 = 'prompt> '
with self.run_shell(
shell='bash', rc=f'''
PS1="{ps1}"
''') as pty:
try:
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
except TimeoutError as e:
raise AssertionError(f'Cursor was not changed to beam. Screen contents: {repr(pty.screen_contents())}') from e
pty.wait_till(lambda: pty.screen_contents().count(ps1) == 1)
self.ae(pty.screen_contents(), ps1)
pty.wait_till(lambda: pty.callbacks.titlebuf[-1:] == ['~'])
self.ae(pty.callbacks.titlebuf[-1], '~')
pty.callbacks.clear()
pty.send_cmd_to_child('mkdir test && ls -a')
pty.wait_till(lambda: pty.callbacks.titlebuf[-2:] == ['mkdir test && ls -a', '~'])
pty.wait_till(lambda: pty.screen_contents().count(ps1) == 2)
q = '\n'.join(str(pty.screen.line(i)) for i in range(1, pty.screen.cursor.y))
self.ae(pty.last_cmd_output(), q)
# shrink the screen
pty.write_to_child(r'echo $COLUMNS')
pty.set_window_size(rows=20, columns=40)
pty.process_input_from_child()
def redrawn():
q = pty.screen_contents()
return '$COLUMNS' in q and q.count(ps1) == 2
pty.wait_till(redrawn)
self.ae(ps1 + 'echo $COLUMNS', str(pty.screen.line(pty.screen.cursor.y)))
pty.write_to_child('\r')
pty.wait_till(lambda: pty.screen_contents().count(ps1) == 3)
self.ae('40', str(pty.screen.line(pty.screen.cursor.y - 1)))
self.ae(ps1 + 'echo $COLUMNS', str(pty.screen.line(pty.screen.cursor.y - 2)))
pty.send_cmd_to_child('clear')
pty.wait_till(lambda: pty.screen_contents() == ps1)
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
pty.send_cmd_to_child('cat')
pty.wait_till(lambda: pty.screen.cursor.shape == 0)
pty.write_to_child('\x04')
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
pty.write_to_child('\x04')
pty.send_cmd_to_child('clear')
pty.wait_till(lambda: pty.callbacks.titlebuf)
with self.run_shell(shell='bash', rc=f'''PS1="{ps1}"''') as pty:
pty.callbacks.clear()
pty.send_cmd_to_child('printf "%s\x16\a%s" "a" "b"')
pty.wait_till(lambda: pty.screen_contents().count(ps1) == 2)
self.ae(pty.screen_contents(), f'{ps1}printf "%s^G%s" "a" "b"\nab{ps1}')
pty.send_cmd_to_child('echo $HISTFILE')
pty.wait_till(lambda: '.bash_history' in pty.screen_contents())
for ps1 in ('line1\\nline\\2\\prompt> ', 'line1\nprompt> ', 'line1\\nprompt> ',):
with self.subTest(ps1=ps1), self.run_shell(
shell='bash', rc=f'''
PS1="{ps1}"
''') as pty:
ps1 = ps1.replace('\\n', '\n')
pty.wait_till(lambda: pty.screen_contents().count(ps1) == 1)
pty.send_cmd_to_child('echo test')
pty.wait_till(lambda: pty.screen_contents().count(ps1) == 2)
self.ae(pty.screen_contents(), f'{ps1}echo test\ntest\n{ps1}')
pty.write_to_child(r'echo $COLUMNS')
pty.set_window_size(rows=20, columns=40)
pty.process_input_from_child()
pty.wait_till(redrawn)
self.ae(ps1.splitlines()[-1] + 'echo $COLUMNS', str(pty.screen.line(pty.screen.cursor.y)))
pty.write_to_child('\r')
pty.wait_till(lambda: pty.screen_contents().count(ps1) == 3)
self.ae('40', str(pty.screen.line(pty.screen.cursor.y - len(ps1.splitlines()))))
self.ae(ps1.splitlines()[-1] + 'echo $COLUMNS', str(pty.screen.line(pty.screen.cursor.y - 1 - len(ps1.splitlines()))))
# test startup file sourcing
def setup_env(excluded, argv, home_dir, rc='', shell='bash'):
ans = basic_shell_env(home_dir)
setup_bash_env(ans, argv)
for x in {'profile', 'bash.bashrc', '.bash_profile', '.bash_login', '.profile', '.bashrc', 'rcfile'} - excluded:
with open(os.path.join(home_dir, x), 'w') as f:
print(f'echo {x}', file=f)
ans['KITTY_BASH_ETC_LOCATION'] = home_dir
return ans
def run_test(argv, *expected, excluded=()):
with self.subTest(argv=argv), self.run_shell(shell='bash', setup_env=partial(setup_env, set(excluded)), cmd=argv) as pty:
pty.wait_till(lambda: '$' in pty.screen_contents())
q = pty.screen_contents()
for x in expected:
self.assertIn(x, q)
run_test('bash', 'bash.bashrc', '.bashrc')
run_test('bash --rcfile rcfile', 'bash.bashrc', 'rcfile')
run_test('bash --init-file rcfile', 'bash.bashrc', 'rcfile')
run_test('bash --norc')
run_test('bash -l', 'profile', '.bash_profile')
run_test('bash --noprofile -l')
run_test('bash -l', 'profile', '.bash_login', excluded=('.bash_profile',))
run_test('bash -l', 'profile', '.profile', excluded=('.bash_profile', '.bash_login'))

View File

@@ -3,14 +3,27 @@
import os
from . import BaseTest
from kittens.ssh.main import get_connection_data
from kitty.utils import SSHConnectionData
from . import BaseTest
class SSHTest(BaseTest):
def test_basic_pty_operations(self):
pty = self.create_pty('echo hello')
pty.process_input_from_child()
self.ae(pty.screen_contents(), 'hello')
pty = self.create_pty(self.cmd_to_run_python_code('''\
import array, fcntl, sys, termios
buf = array.array('H', [0, 0, 0, 0])
fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, buf)
print(' '.join(map(str, buf)))'''), lines=13, cols=77)
pty.process_input_from_child()
self.ae(pty.screen_contents(), '13 77 770 260')
def test_ssh_connection_data(self):
def t(cmdline, binary='ssh', host='main', port=None, identity_file=''):
if identity_file:

View File

@@ -259,6 +259,7 @@ read_exe_path(char *exe, size_t buf_sz) {
#endif // }}}
int main(int argc, char *argv[]) {
if (argc < 1 || !argv) { fprintf(stderr, "Invalid argc/argv\n"); return 1; }
char exe[PATH_MAX+1] = {0};
char exe_dir_buf[PATH_MAX+1] = {0};
FREE_AFTER_FUNCTION const char *lc_ctype = NULL;

View File

@@ -23,6 +23,7 @@ import requests
os.chdir(os.path.dirname(os.path.abspath(__file__)))
docs_dir = os.path.abspath('docs')
publish_dir = os.path.abspath(os.path.join('..', 'kovidgoyal.github.io', 'kitty'))
building_nightly = False
with open('kitty/constants.py') as f:
raw = f.read()
nv = re.search(r'^version: Version\s+=\s+Version\((\d+), (\d+), (\d+)\)', raw, flags=re.MULTILINE)
@@ -58,18 +59,26 @@ def call(*cmd: str, cwd: Optional[str] = None, echo: bool = False) -> None:
def run_build(args: Any) -> None:
for x in ('64', '32', 'arm64'):
def run_with_retry(cmd: str) -> None:
try:
call(f'python ../bypy linux --arch {x} program', echo=True)
call(cmd, echo=True)
except (SystemExit, Exception):
if x != 'arm64':
needs_retry = 'arm64' in cmd or building_nightly
if not needs_retry:
raise
print('Linux ARM build failed, retrying in a few seconds...', file=sys.stderr)
time.sleep(15)
call(f'python ../bypy linux --arch {x} program', echo=True)
print('Build failed, retrying in a few seconds...', file=sys.stderr)
if 'macos' in cmd:
call('python ../bypy macos shutdown')
time.sleep(25)
call(cmd, echo=True)
for x in ('64', '32', 'arm64'):
cmd = f'python ../bypy linux --arch {x} program'
run_with_retry(cmd)
call(f'python ../bypy linux --arch {x} shutdown', echo=True)
call('python ../bypy macos program --sign-installers --notarize', echo=True)
call('python ../bypy macos shutdown')
cmd = 'python ../bypy macos program --sign-installers --notarize'
run_with_retry(cmd)
def run_tag(args: Any) -> None:
@@ -464,6 +473,7 @@ def exec_actions(actions: Iterable[str], args: Any) -> None:
def main() -> None:
global building_nightly
parser = argparse.ArgumentParser(description='Publish kitty')
parser.add_argument(
'--only',
@@ -485,6 +495,7 @@ def main() -> None:
require_penv()
if args.nightly:
with change_to_git_master():
building_nightly = True
exec_actions(NIGHTLY_ACTIONS, args)
return
require_git_master()

136
setup.py
View File

@@ -967,6 +967,23 @@ TryExec=kitty
Exec=kitty
Icon=kitty
Categories=System;TerminalEmulator;
'''
)
with open(os.path.join(deskdir, 'kitty-open.desktop'), 'w') as f:
f.write(
'''\
[Desktop Entry]
Version=1.0
Type=Application
Name=kitty URL Launcher
GenericName=Terminal emulator
Comment=Open URLs with kitty
TryExec=kitty
Exec=kitty +open %U
Icon=kitty
Categories=System;TerminalEmulator;
NoDisplay=true
MimeType=image/*;application/x-sh;inode/directory;text/*;x-scheme-handler/kitty;
'''
)
@@ -1006,26 +1023,98 @@ def macos_info_plist() -> bytes:
{
'CFBundleTypeName': 'Text files',
'LSItemContentTypes': ['public.text'],
'LSTypeIsPackage': False,
'CFBundleTypeRole': 'Editor',
'LSHandlerRank': 'Alternate',
},
{
'CFBundleTypeName': 'Image files',
'LSItemContentTypes': ['public.image'],
'LSTypeIsPackage': False,
'CFBundleTypeRole': 'Viewer',
'LSHandlerRank': 'Alternate',
},
# Allows dragging arbitrary files to kitty Dock icon, and list kitty in the Open With context menu.
{
'CFBundleTypeName': 'All files',
'LSItemContentTypes': ['public.content', 'public.data'],
'LSItemContentTypes': ['public.archive', 'public.content', 'public.data'],
'CFBundleTypeRole': 'Editor',
'LSHandlerRank': 'Alternate',
},
]
url_schemes = [
{
'CFBundleURLName': 'File URL',
'CFBundleURLSchemes': ['file'],
},
{
'CFBundleURLName': 'FTP URL',
'CFBundleURLSchemes': ['ftp', 'ftps'],
},
{
'CFBundleURLName': 'Gemini URL',
'CFBundleURLSchemes': ['gemini'],
},
{
'CFBundleURLName': 'Git URL',
'CFBundleURLSchemes': ['git'],
},
{
'CFBundleURLName': 'Gopher URL',
'CFBundleURLSchemes': ['gopher'],
},
{
'CFBundleURLName': 'HTTP URL',
'CFBundleURLSchemes': ['http', 'https'],
},
{
'CFBundleURLName': 'IRC URL',
'CFBundleURLSchemes': ['irc', 'irc6', 'ircs'],
},
{
'CFBundleURLName': 'kitty URL',
'CFBundleURLSchemes': ['kitty'],
'LSHandlerRank': 'Owner',
'LSIsAppleDefaultForScheme': True,
},
{
'CFBundleURLName': 'Mail Address URL',
'CFBundleURLSchemes': ['mailto'],
},
{
'CFBundleURLName': 'News URL',
'CFBundleURLSchemes': ['news', 'nntp'],
},
{
'CFBundleURLName': 'SSH and SFTP URL',
'CFBundleURLSchemes': ['ssh', 'sftp'],
},
{
'CFBundleURLName': 'Telnet URL',
'CFBundleURLSchemes': ['telnet'],
},
]
services = [
{
'NSMenuItem': {'default': f'New {appname} Tab Here'},
'NSMessage': 'openTab',
'NSRequiredContext': {'NSTextContent': 'FilePath'},
'NSSendTypes': ['NSFilenamesPboardType', 'public.plain-text'],
},
{
'NSMenuItem': {'default': f'New {appname} Window Here'},
'NSMessage': 'openOSWindow',
'NSRequiredContext': {'NSTextContent': 'FilePath'},
'NSSendTypes': ['NSFilenamesPboardType', 'public.plain-text'],
},
{
'NSMenuItem': {'default': f'Open with {appname}'},
'NSMessage': 'openFileURLs',
'NSRequiredContext': {'NSTextContent': 'FilePath'},
'NSSendTypes': ['NSFilenamesPboardType', 'public.plain-text'],
},
]
pl = dict(
# Naming
CFBundleName=appname,
@@ -1043,7 +1132,6 @@ def macos_info_plist() -> bytes:
# Categorization
CFBundlePackageType='APPL',
CFBundleSignature='????',
CFBundleDocumentTypes=docs,
LSApplicationCategoryType='public.app-category.utilities',
# App Execution
CFBundleExecutable=appname,
@@ -1061,21 +1149,11 @@ def macos_info_plist() -> bytes:
NSSupportsAutomaticGraphicsSwitching=True,
# Needed for dark mode in Mojave when linking against older SDKs
NSRequiresAquaSystemAppearance='NO',
# Document and URL Types
CFBundleDocumentTypes=docs,
CFBundleURLTypes=url_schemes,
# Services
NSServices=[
{
'NSMenuItem': {'default': f'New {appname} Tab Here'},
'NSMessage': 'openTab',
'NSRequiredContext': {'NSTextContent': 'FilePath'},
'NSSendTypes': ['NSFilenamesPboardType', 'public.plain-text'],
},
{
'NSMenuItem': {'default': f'New {appname} Window Here'},
'NSMessage': 'openOSWindow',
'NSRequiredContext': {'NSTextContent': 'FilePath'},
'NSSendTypes': ['NSFilenamesPboardType', 'public.plain-text'],
},
],
NSServices=services,
# Calendar and Reminders
NSCalendarsUsageDescription=access('your calendar data.'),
NSRemindersUsageDescription=access('your reminders.'),
@@ -1413,7 +1491,7 @@ def build_dep() -> None:
p.add_argument(
'--platform',
default='all',
choices='all macos linux linux-x86'.split(),
choices='all macos linux linux-32 linux-arm64 linux-64'.split(),
help='Platforms to build the dep for'
)
p.add_argument(
@@ -1423,15 +1501,25 @@ def build_dep() -> None:
help='Names of the dependencies, if none provided, build all'
)
args = p.parse_args(sys.argv[2:], namespace=Options)
linux_platforms = [
['linux', '--arch=64'],
['linux', '--arch=32'],
['linux', '--arch=arm64'],
]
if args.platform == 'all':
platforms = ['linux', 'linux 32', 'macos']
elif args.platform == 'linux-x86':
platforms = ['linux 32']
platforms = linux_platforms + [['macos']]
elif args.platform == 'linux':
platforms = linux_platforms
elif args.platform == 'macos':
platforms = [['macos']]
elif '-' in args.platform:
parts = args.platform.split('-')
platforms = [[parts[0], f'--arch={parts[1]}']]
else:
platforms = [args.platform]
raise SystemExit(f'Unknown platform: {args.platform}')
base = [sys.executable, '../bypy']
for pf in platforms:
cmd = base + pf.split() + args.deps
cmd = base + pf + ['dependencies'] + args.deps
run_tool(cmd)

View File

@@ -20,13 +20,58 @@ _ksi_main() {
_ksi_debug_print() {
# print a line to STDOUT of parent kitty process
builtin local b=$(command base64 <<< "${@}" | tr -d \\n)
builtin printf "\eP@kitty-print|%s\e\\" "$b"
# "
builtin local b
b=$(builtin command base64 <<< "${@}")
builtin printf "\eP@kitty-print|%s\e\\" "${b//\\n}"
}
_ksi_set_mark() {
_ksi_prompt["${1}_mark"]="\[\e]133;k;${1}_kitty\a\]"
_ksi_safe_source() {
if [[ -f "$1" && -r "$1" ]]; then
builtin source "$1";
builtin return 0;
fi
builtin return 1;
}
if [[ -n "$KITTY_BASH_INJECT" ]]; then
builtin unset ENV;
if [[ -z "$HOME" ]]; then HOME=~; fi
if [[ -z "$KITTY_BASH_ETC_LOCATION" ]]; then KITTY_BASH_ETC_LOCATION="/etc"; fi
if [[ "$KITTY_BASH_INJECT" == *"posix"* ]]; then
_ksi_safe_source "$KITTY_BASH_POSIX_ENV" && builtin export ENV="$KITTY_BASH_POSIX_ENV";
else
builtin set +o posix;
if [[ -n "$KITTY_BASH_UNEXPORT_HISTFILE" ]]; then
export -n HISTFILE;
unset KITTY_BASH_UNEXPORT_HISTFILE;
fi
# See run_startup_files() in shell.c in the Bash source code
if builtin shopt -q login_shell; then
if [[ "$KITTY_BASH_INJECT" != *"no-profile"* ]]; then
_ksi_safe_source "$KITTY_BASH_ETC_LOCATION/profile";
_ksi_safe_source "$HOME/.bash_profile" || _ksi_safe_source "$HOME/.bash_login" || _ksi_safe_source "$HOME/.profile";
fi
else
if [[ "$KITTY_BASH_INJECT" != *"no-rc"* ]]; then
# Linux distros build bash with -DSYS_BASHRC. Unfortunately, there is
# no way to to probe bash for it and different distros use different files
_ksi_safe_source "$KITTY_BASH_ETC_LOCATION/bash.bashrc" # Arch, Debian, Ubuntu
# Fedora uses /etc/bashrc sourced from ~/.bashrc instead of SYS_BASHRC
if [[ -z "$KITTY_BASH_RCFILE" ]]; then KITTY_BASH_RCFILE="$HOME/.bashrc"; fi
_ksi_safe_source "$KITTY_BASH_RCFILE";
fi
fi
fi
builtin unset KITTY_BASH_RCFILE;
builtin unset KITTY_BASH_POSIX_ENV;
builtin unset KITTY_BASH_INJECT;
builtin unset KITTY_BASH_ETC_LOCATION;
fi
builtin unset -f _ksi_safe_source
_ksi_set_mark() {
_ksi_prompt["${1}_mark"]="\[\e]133;k;${1}_kitty\a\]"
}
_ksi_set_mark start
@@ -49,18 +94,31 @@ _ksi_main() {
PS0=${PS0//\\\[\\e\]133;k;start_suffix_kitty\\a\\\]*end_suffix_kitty\\a\\\]}
PS0="${PS0}${_ksi_prompt[ps0_suffix]}"
fi
# restore PS1 to its pristine state without our additions
if [[ -n "${_ksi_prompt[ps1]}" ]]; then
PS1=${PS1//\\\[\\e\]133;k;start_kitty\\a\\\]*end_kitty\\a\\\]}
PS1="${_ksi_prompt[ps1]}$PS1"
if [[ "${_ksi_prompt[mark]}" == "y" ]]; then
# bash does not redraw the leading lines in a multiline prompt so
# mark them as secondary prompts
PS1=${PS1//\\\[\\e\]133;k;start_secondary_kitty\\a\\\]*end_secondary_kitty\\a\\\]}
PS1=${PS1//"\n"/${_ksi_prompt[secondary_prompt]}}
fi
PS1=${PS1//\\\[\\e\]133;k;start_secondary_kitty\\a\\\]*end_secondary_kitty\\a\\\]}
fi
if [[ -n "${_ksi_prompt[ps1_suffix]}" ]]; then
PS1=${PS1//\\\[\\e\]133;k;start_suffix_kitty\\a\\\]*end_suffix_kitty\\a\\\]}
fi
if [[ -n "${_ksi_prompt[ps1]}" ]]; then
if [[ "${_ksi_prompt[mark]}" == "y" && ( "${PS1}" == *"\n"* || "${PS1}" == *$'\n'* ) ]]; then
builtin local oldval
oldval=$(builtin shopt -p extglob)
builtin shopt -s extglob
# bash does not redraw the leading lines in a multiline prompt so
# mark the last line as a secondary prompt. Otherwise on resize the
# lines before the last line will be erased by kitty.
# the first part removes everything from the last \n onwards
# the second part appends a newline with the secondary marking
# the third part appends everything after the last newline
PS1=${PS1%@('\n'|$'\n')*}${_ksi_prompt[secondary_prompt]}${PS1##*@('\n'|$'\n')};
builtin eval "$oldval"
fi
PS1="${_ksi_prompt[ps1]}$PS1"
fi
if [[ -n "${_ksi_prompt[ps1_suffix]}" ]]; then
PS1="${PS1}${_ksi_prompt[ps1_suffix]}"
fi
if [[ -n "${_ksi_prompt[ps2]}" ]]; then
@@ -69,12 +127,12 @@ _ksi_main() {
fi
}
if [[ "${_ksi_prompt[cursor]}" == "y" ]]; then
if [[ "${_ksi_prompt[cursor]}" == "y" ]]; then
_ksi_prompt[ps1_suffix]+="\[\e[5 q\]" # blinking bar cursor
_ksi_prompt[ps0_suffix]+="\[\e[0 q\]" # blinking default cursor
fi
if [[ "${_ksi_prompt[title]}" == "y" ]]; then
if [[ "${_ksi_prompt[title]}" == "y" ]]; then
# see https://www.gnu.org/software/bash/manual/html_node/Controlling-the-Prompt.html#Controlling-the-Prompt
# we use suffix here because some distros add title setting to their bashrc files by default
_ksi_prompt[ps1_suffix]+="\[\e]2;\w\a\]"
@@ -82,27 +140,28 @@ _ksi_main() {
_ksi_debug_print "ignoreboth or ignorespace present in bash HISTCONTROL setting, showing running command in window title will not be robust"
fi
_ksi_get_current_command() {
local last_cmd=$(HISTTIMEFORMAT= builtin history 1)
builtin local last_cmd
last_cmd=$(HISTTIMEFORMAT= builtin history 1)
last_cmd="${last_cmd#*[[:digit:]]*[[:space:]]}" # remove leading history number
last_cmd="${last_cmd#"${last_cmd%%[![:space:]]*}"}" # remove remaining leading whitespace
builtin printf "\e]2;%s\a" "${last_cmd}"
builtin printf "\e]2;%s\a" "${last_cmd//[[:cntrl:]]}" # remove any control characters
}
_ksi_prompt[ps0_suffix]+='$(_ksi_get_current_command)'
fi
if [[ "${_ksi_prompt[mark]}" == "y" ]]; then
if [[ "${_ksi_prompt[mark]}" == "y" ]]; then
_ksi_prompt[ps1]+="\[\e]133;A\a\]"
_ksi_prompt[ps2]+="\[\e]133;A;k=s\a\]"
_ksi_prompt[ps0]+="\[\e]133;C\a\]"
fi
if [[ "${_ksi_prompt[complete]}" == "y" ]]; then
if [[ "${_ksi_prompt[complete]}" == "y" ]]; then
_ksi_completions() {
builtin local src
builtin local limit
# Send all words up to the word the cursor is currently on
builtin let limit=1+$COMP_CWORD
src=$(builtin printf "%s\n" "${COMP_WORDS[@]:0:$limit}" | command kitty +complete bash)
src=$(builtin printf "%s\n" "${COMP_WORDS[@]:0:$limit}" | builtin command kitty +complete bash)
if [[ $? == 0 ]]; then
builtin eval ${src}
fi
@@ -112,19 +171,19 @@ _ksi_main() {
# wrap our prompt additions in markers we can use to remove them using
# bash's anemic pattern substitution
if [[ -n "${_ksi_prompt[ps0]}" ]]; then
if [[ -n "${_ksi_prompt[ps0]}" ]]; then
_ksi_prompt[ps0]="${_ksi_prompt[start_mark]}${_ksi_prompt[ps0]}${_ksi_prompt[end_mark]}"
fi
if [[ -n "${_ksi_prompt[ps0_suffix]}" ]]; then
if [[ -n "${_ksi_prompt[ps0_suffix]}" ]]; then
_ksi_prompt[ps0_suffix]="${_ksi_prompt[start_suffix_mark]}${_ksi_prompt[ps0_suffix]}${_ksi_prompt[end_suffix_mark]}"
fi
if [[ -n "${_ksi_prompt[ps1]}" ]]; then
if [[ -n "${_ksi_prompt[ps1]}" ]]; then
_ksi_prompt[ps1]="${_ksi_prompt[start_mark]}${_ksi_prompt[ps1]}${_ksi_prompt[end_mark]}"
fi
if [[ -n "${_ksi_prompt[ps1_suffix]}" ]]; then
if [[ -n "${_ksi_prompt[ps1_suffix]}" ]]; then
_ksi_prompt[ps1_suffix]="${_ksi_prompt[start_suffix_mark]}${_ksi_prompt[ps1_suffix]}${_ksi_prompt[end_suffix_mark]}"
fi
if [[ -n "${_ksi_prompt[ps2]}" ]]; then
if [[ -n "${_ksi_prompt[ps2]}" ]]; then
_ksi_prompt[ps2]="${_ksi_prompt[start_mark]}${_ksi_prompt[ps2]}${_ksi_prompt[end_mark]}"
fi
builtin unset _ksi_prompt[start_mark]
@@ -135,13 +194,19 @@ _ksi_main() {
builtin unset _ksi_prompt[end_secondary_mark]
# install our prompt command, using an array if it is unset or already an array,
# otherwise append a string
# otherwise append a string. We check if _ksi_prompt_command exists as some shell
# scripts stupidly export PROMPT_COMMAND making it inherited by all programs launched
# from the shell
builtin local pc
pc='builtin declare -F _ksi_prompt_command > /dev/null 2> /dev/null && _ksi_prompt_command'
if [[ -z "${PROMPT_COMMAND}" ]]; then
PROMPT_COMMAND=([0]="_ksi_prompt_command")
PROMPT_COMMAND=([0]="$pc")
elif [[ $(builtin declare -p PROMPT_COMMAND 2> /dev/null) =~ 'declare -a PROMPT_COMMAND' ]]; then
PROMPT_COMMAND+=("_ksi_prompt_command")
PROMPT_COMMAND+=("$pc")
else
PROMPT_COMMAND+="; _ksi_prompt_command"
PROMPT_COMMAND="${PROMPT_COMMAND%% }"
PROMPT_COMMAND="${PROMPT_COMMAND%%;}"
PROMPT_COMMAND+="; $pc"
fi
}
_ksi_main

View File

@@ -1,16 +1,10 @@
#!/bin/fish
status is-interactive || exit 0
not functions -q __ksi_schedule || exit 0
function __ksi_schedule --on-event fish_prompt -d "Setup kitty integration after other scripts have run, we hope"
functions --erase __ksi_schedule
test -n "$KITTY_SHELL_INTEGRATION" || return 0
# To use fish's autoloading feature, kitty prepends the vendored integration script directory to XDG_DATA_DIRS.
# The original paths needs to be restored here to not affect other programs.
# In particular, if the original XDG_DATA_DIRS does not exist, it needs to be removed.
if set -q XDG_DATA_DIRS KITTY_FISH_XDG_DATA_DIR
# To use fish's autoloading feature, kitty prepends the vendored integration script directory to XDG_DATA_DIRS.
# The original paths needs to be restored here to not affect other programs.
# In particular, if the original XDG_DATA_DIRS does not exist, it needs to be removed.
if set -q KITTY_FISH_XDG_DATA_DIR
if set -q XDG_DATA_DIRS
set --global --export --path XDG_DATA_DIRS "$XDG_DATA_DIRS"
if set -l index (contains -i "$KITTY_FISH_XDG_DATA_DIR" $XDG_DATA_DIRS)
set --erase --global XDG_DATA_DIRS[$index]
@@ -20,23 +14,32 @@ function __ksi_schedule --on-event fish_prompt -d "Setup kitty integration after
set --global --export --unpath XDG_DATA_DIRS "$XDG_DATA_DIRS"
end
end
set --erase KITTY_FISH_XDG_DATA_DIR
end
status is-interactive || exit 0
not functions -q __ksi_schedule || exit 0
# Check fish version 3.3.0+ efficiently and fallback to check the last working version 3.2.0, exit on outdated versions.
# "Warning: Update fish to version 3.3.0+ to enable kitty shell integration.\n"
set -q fish_killring || set -q status_generation || string match -qnv "3.1.*" "$version"
or echo -en "\eP@kitty-print|V2FybmluZzogVXBkYXRlIGZpc2ggdG8gdmVyc2lvbiAzLjMuMCsgdG8gZW5hYmxlIGtpdHR5IHNoZWxsIGludGVncmF0aW9uLgo=\e\\" && exit 0 || exit 0
function __ksi_schedule --on-event fish_prompt -d "Setup kitty integration after other scripts have run, we hope"
functions --erase __ksi_schedule
test -n "$KITTY_SHELL_INTEGRATION" || return 0
set --local _ksi (string split " " -- "$KITTY_SHELL_INTEGRATION")
set --erase KITTY_SHELL_INTEGRATION KITTY_FISH_XDG_DATA_DIR
set --erase KITTY_SHELL_INTEGRATION
# Enable cursor shape changes for default mode and vi mode
if not contains "no-cursor" $_ksi
and not functions -q __ksi_set_cursor
function __ksi_block_cursor --on-event fish_preexec -d "Set cursor shape to blinking default shape before executing command"
printf "\e[0 q"
end
function __ksi_set_cursor --on-variable fish_key_bindings -d "Set the cursor shape for different modes when switching key bindings"
if test "$fish_key_bindings" = fish_default_key_bindings
not functions -q __ksi_bar_cursor || return
function __ksi_bar_cursor --on-event fish_prompt -d "Set cursor shape to blinking bar on prompt"
printf "\e[5 q"
echo -en "\e[5 q"
end
# Change the cursor shape on first run
set -q argv[1]
and __ksi_bar_cursor
else
functions --erase __ksi_bar_cursor
contains "$fish_key_bindings" fish_vi_key_bindings fish_hybrid_key_bindings
@@ -56,85 +59,54 @@ function __ksi_schedule --on-event fish_prompt -d "Setup kitty integration after
end
# Change the cursor shape for current mode
test "$fish_bind_mode" = "insert" && printf "\e[5 q" || printf "\e[1 q"
test "$fish_bind_mode" = "insert" && echo -en "\e[5 q" || echo -en "\e[1 q"
end
__ksi_set_cursor
functions -q __ksi_bar_cursor
and __ksi_bar_cursor
function __ksi_default_cursor --on-event fish_preexec -d "Set cursor shape to blinking default shape before executing command"
echo -en "\e[0 q"
end
__ksi_set_cursor init
end
# Enable prompt marking with OSC 133
if not contains "no-prompt-mark" $_ksi
and not set -q __ksi_prompt_state
set --global __ksi_prompt_state first-run
function __ksi_function_is_not_empty -d "Check if the specified function exists and is not empty"
functions --no-details $argv[1] | string match -qnvr '^ *(#|function |end$|$)'
end
function __ksi_mark -d "Tell kitty to mark the current cursor position using OSC 133"
printf "\e]133;%s\a" "$argv[1]"
end
function __ksi_prompt_start
# Preserve the command exit code from $status
set --local cmd_status $status
if contains "$__ksi_prompt_state" post-exec first-run
__ksi_mark D
end
function __ksi_mark_prompt_start --on-event fish_prompt
contains "$__ksi_prompt_state" post-exec pre-exec ""
and echo -en "\e]133;D\a"
set --global __ksi_prompt_state prompt-start
__ksi_mark A
return $cmd_status
end
function __ksi_prompt_end
set --local cmd_status $status
# fish trims one trailing newline from the output of fish_prompt, so
# we need to do the same. See https://github.com/kovidgoyal/kitty/issues/4032
set --local op (__ksi_original_fish_prompt) # op is a list because fish splits on newlines in command substitution
if set -q op[2]
printf '%s\n' $op[1..-2] # print all but last element of the list, each followed by a new line
end
printf '%s' $op[-1] # print the last component without a newline
set --global __ksi_prompt_state prompt-end
__ksi_mark B
return $cmd_status
echo -en "\e]133;A\a"
end
__ksi_mark_prompt_start
functions -c fish_prompt __ksi_original_fish_prompt
if __ksi_function_is_not_empty fish_mode_prompt
# See https://github.com/starship/starship/issues/1283
# for why we have to test for a non-empty fish_mode_prompt
functions -c fish_mode_prompt __ksi_original_fish_mode_prompt
function fish_mode_prompt
__ksi_prompt_start
__ksi_original_fish_mode_prompt
end
function fish_prompt
__ksi_prompt_end
end
else
function fish_prompt
__ksi_prompt_start
__ksi_prompt_end
end
function fish_prompt
# fish trims one trailing newline from the output of fish_prompt, so
# we need to do the same. See https://github.com/kovidgoyal/kitty/issues/4032
# op is a list because fish splits on newlines in command substitution
set --local op (__ksi_original_fish_prompt)
# print all but last element of the list, each followed by a new line
set -q op[2]
and printf '%s\n' $op[1..-2]
# print the last component without a newline
printf '%s' $op[-1]
set --global __ksi_prompt_state prompt-end
echo -en "\e]133;B\a"
end
function __ksi_mark_output_start --on-event fish_preexec
set --global __ksi_prompt_state pre-exec
__ksi_mark C
echo -en "\e]133;C\a"
end
function __ksi_mark_output_end --on-event fish_postexec
set --global __ksi_prompt_state post-exec
__ksi_mark "D;$status"
echo -en "\e]133;D;$status\a"
end
# With prompt marking kitty clears the current prompt on resize so we need
# fish to redraw it
set --global fish_handle_reflow 1
functions --erase __ksi_function_is_not_empty
# With prompt marking, kitty clears the current prompt on resize,
# so we need fish to redraw it.
set --global fish_handle_reflow 1
end
end

View File

@@ -5,8 +5,8 @@ import importlib
import os
import sys
import warnings
from tempfile import TemporaryDirectory
from contextlib import contextmanager
from tempfile import TemporaryDirectory
from typing import Iterator
base = os.path.dirname(os.path.abspath(__file__))
@@ -32,8 +32,11 @@ def init_env() -> None:
def main() -> None:
warnings.simplefilter('error')
current_home = os.path.expanduser('~') + os.sep
paths = os.environ.get('PATH', '/usr/local/sbin:/usr/local/bin:/usr/bin').split(os.pathsep)
path = os.pathsep.join(x for x in paths if not x.startswith(current_home))
with TemporaryDirectory() as tdir, env_vars(
PYTHONWARNINGS='error', HOME=tdir, USERPROFILE=tdir,
PYTHONWARNINGS='error', HOME=tdir, USERPROFILE=tdir, PATH=path,
XDG_CONFIG_HOME=os.path.join(tdir, '.config'),
XDG_CONFIG_DIRS=os.path.join(tdir, '.config'),
XDG_DATA_DIRS=os.path.join(tdir, '.local', 'xdg'),

View File

@@ -57,4 +57,4 @@ with tempfile.NamedTemporaryFile(prefix='install-dmg-', suffix='.py') as f:
f.write(script.encode('utf-8'))
f.flush()
run(f'scp bypy/b/macos/dist/{dmg} {f.name} {HOST}:/tmp')
run(f'ssh {HOST} python /tmp/{os.path.basename(f.name)} /tmp/{dmg}')
run(f'ssh {HOST} python3 /tmp/{os.path.basename(f.name)} /tmp/{dmg}')