Compare commits

...

345 Commits

Author SHA1 Message Date
Kovid Goyal
4f05024550 version 0.12.0 2018-09-01 09:16:29 +05:30
Kovid Goyal
4d2d9d5574 Do not change wheel event reporting when not using high resolution scroll events 2018-08-31 15:28:48 +05:30
Kovid Goyal
14f44e79f7 Remove debug printf 2018-08-31 07:56:21 +05:30
Kovid Goyal
ab960ea12d Linux: Fix slow startup on some systems caused by GLFW searching for joysticks. Since kitty does not use joysticks, disable joystick support.
Fixes #830
2018-08-31 07:51:19 +05:30
Kovid Goyal
d276317e3a Remote control: Allow changing the current window layout with a new goto-layout command
Fixes #845
2018-08-30 10:23:20 +05:30
Kovid Goyal
8b3b0064a9 ... 2018-08-29 18:57:51 +05:30
Kovid Goyal
66b890bfd3 Merge branch 'alternate_screen_fast_scrolling' of https://github.com/Luflosi/kitty 2018-08-29 17:56:29 +05:30
Luflosi
2454ecd586 Allow for faster scrolling in alternate screen mode 2018-08-29 14:04:12 +02:00
Kovid Goyal
b891f4f874 Better error message for attempts to use CSI codes to resize the window
nvim does this for some odd reason
2018-08-29 10:09:06 +05:30
Kovid Goyal
92d06b3088 title stack should be LIFO not FIFO 2018-08-29 10:00:15 +05:30
Kovid Goyal
3067103b18 Implement window title stack
Used by new versions of vim
2018-08-29 09:56:52 +05:30
Kovid Goyal
0e248b3faa Write escape codes to children atomically
Ensure a partial escape code is never written
2018-08-29 09:01:52 +05:30
Kovid Goyal
d1a76bbcd9 Merge branch 'typo' of https://github.com/Luflosi/kitty 2018-08-29 07:30:01 +05:30
Luflosi
1d91edfb00 dont -> don't 2018-08-29 03:18:25 +02:00
Kovid Goyal
cf7ecd5e66 Do not import fast_data_types at the top level of utility modules 2018-08-28 10:27:06 +05:30
Kovid Goyal
63a2ecb89c Merge branch 'writePlist_deprecation' of https://github.com/Luflosi/kitty 2018-08-28 04:54:17 +05:30
Luflosi
8e6b58b1db Fix deprecation warning on macOS 2018-08-27 23:54:45 +02:00
Kovid Goyal
0f594ef950 Ensure multiplication is not unsigned 2018-08-27 15:47:32 +05:30
Kovid Goyal
0c1b76c17a Fix completion of kitten names not working 2018-08-27 13:23:17 +05:30
Kovid Goyal
1c11ba8dbd ... 2018-08-27 13:11:30 +05:30
Kovid Goyal
529d5b2226 Dont store the options spec at the top level of the module 2018-08-27 13:06:17 +05:30
Kovid Goyal
9fe16867c6 Do not import config_data at the top-level of the cli module
Importing it is slow and it is not needed in many cases, for instance
when used from kittens
2018-08-27 12:59:10 +05:30
Kovid Goyal
1d275c6b35 Only import the ast module on demand 2018-08-27 12:43:20 +05:30
Kovid Goyal
d19b29ced0 Fix incorrect calculation of pending_scroll_pixels 2018-08-27 06:51:25 +05:30
Kovid Goyal
bbefcb2cd2 Fix spaces after file/dir names and config directive names in bash completion
Since bash has no concept of completion groups with customizable
behavior, unlike zsh, we tell it to never add trailing spaces and
add the spaces ourselves.
2018-08-26 18:22:40 +05:30
Kovid Goyal
c864cba4a2 Completion of image names for the icat kitten 2018-08-26 11:49:56 +05:30
Kovid Goyal
42546f4371 Implement completion for the kitty command
Implemented for bash and zsh. Completion scripts are just thin wrappers
around kitty + complete which knows how to complete kitty command lines.
Should make it easy to add completion for other shells in the future.
2018-08-26 10:39:51 +05:30
Kovid Goyal
ff09bdcbf1 Completion for kittens 2018-08-26 09:42:26 +05:30
Kovid Goyal
f36a9fffa3 Completion for bash 2018-08-26 09:16:02 +05:30
Kovid Goyal
933cced76c Implement completion for remote control commands 2018-08-25 14:51:59 +05:30
Kovid Goyal
c51e7d3bde More work on completions 2018-08-25 11:27:22 +05:30
Kovid Goyal
d3b31224cd More work on completions 2018-08-24 12:15:27 +05:30
Kovid Goyal
ec9bb2302c ... 2018-08-24 11:03:30 +05:30
Kovid Goyal
009ef54de7 Implement high precision scrolling with the trackpad on platforms such as macOS and Wayland that implement it.
Fixes #819. Note that I have no idea whether the code works well on
retina screens, might have to multiple the pixel count by the scale
factor on those screens?
2018-08-24 11:00:58 +05:30
Kovid Goyal
0e0e25f986 Avoid double filter for completions in zsh 2018-08-24 08:13:59 +05:30
Kovid Goyal
8b02b89bd6 Add an is_focused field to the output of kitty @ ls 2018-08-23 16:07:26 +05:30
Kovid Goyal
8ba3c4ba50 Group completion matches 2018-08-23 15:34:54 +05:30
Kovid Goyal
16764c79b2 Start work on completion for the kitty command 2018-08-23 15:10:13 +05:30
Kovid Goyal
96c444d041 Workaround for bug in glfw causing crash when hotplugging monitors
Fixes #812
2018-08-21 12:52:16 +05:30
Kovid Goyal
ec1c72e9a3 Fix compiler warning 2018-08-20 10:55:49 +05:30
Kovid Goyal
f9b1f6be5c Fix #809 2018-08-20 10:24:09 +05:30
Kovid Goyal
9201d27875 Document kitty's extension to the OSC 52 protocol 2018-08-13 07:27:08 +05:30
Kovid Goyal
ecbe79e04f Update glfw from upstream 2018-08-12 21:36:19 +05:30
Kovid Goyal
6e34a7559b Fix one-pixel line appearing at window edges at some window sizes when displaying images with background opacity enabled
Fixes #741
2018-08-12 07:22:25 +05:30
Kovid Goyal
ea8b17565d Make ctrl+tab next_tab by default on macOS 2018-08-11 07:55:43 +05:30
Kovid Goyal
a143faaf05 macOS: Fix control+tab key combination not working
Fixes #801
2018-08-11 07:18:24 +05:30
Kovid Goyal
acdb56ad5d ... 2018-08-11 06:52:05 +05:30
Kovid Goyal
1f90238b18 If malloc fails remove timer from queue 2018-08-09 08:52:22 +05:30
Kovid Goyal
89b636376e Possible fix for #784 2018-08-09 08:26:25 +05:30
Kovid Goyal
59f39ae3ab Fix detection of xprop on systems with older xprop
Fixes #798
2018-08-08 15:31:15 +05:30
Kovid Goyal
a92b20f85a Only do manual focus follows mouse on macOS 2018-08-07 07:56:56 +05:30
Kovid Goyal
e71c432b7c ... 2018-08-06 20:13:16 +05:30
Kovid Goyal
b6a5d09d4f Allow enabling remote control in only some kitty windows 2018-08-06 20:04:12 +05:30
Kovid Goyal
9235d2b8e4 ... 2018-08-06 18:38:23 +05:30
Kovid Goyal
605847fd75 Add --no-response to kitty @ focus-window 2018-08-06 18:17:45 +05:30
Kovid Goyal
6222273adc ... 2018-08-06 12:27:20 +05:30
Kovid Goyal
6a759ea7d5 ... 2018-08-06 12:26:57 +05:30
Kovid Goyal
d8a7114dba macOS: Disable the custom mouse cursor. Using a custom cursor fails on dual GPU machines.
I give up, Apple users will just have to live with the limitations of their choice of OS.
See #794
2018-08-06 12:22:01 +05:30
Kovid Goyal
ab818ed89f IBUS: Fix update of pre-edit text not working correctly
Fixes #793
2018-08-06 11:30:33 +05:30
Kovid Goyal
1bb6540b64 Better debug output for pre-edit text 2018-08-06 11:08:12 +05:30
Kovid Goyal
ddd8209c45 Fix crash when pressing enter while using IBUS 2018-08-06 10:35:46 +05:30
Kovid Goyal
1dc3dfe58f Distinguish shifted Pgup and PgDn keys in normal and application mode
Fixes #791
2018-08-05 17:17:15 +05:30
Kovid Goyal
634d262b64 When using the --listen-on option have kitty @ automatically connect to the socket even when --to is not specified
The listen on value is set in the env of the child processes, so kitty @
can read it when run from inside kitty.
2018-08-04 22:06:47 +05:30
Kovid Goyal
d964146f8c Use ctermid() instead of hardcoding /dev/tty 2018-08-04 20:58:01 +05:30
Kovid Goyal
05b817f5f5 Micro-optimization 2018-08-04 20:34:12 +05:30
Kovid Goyal
eb2ec1833c Use a pipe rather than a signal to wait for terminal ready
Simpler code, more robust since there is no longer a race between the
installation of the signal handler and the dispatch of the signal
2018-08-04 20:24:06 +05:30
Kovid Goyal
094ddd9333 Round-trip the zwj unicode character
Rendering of sequences containing zwj is still not implemented, since it
can cause the collapse of an unbounded number of characters into a
single cell. However, kitty at least preserves the zwj by storing it as
a combining character.
2018-08-04 18:29:45 +05:30
Kovid Goyal
e05d48a574 Also prevent long running diff workers from causing a hang on exit 2018-08-04 17:51:33 +05:30
Kovid Goyal
927bfcd37a Use only a single diff executor 2018-08-04 17:46:05 +05:30
Kovid Goyal
42cabace47 Better fix for long running highlight workers 2018-08-04 17:41:37 +05:30
Kovid Goyal
9a1f14d05c diff kitten: Do not highlight extremely large files
python's asyncio module has no way to tell a process executor object to
kill its worker processes, so highlighting very large files cause the
kitten to hang around o exit till the highlighting is done
2018-08-04 17:30:45 +05:30
Kovid Goyal
4189aa8f14 Add a --no-response argument to kitty @ new-window
See #786
2018-08-04 15:57:43 +05:30
Kovid Goyal
5fa5bf8142 Add a test for unicode regional indicator widths 2018-08-04 10:56:35 +05:30
Kovid Goyal
000c1cf306 Implement support for emoji skin tone modifiers
Fixes #787
2018-08-04 10:06:25 +05:30
Kovid Goyal
28447d3389 Only launch child processes after window size is available in the termios structure
The delay is minimal ~ 1ms and there are apparently some applications
such as kakoune that rely on the window size being available on startup.

Since kitty always sizes windows with some size on startup, we can
afford to wait.

Fixes #786
2018-08-04 09:19:11 +05:30
Kovid Goyal
ab7f3310c4 Dont crash if XRRGetCrtcInfo() returns NULL 2018-08-03 16:17:25 +05:30
Kovid Goyal
e5a720c6fa Fix detection of URLs in HTML source code (URLs inside quotes)
Fixes #785
2018-08-03 12:28:23 +05:30
Kovid Goyal
8d20dbe81d Use the ! modifier when resizing images via ImageMagick
Should hopefully make ImageMagick respect the specified width and height
exactly.
2018-08-01 12:03:36 +05:30
Kovid Goyal
1fc666d82c Add an Info.plist key to enable support for dark mode in Mojave
Mojave uses tis key to determine how to draw the titlebar.
See https://developer.apple.com/documentation/appkit/nsappearancecustomization/choosing_a_specific_appearance_for_your_app?language=objc
2018-08-01 03:53:17 +05:30
Kovid Goyal
1b2a1a1e77 Add a check that tic works as expected
Apparently on FreeBSD it exits with no errors but fails to write the
compiled terminfo file.
2018-07-31 09:01:23 +05:30
Kovid Goyal
9af3081f45 The focus_follows_mouse option now also works across top-level kitty OS windows
Fixes #772
2018-07-30 21:26:20 +05:30
Kovid Goyal
78def6e6bf kitty @ new-window: Add a new option --window-type
To create top-level OS windows. Fixes #770
2018-07-30 08:40:26 +05:30
Kovid Goyal
b4269199b4 Fix #771 2018-07-30 07:58:34 +05:30
Kovid Goyal
cb289125ab Use _GNU_SOURCE instead of POSIX_SOURCE for fileno()
Maybe that will work with FreeBSD
2018-07-29 21:43:02 +05:30
Kovid Goyal
a4e3ce31dd Merge branch 'fix_window_decorations' of https://github.com/danbtl/kitty 2018-07-29 20:05:11 +05:30
Daniel Beitler
8a725d3d3e Fix hiding window decorations under X11/Wayland 2018-07-29 10:12:07 -04:00
Kovid Goyal
c48fa0f078 Silence the OpenGL deprecation warnings on macOS 10.14 2018-07-29 19:35:43 +05:30
Kovid Goyal
31aa782362 Update URL to kitty home page in Info.plist 2018-07-29 19:03:41 +05:30
Kovid Goyal
892637683c Fix controlling terminal not being set on BSD
Fixes #764
2018-07-29 13:48:14 +05:30
Kovid Goyal
1cf6f28fe8 When compiling python bytecode for the packages optimize it to level-2 and use relative paths in the pyc files 2018-07-29 13:20:21 +05:30
Kovid Goyal
29f9394257 ... 2018-07-29 13:06:26 +05:30
Kovid Goyal
8fd92df7ae Also fix launcher for netbsd 2018-07-29 12:54:24 +05:30
Kovid Goyal
374f7abb5d Fix package launcher for FreeBSD
Fixes #763
2018-07-29 12:52:24 +05:30
Kovid Goyal
3d1e604a6c Make the glfw strdup implementation more efficient 2018-07-29 12:21:56 +05:30
Kovid Goyal
1ca49b9380 Fix #761 2018-07-29 12:15:21 +05:30
Kovid Goyal
9bd9cf8052 typo 2018-07-29 11:18:12 +05:30
Kovid Goyal
ed934a0206 Fix #758 2018-07-29 11:07:36 +05:30
Kovid Goyal
dc891760ec Fix pthread_setname_np on openbsd as well
openbsd apparently follows freebsd for this function
2018-07-29 10:34:54 +05:30
Kovid Goyal
97401f843a Sigh. Apparently _XOPEN_SOURCE is needed on older linux systems for fileno() 2018-07-28 23:54:23 +05:30
Kovid Goyal
833865cb28 Re-enable brew based build on travis
New harfbuzz release will hopefully work again
2018-07-28 23:54:23 +05:30
Kovid Goyal
02635d506e Dont use _XOPEN_SOURCE when building
Apparently it has exactly opposite semantics for glibc and whatever libcs
are used on BSD. What a clusterfuck.
2018-07-28 22:56:18 +05:30
Kovid Goyal
f018e1a075 Nicer check for is_binary 2018-07-28 14:02:01 +05:30
Kovid Goyal
c79cde43ae ... 2018-07-28 14:00:18 +05:30
Kovid Goyal
8a812c9d02 diff kitten: Fix error when only RHS file is a binary file
Fixes #752
2018-07-28 13:59:32 +05:30
Kovid Goyal
3df78de3f8 Update pending mode escape code parsing to match latest shed color 2018-07-28 10:56:34 +05:30
Kovid Goyal
bd4831e2ee Some explicit includes needed for FreeBSD 2018-07-26 19:50:38 +05:30
Kovid Goyal
341a34ea9e Assume file extensions are up to 7 chars long
See .desktop files
2018-07-26 18:37:13 +05:30
Kovid Goyal
011866a101 Add a keyboard shortcut to reset the terminal
It takes parameters so you can define your own shortcuts to clear the screen/scrollback also. Fixes #747
2018-07-26 17:29:13 +05:30
Kovid Goyal
1486af8770 More debug output when using --debug-keyboard 2018-07-26 08:41:16 +05:30
Kovid Goyal
f06e04ffc5 Fix #745 2018-07-26 07:03:41 +05:30
Kovid Goyal
d72098bdf8 Preserve the mouse selection even when the contents of the screen are scrolled or overwritten provided the new text does not intersect the selected lines.
Requires a small CPU usage overhead when scrolling/drawing characters
even in the absence of a selection. I couldn't isolate the overhead from
statistical noise in my testing.
2018-07-25 16:16:57 +05:30
Kovid Goyal
57871e1983 Restore sys.path after importing custom kitten 2018-07-25 09:51:04 +05:30
Kovid Goyal
21dbb0e820 Improve the help message for the kitten remote control cmd 2018-07-25 09:49:01 +05:30
Kovid Goyal
007f377f16 Allow running kitten via the remote control system
Fixes #738
2018-07-25 09:45:11 +05:30
Kovid Goyal
da15477aaf Make the choice of default editor a bit more robust 2018-07-25 08:57:44 +05:30
Kovid Goyal
4f34ad0a8d ... 2018-07-24 14:14:34 +05:30
Kovid Goyal
6ed9dccde2 Remove unused parameter 2018-07-24 13:51:45 +05:30
Kovid Goyal
ce2b3265be Remote control: Allow matching windows by the environment variables of their child process as well 2018-07-24 12:40:59 +05:30
Kovid Goyal
14ea69c9c7 Pass pid of child process as env var to kittens 2018-07-24 11:28:34 +05:30
Kovid Goyal
72e2307c16 Code to get the env vars of a process 2018-07-24 11:27:10 +05:30
Kovid Goyal
0dd3334811 Implement getting cmdline of process on macOS as well 2018-07-24 11:07:53 +05:30
Kovid Goyal
5485436ea1 ... 2018-07-24 10:18:37 +05:30
Kovid Goyal
9e044750ce When running kittens use the cwd of the child process as the kittens' cwd 2018-07-24 10:14:17 +05:30
Kovid Goyal
f84cb2f07a ... 2018-07-24 09:32:21 +05:30
Kovid Goyal
e0ff6bcc5d Implement protocol for atomic screen updates
See https://gitlab.com/gnachman/iterm2/wikis/synchronized-updates-spec
2018-07-23 13:24:49 +05:30
Kovid Goyal
1695f6800c Expand links to kitty docs in sample kitty.conf 2018-07-20 21:21:52 +05:30
Kovid Goyal
810f149501 Fix #731 2018-07-20 21:04:07 +05:30
Kovid Goyal
05d51d8693 Fix #730 2018-07-20 15:55:39 +05:30
Kovid Goyal
ac4c819f57 Add a warning not to edit the apc parsers header files 2018-07-20 09:45:05 +05:30
Kovid Goyal
e7cc654bb4 Make error message when creating glfw window fails a little more useful 2018-07-20 09:36:42 +05:30
Kovid Goyal
bf37e3f77f Format generated header using clang-format 2018-07-19 18:56:10 +05:30
Kovid Goyal
af9f7bb0d2 Generate the code to parse APC escape codes
Allows it to be re-used for other APC codes
if needed.
2018-07-19 18:43:26 +05:30
Kovid Goyal
0b7abf5690 Fix test that needed to be changed for new prerendered sprites 2018-07-19 14:21:09 +05:30
Kovid Goyal
490ae248f8 Render all cursor shapes in the cell shader 2018-07-19 14:05:15 +05:30
Kovid Goyal
e552bcb0c6 Prerender cursor sprites 2018-07-19 14:03:39 +05:30
Kovid Goyal
2c27316c4a Get rid of the cursor program
Cursors will now be drawn in the cell program using a sprite
2018-07-19 12:46:25 +05:30
Kovid Goyal
c49dd12855 Pass in cursor shape to cell shader 2018-07-19 12:13:39 +05:30
Kovid Goyal
544f97a3fe ... 2018-07-19 11:18:31 +05:30
Kovid Goyal
6a909e0700 Get rid of the last branch in the cell shader 2018-07-19 10:56:28 +05:30
Kovid Goyal
40a6ad9a1f ... 2018-07-19 06:07:04 +05:30
Kovid Goyal
039f2b49e8 Remove workaround for #654 as it has been upstreamed 2018-07-19 05:11:03 +05:30
Kovid Goyal
5ae2406a03 macOS: Allow scrolling window contents using mouse wheel/trackbar even when the window is not the active window
Fixes #729
2018-07-18 10:34:36 +05:30
Kovid Goyal
2c3235d452 Remove need to call super-class on_resize method 2018-07-15 16:19:54 +05:30
Kovid Goyal
d2ea71a403 Remove unused code 2018-07-15 16:14:40 +05:30
Kovid Goyal
feb97b2313 Use asyncio for jobs in the diff kitten 2018-07-15 16:09:52 +05:30
Kovid Goyal
0b662ecb9a Refactor the kittens tui loop to use asyncio
Gets us timers and various jobs for free, and makes it easier to
integrate with libraries that use asyncio from the larger python
ecosystem.
2018-07-15 15:27:35 +05:30
Kovid Goyal
3d0da77c80 Revert change to set PYTHONIOENCODING on macOS
It is not needed in python 3.7 as the POSIX locale is
now UTF-8 by default
2018-07-15 12:19:48 +05:30
Kovid Goyal
9918397190 Add an option :opt:env to set environment variables in child processes from kitty.conf 2018-07-15 12:16:43 +05:30
Kovid Goyal
b3e2562c00 macOS bundle: Set the PYTHONIOENCODING env var to utf-8
There are apparently still some situations where python ends up with
ascii as its I/O encoding on macOS. See #724
2018-07-15 10:38:40 +05:30
Kovid Goyal
255cacf912 kitty shell prompt: fallback to plain ascii prompt in case of unicode errors 2018-07-14 06:35:43 +05:30
Kovid Goyal
8268f9f612 Add a section about remote control to the main page 2018-07-14 06:06:59 +05:30
Kovid Goyal
92b8798cad Add note about ctrl+alt to select in columns 2018-07-14 06:02:17 +05:30
Kovid Goyal
fbb4db8a83 Allocate talk data on the heap
Most users will talk to 0, 1, 2 peers simultaneously at the most,
so allocate memory for them dynamically.
2018-07-13 17:13:59 +05:30
Kovid Goyal
19a672caf2 Allow kitty to handle talking to more simultaneous peers 2018-07-13 16:46:08 +05:30
Kovid Goyal
5a85c890ba Add a printf annotation of the log_error function 2018-07-13 16:39:47 +05:30
Kovid Goyal
55556aaac4 Ensure queued display events are dispatched when polling/waiting for events 2018-07-13 10:56:12 +05:30
Kovid Goyal
31853ee20c Handle errno better when polling for events 2018-07-13 10:33:18 +05:30
Kovid Goyal
037bfeff82 Give IBUS a maximum of three seconds to respond to a key press
Previously was using the default timeout, which is 25 seconds
2018-07-12 18:18:21 +05:30
Kovid Goyal
bf1c919547 When de-activating overlay line, mark it dirty 2018-07-12 18:18:21 +05:30
Kovid Goyal
202ec17a08 Add a changelog entry for IBus support
Fixes #469
2018-07-12 18:18:21 +05:30
Kovid Goyal
98ede457a0 Do not hand on startup if the ibus daemon is hung 2018-07-12 18:18:21 +05:30
Kovid Goyal
3c772c3576 ... 2018-07-12 18:18:21 +05:30
Kovid Goyal
5b1eaa741c IME input: Show the pre-edit text while the IME is in progress
Text is show in reverse video
2018-07-12 18:18:21 +05:30
Kovid Goyal
1a1aa2d6aa Nicer debug print for key debugging 2018-07-12 18:18:20 +05:30
Kovid Goyal
6e9aecdff3 Fix Shift key not working with IBUS
Fixes #721
2018-07-12 18:18:20 +05:30
Kovid Goyal
95187632bf Allow using GLFW_IM_MODULE to activate ibus 2018-07-12 18:18:20 +05:30
Kovid Goyal
e3e28a023c Fix exporting to glfw 2018-07-12 18:18:20 +05:30
Kovid Goyal
65bb71779a Install dbus on travis 2018-07-12 18:18:20 +05:30
Kovid Goyal
29dd766a67 Fix unused vars warning 2018-07-12 18:18:20 +05:30
Kovid Goyal
851145e69e Add some debug prints for IBUS key handling 2018-07-12 18:18:20 +05:30
Kovid Goyal
b2e300c440 Simplify key round-tripping to IBUS
Only send to IBUS after full xkb processing. This is needed as otherwise
modifier state tracking does not work reliably. If you use alt+tab to
switch away from the window, the window never receives the release event
for the alt modifier.

The downside is that now if the uer has both XCompose and ibus active,
the who knows what will happen.
2018-07-12 18:18:20 +05:30
Kovid Goyal
38998a4869 Notify IBUS when focus is gained/lost 2018-07-12 18:18:19 +05:30
Kovid Goyal
21f1fd55f8 Have the IBUS popup display at the cursor position 2018-07-12 18:18:19 +05:30
Kovid Goyal
c6c6a18375 Handle keys returned by IBUS 2018-07-12 18:18:19 +05:30
Kovid Goyal
56063b283e Implement text commit from the IME system 2018-07-12 18:18:19 +05:30
Kovid Goyal
d27e7ead49 Print an error if too many timers/watches are added 2018-07-12 18:18:19 +05:30
Kovid Goyal
342164ede1 Add API for the application to notify the IME system of state changes 2018-07-12 18:18:19 +05:30
Kovid Goyal
1061e40b98 Bi-directional glfw updates 2018-07-12 18:18:19 +05:30
Kovid Goyal
377115a594 Add a GLFW specific env var to toggle IBUS 2018-07-12 18:18:19 +05:30
Kovid Goyal
e405b68da1 ... 2018-07-12 18:18:18 +05:30
Kovid Goyal
d875d7baed Implement the handler for ibus signals 2018-07-12 18:18:18 +05:30
Kovid Goyal
9c48e4965c Send key events to ibus 2018-07-12 18:18:18 +05:30
Kovid Goyal
8675659fcb Fix remaining sync BUS calls
Also work on IBUS integration
2018-07-12 18:18:18 +05:30
Kovid Goyal
3b226618ad Ensure no ibus comms take place till the connection is fully setup 2018-07-12 18:18:18 +05:30
Kovid Goyal
6879a492dc Give watches and timers names to ease debugging 2018-07-12 18:18:18 +05:30
Kovid Goyal
4070255dde Simplify watch->fd matching 2018-07-12 18:18:18 +05:30
Kovid Goyal
dd9e484336 Make the DBUS APIs async 2018-07-12 18:18:18 +05:30
Kovid Goyal
78cda7aa74 Implement dispatch for the IBUS connection 2018-07-12 18:18:17 +05:30
Kovid Goyal
9ced447fa3 Make timer dispatch more efficient 2018-07-12 18:18:17 +05:30
Kovid Goyal
8e665ffedc Implement DBUS timeout functions 2018-07-12 18:18:17 +05:30
Kovid Goyal
99ea6c08a7 Implement timers for the linux event loops
Needed for dbus integration. Also cleanup the event handling code.
X11 and Wayland now share most of their event polling and dispatch logic.
2018-07-12 18:18:17 +05:30
Kovid Goyal
b0bfa2b2fc ... 2018-07-12 18:18:17 +05:30
Kovid Goyal
3797294838 Use an id for watches 2018-07-12 18:18:17 +05:30
Kovid Goyal
3d6995d1e6 Work on integration of DBUS into event loop 2018-07-12 18:18:17 +05:30
Kovid Goyal
e91eb27e56 Refactor linux backend event loops
Allow waiting for events on an arbitrary number of fds. Needed
for async DBUS integration.
2018-07-12 18:18:17 +05:30
Kovid Goyal
3a4b614ae0 IBUS does not need a connection to the DBUS session bus 2018-07-12 18:18:16 +05:30
Kovid Goyal
26a6d6bef9 Set startup IBUS cursor and focus state 2018-07-12 18:18:16 +05:30
Kovid Goyal
dca81589a7 Finish code to connect to IBUS daemon 2018-07-12 18:18:16 +05:30
Kovid Goyal
5b7c697dfe Connect to IBUS 2018-07-12 18:18:16 +05:30
Kovid Goyal
18d6b8a0ad Code to read the IBUS address 2018-07-12 18:18:16 +05:30
Kovid Goyal
cdee601137 Start work on IBUS support 2018-07-12 18:18:16 +05:30
Kovid Goyal
e74c08ff39 Disable brew based travis build
Building the latest release of harfbuzz is failing
2018-07-12 17:42:33 +05:30
Kovid Goyal
5bfe2fb8d3 Make using a fixed color for the text under the cursor the default 2018-07-12 16:52:38 +05:30
Kovid Goyal
55fc5c4358 Add an option :opt:cursor_text_color to render text under the cursor in a fixed color.
See #126
2018-07-12 12:50:42 +05:30
Kovid Goyal
d6a603d536 Change the default publish action from build+ to all 2018-07-10 22:05:33 +05:30
Kovid Goyal
5c85d915cc ... 2018-07-10 22:00:34 +05:30
Kovid Goyal
57f8519a52 version 0.11.3 2018-07-10 21:42:01 +05:30
Kovid Goyal
056be45dd5 ... 2018-07-10 21:30:30 +05:30
Kovid Goyal
3ee22e545e macOS: Workaround for switching from fullscreen to windowed mode with the titlebar hidden causing window resizing to not work.
Fixes #711
2018-07-10 21:29:30 +05:30
Kovid Goyal
db5aa36f44 Fix deprecation warning from newer versions of wayland-scanner 2018-07-10 19:48:37 +05:30
Kovid Goyal
cc767061cc Linux: Fix compilation with python 3.7
Fixes #714
2018-07-10 18:47:05 +05:30
Kovid Goyal
1d22b75090 Fix a regression in the previous release that caused pasting large amounts of text to be duplicated
Fixes #709
2018-07-08 16:25:08 +05:30
Kovid Goyal
4313531432 macOS: Use a custom mouse cursor that shows up well on both light and dark backgrounds
Fixes #359. Also, update GLFW from upstream for retina cursor setting
support.
2018-07-07 10:43:30 +05:30
Kovid Goyal
1faddeb402 Also return image dimensions when loading PNG 2018-07-07 08:04:11 +05:30
Kovid Goyal
2b035739f8 Make the PNG reader code re-useable 2018-07-07 07:46:15 +05:30
Kovid Goyal
aeed20087e Fix segfault when opening more tabs than would fit with even one character titles on the tab bar. Fixes #706 2018-07-07 05:35:56 +05:30
Kovid Goyal
a239f41495 minimal borders for the grid layout 2018-07-06 20:23:56 +05:30
Kovid Goyal
b36d6967a5 DRYer 2018-07-06 17:54:01 +05:30
Kovid Goyal
7c1d13b7db Minimal borders for vertical and horizontal layouts 2018-07-06 17:46:08 +05:30
Kovid Goyal
3e99949db8 Implement minimal borders in the Fat layout 2018-07-06 17:35:23 +05:30
Kovid Goyal
aa12a65a8f Draw only the minimum borders needed for inactive windows
Fixes #699

Note that currently this is only implemented by the Tall layout.
2018-07-06 17:30:53 +05:30
Kovid Goyal
18804efb7e Let the layout control which window borders are drawn 2018-07-06 17:14:25 +05:30
Kovid Goyal
a5d27196ea Remove unnecessary code 2018-07-06 13:17:51 +05:30
Kovid Goyal
5af6d88ffb Support drawing individual border widths 2018-07-06 13:17:51 +05:30
Kovid Goyal
472a3bc69f Refactor layout dimension to use per window decoration sizes 2018-07-06 13:17:51 +05:30
Kovid Goyal
f0f1c911ea Track window neighbors 2018-07-06 13:17:50 +05:30
Kovid Goyal
fcb0033d25 When drag-scrolling stop the scroll when the mouse button is released. 2018-07-06 13:01:49 +05:30
Kovid Goyal
c8b1e76783 When clicking in the margin/border area of a window map the click to the nearest cell in the window. Avoids selection with the mouse failing when starting the selection just outside the window. 2018-07-06 12:32:08 +05:30
Kovid Goyal
b50f621e61 DRYer 2018-07-06 10:55:03 +05:30
Kovid Goyal
77c4f5fecc When dragging to select with the mouse "grab" the mouse so that if it strays into neighboring windows, the selection is still updated
Fixes #624
2018-07-06 10:33:01 +05:30
Kovid Goyal
e27ea3337a Prevent integer wrap on xlimit 2018-07-06 09:50:34 +05:30
Kovid Goyal
23fbf6f157 Fix triple-click to select line not working when the entire line is filled
Fixes #703
2018-07-06 08:17:48 +05:30
Kovid Goyal
321e5081ed Merge branch 'reproducible-docs' of https://github.com/jamessan/kitty 2018-07-05 20:37:57 +05:30
James McCoy
33cac29f87 seq_as_rst: Sort "Choices" so they are consistently formatted 2018-07-05 11:00:54 -04:00
Kovid Goyal
12be438dea ... 2018-07-05 12:30:11 +05:30
Kovid Goyal
00092db6ad Use an enum for border colors 2018-07-05 11:52:32 +05:30
Kovid Goyal
619c3a0025 macOS: Add an option macos_window_resizable to control if kitty top-level windows are resizable using the mouse or not
Fixes #698
2018-07-03 06:49:28 +05:30
Kovid Goyal
f12697c897 Better error when kitty config dir is a file
See #697
2018-07-03 06:30:29 +05:30
Kovid Goyal
4bf0e0ea23 version 0.11.2 2018-07-01 07:58:46 +05:30
Kovid Goyal
c7879d06c9 Output native key names in --debug-config 2018-06-30 18:48:39 +05:30
Kovid Goyal
05227e7dfb Fix --debug-config failing when native keys present 2018-06-30 18:42:19 +05:30
Kovid Goyal
9805d164d0 Remove unknown flags from brew install for harfbuzz on travis 2018-06-30 18:34:27 +05:30
Kovid Goyal
05384c46e7 ... 2018-06-30 18:25:45 +05:30
Kovid Goyal
c2679eff9a Fix compiler warning
The compiler warning is harmless, since we exec immediately after
using the const char* but since I have a no warning policy...
2018-06-30 18:15:08 +05:30
Kovid Goyal
3ec66c0b9f Fix regression in previous release that caused --debug-config to fail when printing shortcuts
Forgot to adjust the print shortcuts code for native shortcuts.
Fixes #695
2018-06-30 17:48:25 +05:30
Kovid Goyal
9277b80fad Remove unused code 2018-06-30 12:01:56 +05:30
Kovid Goyal
568a4c9b5d Re-raise KeyError idiomatically 2018-06-29 13:49:08 +05:30
Kovid Goyal
4de18688ca Merge branch 'fix/conf-utils-keyparser' of https://github.com/arubertoson/kitty 2018-06-29 13:42:21 +05:30
Marcus Albertsson
12975347ee Fix: Exception in conf utils
The bug was related to trying to split a string with unstript spaces in
the action parameter. It resulted in a unbound error as there was no
previous raise when getting the key from args_funcs. The fix is twofold,
it makes sure that we raise the keyerror if we're trying to grab a non
existing key and we're stripping the spaces from the action before
splitting.

This should fix both the unbound error and the not being able to resolve
correct actions.
2018-06-29 08:56:25 +01:00
Kovid Goyal
e35c3cc913 Add an option single_window_margin_width to allow different margins when only a single window is visible in the layout
Fixes #688
2018-06-29 09:56:05 +05:30
Kovid Goyal
65fef81f29 hints kitten: Add an option to select multiple items
Fixes #687
2018-06-29 06:49:16 +05:30
Kovid Goyal
e3f60e6fa7 Update CHANGELOG 2018-06-28 20:34:56 +05:30
Kovid Goyal
0c88e11cd7 More useful exception when unexpected output encountered from a kitten 2018-06-27 14:13:40 +05:30
Kovid Goyal
d689c21283 Fix pasting large amounts of text very slow.
Fixes #682

Apparently when refactoring child.py I somehow lost the code to make the
child fd non-blocking.
2018-06-26 18:20:46 +05:30
Kovid Goyal
5362d1a7a6 Micro-optimization 2018-06-26 16:02:05 +05:30
Kovid Goyal
3b1490020d ... 2018-06-26 12:35:38 +05:30
Kovid Goyal
b8ba25b81f Add a link to the kitty source code for packagers 2018-06-26 12:03:07 +05:30
Kovid Goyal
8bfdc47998 macOS: Fix an error in remote control when using --listen-on. Fixes #679 2018-06-25 12:33:54 +05:30
Kovid Goyal
2491fafe98 Update glfw from upstream 2018-06-23 14:59:36 +05:30
Kovid Goyal
b5bc466aef Merge branch 'master' of https://github.com/Ma-Shell/kitty 2018-06-23 13:54:47 +05:30
Marcel Schumacher
cfd6ea4301 bugfix for get-text with extent=screen 2018-06-23 10:13:37 +02:00
Kovid Goyal
2cdf41f190 ... 2018-06-23 13:20:53 +05:30
Kovid Goyal
9169d0aa9b ... 2018-06-23 10:34:35 +05:30
Kovid Goyal
05a25e3531 redirect to kitty website 2018-06-23 10:30:34 +05:30
Kovid Goyal
2852ce9a4d Update glfw from upstream 2018-06-23 10:23:21 +05:30
Kovid Goyal
20f8f2c401 Removed unused code
The template is no longer needed since I switched my global
template to py3
2018-06-23 10:03:37 +05:30
Kovid Goyal
0995187080 Remove unused code
linux-installer.sh was accidentally committed to the kitty repo
2018-06-23 09:58:20 +05:30
Kovid Goyal
d10eac1b02 Merge branch 'fix-new_os_window' of https://github.com/blueyed/kitty 2018-06-23 08:28:11 +05:30
Daniel Hahler
2cc2f803f1 create_session: handle args=None
Fixes `new_os_window`:

    scancode: 0x39 release: 0 clean_sym: n composed_sym: N mods: ctrl+shift glfw_key: N
    Traceback (most recent call last):
      File "…/Vcs/kitty/kitty/boss.py", line 431, in dispatch_special_key
        return self.dispatch_action(key_action)
      File "…/Vcs/kitty/kitty/boss.py", line 487, in dispatch_action
        passthrough = f(*key_action.args)
      File "…/Vcs/kitty/kitty/boss.py", line 199, in new_os_window
        self._new_os_window(args)
      File "…/Vcs/kitty/kitty/boss.py", line 195, in _new_os_window
        startup_session = create_session(self.opts, special_window=sw, cwd_from=cwd_from)
      File "…/Vcs/kitty/kitty/session.py", line 127, in create_session
        if args.hold:
2018-06-23 02:57:47 +02:00
Kovid Goyal
66a52ac588 Also ignore failure to read readline history in the ask kitten 2018-06-22 17:23:55 +05:30
Kovid Goyal
32f22e0ada kitty shell: Ignore failure to read readline history file 2018-06-22 17:23:01 +05:30
Kovid Goyal
c037f53534 ... 2018-06-22 15:35:24 +05:30
Kovid Goyal
aba84581f5 When running kitty from within kitty, use a full path
Fixes inability to run kitty from the shell when the users shell scripts
reset PATH. See #668
2018-06-22 15:33:07 +05:30
Kovid Goyal
82f6fbff77 Add a --hold command line option to stay open after the child process exits
Fixes #667
2018-06-22 13:52:03 +05:30
Kovid Goyal
d16215ab6e Initialize glfw for --debug-config as well 2018-06-22 13:30:03 +05:30
Kovid Goyal
45b0233f60 Update GLFW from upstream
Use the new init hint for keyboard debugging
2018-06-22 13:26:41 +05:30
Kovid Goyal
1ce4366972 ... 2018-06-22 12:44:08 +05:30
Kovid Goyal
c8fc21d336 Linux: Allow using XKB key names to bind shortcuts to keys not supported by GLFW
Useful to bind keys such as the play/pause or volume buttons. Also can
be used to bind non-ascii keys on international keyboards. Fixes #665
2018-06-22 12:41:50 +05:30
Kovid Goyal
5dd3243674 Update glfw from upstream 2018-06-22 10:21:51 +05:30
Kovid Goyal
815dd99f7c Keep the Debian PC police happy
Fixes #661
2018-06-21 13:12:45 +05:30
Kovid Goyal
bf0dcb1d2e Update version of glad used to generate OpenGL wrapper
Also fix #654
2018-06-20 12:43:26 +05:30
Kovid Goyal
031fc10859 Merge branch 'title-command-line-parameter' of https://github.com/lamby/kitty 2018-06-19 13:48:55 +05:30
Chris Lamb
a644e4c31e Support "-T" as an alias for "--title".
Thank you for developing Kitty.

Currently kitty is not available on the list of "alternatives" for a
terminal emulator on Debian GNU/Linux and derivatives. This is because
Debian Policy states that packages that provide a terminal emulator must
provide, inter alia, support the command-line option -T title, which
creates a new terminal window with the specified window title [0].

This PR therefore adds -T as an alias for --title, thus bringing kitty into
compliance and allowing cleaner integration into the Debian desktop.

This issue was originally filed as #900704.

  [0] https://www.debian.org/doc/debian-policy/#packages-providing-a-terminal-emulator
  [1] https://bugs.debian.org/900704

Signed-off-by: Chris Lamb <lamby@debian.org>
2018-06-19 10:10:43 +02:00
Kovid Goyal
0398dbdcbe Fix extra blank lines when using log_error() 2018-06-19 12:51:54 +05:30
Kovid Goyal
84b2fbba28 --debug-font-fallback also prints out what basic fonts were matched
Fixes #656
2018-06-19 12:48:33 +05:30
Kovid Goyal
491f233d33 diff kitten: When triggering a search scroll to the first match automatically 2018-06-19 10:30:52 +05:30
Kovid Goyal
5e38af6d24 When closing a kitty window reset the mouse cursor to its default shape and ensure it is visible. Fixes #655 2018-06-19 10:19:17 +05:30
Kovid Goyal
4c0f68150a Merge branch 'master' of https://github.com/Luflosi/kitty 2018-06-18 18:41:12 +05:30
Luflosi
3f25f70783 Ignore .DS_Store 2018-06-18 14:39:12 +02:00
Kovid Goyal
d7007f77aa DRYer 2018-06-18 12:18:04 +05:30
Kovid Goyal
94273941ad ... 2018-06-18 12:10:42 +05:30
Kovid Goyal
c09542ea6a Speed up reading of command responses 2018-06-18 11:42:32 +05:30
Kovid Goyal
c5583d380d No need to use select() for TTYIO
Instead make read() block with timeout via termios
2018-06-18 11:10:16 +05:30
Kovid Goyal
f820f72768 Add a kitty @ get-colors command 2018-06-18 10:02:05 +05:30
Kovid Goyal
6db1f8e3e3 Dont read more data than needed in TTYIO 2018-06-18 08:36:47 +05:30
Kovid Goyal
2aba3e249c Linux installer: Workaround for bug in 3.0 <= python <= 3.5 2018-06-18 07:21:44 +05:30
Kovid Goyal
f66db65974 version 0.11.1 2018-06-17 10:57:55 +05:30
Kovid Goyal
5b6038db35 Dont include a literal dump of the conf files
Since all the config options are documented separately on the page in
any case, there is not much need for it. And now that the conf files are
downloadable, it is probably nicer to just view/edit them directly in
the users favorite pager/editor rather than the browser.
2018-06-16 22:31:21 +05:30
Kovid Goyal
9a365b0968 Merge branch 'docs' of https://github.com/blueyed/kitty 2018-06-16 22:26:45 +05:30
Daniel Hahler
8a0cd73821 docs: improve generation of default config files
- config files are placed into `docs/generated/conf/`, using the
  expected name, and without any indentation
- `literalinclude::` is used instead of `include::`
- uses `:download:` instead of `:file:` to allow for downloading the
  generated files easily
2018-06-16 18:35:58 +02:00
Kovid Goyal
49b065cbe7 Use conf_dir instead of cwd as base for session file 2018-06-16 10:22:46 +05:30
Kovid Goyal
77f651cb5d Expand env vars for the start_session config variable 2018-06-16 10:18:48 +05:30
Kovid Goyal
fedde51c12 ... 2018-06-16 10:16:06 +05:30
Kovid Goyal
f710d6906e Add a linkcheck target to check links in the docs 2018-06-16 09:27:39 +05:30
Kovid Goyal
c9a751360e Update CHANGELOG 2018-06-16 09:25:00 +05:30
Kovid Goyal
ad97315925 diff kitten: Misc fixes to msg display in the status line 2018-06-16 08:57:02 +05:30
Kovid Goyal
e8f7a2b345 Dont send on_text events with empty text 2018-06-16 08:55:10 +05:30
Kovid Goyal
5bc8d05f0a ... 2018-06-16 08:49:08 +05:30
Kovid Goyal
3669d4135c diff kitten: Add keybindings to search without regex 2018-06-16 08:45:42 +05:30
Kovid Goyal
73b870bb87 ... 2018-06-16 08:41:19 +05:30
Kovid Goyal
6b4217c12a Use a single build script on Travis
Allows the build to fail faster if any of the intermediate steps fails
2018-06-16 08:33:07 +05:30
Kovid Goyal
8b4fe494cf Format matches count the same as scroll percent 2018-06-16 08:24:32 +05:30
Kovid Goyal
35041e7709 Fix docs links 2018-06-16 08:19:33 +05:30
Kovid Goyal
12cbcf1c17 Add an option to kitty.conf to specify a default startup session
Fixes #641
2018-06-16 08:14:11 +05:30
Kovid Goyal
26f50a451e Add a section on configuration to the main page in the docs 2018-06-16 07:45:24 +05:30
Kovid Goyal
11f3d7e215 ... 2018-06-15 17:53:04 +05:30
Kovid Goyal
a7d533cfe5 Increase contrast of link color with text color in docs 2018-06-15 15:38:30 +05:30
Kovid Goyal
52ec4dc21b Allow cross-linking to @ command documentation 2018-06-15 15:31:05 +05:30
Kovid Goyal
6f7ea49008 Switch to high number OSC codes instead of DCS string codes for dynamic color push/pop 2018-06-15 15:21:46 +05:30
Kovid Goyal
84b99b1b3d diff kitten: Clear the search when pressing Esc 2018-06-15 15:00:09 +05:30
Kovid Goyal
1022d8a0ee diff kitten: Show the number of matches in the status bar when doing a search 2018-06-15 14:57:03 +05:30
Kovid Goyal
cd6a46730a ... 2018-06-15 14:48:59 +05:30
Kovid Goyal
59743fd7a1 diff kitten: Allow theming the selection background/foreground as well 2018-06-15 14:43:39 +05:30
Kovid Goyal
07bd371e21 Re-org diff kitten docs
Move the auto-generated bits to the end
2018-06-15 14:31:13 +05:30
Kovid Goyal
fe3b10a8fb diff kitten: Implement searching for text in the diff
Fixes #574
2018-06-15 14:28:42 +05:30
Kovid Goyal
c994bc1d89 Add more clarification about kitty's performance goals 2018-06-15 13:51:49 +05:30
Kovid Goyal
22cffef7c8 diff kitten: Use a single-width replacement for control chars
Preserves text width better, and conveys the essential message, namely
non-printable characters. Most people wont know what <hex code> is in
any case.
2018-06-15 08:08:21 +05:30
Kovid Goyal
5dfb8b1d80 diff kitten: Display CRLF line endings using the unicode return symbol instead of <d> as it is less intrusive.
Fixes #638
2018-06-15 07:08:57 +05:30
Kovid Goyal
82f9f002bd diff kitten: Fix default foreground/background colors not being restored when kitten quits. Fixes #637 2018-06-14 23:25:13 +05:30
Kovid Goyal
9f51506422 ... 2018-06-14 22:07:34 +05:30
Kovid Goyal
2a8295d71c diff kitten: Better error message when one of the arguments does not exist 2018-06-14 21:53:13 +05:30
Kovid Goyal
7214b66aa5 A new tui API for simple line editing
Handles basic line-editing with the extended keyboard protocol and
support for wide chars.
Currently used by the unicode input kitten.
2018-06-14 13:11:48 +05:30
Kovid Goyal
a7d9c63a4e wcwidth() for not assigned code points should be -4
That way, wcswidth() computes their width as 1, which matches how they
are rendered
2018-06-14 10:27:42 +05:30
Kovid Goyal
61dd52b50f Ignore the non-characters from the unicode standard in addition to ignoring the control characters 2018-06-14 10:20:13 +05:30
Kovid Goyal
a157f3c5e3 Fix kitty @ set-colors --all not working when more than one window present
Fixes #632
2018-06-13 14:21:55 +05:30
Kovid Goyal
fc02246373 ... 2018-06-13 07:46:11 +05:30
Kovid Goyal
adb8a4676c Fix a regression that broke the legacy increase/decrease_font_size actions 2018-06-13 07:42:12 +05:30
Kovid Goyal
f78d05a57f Clear scrollback on reset
Fixes #631
2018-06-13 07:20:19 +05:30
Kovid Goyal
301d34fecf Ignore errors in talk socket shutdown
macOS apparently shuts down the socket before shutdown can be called.
2018-06-12 21:15:43 +05:30
Kovid Goyal
0848816644 ... 2018-06-12 21:03:02 +05:30
Kovid Goyal
ba7f0132f2 Add a command line option to make --single-instance wait for the closing of the newly opened window before quitting. Fixes #630 2018-06-12 20:53:25 +05:30
Kovid Goyal
561fe81d00 Use base instead of cwd when checking for docs 2018-06-12 09:24:36 +05:30
Kovid Goyal
2db83ecf1e Run make docs for linux-package only if docs are not found 2018-06-12 09:20:18 +05:30
Kovid Goyal
1b8c6b23e1 Dont run make docs to build linux-package
The pre-built docs are now included in the official kitty source code
release. This means that building linux-package does not require
sphinx anymore.
2018-06-12 09:15:43 +05:30
Kovid Goyal
b6622b9458 Fix paths to built bundles 2018-06-12 08:53:22 +05:30
154 changed files with 38975 additions and 33712 deletions

1
.gitignore vendored
View File

@@ -15,3 +15,4 @@ __pycache__
glfw/wayland-*-client-protocol.[ch]
docs/_build
docs/generated
.DS_Store

View File

@@ -17,6 +17,7 @@ matrix:
- libxinerama-dev
- libxcursor-dev
- libxcb-xkb-dev
- libdbus-1-dev
- os: linux
dist: trusty
@@ -37,6 +38,7 @@ matrix:
- libxcursor-dev
- libxcb-xkb-dev
- libpng-dev
- libdbus-1-dev
- python3-pil
- os: linux
@@ -58,6 +60,7 @@ matrix:
- libxcursor-dev
- libxcb-xkb-dev
- libpng-dev
- libdbus-1-dev
- python3-pil
- os: linux
@@ -79,6 +82,7 @@ matrix:
- libxcursor-dev
- libxcb-xkb-dev
- libpng-dev
- libdbus-1-dev
- python3-pil
- clang
- os: osx
@@ -103,7 +107,7 @@ install: |
if [[ "$USE_BREW" == "1" ]]; then
brew update;
brew upgrade python;
brew install harfbuzz --without-glib --without-gobject-introspection --without-graphite2 --without-icu4c --without-freetype;
brew install harfbuzz --without-graphite2 --without-icu4c --without-freetype;
brew install imagemagick
brew install optipng
brew install librsvg
@@ -150,9 +154,17 @@ before_script:
- echo LD_LIBRARY_PATH=$LD_LIBRARY_PATH PKG_CONFIG_PATH=$PKG_CONFIG_PATH
- $PYTHON setup.py build --debug --verbose $SANITIZE_ARG;
script:
- if grep -Inr '\s$' kitty kitty_tests kittens docs *.py *.asciidoc *.rst .gitattributes .gitignore; then echo Trailing whitespace found, aborting.; exit 1; fi
- if [[ -z $SANITIZE_ARG ]]; then $PYTHON test.py; else ./asan-launcher test.py; fi
- if [[ "$RUN_FLAKE" == "1" ]]; then flake8 --count .; fi
- if [[ "$BUILD_PKG" == "linux-package" ]]; then make FAIL_WARN=-W man && make FAIL_WARN=-W html; fi
- if [[ ! -z "$BUILD_PKG" ]]; then $PYTHON setup.py $BUILD_PKG; fi
script: |
set -e
if grep -Inr '\s$' kitty kitty_tests kittens docs *.py *.asciidoc *.rst .gitattributes .gitignore; then echo Trailing whitespace found, aborting.; exit 1; fi
if [[ -z "$SANITIZE_ARG" ]]; then $PYTHON test.py; else ./asan-launcher test.py; fi
if [[ "$RUN_FLAKE" == "1" ]]; then flake8 --count .; fi
if [[ "$BUILD_PKG" == "linux-package" ]]; then make FAIL_WARN=-W man && make FAIL_WARN=-W html; fi
if [[ ! -z "$BUILD_PKG" ]]; then $PYTHON setup.py $BUILD_PKG; fi
set +e

View File

@@ -1,5 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: %YEAR%, %USER% <%MAIL%>
%HERE%

View File

@@ -39,4 +39,7 @@ man:
html:
$(MAKE) FAIL_WARN=$(FAIL_WARN) -C docs html
linkcheck:
$(MAKE) FAIL_WARN=$(FAIL_WARN) -C docs linkcheck
docs: man html

View File

@@ -26,6 +26,18 @@ def runpy(args):
exec(args[1])
def hold(args):
import subprocess
ret = subprocess.Popen(args[1:]).wait()
sys.stdin.read()
raise SystemExit(ret)
def complete(args):
from kitty.complete import main
main(args[1:], entry_points, namespaced_entry_points)
def launch(args):
import runpy
sys.argv = args[1:]
@@ -61,6 +73,8 @@ entry_points = {
'+': namespaced,
}
namespaced_entry_points = {k: v for k, v in entry_points.items() if k[0] not in '+@'}
namespaced_entry_points['hold'] = hold
namespaced_entry_points['complete'] = complete
def setup_openssl_environment():

View File

@@ -28,7 +28,7 @@ Build-time dependencies:
* gcc or clang
* pkg-config
* For building on Linux in addition to the above dependencies you might also need to install the ``-dev`` packages for:
``libxcursor-dev``, ``libxrandr-dev``, ``libxi-dev``, ``libxinerama-dev``, ``libgl1-mesa-dev`` and ``libxkbcommon-x11-dev``,
``libdbus-1-dev``, ``libxcursor-dev``, ``libxrandr-dev``, ``libxi-dev``, ``libxinerama-dev``, ``libgl1-mesa-dev`` and ``libxkbcommon-x11-dev``,
if they are not already installed by your distro.
Install and run from source
@@ -60,31 +60,35 @@ just ``kitty``.
Note for Linux/macOS packagers
----------------------------------
While kitty does use python, it is not a traditional python package, so please
The released |kitty| source code is available as a `tarball`_ from
`the GitHub releases page <https://github.com/kovidgoyal/kitty/releases>`_.
While |kitty| does use python, it is not a traditional python package, so please
do not install it in site-packages.
Instead run::
python3 setup.py linux-package
This will install kitty into the directory :file:`linux-package`. You can run kitty
This will install |kitty| into the directory :file:`linux-package`. You can run |kitty|
with :file:`linux-package/bin/kitty`. All the files needed to run kitty will be in
:file:`linux-package/lib/kitty`. The terminfo file will be installed into
:file:`linux-package/share/terminfo`. Simply copy these files into :file:`/usr` to install
kitty. In other words, :file:`linux-package` is the staging area into which kitty is
|kitty|. In other words, :file:`linux-package` is the staging area into which |kitty| is
installed. You can choose a different staging area, by passing the ``--prefix``
argument to :file:`setup.py`.
You should probably split kitty into two packages, :file:`kitty-terminfo` that
You should probably split |kitty| into two packages, :file:`kitty-terminfo` that
installs the terminfo file and :file:`kitty` that installs the main program.
This allows users to install the terminfo file on servers into which they ssh,
without needing to install all of kitty.
without needing to install all of |kitty|.
.. note::
You need a couple of extra dependencies to build linux-package.
:file:`tic` to compile terminfo files, usually found in the
development package of :file:`ncurses`. Also, :file:`sphinx-build`
from the `Sphinx documentation generator
development package of :file:`ncurses`. Also, if you are building from
a git checkout instead of the released source code tarball, you will
need :file:`sphinx-build` from the `Sphinx documentation generator
<http://www.sphinx-doc.org/>`_.
This applies to creating packages for kitty for macOS package managers such as
This applies to creating packages for |kitty| for macOS package managers such as
brew or MacPorts as well.

View File

@@ -3,6 +3,177 @@ Changelog
|kitty| is a feature full, cross-platform, *fast*, GPU based terminal emulator.
0.12.0 [2018-09-01]
------------------------------
- Preserve the mouse selection even when the contents of the screen are
scrolled or overwritten provided the new text does not intersect the
selected lines.
- Linux: Implement support for Input Method Extensions (multilingual input
using standard keyboards) via `IBus
<https://github.com/ibus/ibus/wiki/ReadMe>`_ (:iss:`469`)
- Implement completion for the kitty command in bash and zsh. See
:ref:`completion`.
- Render the text under the cursor in a fixed color, configurable via
the option :opt:`cursor_text_color` (:iss:`126`)
- Add an option :opt:`env` to set environment variables in child processes
from kitty.conf
- Implement high precision scrolling with the trackpad on platforms such as
macOS and Wayland that implement it. (:pull:`819`)
- macOS: Allow scrolling window contents using mouse wheel/trackpad even when the
window is not the active window (:iss:`729`)
- Remote control: Allow changing the current window layout with a new
:ref:`at_goto-layout` command (:iss:`845`)
- Remote control: Allow matching windows by the environment variables of their
child process as well
- Allow running kittens via the remote control system (:iss:`738`)
- Allow enabling remote control in only some kitty windows
- Add a keyboard shortcut to reset the terminal (:sc:`reset_terminal`). It
takes parameters so you can define your own shortcuts to clear the
screen/scrollback also (:iss:`747`)
- Fix one-pixel line appearing at window edges at some window sizes when
displaying images with background opacity enabled (:iss:`741`)
- diff kitten: Fix error when right hand side file is binary and left hand side
file is text (:pull:`752`)
- kitty @ new-window: Add a new option :option:`kitty @ new-window --window-type`
to create top-level OS windows (:iss:`770`)
- macOS: The :opt:`focus_follows_mouse` option now also works across top-level kitty OS windows
(:iss:`754`)
- Fix detection of URLs in HTML source code (URLs inside quotes) (:iss:`785`)
- Implement support for emoji skin tone modifiers (:iss:`787`)
- Round-trip the zwj unicode character. Rendering of sequences containing zwj
is still not implemented, since it can cause the collapse of an unbounded
number of characters into a single cell. However, kitty at least preserves
the zwj by storing it as a combining character.
- macOS: Disable the custom mouse cursor. Using a custom cursor fails on dual
GPU machines. I give up, Apple users will just have to live with the
limitations of their choice of OS. (:iss:`794`)
- macOS: Fix control+tab key combination not working (:iss:`801`)
- Linux: Fix slow startup on some systems caused by GLFW searching for
joysticks. Since kitty does not use joysticks, disable joystick support.
(:iss:`830`)
0.11.3 [2018-07-10]
------------------------------
- Draw only the minimum borders needed for inactive windows. That is only the borders
that separate the inactive window from a neighbor. Note that setting
a non-zero window margin overrides this and causes all borders to be drawn.
The old behavior of drawing all borders can be restored via the
:opt:`draw_minimal_borders` setting in kitty.conf. (:iss:`699`)
- macOS: Add an option :opt:`macos_window_resizable` to control if kitty
top-level windows are resizable using the mouse or not (:iss:`698`)
- macOS: Use a custom mouse cursor that shows up well on both light and dark backgrounds
(:iss:`359`)
- macOS: Workaround for switching from fullscreen to windowed mode with the
titlebar hidden causing window resizing to not work. (:iss:`711`)
- Fix triple-click to select line not working when the entire line is filled
(:iss:`703`)
- When dragging to select with the mouse "grab" the mouse so that if it strays
into neighboring windows, the selection is still updated (:pull:`624`)
- When clicking in the margin/border area of a window, map the click to the
nearest cell in the window. Avoids selection with the mouse failing when
starting the selection just outside the window.
- When drag-scrolling stop the scroll when the mouse button is released.
- Fix a regression in the previous release that caused pasting large amounts
of text to be duplicated (:iss:`709`)
0.11.2 [2018-07-01]
------------------------------
- Linux: Allow using XKB key names to bind shortcuts to keys not supported by GLFW (:pull:`665`)
- kitty shell: Ignore failure to read readline history file. Happens if the
user migrates their kitty cache directory between systems with incompatible
readline implementations.
- macOS: Fix an error in remote control when using --listen-on (:iss:`679`)
- hints kitten: Add a :option:`kitty +kitten hints --multiple` option to select
multiple items (:iss:`687`)
- Fix pasting large amounts of text very slow (:iss:`682`)
- Add an option :opt:`single_window_margin_width` to allow different margins
when only a single window is visible in the layout (:iss:`688`)
- Add a :option:`kitty --hold` command line option to stay open after the child process exits (:iss:`667`)
- diff kitten: When triggering a search scroll to the first match automatically
- :option:`kitty --debug-font-fallback` also prints out what basic fonts were matched
- When closing a kitty window reset the mouse cursor to its default shape and ensure it is visible (:iss:`655`).
- Remote control: Speed-up reading of command responses
- Linux installer: Fix installer failing on systems with python < 3.5
- Support "-T" as an alias for "--title" (:pull:`659`)
- Fix a regression in the previous release that broke using :option:`kitty
--debug-config` with custom key mappings (:iss:`695`)
0.11.1 [2018-06-17]
------------------------------
- diff kitten: Implement searching for text in the diff (:iss:`574`)
- Add an option :opt:`startup_session` to :file:`kitty.conf` to specify a
default startup session (:iss:`641`)
- Add a command line option :option:`kitty --wait-for-single-instance-window-close`
to make :option:`kitty --single-instance` wait for the closing of the newly opened
window before quitting (:iss:`630`)
- diff kitten: Allow theming the selection background/foreground as well
- diff kitten: Display CRLF line endings using the unicode return symbol
instead of <d> as it is less intrusive (:iss:`638`)
- diff kitten: Fix default foreground/background colors not being restored when
kitten quits (:iss:`637`)
- Fix :option:`kitty @ set-colors --all` not working when more than one window
present (:iss:`632`)
- Fix a regression that broke the legacy increase/decrease_font_size actions
- Clear scrollback on reset (:iss:`631`)
0.11.0 [2018-06-12]
------------------------------
@@ -196,7 +367,7 @@ Changelog
- When triple-clicking select all wrapped lines belonging to a single logical line.
- hints kitten: Detect bracketed URLs and dont include the closing bracket in the URL.
- hints kitten: Detect bracketed URLs and don't include the closing bracket in the URL.
- When calling pass_selection_to_program use the current directory of the child
process as the cwd of the program.

View File

@@ -85,6 +85,8 @@ pygments_style = 'sphinx'
rst_prolog = '''
.. |kitty| replace:: *kitty*
.. |version| replace:: VERSION
.. _tarball: https://github.com/kovidgoyal/kitty/releases/download/vVERSION/kitty-VERSION.tar.xz
.. role:: green
.. role:: italic
.. role:: bold
@@ -92,7 +94,7 @@ rst_prolog = '''
.. role:: title
.. role:: env
'''
'''.replace('VERSION', str_version)
# -- Options for HTML output -------------------------------------------------
@@ -115,6 +117,9 @@ html_theme_options = {
'github_banner': True,
'github_user': 'kovidgoyal',
'github_repo': 'kitty',
# increase contrast of link color with text color
'link': '#00587d',
'link_hover': 'green',
}
@@ -245,6 +250,7 @@ def write_cli_docs(all_kitten_names):
from kitty.cmds import cli_params_for
for cmd_name in all_commands:
func = cmap[cmd_name]
p(f'.. _at_{func.name}:\n')
p('kitty @', func.name + '\n' + '-' * 120)
p('.. program::', 'kitty @', func.name)
p('\n\n' + as_rst(*cli_params_for(func)))
@@ -496,35 +502,30 @@ def write_conf_docs(app, all_kitten_names):
sc_role.warn_dangling = True
sc_role.process_link = process_shortcut_link
def generate(all_options, name='kitty'):
def generate_default_config(all_options, name):
from kitty.conf.definition import as_conf_file
from textwrap import indent
with open(f'generated/conf-{name}.rst', 'w', encoding='utf-8') as f:
print('.. highlight:: conf\n', file=f)
f.write(render_conf(name, all_options.values()))
with open(f'generated/conf-{name}-literal.rst', 'w', encoding='utf-8') as f:
print('.. code-block:: conf\n', file=f)
conf_name = re.sub(r'^kitten-', '', name) + '.conf'
with open(f'generated/conf/{conf_name}', 'w', encoding='utf-8') as f:
text = '\n'.join(as_conf_file(all_options.values()))
text = indent(text, ' ', lambda l: True)
print(text, file=f)
from kitty.config_data import all_options
generate(all_options)
generate_default_config(all_options, 'kitty')
from kittens.runner import get_kitten_conf_docs
for kitten in all_kitten_names:
all_options = get_kitten_conf_docs(kitten)
if all_options:
generate(all_options, f'kitten-{kitten}')
generate_default_config(all_options, f'kitten-{kitten}')
# }}}
def setup(app):
try:
os.mkdir('generated')
except FileExistsError:
pass
os.makedirs('generated/conf', exist_ok=True)
from kittens.runner import all_kitten_names
all_kitten_names = all_kitten_names()
write_cli_docs(all_kitten_names)

View File

@@ -35,6 +35,6 @@ expanded, so :code:`${USER}.conf` becomes :file:`name.conf` if
Sample kitty.conf
^^^^^^^^^^^^^^^^^^^^^
Below is a sample :file:`kitty.conf` with all default settings.
.. include:: /generated/conf-kitty-literal.rst
You can download a sample :file:`kitty.conf` file with all default settings and
comments describing each setting by clicking: :download:`sample kitty.conf
</generated/conf/kitty.conf>`.

View File

@@ -83,7 +83,7 @@ How do I change the colors in a running kitty instance?
You can either use the
`OSC terminal escape codes <http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands>`_
to set colors or you can enable :doc:`remote control <remote-control>`
for |kitty| and use ``kitty @ set-colors --help``.
for |kitty| and use :ref:`at_set-colors`.
How do I specify command line options for kitty on macOS?

View File

@@ -372,9 +372,9 @@ Key Value Default Description
**Keys for deleting images**
-----------------------------------------------------------
``d`` Single character. ``a`` What to delete.
``(a, A, c, C, p,
P, q, Q, x, X, y,
Y, z, Z)``.
``(a, A, c, C, i,
I, p, P, q, Q, x, X,
y, Y, z, Z)``.
======= ==================== ========= =================

View File

@@ -1,7 +1,7 @@
:tocdepth: 2
==========================================================
kitty - the fast, featureful, GPU based, terminal emulator
kitty - the fast, featureful, GPU based terminal emulator
==========================================================
.. container:: major-features
@@ -30,7 +30,7 @@ kitty - the fast, featureful, GPU based, terminal emulator
the window/tab layout, working directories and programs to run on startup.
* Cross-platform: |kitty| works on Linux and macOS, but because it uses only
OpenGL for rendering, it should be trivial to port to other platforms.
OpenGL for rendering, it should be trivial to port to other Unix-like platforms.
* Allows you to open :ref:`the scrollback buffer <scrollback>` in a
separate window using arbitrary programs of your choice. This is useful for
@@ -122,7 +122,7 @@ Action Shortcut
======================== =======================
New tab :sc:`new_tab`
Close tab :sc:`close_tab`
Next tab :sc:`next_tab`
Next tab :sc:`next_tab` (also :kbd:`control+tab` on macOS)
Previous tab :sc:`previous_tab`
Next layout :sc:`next_layout`
Move tab forward :sc:`move_tab_forward`
@@ -165,6 +165,7 @@ Restore font size :sc:`reset_font_size`
Toggle fullscreen :sc:`toggle_fullscreen`
Input unicode character :sc:`input_unicode_character`
Click URL using the keyboard :sc:`open_url`
Reset the terminal :sc:`reset_terminal`
Pass current selection to program :sc:`pass_selection_to_program`
Edit |kitty| config file :sc:`edit_config_file`
Open a |kitty| shell :sc:`kitty_shell`
@@ -250,14 +251,31 @@ Some prominent kittens:
Copy/paste to the clipboard from shell scripts, even over SSH.
Configuring kitty
-------------------
|kitty| is highly configurable, everything from keyboard shortcuts to
painting frames-per-second. For details and a sample :file:`kitty.conf`,
see the :doc:`configuration docs <conf>`.
.. _sessions:
Remote control
------------------
|kitty| has a very powerful system that allows you to control it from the
:doc:`shell prompt, even over SSH <remote-control>`. You can change colors,
fonts, open new windows, tabs, set their titles, change window layout, get text
from one window and send text to another, etc, etc. The possibilities are
endless. See the :doc:`tutorial <remote-control>` to get started.
Startup Sessions
------------------
You can control the tabs, window layout, working directory, startup
programs, etc. by creating a "session" file and using the :option:`kitty --session`
command line flag. For example:
You can control the tabs, window layout, working directory, startup programs,
etc. by creating a "session" file and using the :option:`kitty --session`
command line flag or the :opt:`startup_session` option in :file:`kitty.conf`.
For example:
.. code-block:: session
@@ -295,6 +313,8 @@ Mouse features
* You can also hold down :kbd:`ctrl+shift` and click on a URL to open it in a browser.
* You can double click to select a word and triple click to select a line.
* You can right click to extend a previous selection
* You can hold down :kbd:`ctrl+alt` and drag with the mouse to select in
columns
Font control
@@ -330,6 +350,39 @@ Frequently Asked Questions
The list of Frequently Asked Questions (*FAQ*) is :doc:`available here <faq>`.
.. _completion:
Completion for kitty
---------------------------------
|kitty| comes with completion for the ``kitty`` command for popular shells.
bash
~~~~~~~~
Add the following to your :file:`~/.bashrc`
.. code-block:: sh
source <(kitty + complete setup bash)
zsh
~~~~~~~~~
Add the following to your :file:`~/.zshrc`
.. code-block:: sh
autoload -Uz compinit
compinit
# Completion for kitty
kitty + complete setup zsh | source /dev/stdin
The important thing above is to make sure the call to |kitty| to load the zsh
completions happens after the call to :file:`compinit`.
Changelog
------------------

View File

@@ -72,8 +72,8 @@ class Reporter: # {{{
def get_latest_release_data():
print('Checking for latest release on GitHub...')
req = urllib.Request('https://api.github.com/repos/kovidgoyal/kitty/releases/latest', headers={'Accept': 'application/vnd.github.v3+json'})
res = urllib.urlopen(req)
data = json.load(res)
res = urllib.urlopen(req).read().decode('utf-8')
data = json.loads(res)
html_url = data['html_url'].replace('/tag/', '/download/').rstrip('/')
for asset in data.get('assets', ()):
name = asset['name']

View File

@@ -5,7 +5,7 @@ See :ref:`extended-key-protocol` for more information and `this table in JSON
format <https://github.com/kovidgoyal/kitty/blob/master/key_encoding.json>`_.
===================== ======================
Name Encoded representation (base64)
Name Encoded representation
0 ``G``
1 ``H``
2 ``I``
@@ -126,4 +126,3 @@ X ``p``
Y ``q``
Z ``r``
===================== ======================

View File

@@ -31,7 +31,7 @@ Major Features
Installation
---------------
Simply install :ref:`kitty <quickstart>`. You also need
Simply :ref:`install kitty <quickstart>`. You also need
to have either the `git <https://git-scm.com/>`_ program or the ``diff`` program
installed. Additionally, for syntax highlighting to work,
`pygments <http://pygments.org/>`_ must be installed (note that pygments is
@@ -67,37 +67,29 @@ Keyboard controls
========================= ===========================
Action Shortcut
========================= ===========================
Quit ``q, Ctrl+c``
Scroll line up ``k, up``
Scroll line down ``j, down``
Scroll page up ``PgUp``
Scroll page down ``PgDn``
Scroll to top ``Home``
Scroll to bottom ``End``
Scroll to next page ``Space, PgDn``
Scroll to previous page ``PgUp``
Scroll to next change ``n``
Scroll to previous change ``p``
Increase lines of context ``+``
Decrease lines of context ``-``
All lines of context ``a``
Restore default context ``=``
Quit :kbd:`q, Ctrl+c, Esc`
Scroll line up :kbd:`k, up`
Scroll line down :kbd:`j, down`
Scroll page up :kbd:`PgUp`
Scroll page down :kbd:`PgDn`
Scroll to top :kbd:`Home`
Scroll to bottom :kbd:`End`
Scroll to next page :kbd:`Space, PgDn`
Scroll to previous page :kbd:`PgUp`
Scroll to next change :kbd:`n`
Scroll to previous change :kbd:`p`
Increase lines of context :kbd:`+`
Decrease lines of context :kbd:`-`
All lines of context :kbd:`a`
Restore default context :kbd:`=`
Search forwards :kbd:`/`
Search backwards :kbd:`?`
Clear search :kbd:`Esc`
Scroll to next match :kbd:`>, .`
Scroll to previous match :kbd:`<, ,`
========================= ===========================
Configuration
------------------------
You can configure the colors used, keyboard shortcuts, the diff implementation,
the default lines of context, etc. by creating a :file:`diff.conf` file in
your :ref:`kitty config folder <confloc>`. See below for the supported
configuration directives.
.. include:: /generated/conf-kitten-diff.rst
Integrating with git
-----------------------
@@ -122,11 +114,6 @@ Now to use kitty-diff to view git diffs, you can simply do::
Once again, creating an alias for this command is useful.
Command Line Interface
-------------------------
.. include:: /generated/cli-kitten-diff.rst
Why does this work only in kitty?
----------------------------------------
@@ -142,9 +129,29 @@ And fundamentally, it's kitty only because I wrote it for myself, and I am
highly unlikely to use any other terminals :)
Configuration
------------------------
You can configure the colors used, keyboard shortcuts, the diff implementation,
the default lines of context, etc. by creating a :file:`diff.conf` file in
your :ref:`kitty config folder <confloc>`. See below for the supported
configuration directives.
.. include:: /generated/conf-kitten-diff.rst
Command Line Interface
-------------------------
.. include:: /generated/cli-kitten-diff.rst
Sample diff.conf
-----------------
Below is a sample :file:`diff.conf` with all default settings.
.. include:: /generated/conf-kitten-diff-literal.rst
You can download a sample :file:`diff.conf` file with all default settings and
comments describing each setting by clicking: :download:`sample diff.conf
</generated/conf/diff.conf>`.

View File

@@ -18,5 +18,5 @@ typing a leading period and then the two character index and pressing Enter.
In :guilabel:`Name` mode you instead type words from the character name and use the arrow
keys/tab to select the character from the displayed matches. You can also type
a leading period and the index for the match if you dont like to use arrow
a leading period and the index for the match if you don't like to use arrow
keys.

View File

@@ -43,3 +43,17 @@ konsole 29 - 31%
As you can see, |kitty| uses much less CPU than all terminals, except xterm, but
its scrolling "smoothness" is much better than that of xterm (at least to my,
admittedly biased, eyes).
.. _perf-cat:
.. note::
Some people have asked why kitty does not perform better than terminal XXX in
the test of sinking large amounts of data, such as catting a large text
file. The answer is because this is not a goal for kitty. kitty
deliberately throttles input parsing and output rendering to minimize
resource usage while still being able to sink output faster than any real
world program can produce it. Reducing CPU usage, and hence battery drain
while achieving instant response times and smooth scrolling to a human eye
is a far more important goal.

View File

@@ -165,3 +165,44 @@ For example, to set the background color to blue in a
rectangular region of the screen from (3, 4) to (10, 11), you use::
<ESC>[2*x<ESC>[4;3;11;10;44$r<ESC>[*x
Saving and restoring the default foreground/background/selection/cursor colors
---------------------------------------------------------------------------------
It is often useful for a full screen application with its own color themes
to set the default foreground, background, selection and cursor colors. This
allows for various performance optimizations when drawing the screen. The
problem is that if the user previously used the escape codes to change these
colors herself, then running the full screen application will lose her
changes even after it exits. To avoid this, kitty introduces a new pair of
*OSC* escape codes to push and pop the current color values from a stack::
<ESC>]30001<ESC>\ # push onto stack
<ESC>]30101<ESC>\ # pop from stack
These escape codes save/restore the so called *dynamic colors*, default
background, default foreground, selection background, selection foreground and
cursor color.
Pasting to clipboard
----------------------
|kitty| implements the OSC 52 escape code protocol to get/set the clipboard
contents (controlled via the :opt:`clipboard_control` setting). There is one
difference in kitty's implementation compared to some other terminal emulators.
|kitty| allows sending arbitrary amounts of text to the clipboard. It does so
by modifying the protocol slightly. Successive OSC 52 escape codes to set the
clipboard will concatenate, so::
<ESC>]52;c;<payload1><ESC>\
<ESC>]52;c;<payload2><ESC>\
will result in the clipboard having the contents ``payload1 + payload2``. To
send a new string to the clipboard send an OSC 52 sequence with an invalid payload
first, for example::
<ESC>]52;c;!<ESC>\
Here ``!`` is not valid base64 encoded text, so it clears the clipboard.

View File

@@ -103,7 +103,7 @@ instance of |kitty| using the :option:`kitty @ --to` command line argument to ``
kitty @ --to unix:/tmp/mykitty ls
Note that is all you want to do is run a single |kitty| "daemon" and have subsequent
Note that if all you want to do is run a single |kitty| "daemon" and have subsequent
|kitty| invocations appear as new top-level windows, you can use the simpler :option:`kitty --single-instance`
option, see ``kitty --help`` for that.
@@ -116,7 +116,23 @@ shell with completion for |kitty| command names and options.
You can even open the |kitty| shell inside a running |kitty| using a simple
keyboard shortcut (:sc:`kitty_shell` by default). This has the added
advantage that you dont need to use ``allow_remote_control`` to make it work.
advantage that you don't need to use ``allow_remote_control`` to make it work.
Allowing only some windows to control kitty
----------------------------------------------
If you do not want to allow all programs running in |kitty| to control it, you can selectively
enable remote control for only some |kitty| windows. Simply create a shortcut
such as::
map ctrl+k new_window @ some_program
Then programs running in windows created with that shortcut can use ``kitty @``
to control kitty. Note that any program with the right level of permissions can
still write to the pipes of any other program on the same computer and
therefore can control |kitty|. It can, however, be useful to block programs
running on other computers (for example, over ssh) or as other users.
.. include:: generated/cli-kitty-at.rst

264
gen-apc-parsers.py Executable file
View File

@@ -0,0 +1,264 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import subprocess
from collections import defaultdict
def resolve_keys(keymap):
ans = defaultdict(list)
for ch, (attr, atype) in keymap.items():
if atype not in ('int', 'uint'):
atype = 'flag'
ans[atype].append(ch)
return ans
def enum(keymap):
lines = []
for ch, (attr, atype) in keymap.items():
lines.append(f"{attr}='{ch}'")
return '''
enum KEYS {{
{}
}};
'''.format(',\n'.join(lines))
def parse_key(keymap):
lines = []
for attr, atype in keymap.values():
vs = atype.upper() if atype in ('uint', 'int') else 'FLAG'
lines.append(f'case {attr}: value_state = {vs}; break;')
return ' \n'.join(lines)
def parse_flag(keymap, type_map, command_class):
lines = []
for ch in type_map['flag']:
attr, allowed_values = keymap[ch]
q = ' && '.join(f"g.{attr} != '{x}'" for x in allowed_values)
lines.append(f'''
case {attr}: {{
g.{attr} = screen->parser_buf[pos++] & 0xff;
if ({q}) {{
REPORT_ERROR("Malformed {command_class} control block, unknown flag value for {attr}: 0x%x", g.{attr});
return;
}};
}}
break;
''')
return ' \n'.join(lines)
def parse_number(keymap):
int_keys = [f'I({attr})' for attr, atype in keymap.values() if atype == 'int']
uint_keys = [f'U({attr})' for attr, atype in keymap.values() if atype == 'uint']
return '; '.join(int_keys), '; '.join(uint_keys)
def cmd_for_report(report_name, keymap, type_map, payload_allowed):
def group(atype, conv):
flag_fmt, flag_attrs = [], []
cv = {'flag': 'c', 'int': 'i', 'uint': 'I'}[atype]
for ch in type_map[atype]:
flag_fmt.append('s' + cv)
attr = keymap[ch][0]
flag_attrs.append(f'"{attr}", {conv}g.{attr}')
return ' '.join(flag_fmt), ', '.join(flag_attrs)
flag_fmt, flag_attrs = group('flag', '')
int_fmt, int_attrs = group('int', '(int)')
uint_fmt, uint_attrs = group('uint', '(unsigned int)')
fmt = f'{flag_fmt} {uint_fmt} {int_fmt}'
if payload_allowed:
ans = [f'REPORT_VA_COMMAND("s {{{fmt} sI}} y#", "{report_name}",']
else:
ans = [f'REPORT_VA_COMMAND("s {{{fmt}}}", "{report_name}",']
ans.append(',\n '.join((flag_attrs, uint_attrs, int_attrs)))
if payload_allowed:
ans.append(', "payload_sz", g.payload_sz, payload, g.payload_sz')
ans.append(');')
return '\n'.join(ans)
def generate(function_name, callback_name, report_name, keymap, command_class, initial_key='a', payload_allowed=True):
type_map = resolve_keys(keymap)
keys_enum = enum(keymap)
handle_key = parse_key(keymap)
flag_keys = parse_flag(keymap, type_map, command_class)
int_keys, uint_keys = parse_number(keymap)
report_cmd = cmd_for_report(report_name, keymap, type_map, payload_allowed)
if payload_allowed:
payload_after_value = "case ';': state = PAYLOAD; break;"
payload = payload = ', PAYLOAD'
parr = 'static uint8_t payload[4096];'
payload_case = f'''
case PAYLOAD: {{
sz = screen->parser_buf_pos - pos;
const char *err = base64_decode(screen->parser_buf + pos, sz, payload, sizeof(payload), &g.payload_sz);
if (err != NULL) {{ REPORT_ERROR("Failed to parse {command_class} command payload with error: %s", err); return; }}
pos = screen->parser_buf_pos;
}}
break;
'''
callback = f'{callback_name}(screen, &g, payload)'
else:
payload_after_value = payload = parr = payload_case = ''
callback = f'{callback_name}(screen, &g)'
return f'''
static inline void
{function_name}(Screen *screen, PyObject UNUSED *dump_callback) {{
unsigned int pos = 1;
enum PARSER_STATES {{ KEY, EQUAL, UINT, INT, FLAG, AFTER_VALUE {payload} }};
enum PARSER_STATES state = KEY, value_state = FLAG;
static {command_class} g;
unsigned int i, code;
uint64_t lcode;
bool is_negative;
memset(&g, 0, sizeof(g));
size_t sz;
{parr}
{keys_enum}
enum KEYS key = '{initial_key}';
while (pos < screen->parser_buf_pos) {{
switch(state) {{
case KEY:
key = screen->parser_buf[pos++];
state = EQUAL;
switch(key) {{
{handle_key}
default:
REPORT_ERROR("Malformed {command_class} control block, invalid key character: 0x%x", key);
return;
}}
break;
case EQUAL:
if (screen->parser_buf[pos++] != '=') {{
REPORT_ERROR("Malformed {command_class} control block, no = after key, found: 0x%x instead", screen->parser_buf[pos-1]);
return;
}}
state = value_state;
break;
case FLAG:
switch(key) {{
{flag_keys}
default:
break;
}}
state = AFTER_VALUE;
break;
case INT:
#define READ_UINT \\
for (i = pos; i < MIN(screen->parser_buf_pos, pos + 10); i++) {{ \\
if (screen->parser_buf[i] < '0' || screen->parser_buf[i] > '9') break; \\
}} \\
if (i == pos) {{ REPORT_ERROR("Malformed {command_class} control block, expecting an integer value for key: %c", key & 0xFF); return; }} \\
lcode = utoi(screen->parser_buf + pos, i - pos); pos = i; \\
if (lcode > UINT32_MAX) {{ REPORT_ERROR("Malformed {command_class} control block, number is too large"); return; }} \\
code = lcode;
is_negative = false;
if(screen->parser_buf[pos] == '-') {{ is_negative = true; pos++; }}
#define I(x) case x: g.x = is_negative ? 0 - (int32_t)code : (int32_t)code; break
READ_UINT;
switch(key) {{
{int_keys};
default: break;
}}
state = AFTER_VALUE;
break;
#undef I
case UINT:
READ_UINT;
#define U(x) case x: g.x = code; break
switch(key) {{
{uint_keys};
default: break;
}}
state = AFTER_VALUE;
break;
#undef U
#undef READ_UINT
case AFTER_VALUE:
switch (screen->parser_buf[pos++]) {{
default:
REPORT_ERROR("Malformed {command_class} control block, expecting a comma or semi-colon after a value, found: 0x%x",
screen->parser_buf[pos - 1]);
return;
case ',':
state = KEY;
break;
{payload_after_value}
}}
break;
{payload_case}
}} // end switch
}} // end while
switch(state) {{
case EQUAL:
REPORT_ERROR("Malformed {command_class} control block, no = after key"); return;
case INT:
case UINT:
REPORT_ERROR("Malformed {command_class} control block, expecting an integer value"); return;
case FLAG:
REPORT_ERROR("Malformed {command_class} control block, expecting a flag value"); return;
default:
break;
}}
{report_cmd}
{callback};
}}
'''
def write_header(text, path):
with open(path, 'w') as f:
print(f'// This file is generated by {__file__} do not edit!', file=f, end='\n\n')
print('#pragma once', file=f)
print(text, file=f)
subprocess.check_call(['clang-format', '-i', path])
def graphics_parser():
flag = frozenset
keymap = {
'a': ('action', flag('tTqpd')),
'd': ('delete_action', flag('aAiIcCpPqQxXyYzZ')),
't': ('transmission_type', flag('dfts')),
'o': ('compressed', flag('z')),
'f': ('format', 'uint'),
'm': ('more', 'uint'),
'i': ('id', 'uint'),
'w': ('width', 'uint'),
'h': ('height', 'uint'),
'x': ('x_offset', 'uint'),
'y': ('y_offset', 'uint'),
'v': ('data_height', 'uint'),
's': ('data_width', 'uint'),
'S': ('data_sz', 'uint'),
'O': ('data_offset', 'uint'),
'c': ('num_cells', 'uint'),
'r': ('num_lines', 'uint'),
'X': ('cell_x_offset', 'uint'),
'Y': ('cell_y_offset', 'uint'),
'z': ('z_index', 'int'),
}
text = generate('parse_graphics_code', 'screen_handle_graphics_command', 'graphics_command', keymap, 'GraphicsCommand')
write_header(text, 'kitty/parse-graphics-command.h')
graphics_parser()

View File

@@ -15,6 +15,13 @@ from urllib.request import urlopen
os.chdir(os.path.dirname(os.path.abspath(__file__)))
non_characters = frozenset(range(0xfffe, 0x10ffff, 0x10000))
non_characters |= frozenset(range(0xffff, 0x10ffff + 1, 0x10000))
non_characters |= frozenset(range(0xfdd0, 0xfdf0))
if len(non_characters) != 66:
raise SystemExit('non_characters table incorrect')
emoji_skin_tone_modifiers = frozenset(range(0x1f3fb, 0x1F3FF + 1))
def get_data(fname, folder='UCD'):
url = f'https://www.unicode.org/Public/{folder}/latest/{fname}'
@@ -35,7 +42,8 @@ def get_data(fname, folder='UCD'):
class_maps = {}
name_map = {}
word_search_map = defaultdict(set)
marks = set()
zwj = 0x200d
marks = set(emoji_skin_tone_modifiers) | {zwj}
not_assigned = set(range(0, sys.maxunicode))
@@ -189,11 +197,13 @@ def gen_emoji():
p('\treturn false;\n}')
def category_test(name, p, classes, comment, static=False):
def category_test(name, p, classes, comment, static=False, extra_chars=frozenset(), exclude=frozenset()):
static = 'static inline ' if static else ''
chars = set()
for c in classes:
chars |= class_maps[c]
chars |= extra_chars
chars -= exclude
p(f'{static}bool\n{name}(char_type code) {{')
p(f'\t// {comment} ({len(chars)} codepoints)' + ' {{' '{')
p('\tswitch(code) {')
@@ -244,8 +254,16 @@ def gen_ucd():
cz = {c for c in class_maps if c[0] in 'CZ'}
with create_header('kitty/unicode-data.c') as p:
p('#include "unicode-data.h"')
category_test('is_combining_char', p, {c for c in class_maps if c.startswith('M')}, 'M category (marks)')
category_test('is_ignored_char', p, 'Cc Cf Cs'.split(), 'Control characters (Cc Cf Cs)')
category_test(
'is_combining_char', p,
{c for c in class_maps if c.startswith('M')},
'M category (marks)',
# See https://github.com/harfbuzz/harfbuzz/issues/169
extra_chars=emoji_skin_tone_modifiers | {zwj}
)
category_test(
'is_ignored_char', p, 'Cc Cf Cs'.split(),
'Control characters and non-characters', extra_chars=non_characters, exclude={zwj})
category_test('is_word_char', p, {c for c in class_maps if c[0] in 'LN'}, 'L and N categories')
category_test('is_CZ_category', p, cz, 'C and Z categories')
category_test('is_P_category', p, {c for c in class_maps if c[0] == 'P'}, 'P category (punctuation)')
@@ -258,7 +276,7 @@ def gen_ucd():
p('combining_type mark_for_codepoint(char_type c) {')
rmap = codepoint_to_mark_map(p, mark_map)
p('}\n')
if rmap[0xfe0e] != 1275:
if rmap[0xfe0e] != 1281:
raise ValueError('The mark for 0xfe0e has changed, you have to update VS15 to {} and VS16 to {} in unicode-data.h'.format(
rmap[0xfe0e], rmap[0xfe0f]
))
@@ -393,7 +411,7 @@ def gen_wcwidth():
add(p, 'East Asian double width', doublewidth, 2)
add(p, 'Emoji Presentation', emoji_categories['Emoji_Presentation'], 2)
add(p, 'Not assigned in the unicode character database', not_assigned, -1)
add(p, 'Not assigned in the unicode character database', not_assigned, -4)
p('\t\tdefault: return 1;')
p('\t}')

263
glfw/backend_utils.c vendored Normal file
View File

@@ -0,0 +1,263 @@
/*
* backend_utils.c
* Copyright (C) 2018 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#define _GNU_SOURCE
#include "backend_utils.h"
#include "internal.h"
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <float.h>
#include <time.h>
#include <stdio.h>
#ifdef __NetBSD__
#define ppoll pollts
#endif
static inline double
monotonic() {
struct timespec ts = {0};
#ifdef CLOCK_HIGHRES
clock_gettime(CLOCK_HIGHRES, &ts);
#elif CLOCK_MONOTONIC_RAW
clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
#else
clock_gettime(CLOCK_MONOTONIC, &ts);
#endif
return (((double)ts.tv_nsec) / 1e9) + (double)ts.tv_sec;
}
void
update_fds(EventLoopData *eld) {
for (nfds_t i = 0; i < eld->watches_count; i++) {
Watch *w = eld->watches + i;
eld->fds[i].fd = w->fd;
eld->fds[i].events = w->enabled ? w->events : 0;
}
}
static id_type watch_counter = 0;
id_type
addWatch(EventLoopData *eld, const char* name, int fd, int events, int enabled, watch_callback_func cb, void *cb_data) {
if (eld->watches_count >= sizeof(eld->watches)/sizeof(eld->watches[0])) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Too many watches added");
return 0;
}
Watch *w = eld->watches + eld->watches_count++;
w->name = name;
w->fd = fd; w->events = events; w->enabled = enabled;
w->callback = cb;
w->callback_data = cb_data;
w->id = ++watch_counter;
update_fds(eld);
return w->id;
}
#define removeX(which, item_id, update_func) {\
for (nfds_t i = 0; i < eld->which##_count; i++) { \
if (eld->which[i].id == item_id) { \
eld->which##_count--; \
if (i < eld->which##_count) { \
memmove(eld->which + i, eld->which + i + 1, sizeof(eld->which[0]) * (eld->which##_count - i)); \
} \
update_func(eld); break; \
}}}
void
removeWatch(EventLoopData *eld, id_type watch_id) {
removeX(watches, watch_id, update_fds);
}
void
toggleWatch(EventLoopData *eld, id_type watch_id, int enabled) {
for (nfds_t i = 0; i < eld->watches_count; i++) {
if (eld->watches[i].id == watch_id) {
if (eld->watches[i].enabled != enabled) {
eld->watches[i].enabled = enabled;
update_fds(eld);
}
break;
}
}
}
static id_type timer_counter = 0;
static int
compare_timers(const void *a_, const void *b_) {
const Timer *a = (const Timer*)a_, *b = (const Timer*)b_;
return (a->trigger_at > b->trigger_at) ? 1 : (a->trigger_at < b->trigger_at) ? -1 : 0;
}
static inline void
update_timers(EventLoopData *eld) {
if (eld->timers_count > 1) qsort(eld->timers, eld->timers_count, sizeof(eld->timers[0]), compare_timers);
}
id_type
addTimer(EventLoopData *eld, const char *name, double interval, int enabled, timer_callback_func cb, void *cb_data) {
if (eld->timers_count >= sizeof(eld->timers)/sizeof(eld->timers[0])) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Too many timers added");
return 0;
}
Timer *t = eld->timers + eld->timers_count++;
t->interval = interval;
t->name = name;
t->trigger_at = enabled ? monotonic() + interval : DBL_MAX;
t->callback = cb;
t->callback_data = cb_data;
t->id = ++timer_counter;
update_timers(eld);
return t->id;
}
void
removeTimer(EventLoopData *eld, id_type timer_id) {
removeX(timers, timer_id, update_timers);
}
void
toggleTimer(EventLoopData *eld, id_type timer_id, int enabled) {
for (nfds_t i = 0; i < eld->timers_count; i++) {
if (eld->timers[i].id == timer_id) {
double trigger_at = enabled ? (monotonic() + eld->timers[i].interval) : DBL_MAX;
if (trigger_at != eld->timers[i].trigger_at) {
eld->timers[i].trigger_at = trigger_at;
update_timers(eld);
}
break;
}
}
}
void
changeTimerInterval(EventLoopData *eld, id_type timer_id, double interval) {
for (nfds_t i = 0; i < eld->timers_count; i++) {
if (eld->timers[i].id == timer_id) {
eld->timers[i].interval = interval;
break;
}
}
}
double
prepareForPoll(EventLoopData *eld, double timeout) {
for (nfds_t i = 0; i < eld->watches_count; i++) eld->fds[i].revents = 0;
if (!eld->timers_count || eld->timers[0].trigger_at == DBL_MAX) return timeout;
double now = monotonic(), next_repeat_at = eld->timers[0].trigger_at;
if (timeout < 0 || now + timeout > next_repeat_at) {
timeout = next_repeat_at <= now ? 0 : next_repeat_at - now;
}
return timeout;
}
int
pollWithTimeout(struct pollfd *fds, nfds_t nfds, double timeout) {
const long seconds = (long) timeout;
const long nanoseconds = (long) ((timeout - seconds) * 1e9);
struct timespec tv = { seconds, nanoseconds };
return ppoll(fds, nfds, &tv, NULL);
}
static void
dispatchEvents(EventLoopData *eld) {
for (nfds_t i = 0; i < eld->watches_count; i++) {
Watch *ww = eld->watches + i;
struct pollfd *pfd = eld->fds + i;
if (pfd->revents & ww->events) {
ww->ready = 1;
if (ww->callback) ww->callback(ww->fd, pfd->revents, ww->callback_data);
} else ww->ready = 0;
}
}
unsigned
dispatchTimers(EventLoopData *eld) {
if (!eld->timers_count || eld->timers[0].trigger_at == DBL_MAX) return 0;
static struct { timer_callback_func func; id_type id; void* data; } dispatches[sizeof(eld->timers)/sizeof(eld->timers[0])];
unsigned num_dispatches = 0;
double now = monotonic();
for (nfds_t i = 0; i < eld->timers_count && eld->timers[i].trigger_at <= now; i++) {
eld->timers[i].trigger_at = now + eld->timers[i].interval;
dispatches[num_dispatches].func = eld->timers[i].callback;
dispatches[num_dispatches].id = eld->timers[i].id;
dispatches[num_dispatches].data = eld->timers[i].callback_data;
num_dispatches++;
}
// we dispatch separately so that the callbacks can modify timers
for (unsigned i = 0; i < num_dispatches; i++) {
dispatches[i].func(dispatches[i].id, dispatches[i].data);
}
if (num_dispatches) update_timers(eld);
return num_dispatches;
}
static void
drain_wakeup_fd(int fd, int events, void* data) {
static char drain_buf[64];
while(read(fd, drain_buf, sizeof(drain_buf)) < 0 && errno == EINTR);
}
void
initPollData(EventLoopData *eld, int wakeup_fd, int display_fd) {
addWatch(eld, "display", display_fd, POLLIN, 1, NULL, NULL);
addWatch(eld, "wakeup", wakeup_fd, POLLIN, 1, drain_wakeup_fd, NULL);
}
int
pollForEvents(EventLoopData *eld, double timeout) {
int read_ok = 0;
timeout = prepareForPoll(eld, timeout);
int result;
double end_time = monotonic() + timeout;
while(1) {
if (timeout >= 0) {
errno = 0;
result = pollWithTimeout(eld->fds, eld->watches_count, timeout);
int saved_errno = errno;
dispatchTimers(eld);
if (result > 0) {
dispatchEvents(eld);
read_ok = eld->watches[0].ready;
break;
}
timeout = end_time - monotonic();
if (timeout <= 0) break;
if (result < 0 && (saved_errno == EINTR || saved_errno == EAGAIN)) continue;
break;
} else {
errno = 0;
result = poll(eld->fds, eld->watches_count, -1);
int saved_errno = errno;
dispatchTimers(eld);
if (result > 0) {
dispatchEvents(eld);
read_ok = eld->watches[0].ready;
}
if (result < 0 && (saved_errno == EINTR || saved_errno == EAGAIN)) continue;
break;
}
}
return read_ok;
}
void
closeFds(int *fds, size_t count) {
while(count--) {
if (*fds > 0) {
close(*fds);
*fds = -1;
}
fds++;
}
}

71
glfw/backend_utils.h vendored
View File

@@ -25,42 +25,49 @@
//========================================================================
#pragma once
#include <unistd.h>
#include <poll.h>
#include <errno.h>
#include <unistd.h>
#ifdef __NetBSD__
#define ppoll pollts
#endif
typedef unsigned long long id_type;
typedef void(*watch_callback_func)(int, int, void*);
typedef void(*timer_callback_func)(id_type, void*);
static inline void
drainFd(int fd) {
static char drain_buf[64];
while(read(fd, drain_buf, sizeof(drain_buf)) < 0 && errno == EINTR);
}
typedef struct {
int fd, events, enabled, ready;
watch_callback_func callback;
void *callback_data;
id_type id;
const char *name;
} Watch;
typedef struct {
id_type id;
double interval, trigger_at;
timer_callback_func callback;
void *callback_data;
const char *name;
} Timer;
static inline int
pollWithTimeout(struct pollfd *fds, nfds_t nfds, double timeout) {
const long seconds = (long) timeout;
const long nanoseconds = (long) ((timeout - seconds) * 1e9);
struct timespec tv = { seconds, nanoseconds };
return ppoll(fds, nfds, &tv, NULL);
}
typedef struct {
struct pollfd fds[32];
int wakeupFds[2];
nfds_t watches_count, timers_count;
Watch watches[32];
Timer timers[128];
} EventLoopData;
static inline void
initPollData(struct pollfd *fds, int wakeup_fd, int display_fd) {
fds[0].fd = wakeup_fd; fds[1].fd = display_fd;
fds[0].events = POLLIN; fds[1].events = POLLIN;
}
static inline void
closeFds(int *fds, size_t count) {
while(count--) {
if (*fds > 0) {
close(*fds);
*fds = -1;
}
fds++;
}
}
id_type addWatch(EventLoopData *eld, const char *name, int fd, int events, int enabled, watch_callback_func cb, void *cb_data);
void removeWatch(EventLoopData *eld, id_type watch_id);
void toggleWatch(EventLoopData *eld, id_type watch_id, int enabled);
id_type addTimer(EventLoopData *eld, const char *name, double interval, int enabled, timer_callback_func cb, void *cb_data);
void removeTimer(EventLoopData *eld, id_type timer_id);
void toggleTimer(EventLoopData *eld, id_type timer_id, int enabled);
void changeTimerInterval(EventLoopData *eld, id_type timer_id, double interval);
double prepareForPoll(EventLoopData *eld, double timeout);
int pollWithTimeout(struct pollfd *fds, nfds_t nfds, double timeout);
int pollForEvents(EventLoopData *eld, double timeout);
unsigned dispatchTimers(EventLoopData *eld);
void closeFds(int *fds, size_t count);
void initPollData(EventLoopData *eld, int wakeup_fd, int display_fd);

View File

@@ -69,7 +69,6 @@ static void changeToResourcesDirectory(void)
static void createKeyTables(void)
{
int scancode;
_glfw.ns.debug_keyboard = getenv("GLFW_DEBUG_KEYBOARD") != NULL;
memset(_glfw.ns.keycodes, -1, sizeof(_glfw.ns.keycodes));
memset(_glfw.ns.scancodes, -1, sizeof(_glfw.ns.scancodes));

View File

@@ -121,7 +121,6 @@ typedef struct _GLFWlibraryNS
char keyName[64];
char text[256];
GLFWbool debug_keyboard;
short int keycodes[256];
short int scancodes[GLFW_KEY_LAST + 1];
char* clipboardString;

View File

@@ -209,7 +209,7 @@ static int translateFlags(NSUInteger flags)
return mods;
}
#define debug_key(...) if (_glfw.ns.debug_keyboard) NSLog(__VA_ARGS__)
#define debug_key(...) if (_glfw.hints.init.debugKeyboard) NSLog(__VA_ARGS__)
static inline const char*
format_mods(int mods) {
@@ -739,7 +739,7 @@ static GLFWapplicationshouldhandlereopenfun handle_reopen_callback = NULL;
}
const NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited |
NSTrackingActiveInKeyWindow |
NSTrackingActiveAlways |
NSTrackingEnabledDuringMouseDrag |
NSTrackingCursorUpdate |
NSTrackingInVisibleRect |
@@ -845,7 +845,7 @@ is_ascii_control_char(char x) {
debug_key(@"scancode: 0x%x (%s) %stext: %s glfw_key: %s\n",
scancode, safe_name_for_scancode(scancode), format_mods(mods),
format_text(_glfw.ns.text), _glfwGetKeyName(key));
if (is_ascii_control_char(_glfw.ns.text[0])) _glfw.ns.text[0] = 0; // dont send text for ascii control codes
if (is_ascii_control_char(_glfw.ns.text[0])) _glfw.ns.text[0] = 0; // don't send text for ascii control codes
_glfwInputKeyboard(window, key, scancode, GLFW_PRESS, mods, _glfw.ns.text, 0);
}
@@ -884,15 +884,10 @@ is_ascii_control_char(char x) {
deltaX = [event scrollingDeltaX];
deltaY = [event scrollingDeltaY];
if ([event hasPreciseScrollingDeltas])
{
deltaX *= 0.1;
deltaY *= 0.1;
}
int flags = [event hasPreciseScrollingDeltas] ? 1 : 0;
if (fabs(deltaX) > 0.0 || fabs(deltaY) > 0.0)
_glfwInputScroll(window, deltaX, deltaY);
_glfwInputScroll(window, deltaX, deltaY, flags);
}
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
@@ -1061,18 +1056,39 @@ is_ascii_control_char(char x) {
@implementation GLFWApplication
// From http://cocoadev.com/index.pl?GameKeyboardHandlingAlmost
// This works around an AppKit bug, where key up events while holding
// down the command key don't get sent to the key window.
- (void)sendEvent:(NSEvent *)event
{
if ([event type] == NSEventTypeKeyUp &&
([event modifierFlags] & NSEventModifierFlagCommand))
{
[[self keyWindow] sendEvent:event];
NSEventType etype = [event type];
NSEventModifierFlags flags;
switch(etype) {
case NSEventTypeKeyUp:
flags = [event modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask;
if (flags & NSEventModifierFlagCommand) {
// From http://cocoadev.com/index.pl?GameKeyboardHandlingAlmost
// This works around an AppKit bug, where key up events while holding
// down the command key don't get sent to the key window.
[[self keyWindow] sendEvent:event];
return;
}
if (event.keyCode == kVK_Tab && (flags == NSEventModifierFlagControl || flags == (NSEventModifierFlagControl | NSEventModifierFlagShift))) {
// Cocoa swallows Ctrl+Tab to cycle between views
[[self keyWindow].contentView keyUp:event];
return;
}
break;
case NSEventTypeKeyDown:
flags = [event modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask;
if (event.keyCode == kVK_Tab && (flags == NSEventModifierFlagControl || flags == (NSEventModifierFlagControl | NSEventModifierFlagShift))) {
// Cocoa swallows Ctrl+Tab to cycle between views
[[self keyWindow].contentView keyDown:event];
return;
}
break;
default:
break;
}
else
[super sendEvent:event];
[super sendEvent:event];
}
@@ -1083,7 +1099,7 @@ is_ascii_control_char(char x) {
}
- (void)loadMainMenu
{ // removed by Kovid as it generated compiler warnings
{ // removed by Kovid as it generated compiler warnings
}
@end
@@ -1886,44 +1902,44 @@ int _glfwPlatformGetKeyScancode(int key)
int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
const GLFWimage* image,
int xhot, int yhot)
int xhot, int yhot, int count)
{
NSImage* native;
NSBitmapImageRep* rep;
if (!initializeAppKit())
return GLFW_FALSE;
rep = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:NULL
pixelsWide:image->width
pixelsHigh:image->height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSCalibratedRGBColorSpace
bitmapFormat:NSAlphaNonpremultipliedBitmapFormat
bytesPerRow:image->width * 4
bitsPerPixel:32];
if (rep == nil)
native = [[NSImage alloc] initWithSize:NSMakeSize(image->width, image->height)];
if (native == nil)
return GLFW_FALSE;
memcpy([rep bitmapData], image->pixels, image->width * image->height * 4);
for (int i = 0; i < count; i++) {
const GLFWimage *src = image + i;
rep = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:NULL
pixelsWide:src->width
pixelsHigh:src->height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSCalibratedRGBColorSpace
bitmapFormat:NSAlphaNonpremultipliedBitmapFormat
bytesPerRow:src->width * 4
bitsPerPixel:32];
if (rep == nil)
return GLFW_FALSE;
native = [[NSImage alloc] initWithSize:NSMakeSize(image->width, image->height)];
[native addRepresentation:rep];
memcpy([rep bitmapData], src->pixels, src->width * src->height * 4);
[native addRepresentation:rep];
[rep release];
}
cursor->ns.object = [[NSCursor alloc] initWithImage:native
hotSpot:NSMakePoint(xhot, yhot)];
[native release];
[rep release];
if (cursor->ns.object == nil)
return GLFW_FALSE;
return GLFW_TRUE;
}

295
glfw/dbus_glfw.c vendored Normal file
View File

@@ -0,0 +1,295 @@
//========================================================================
// GLFW 3.3 XKB - www.glfw.org
//------------------------------------------------------------------------
// Copyright (c) 2018 Kovid Goyal <kovid@kovidgoyal.net>
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would
// be appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not
// be misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source
// distribution.
//
//========================================================================
#include "internal.h"
#include "dbus_glfw.h"
#include <stdlib.h>
#include <string.h>
static inline void
report_error(DBusError *err, const char *fmt, ...) {
static char buf[1024];
va_list args;
va_start(args, fmt);
int n = vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
snprintf(buf + n, sizeof(buf), ". DBUS error: %s", err->message);
_glfwInputError(GLFW_PLATFORM_ERROR, "%s", buf);
dbus_error_free(err);
}
static _GLFWDBUSData *dbus_data = NULL;
GLFWbool
glfw_dbus_init(_GLFWDBUSData *dbus, EventLoopData *eld) {
dbus->eld = eld;
dbus_data = dbus;
return GLFW_TRUE;
}
static void
on_dbus_watch_ready(int fd, int events, void *data) {
DBusWatch *watch = (DBusWatch*)data;
unsigned int flags = 0;
if (events & POLLERR) flags |= DBUS_WATCH_ERROR;
if (events & POLLHUP) flags |= DBUS_WATCH_HANGUP;
if (events & POLLIN) flags |= DBUS_WATCH_READABLE;
if (events & POLLOUT) flags |= DBUS_WATCH_WRITABLE;
dbus_watch_handle(watch, flags);
}
static inline int
events_for_watch(DBusWatch *watch) {
int events = 0;
unsigned int flags = dbus_watch_get_flags(watch);
if (flags & DBUS_WATCH_READABLE) events |= POLLIN;
if (flags & DBUS_WATCH_WRITABLE) events |= POLLOUT;
return events;
}
static dbus_bool_t
add_dbus_watch(DBusWatch *watch, void *data) {
id_type watch_id = addWatch(dbus_data->eld, data, dbus_watch_get_unix_fd(watch), events_for_watch(watch), dbus_watch_get_enabled(watch), on_dbus_watch_ready, watch);
if (!watch_id) return FALSE;
id_type *idp = malloc(sizeof(id_type));
if (!idp) return FALSE;
*idp = watch_id;
dbus_watch_set_data(watch, idp, free);
return TRUE;
}
static void
remove_dbus_watch(DBusWatch *watch, void *data) {
id_type *idp = dbus_watch_get_data(watch);
if (idp) removeWatch(dbus_data->eld, *idp);
}
static void
toggle_dbus_watch(DBusWatch *watch, void *data) {
id_type *idp = dbus_watch_get_data(watch);
if (idp) toggleWatch(dbus_data->eld, *idp, dbus_watch_get_enabled(watch));
}
static void
on_dbus_timer_ready(id_type timer_id, void *data) {
if (data) {
DBusTimeout *t = (DBusTimeout*)data;
dbus_timeout_handle(t);
}
}
static dbus_bool_t
add_dbus_timeout(DBusTimeout *timeout, void *data) {
int enabled = dbus_timeout_get_enabled(timeout) ? 1 : 0;
double interval = ((double)dbus_timeout_get_interval(timeout)) / 1000.0;
if (interval < 0) return FALSE;
id_type timer_id = addTimer(dbus_data->eld, data, interval, enabled, on_dbus_timer_ready, timeout);
if (!timer_id) return FALSE;
id_type *idp = malloc(sizeof(id_type));
if (!idp) {
removeTimer(dbus_data->eld, timer_id);
return FALSE;
}
*idp = timer_id;
dbus_timeout_set_data(timeout, idp, free);
return TRUE;
}
static void
remove_dbus_timeout(DBusTimeout *timeout, void *data) {
id_type *idp = dbus_timeout_get_data(timeout);
if (idp) removeTimer(dbus_data->eld, *idp);
}
static void
toggle_dbus_timeout(DBusTimeout *timeout, void *data) {
id_type *idp = dbus_timeout_get_data(timeout);
if (idp) toggleTimer(dbus_data->eld, *idp, dbus_timeout_get_enabled(timeout));
}
DBusConnection*
glfw_dbus_connect_to(const char *path, const char* err_msg, const char *name, GLFWbool register_on_bus) {
DBusError err;
dbus_error_init(&err);
DBusConnection *ans = dbus_connection_open_private(path, &err);
if (!ans) {
report_error(&err, err_msg);
return NULL;
}
dbus_connection_set_exit_on_disconnect(ans, FALSE);
dbus_error_free(&err);
if (register_on_bus) {
if (!dbus_bus_register(ans, &err)) {
report_error(&err, err_msg);
return NULL;
}
}
if (!dbus_connection_set_watch_functions(ans, add_dbus_watch, remove_dbus_watch, toggle_dbus_watch, (void*)name, NULL)) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Failed to set DBUS watches on connection to: %s", path);
dbus_connection_close(ans);
dbus_connection_unref(ans);
return NULL;
}
if (!dbus_connection_set_timeout_functions(ans, add_dbus_timeout, remove_dbus_timeout, toggle_dbus_timeout, (void*)name, NULL)) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Failed to set DBUS timeout functions on connection to: %s", path);
dbus_connection_close(ans);
dbus_connection_unref(ans);
return NULL;
}
return ans;
}
void
glfw_dbus_dispatch(DBusConnection *conn) {
while(dbus_connection_dispatch(conn) == DBUS_DISPATCH_DATA_REMAINS);
}
void
glfw_dbus_terminate(_GLFWDBUSData *dbus) {
if (dbus_data) {
dbus_data->eld = NULL;
dbus_data = NULL;
}
}
void
glfw_dbus_close_connection(DBusConnection *conn) {
dbus_connection_close(conn);
dbus_connection_unref(conn);
}
GLFWbool
glfw_dbus_get_args(DBusMessage *msg, const char *failmsg, ...) {
DBusError err;
dbus_error_init(&err);
va_list ap;
va_start(ap, failmsg);
int firstarg = va_arg(ap, int);
GLFWbool ret = dbus_message_get_args_valist(msg, &err, firstarg, ap) ? GLFW_TRUE : GLFW_FALSE;
va_end(ap);
if (!ret) report_error(&err, failmsg);
return ret;
}
typedef struct {
dbus_pending_callback callback;
void *user_data;
} MethodResponse;
static const char*
format_message_error(DBusError *err) {
static char buf[1024];
snprintf(buf, sizeof(buf), "[%s] %s", err->name ? err->name : "", err->message);
return buf;
}
static void
method_reply_received(DBusPendingCall *pending, void *user_data) {
MethodResponse *res = (MethodResponse*)user_data;
DBusMessage *msg = dbus_pending_call_steal_reply(pending);
if (msg) {
DBusError err;
dbus_error_init(&err);
if (dbus_set_error_from_message(&err, msg)) res->callback(NULL, format_message_error(&err), res->user_data);
else res->callback(msg, NULL, res->user_data);
dbus_message_unref(msg);
}
}
static GLFWbool
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 GLFW_FALSE;
DBusMessage *msg = dbus_message_new_method_call(node, path, interface, method);
if (!msg) return GLFW_FALSE;
GLFWbool retval = GLFW_FALSE;
MethodResponse *res = malloc(sizeof(MethodResponse));
if (!res) { dbus_message_unref(msg); return GLFW_FALSE; }
res->callback = callback;
res->user_data = user_data;
int firstarg = va_arg(ap, int);
if ((firstarg == DBUS_TYPE_INVALID) || dbus_message_append_args_valist(msg, firstarg, ap)) {
if (callback) {
DBusPendingCall *pending = NULL;
if (dbus_connection_send_with_reply(conn, msg, &pending, timeout)) {
dbus_pending_call_set_notify(pending, method_reply_received, res, free);
retval = GLFW_TRUE;
} else {
_glfwInputError(GLFW_PLATFORM_ERROR, "Failed to call DBUS method: %s on node: %s and interface: %s out of memory", method, node, interface);
}
} else {
if (dbus_connection_send(conn, msg, NULL)) {
retval = GLFW_TRUE;
} else {
_glfwInputError(GLFW_PLATFORM_ERROR, "Failed to call DBUS method: %s on node: %s and interface: %s out of memory", method, node, interface);
}
}
} else {
_glfwInputError(GLFW_PLATFORM_ERROR, "Failed to call DBUS method: %s on node: %s and interface: %s could not add arguments", method, node, interface);
}
dbus_message_unref(msg);
return retval;
}
GLFWbool
glfw_dbus_call_method_with_reply(DBusConnection *conn, const char *node, const char *path, const char *interface, const char *method, int timeout, dbus_pending_callback callback, void* user_data, ...) {
GLFWbool retval;
va_list ap;
va_start(ap, user_data);
retval = call_method(conn, node, path, interface, method, timeout, callback, user_data, ap);
va_end(ap);
return retval;
}
GLFWbool
glfw_dbus_call_method_no_reply(DBusConnection *conn, const char *node, const char *path, const char *interface, const char *method, ...) {
GLFWbool retval;
va_list ap;
va_start(ap, method);
retval = call_method(conn, node, path, interface, method, DBUS_TIMEOUT_USE_DEFAULT, NULL, NULL, ap);
va_end(ap);
return retval;
}
int
glfw_dbus_match_signal(DBusMessage *msg, const char *interface, ...) {
va_list ap;
va_start(ap, interface);
int ans = -1, num = -1;
while(1) {
num++;
const char *name = va_arg(ap, const char*);
if (!name) break;
if (dbus_message_is_signal(msg, interface, name)) { ans = num; break; }
}
va_end(ap);
return ans;
}

50
glfw/dbus_glfw.h vendored Normal file
View File

@@ -0,0 +1,50 @@
//========================================================================
// GLFW 3.3 XKB - www.glfw.org
//------------------------------------------------------------------------
// Copyright (c) 2018 Kovid Goyal <kovid@kovidgoyal.net>
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would
// be appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not
// be misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source
// distribution.
//
//========================================================================
#pragma once
#include <dbus/dbus.h>
#include "backend_utils.h"
typedef void(*dbus_pending_callback)(DBusMessage *msg, const char* err, void* data);
typedef struct {
EventLoopData* eld;
} _GLFWDBUSData;
GLFWbool glfw_dbus_init(_GLFWDBUSData *dbus, EventLoopData *eld);
void glfw_dbus_terminate(_GLFWDBUSData *dbus);
DBusConnection* glfw_dbus_connect_to(const char *path, const char* err_msg, const char* name, GLFWbool register_on_bus);
void glfw_dbus_close_connection(DBusConnection *conn);
GLFWbool
glfw_dbus_call_method_no_reply(DBusConnection *conn, const char *node, const char *path, const char *interface, const char *method, ...);
GLFWbool
glfw_dbus_call_method_with_reply(DBusConnection *conn, const char *node, const char *path, const char *interface, const char *method, int timeout_ms, dbus_pending_callback callback, void *user_data, ...);
void glfw_dbus_dispatch(DBusConnection *);
GLFWbool glfw_dbus_get_args(DBusMessage *msg, const char *failmsg, ...);
int glfw_dbus_match_signal(DBusMessage *msg, const char *interface, ...);

View File

@@ -55,7 +55,7 @@ def init_env(env, pkg_config, at_least_version, module='x11'):
at_least_version('xkbcommon', 0, 5)
if module == 'x11':
for dep in 'x11 xrandr xinerama xcursor xkbcommon xkbcommon-x11 x11-xcb'.split():
for dep in 'x11 xrandr xinerama xcursor xkbcommon xkbcommon-x11 x11-xcb dbus-1'.split():
ans.cflags.extend(pkg_config(dep, '--cflags-only-I'))
ans.ldpaths.extend(pkg_config(dep, '--libs'))
@@ -67,11 +67,13 @@ def init_env(env, pkg_config, at_least_version, module='x11'):
at_least_version('wayland-protocols', *sinfo['wayland_protocols'])
ans.wayland_packagedir = os.path.abspath(pkg_config('wayland-protocols', '--variable=pkgdatadir')[0])
ans.wayland_scanner = os.path.abspath(pkg_config('wayland-scanner', '--variable=wayland_scanner')[0])
scanner_version = tuple(map(int, pkg_config('wayland-scanner', '--modversion')[0].strip().split('.')))
ans.wayland_scanner_code = 'private-code' if scanner_version >= (1, 14, 91) else 'code'
ans.wayland_protocols = tuple(sinfo[module]['protocols'])
for p in ans.wayland_protocols:
ans.sources.append(wayland_protocol_file_name(p))
ans.all_headers.append(wayland_protocol_file_name(p, 'h'))
for dep in 'wayland-egl wayland-client wayland-cursor xkbcommon'.split():
for dep in 'wayland-egl wayland-client wayland-cursor xkbcommon dbus-1'.split():
ans.cflags.extend(pkg_config(dep, '--cflags-only-I'))
ans.ldpaths.extend(pkg_config(dep, '--libs'))
@@ -87,7 +89,7 @@ def build_wayland_protocols(env, run_tool, emphasis, newer, dest_dir):
dest = wayland_protocol_file_name(src, ext)
dest = os.path.join(dest_dir, dest)
if newer(dest, src):
q = 'client-header' if ext == 'h' else 'code'
q = 'client-header' if ext == 'h' else env.wayland_scanner_code
run_tool([env.wayland_scanner, q, src, dest],
desc='Generating {} ...'.format(emphasis(os.path.basename(dest))))
@@ -208,6 +210,7 @@ def generate_wrappers(glfw_header, glfw_native_header):
int32_t glfwGetX11Window(GLFWwindow* window)
void glfwSetX11SelectionString(const char* string)
const char* glfwGetX11SelectionString(void)
int glfwGetXKBScancode(const char* key_name, int case_sensitive)
'''.splitlines():
if line:
functions.append(Function(line.strip(), check_fail=False))
@@ -262,8 +265,8 @@ unload_glfw() {
f.write(code)
def main():
os.chdir(sys.argv[-1])
def from_glfw(glfw_dir):
os.chdir(glfw_dir)
sinfo = collect_source_information()
files_to_copy = set()
for x in sinfo.values():
@@ -280,7 +283,6 @@ def main():
for src in files_to_copy:
shutil.copy2(src, '.')
shutil.copy2(glfw_header, '.')
patch_in_file('internal.h', lambda x: x.replace('../include/GLFW/', ''))
patch_in_file('cocoa_window.m', lambda x: re.sub(
r'[(]void[)]loadMainMenu.+?}', '(void)loadMainMenu\n{ // removed by Kovid as it generated compiler warnings \n}\n', x, flags=re.DOTALL))
json.dump(
@@ -293,5 +295,26 @@ def main():
generate_wrappers(glfw_header, glfw_native_header)
def to_glfw(glfw_dir):
src = base
for x in os.listdir(src):
if x in ('glfw.py', 'glfw3.h', '__pycache__', 'source-info.json') or x.startswith('wayland-'):
continue
xp = os.path.join(src, x)
shutil.copyfile(xp, os.path.join(glfw_dir, 'src', x))
shutil.copyfile(os.path.join(src, 'glfw3.h'), os.path.join(glfw_dir, 'include/GLFW/glfw3.h'))
def main():
glfw_dir = os.path.abspath(os.path.join(base, '../../glfw'))
q = sys.argv[1].lower().replace('_', '-')
if q == 'from-glfw':
from_glfw(glfw_dir)
elif q == 'to-glfw':
to_glfw(glfw_dir)
else:
raise SystemExit('First argument must be one of to-glfw or from-glfw')
if __name__ == '__main__':
main()

38
glfw/glfw3.h vendored
View File

@@ -1057,6 +1057,8 @@ extern "C" {
/*! @addtogroup init
* @{ */
#define GLFW_JOYSTICK_HAT_BUTTONS 0x00050001
#define GLFW_DEBUG_KEYBOARD 0x00050002
#define GLFW_ENABLE_JOYSTICKS 0x00050003
#define GLFW_COCOA_CHDIR_RESOURCES 0x00051001
#define GLFW_COCOA_MENUBAR 0x00051002
@@ -1371,15 +1373,17 @@ typedef void (* GLFWcursorenterfun)(GLFWwindow*,int);
* @param[in] window The window that received the event.
* @param[in] xoffset The scroll offset along the x-axis.
* @param[in] yoffset The scroll offset along the y-axis.
* @param[in] flags A bit-mask providing extra data about the event. flags & 1 will be true if and only if the offset values are "high-precision". Typically pixel values. Otherwise the offset values are number of lines.
*
* @sa @ref scrolling
* @sa @ref glfwSetScrollCallback
*
* @since Added in version 3.0. Replaces `GLFWmousewheelfun`.
* @since Changed in version 4.0. Added `flags` parameter.
*
* @ingroup input
*/
typedef void (* GLFWscrollfun)(GLFWwindow*,double,double);
typedef void (* GLFWscrollfun)(GLFWwindow*,double,double,int);
/*! @brief The function signature for key callbacks.
*
@@ -1398,7 +1402,9 @@ typedef void (* GLFWscrollfun)(GLFWwindow*,double,double);
* @param[in] mods Bit field describing which [modifier keys](@ref mods) were
* held down.
* @param[in] text UTF-8 encoded text generated by this key event or empty string.
* @param[in] reserved Reserved for future use.
* @param[in] Used for Input Method events. Zero for normal key events.
* A value of 1 means the pre-edit text for the input event has been changed.
* A value of 2 means the text should be committed.
*
* @note On X11/Wayland if a modifier other than the modifiers GLFW reports
* (ctrl/shift/alt/super) is used, GLFW will report the shifted key rather
@@ -4006,8 +4012,8 @@ GLFWAPI int glfwGetKey(GLFWwindow* window, int key);
* `GLFW_RELEASE`.
*
* If the @ref GLFW_STICKY_MOUSE_BUTTONS input mode is enabled, this function
* `GLFW_PRESS` the first time you call it for a mouse button that was pressed,
* even if that mouse button has already been released.
* returns `GLFW_PRESS` the first time you call it for a mouse button that was
* pressed, even if that mouse button has already been released.
*
* @param[in] window The desired window.
* @param[in] button The desired [mouse button](@ref buttons).
@@ -4122,6 +4128,7 @@ GLFWAPI void glfwSetCursorPos(GLFWwindow* window, double xpos, double ypos);
* @param[in] image The desired cursor image.
* @param[in] xhot The desired x-coordinate, in pixels, of the cursor hotspot.
* @param[in] yhot The desired y-coordinate, in pixels, of the cursor hotspot.
* @param[in] count The number of images. Used on Cocoa for retina cursors. The first image should be the 1:1 scale image.
* @return The handle of the created cursor, or `NULL` if an
* [error](@ref error_handling) occurred.
*
@@ -4137,11 +4144,11 @@ GLFWAPI void glfwSetCursorPos(GLFWwindow* window, double xpos, double ypos);
* @sa @ref glfwDestroyCursor
* @sa @ref glfwCreateStandardCursor
*
* @since Added in version 3.1.
* @since Added in version 3.1. Changed in 4.0 to add the count parameter.
*
* @ingroup input
*/
GLFWAPI GLFWcursor* glfwCreateCursor(const GLFWimage* image, int xhot, int yhot);
GLFWAPI GLFWcursor* glfwCreateCursor(const GLFWimage* image, int xhot, int yhot, int count);
/*! @brief Creates a cursor with a standard shape.
*
@@ -4226,6 +4233,23 @@ GLFWAPI void glfwSetCursor(GLFWwindow* window, GLFWcursor* cursor);
*/
GLFWAPI GLFWkeyboardfun glfwSetKeyboardCallback(GLFWwindow* window, GLFWkeyboardfun cbfun);
/*! @brief Notifies the OS Input Method Event system of changes to application input state
*
* Used to notify the IME system of changes in state such as focus gained/lost
* and text cursor position.
*
* @param which: What data to notify. 1 means focus and 2 means cursor position.
* @param a, b, c, d: Interpreted based on the value of which. When which is 1
* a is interpreted as a boolean indicating focus gained/lost. When which is 2
* a, b, c, d are the cursor x, y, width and height values (in the window co-ordinate
* system).
*
* @ingroup input
* @since Added in version 4.0
*/
GLFWAPI void glfwUpdateIMEState(GLFWwindow* window, int which, int a, int b, int c, int d);
/*! @brief Sets the mouse button callback.
*
* This function sets the mouse button callback of the specified window, which
@@ -4786,6 +4810,8 @@ GLFWAPI const char* glfwGetGamepadName(int jid);
* @errors Possible errors include @ref GLFW_NOT_INITIALIZED and @ref
* GLFW_INVALID_ENUM.
*
* @thread_safety This function must only be called from the main thread.
*
* @sa @ref gamepad
* @sa @ref glfwUpdateGamepadMappings
* @sa @ref glfwJoystickIsGamepad

427
glfw/ibus_glfw.c vendored Normal file
View File

@@ -0,0 +1,427 @@
//========================================================================
// GLFW 3.3 XKB - www.glfw.org
//------------------------------------------------------------------------
// Copyright (c) 2018 Kovid Goyal <kovid@kovidgoyal.net>
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would
// be appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not
// be misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source
// distribution.
//
//========================================================================
#define _GNU_SOURCE
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <limits.h>
#include "internal.h"
#include "ibus_glfw.h"
#define debug(...) if (_glfw.hints.init.debugKeyboard) printf(__VA_ARGS__);
static const char IBUS_SERVICE[] = "org.freedesktop.IBus";
static const char IBUS_PATH[] = "/org/freedesktop/IBus";
static const char IBUS_INTERFACE[] = "org.freedesktop.IBus";
static const char IBUS_INPUT_INTERFACE[] = "org.freedesktop.IBus.InputContext";
enum Capabilities {
IBUS_CAP_PREEDIT_TEXT = 1 << 0,
IBUS_CAP_AUXILIARY_TEXT = 1 << 1,
IBUS_CAP_LOOKUP_TABLE = 1 << 2,
IBUS_CAP_FOCUS = 1 << 3,
IBUS_CAP_PROPERTY = 1 << 4,
IBUS_CAP_SURROUNDING_TEXT = 1 << 5
};
static inline GLFWbool
test_env_var(const char *name, const char *val) {
const char *q = getenv(name);
return (q && strcmp(q, val) == 0) ? GLFW_TRUE : GLFW_FALSE;
}
static inline GLFWbool
MIN(size_t a, size_t b) {
return a < b ? a : b;
}
static const char*
get_ibus_text_from_message(DBusMessage *msg) {
/* The message structure is (from dbus-monitor)
variant struct {
string "IBusText"
array [
]
string "ash "
variant struct {
string "IBusAttrList"
array [
]
array [
]
}
}
*/
const char *text = NULL;
const char *struct_id = NULL;
DBusMessageIter iter, sub1, sub2;
dbus_message_iter_init(msg, &iter);
if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT) return NULL;
dbus_message_iter_recurse(&iter, &sub1);
if (dbus_message_iter_get_arg_type(&sub1) != DBUS_TYPE_STRUCT) return NULL;
dbus_message_iter_recurse(&sub1, &sub2);
if (dbus_message_iter_get_arg_type(&sub2) != DBUS_TYPE_STRING) return NULL;
dbus_message_iter_get_basic(&sub2, &struct_id);
if (!struct_id || strncmp(struct_id, "IBusText", sizeof("IBusText")) != 0) return NULL;
dbus_message_iter_next(&sub2);
dbus_message_iter_next(&sub2);
if (dbus_message_iter_get_arg_type(&sub2) != DBUS_TYPE_STRING) return NULL;
dbus_message_iter_get_basic(&sub2, &text);
return text;
}
static inline void
send_text(const char *text, int state) {
_GLFWwindow *w = _glfwFocusedWindow();
if (w && w->callbacks.keyboard) {
w->callbacks.keyboard((GLFWwindow*) w, GLFW_KEY_UNKNOWN, 0, GLFW_PRESS, 0, text, state);
}
}
// Connection handling {{{
static DBusHandlerResult
message_handler(DBusConnection *conn, DBusMessage *msg, void *user_data) {
// To monitor signals from IBUS, use
//  dbus-monitor --address `ibus address` "type='signal',interface='org.freedesktop.IBus.InputContext'"
_GLFWIBUSData *ibus = (_GLFWIBUSData*)user_data;
(void)ibus;
const char *text;
switch(glfw_dbus_match_signal(msg, IBUS_INPUT_INTERFACE, "CommitText", "UpdatePreeditText", "HidePreeditText", "ShowPreeditText", NULL)) {
case 0:
text = get_ibus_text_from_message(msg);
debug("IBUS: CommitText: '%s'\n", text ? text : "(nil)");
send_text(text, 2);
break;
case 1:
text = get_ibus_text_from_message(msg);
send_text(text, 1);
debug("IBUS: UpdatePreeditText: '%s'\n", text ? text : "(nil)");
break;
case 2:
debug("IBUS: HidePreeditText\n");
break;
case 3:
debug("IBUS: ShowPreeditText\n");
break;
}
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
static inline const char*
get_ibus_address_file_name(void) {
const char *addr;
static char ans[PATH_MAX];
addr = getenv("IBUS_ADDRESS");
int offset = 0;
if (addr && addr[0]) {
memcpy(ans, addr, MIN(strlen(addr), sizeof(ans)));
return ans;
}
const char *de = getenv("DISPLAY");
if (!de || !de[0]) de = ":0.0";
char *display = _glfw_strdup(de);
const char *host = display;
char *disp_num = strrchr(display, ':');
char *screen_num = strrchr(display, '.');
if (!disp_num) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Could not get IBUS address file name as DISPLAY env var has no colon");
free(display);
return NULL;
}
*disp_num = 0;
disp_num++;
if (screen_num) *screen_num = 0;
if (!*host) host = "unix";
memset(ans, 0, sizeof(ans));
const char *conf_env = getenv("XDG_CONFIG_HOME");
if (conf_env && conf_env[0]) {
offset = snprintf(ans, sizeof(ans), "%s", conf_env);
} else {
conf_env = getenv("HOME");
if (!conf_env || !conf_env[0]) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Could not get IBUS address file name as no HOME env var is set");
free(display);
return NULL;
}
offset = snprintf(ans, sizeof(ans), "%s/.config", conf_env);
}
char *key = dbus_get_local_machine_id();
snprintf(ans + offset, sizeof(ans) - offset, "/ibus/bus/%s-%s-%s", key, host, disp_num);
dbus_free(key);
free(display);
return ans;
}
static inline GLFWbool
read_ibus_address(_GLFWIBUSData *ibus) {
static char buf[1024];
struct stat s;
FILE *addr_file = fopen(ibus->address_file_name, "r");
if (!addr_file) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Failed to open IBUS address file: %s with error: %s", ibus->address_file_name, strerror(errno));
return GLFW_FALSE;
}
int stat_result = fstat(fileno(addr_file), &s);
GLFWbool found = GLFW_FALSE;
while (fgets(buf, sizeof(buf), addr_file)) {
if (strncmp(buf, "IBUS_ADDRESS=", sizeof("IBUS_ADDRESS=")-1) == 0) {
size_t sz = strlen(buf);
if (buf[sz-1] == '\n') buf[sz-1] = 0;
if (buf[sz-2] == '\r') buf[sz-2] = 0;
found = GLFW_TRUE;
break;
}
}
fclose(addr_file); addr_file = NULL;
if (stat_result != 0) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Failed to stat IBUS address file: %s with error: %s", ibus->address_file_name, strerror(errno));
return GLFW_FALSE;
}
ibus->address_file_mtime = s.st_mtime;
if (found) {
free((void*)ibus->address);
ibus->address = _glfw_strdup(buf + sizeof("IBUS_ADDRESS=") - 1);
return GLFW_TRUE;
}
_glfwInputError(GLFW_PLATFORM_ERROR, "Could not find IBUS_ADDRESS in %s", ibus->address_file_name);
return GLFW_FALSE;
}
void
input_context_created(DBusMessage *msg, const char* errmsg, void *data) {
if (errmsg) {
_glfwInputError(GLFW_PLATFORM_ERROR, "IBUS: Failed to create input context with error: %s", errmsg);
return;
}
const char *path = NULL;
if (!glfw_dbus_get_args(msg, "Failed to get IBUS context path from reply", DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID)) return;
_GLFWIBUSData *ibus = (_GLFWIBUSData*)data;
free((void*)ibus->input_ctx_path);
ibus->input_ctx_path = _glfw_strdup(path);
if (!ibus->input_ctx_path) return;
dbus_bus_add_match(ibus->conn, "type='signal',interface='org.freedesktop.IBus.InputContext'", NULL);
DBusObjectPathVTable ibus_vtable = {.message_function = message_handler};
dbus_connection_try_register_object_path(ibus->conn, ibus->input_ctx_path, &ibus_vtable, ibus, NULL);
enum Capabilities caps = IBUS_CAP_FOCUS | IBUS_CAP_PREEDIT_TEXT;
if (!glfw_dbus_call_method_no_reply(ibus->conn, IBUS_SERVICE, ibus->input_ctx_path, IBUS_INPUT_INTERFACE, "SetCapabilities", DBUS_TYPE_UINT32, &caps, DBUS_TYPE_INVALID)) return;
ibus->ok = GLFW_TRUE;
glfw_ibus_set_focused(ibus, GLFW_FALSE);
glfw_ibus_set_cursor_geometry(ibus, 0, 0, 0, 0);
debug("Connected to IBUS daemon for IME input management\n");
}
GLFWbool
setup_connection(_GLFWIBUSData *ibus) {
const char *client_name = "GLFW_Application";
const char *address_file_name = get_ibus_address_file_name();
ibus->ok = GLFW_FALSE;
if (!address_file_name) return GLFW_FALSE;
free((void*)ibus->address_file_name);
ibus->address_file_name = _glfw_strdup(address_file_name);
if (!read_ibus_address(ibus)) return GLFW_FALSE;
if (ibus->conn) {
glfw_dbus_close_connection(ibus->conn);
ibus->conn = NULL;
}
debug("Connecting to IBUS daemon @ %s for IME input management\n", ibus->address);
ibus->conn = glfw_dbus_connect_to(ibus->address, "Failed to connect to the IBUS daemon, with error", "ibus", GLFW_FALSE);
if (!ibus->conn) return GLFW_FALSE;
free((void*)ibus->input_ctx_path); ibus->input_ctx_path = NULL;
if (!glfw_dbus_call_method_with_reply(
ibus->conn, IBUS_SERVICE, IBUS_PATH, IBUS_INTERFACE, "CreateInputContext", DBUS_TIMEOUT_USE_DEFAULT, input_context_created, ibus,
DBUS_TYPE_STRING, &client_name, DBUS_TYPE_INVALID)) {
return GLFW_FALSE;
}
return GLFW_TRUE;
}
void
glfw_connect_to_ibus(_GLFWIBUSData *ibus) {
if (ibus->inited) return;
if (!test_env_var("XMODIFIERS", "@im=ibus") && !test_env_var("GTK_IM_MODULE", "ibus") && !test_env_var("QT_IM_MODULE", "ibus") && !test_env_var("GLFW_IM_MODULE", "ibus")) return;
if (getenv("GLFW_IM_MODULE") && !test_env_var("GLFW_IM_MODULE", "ibus")) return;
ibus->inited = GLFW_TRUE;
setup_connection(ibus);
}
void
glfw_ibus_terminate(_GLFWIBUSData *ibus) {
if (ibus->conn) {
glfw_dbus_close_connection(ibus->conn);
ibus->conn = NULL;
}
#define F(x) if (ibus->x) { free((void*)ibus->x); ibus->x = NULL; }
F(input_ctx_path);
F(address);
F(address_file_name);
#undef F
ibus->ok = GLFW_FALSE;
}
static GLFWbool
check_connection(_GLFWIBUSData *ibus) {
if (!ibus->inited) return GLFW_FALSE;
if (ibus->conn && dbus_connection_get_is_connected(ibus->conn)) {
return ibus->ok;
}
struct stat s;
if (stat(ibus->address_file_name, &s) != 0 || s.st_mtime != ibus->address_file_mtime) {
if (!read_ibus_address(ibus)) return GLFW_FALSE;
setup_connection(ibus);
}
return GLFW_FALSE;
}
void
glfw_ibus_dispatch(_GLFWIBUSData *ibus) {
if (ibus->conn) glfw_dbus_dispatch(ibus->conn);
}
// }}}
static void
simple_message(_GLFWIBUSData *ibus, const char *method) {
if (check_connection(ibus)) {
glfw_dbus_call_method_no_reply(ibus->conn, IBUS_SERVICE, ibus->input_ctx_path, IBUS_INPUT_INTERFACE, method, DBUS_TYPE_INVALID);
}
}
void
glfw_ibus_set_focused(_GLFWIBUSData *ibus, GLFWbool focused) {
simple_message(ibus, focused ? "FocusIn" : "FocusOut");
}
void
glfw_ibus_set_cursor_geometry(_GLFWIBUSData *ibus, int x, int y, int w, int h) {
if (check_connection(ibus)) {
glfw_dbus_call_method_no_reply(ibus->conn, IBUS_SERVICE, ibus->input_ctx_path, IBUS_INPUT_INTERFACE, "SetCursorLocation",
DBUS_TYPE_INT32, &x, DBUS_TYPE_INT32, &y, DBUS_TYPE_INT32, &w, DBUS_TYPE_INT32, &h, DBUS_TYPE_INVALID);
}
}
typedef enum
{
IBUS_SHIFT_MASK = 1 << 0,
IBUS_LOCK_MASK = 1 << 1,
IBUS_CONTROL_MASK = 1 << 2,
IBUS_MOD1_MASK = 1 << 3,
IBUS_MOD2_MASK = 1 << 4,
IBUS_MOD3_MASK = 1 << 5,
IBUS_MOD4_MASK = 1 << 6,
IBUS_MOD5_MASK = 1 << 7,
IBUS_BUTTON1_MASK = 1 << 8,
IBUS_BUTTON2_MASK = 1 << 9,
IBUS_BUTTON3_MASK = 1 << 10,
IBUS_BUTTON4_MASK = 1 << 11,
IBUS_BUTTON5_MASK = 1 << 12,
/* The next few modifiers are used by XKB, so we skip to the end.
* Bits 15 - 23 are currently unused. Bit 29 is used internally.
*/
/* ibus mask */
IBUS_HANDLED_MASK = 1 << 24,
IBUS_FORWARD_MASK = 1 << 25,
IBUS_IGNORED_MASK = IBUS_FORWARD_MASK,
IBUS_SUPER_MASK = 1 << 26,
IBUS_HYPER_MASK = 1 << 27,
IBUS_META_MASK = 1 << 28,
IBUS_RELEASE_MASK = 1 << 30,
IBUS_MODIFIER_MASK = 0x5f001fff
} IBusModifierType;
static inline uint32_t
ibus_key_state(unsigned int glfw_modifiers, int action) {
uint32_t ans = action == GLFW_RELEASE ? IBUS_RELEASE_MASK : 0;
#define M(g, i) if(glfw_modifiers & GLFW_MOD_##g) ans |= i
M(SHIFT, IBUS_SHIFT_MASK);
M(CAPS_LOCK, IBUS_LOCK_MASK);
M(CONTROL, IBUS_CONTROL_MASK);
M(ALT, IBUS_MOD1_MASK);
M(NUM_LOCK, IBUS_MOD2_MASK);
M(SUPER, IBUS_MOD4_MASK);
#undef M
return ans;
}
void
key_event_processed(DBusMessage *msg, const char* errmsg, void *data) {
uint32_t handled = 0;
KeyEvent *ev = (KeyEvent*)data;
GLFWbool is_release = ev->action == GLFW_RELEASE;
GLFWbool failed = GLFW_FALSE;
if (errmsg) {
_glfwInputError(GLFW_PLATFORM_ERROR, "IBUS: Failed to process key with error: %s", errmsg);
failed = GLFW_TRUE;
} else {
glfw_dbus_get_args(msg, "Failed to get IBUS handled key from reply", DBUS_TYPE_BOOLEAN, &handled, DBUS_TYPE_INVALID);
debug("IBUS processed scancode: 0x%x release: %d handled: %u\n", ev->keycode, is_release, handled);
}
glfw_xkb_key_from_ime(ev, handled ? GLFW_TRUE : GLFW_FALSE, failed);
free(ev);
}
GLFWbool
ibus_process_key(const KeyEvent *ev_, _GLFWIBUSData *ibus) {
if (!check_connection(ibus)) return GLFW_FALSE;
KeyEvent *ev = malloc(sizeof(KeyEvent));
if (!ev) return GLFW_FALSE;
memcpy(ev, ev_, sizeof(KeyEvent));
uint32_t state = ibus_key_state(ev->glfw_modifiers, ev->action);
if (!glfw_dbus_call_method_with_reply(
ibus->conn, IBUS_SERVICE, ibus->input_ctx_path, IBUS_INPUT_INTERFACE, "ProcessKeyEvent",
3000, key_event_processed, ev,
DBUS_TYPE_UINT32, &ev->ibus_sym, DBUS_TYPE_UINT32, &ev->ibus_keycode, DBUS_TYPE_UINT32,
&state, DBUS_TYPE_INVALID)) {
free(ev);
return GLFW_FALSE;
}
return GLFW_TRUE;
}

55
glfw/ibus_glfw.h vendored Normal file
View File

@@ -0,0 +1,55 @@
//========================================================================
// GLFW 3.3 XKB - www.glfw.org
//------------------------------------------------------------------------
// Copyright (c) 2018 Kovid Goyal <kovid@kovidgoyal.net>
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would
// be appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not
// be misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source
// distribution.
//
//========================================================================
#pragma once
#include "dbus_glfw.h"
#include <xkbcommon/xkbcommon.h>
typedef struct {
GLFWbool ok, inited;
time_t address_file_mtime;
DBusConnection *conn;
const char *input_ctx_path, *address_file_name, *address;
} _GLFWIBUSData;
typedef struct {
xkb_keycode_t keycode, ibus_keycode;
xkb_keysym_t keysym, ibus_sym;
unsigned int glfw_modifiers;
int action;
GLFWid window_id;
int glfw_keycode;
char text[64];
} KeyEvent;
void glfw_connect_to_ibus(_GLFWIBUSData *ibus);
void glfw_ibus_terminate(_GLFWIBUSData *ibus);
void glfw_ibus_set_focused(_GLFWIBUSData *ibus, GLFWbool focused);
void glfw_ibus_dispatch(_GLFWIBUSData *ibus);
GLFWbool ibus_process_key(const KeyEvent *ev_, _GLFWIBUSData *ibus);
void glfw_ibus_set_cursor_geometry(_GLFWIBUSData *ibus, int x, int y, int w, int h);

14
glfw/init.c vendored
View File

@@ -51,6 +51,8 @@ static GLFWerrorfun _glfwErrorCallback;
static _GLFWinitconfig _glfwInitHints =
{
GLFW_TRUE, // hat buttons
GLFW_FALSE, // debug keyboard
GLFW_TRUE, // enable joystick
{
GLFW_TRUE, // macOS menu bar
GLFW_TRUE // macOS bundle chdir
@@ -114,8 +116,9 @@ static void terminate(void)
char* _glfw_strdup(const char* source)
{
const size_t length = strlen(source);
char* result = calloc(length + 1, 1);
strcpy(result, source);
char* result = malloc(length + 1);
memcpy(result, source, length);
result[length] = 0;
return result;
}
@@ -252,9 +255,15 @@ GLFWAPI void glfwInitHint(int hint, int value)
{
switch (hint)
{
case GLFW_ENABLE_JOYSTICKS:
_glfwInitHints.enableJoysticks = value;
return;
case GLFW_JOYSTICK_HAT_BUTTONS:
_glfwInitHints.hatButtons = value;
return;
case GLFW_DEBUG_KEYBOARD:
_glfwInitHints.debugKeyboard = value;
return;
case GLFW_COCOA_CHDIR_RESOURCES:
_glfwInitHints.ns.chdir = value;
return;
@@ -311,4 +320,3 @@ GLFWAPI GLFWerrorfun glfwSetErrorCallback(GLFWerrorfun cbfun)
_GLFW_SWAP_POINTERS(_glfwErrorCallback, cbfun);
return cbfun;
}

21
glfw/input.c vendored
View File

@@ -286,10 +286,10 @@ void _glfwInputKeyboard(_GLFWwindow* window, int key, int scancode, int action,
// Notifies shared code of a scroll event
//
void _glfwInputScroll(_GLFWwindow* window, double xoffset, double yoffset)
void _glfwInputScroll(_GLFWwindow* window, double xoffset, double yoffset, int flags)
{
if (window->callbacks.scroll)
window->callbacks.scroll((GLFWwindow*) window, xoffset, yoffset);
window->callbacks.scroll((GLFWwindow*) window, xoffset, yoffset, flags);
}
// Notifies shared code of a mouse button click event
@@ -799,11 +799,12 @@ GLFWAPI void glfwSetCursorPos(GLFWwindow* handle, double xpos, double ypos)
}
}
GLFWAPI GLFWcursor* glfwCreateCursor(const GLFWimage* image, int xhot, int yhot)
GLFWAPI GLFWcursor* glfwCreateCursor(const GLFWimage* image, int xhot, int yhot, int count)
{
_GLFWcursor* cursor;
assert(image != NULL);
assert(count > 0);
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
@@ -811,7 +812,7 @@ GLFWAPI GLFWcursor* glfwCreateCursor(const GLFWimage* image, int xhot, int yhot)
cursor->next = _glfw.cursorListHead;
_glfw.cursorListHead = cursor;
if (!_glfwPlatformCreateCursor(cursor, image, xhot, yhot))
if (!_glfwPlatformCreateCursor(cursor, image, xhot, yhot, count))
{
glfwDestroyCursor((GLFWcursor*) cursor);
return NULL;
@@ -908,6 +909,18 @@ GLFWAPI GLFWkeyboardfun glfwSetKeyboardCallback(GLFWwindow* handle, GLFWkeyboard
return cbfun;
}
GLFWAPI void glfwUpdateIMEState(GLFWwindow* handle, int which, int a, int b, int c, int d) {
_GLFWwindow* window = (_GLFWwindow*) handle;
assert(window != NULL);
_GLFW_REQUIRE_INIT();
#if defined(_GLFW_X11) || defined(_GLFW_WAYLAND)
_glfwPlatformUpdateIMEState(window, which, a, b, c, d);
#else
(void)window; (void)which; (void)a; (void)b; (void)c; (void)d;
#endif
}
GLFWAPI GLFWmousebuttonfun glfwSetMouseButtonCallback(GLFWwindow* handle,
GLFWmousebuttonfun cbfun)
{

12
glfw/internal.h vendored
View File

@@ -59,6 +59,7 @@
#define _GLFW_MESSAGE_SIZE 1024
typedef int GLFWbool;
typedef unsigned long long GLFWid;
typedef struct _GLFWerror _GLFWerror;
typedef struct _GLFWinitconfig _GLFWinitconfig;
@@ -242,6 +243,8 @@ struct _GLFWerror
struct _GLFWinitconfig
{
GLFWbool hatButtons;
GLFWbool debugKeyboard;
GLFWbool enableJoysticks;
struct {
GLFWbool menubar;
GLFWbool chdir;
@@ -376,6 +379,7 @@ struct _GLFWwindow
GLFWbool focusOnShow;
GLFWbool shouldClose;
void* userPointer;
GLFWid id;
GLFWvidmode videoMode;
_GLFWmonitor* monitor;
_GLFWcursor* cursor;
@@ -524,6 +528,7 @@ struct _GLFWlibrary
_GLFWerror* errorListHead;
_GLFWcursor* cursorListHead;
_GLFWwindow* windowListHead;
GLFWid focusedWindowId;
_GLFWmonitor** monitors;
int monitorCount;
@@ -599,7 +604,7 @@ void _glfwPlatformGetCursorPos(_GLFWwindow* window, double* xpos, double* ypos);
void _glfwPlatformSetCursorPos(_GLFWwindow* window, double xpos, double ypos);
void _glfwPlatformSetCursorMode(_GLFWwindow* window, int mode);
int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
const GLFWimage* image, int xhot, int yhot);
const GLFWimage* image, int xhot, int yhot, int count);
int _glfwPlatformCreateStandardCursor(_GLFWcursor* cursor, int shape);
void _glfwPlatformDestroyCursor(_GLFWcursor* cursor);
void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor);
@@ -669,6 +674,7 @@ void _glfwPlatformSetWindowResizable(_GLFWwindow* window, GLFWbool enabled);
void _glfwPlatformSetWindowDecorated(_GLFWwindow* window, GLFWbool enabled);
void _glfwPlatformSetWindowFloating(_GLFWwindow* window, GLFWbool enabled);
void _glfwPlatformSetWindowOpacity(_GLFWwindow* window, float opacity);
void _glfwPlatformUpdateIMEState(_GLFWwindow *w, int which, int a, int b, int c, int d);
void _glfwPlatformPollEvents(void);
void _glfwPlatformWaitEvents(void);
@@ -712,7 +718,7 @@ void _glfwInputWindowCloseRequest(_GLFWwindow* window);
void _glfwInputWindowMonitor(_GLFWwindow* window, _GLFWmonitor* monitor);
void _glfwInputKeyboard(_GLFWwindow* window, int key, int scancode, int action, int mods, const char* text, int state);
void _glfwInputScroll(_GLFWwindow* window, double xoffset, double yoffset);
void _glfwInputScroll(_GLFWwindow* window, double xoffset, double yoffset, int flags);
void _glfwInputMouseClick(_GLFWwindow* window, int button, int action, int mods);
void _glfwInputCursorPos(_GLFWwindow* window, double xpos, double ypos);
void _glfwInputCursorEnter(_GLFWwindow* window, GLFWbool entered);
@@ -765,5 +771,7 @@ const char* _glfwGetKeyName(int key);
GLFWbool _glfwInitVulkan(int mode);
void _glfwTerminateVulkan(void);
const char* _glfwGetVulkanResultString(VkResult result);
_GLFWwindow* _glfwFocusedWindow();
_GLFWwindow* _glfwWindowForId(GLFWid id);
char* _glfw_strdup(const char* source);

2
glfw/null_window.c vendored
View File

@@ -267,7 +267,7 @@ void _glfwPlatformSetCursorMode(_GLFWwindow* window, int mode)
int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
const GLFWimage* image,
int xhot, int yhot)
int xhot, int yhot, int count)
{
return GLFW_TRUE;
}

View File

@@ -58,6 +58,8 @@
"posix_time.h",
"posix_thread.h",
"xkb_glfw.h",
"dbus_glfw.h",
"ibus_glfw.h",
"backend_utils.h",
"egl_context.h",
"osmesa_context.h",
@@ -78,8 +80,11 @@
"posix_time.c",
"posix_thread.c",
"xkb_glfw.c",
"dbus_glfw.c",
"ibus_glfw.c",
"egl_context.c",
"osmesa_context.c",
"backend_utils.c",
"linux_joystick.c",
"null_joystick.c"
]
@@ -112,6 +117,8 @@
"headers": [
"x11_platform.h",
"xkb_glfw.h",
"dbus_glfw.h",
"ibus_glfw.h",
"backend_utils.h",
"posix_time.h",
"posix_thread.h",
@@ -126,11 +133,14 @@
"x11_monitor.c",
"x11_window.c",
"xkb_glfw.c",
"dbus_glfw.c",
"ibus_glfw.c",
"posix_time.c",
"posix_thread.c",
"glx_context.c",
"egl_context.c",
"osmesa_context.c",
"backend_utils.c",
"linux_joystick.c",
"null_joystick.c"
]

View File

@@ -142,6 +142,9 @@ typedef enum
// HACK: Define versionhelpers.h functions manually as MinGW lacks the header
BOOL IsWindowsVersionOrGreater(WORD major, WORD minor, WORD sp);
#define IsWindowsXPOrGreater() \
IsWindowsVersionOrGreater(HIBYTE(_WIN32_WINNT_WINXP), \
LOBYTE(_WIN32_WINNT_WINXP), 0)
#define IsWindowsVistaOrGreater() \
IsWindowsVersionOrGreater(HIBYTE(_WIN32_WINNT_VISTA), \
LOBYTE(_WIN32_WINNT_VISTA), 0)
@@ -300,6 +303,7 @@ typedef struct _GLFWlibraryWin32
_GLFWwindow* disabledCursorWindow;
RAWINPUT* rawInput;
int rawInputSize;
UINT mouseTrailSize;
struct {
HINSTANCE instance;

19
glfw/win32_window.c vendored
View File

@@ -529,7 +529,18 @@ static void fitToMonitor(_GLFWwindow* window)
static void acquireMonitor(_GLFWwindow* window)
{
if (!_glfw.win32.acquiredMonitorCount)
{
SetThreadExecutionState(ES_CONTINUOUS | ES_DISPLAY_REQUIRED);
// HACK: When mouse trails are enabled the cursor becomes invisible when
// the OpenGL ICD switches to page flipping
if (IsWindowsXPOrGreater())
{
SystemParametersInfo(SPI_GETMOUSETRAILS, 0, &_glfw.win32.mouseTrailSize, 0);
SystemParametersInfo(SPI_SETMOUSETRAILS, 0, 0, 0);
}
}
if (!window->monitor->window)
_glfw.win32.acquiredMonitorCount++;
@@ -546,8 +557,14 @@ static void releaseMonitor(_GLFWwindow* window)
_glfw.win32.acquiredMonitorCount--;
if (!_glfw.win32.acquiredMonitorCount)
{
SetThreadExecutionState(ES_CONTINUOUS);
// HACK: Restore mouse trail length saved in acquireMonitor
if (IsWindowsXPOrGreater())
SystemParametersInfo(SPI_SETMOUSETRAILS, _glfw.win32.mouseTrailSize, 0, 0);
}
_glfwInputMonitorWindow(window->monitor, NULL);
_glfwRestoreVideoModeWin32(window->monitor);
}
@@ -1839,7 +1856,7 @@ int _glfwPlatformGetKeyScancode(int key)
int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
const GLFWimage* image,
int xhot, int yhot)
int xhot, int yhot, int count)
{
cursor->win32.handle = (HCURSOR) createIcon(image, xhot, yhot, GLFW_FALSE);
if (!cursor->win32.handle)

24
glfw/window.c vendored
View File

@@ -48,6 +48,7 @@ void _glfwInputWindowFocus(_GLFWwindow* window, GLFWbool focused)
if (!focused)
{
int key, button;
_glfw.focusedWindowId = 0;
for (key = 0; key <= GLFW_KEY_LAST; key++)
{
@@ -63,7 +64,28 @@ void _glfwInputWindowFocus(_GLFWwindow* window, GLFWbool focused)
if (window->mouseButtons[button] == GLFW_PRESS)
_glfwInputMouseClick(window, button, GLFW_RELEASE, 0);
}
} else
_glfw.focusedWindowId = window->id;
}
_GLFWwindow* _glfwFocusedWindow() {
if (_glfw.focusedWindowId) {
_GLFWwindow *w = _glfw.windowListHead;
while (w) {
if (w->id == _glfw.focusedWindowId) return w;
w = w->next;
}
}
return NULL;
}
_GLFWwindow* _glfwWindowForId(GLFWid id) {
_GLFWwindow *w = _glfw.windowListHead;
while (w) {
if (w->id == _glfw.focusedWindowId) return w;
w = w->next;
}
return NULL;
}
// Notifies shared code that a window has moved
@@ -185,8 +207,10 @@ GLFWAPI GLFWwindow* glfwCreateWindow(int width, int height,
if (!_glfwIsValidContextConfig(&ctxconfig))
return NULL;
static GLFWid windowIdCounter = 0;
window = calloc(1, sizeof(_GLFWwindow));
window->next = _glfw.windowListHead;
window->id = ++windowIdCounter;
_glfw.windowListHead = window;
window->videoMode.width = width;

43
glfw/wl_init.c vendored
View File

@@ -164,7 +164,7 @@ static void pointerHandleMotion(void* data,
wl_fixed_t sy)
{
_GLFWwindow* window = _glfw.wl.pointerFocus;
const char* cursorName;
const char* cursorName = NULL;
if (!window)
return;
@@ -308,7 +308,7 @@ static void pointerHandleButton(void* data,
state == WL_POINTER_BUTTON_STATE_PRESSED
? GLFW_PRESS
: GLFW_RELEASE,
_glfw.wl.xkb.modifiers);
_glfw.wl.xkb.states.modifiers);
}
static void pointerHandleAxis(void* data,
@@ -319,11 +319,6 @@ static void pointerHandleAxis(void* data,
{
_GLFWwindow* window = _glfw.wl.pointerFocus;
double x = 0.0, y = 0.0;
// Wayland scroll events are in pointer motion coordinate space (think two
// finger scroll). The factor 10 is commonly used to convert to "scroll
// step means 1.0.
const double scrollFactor = 1.0 / 10.0;
if (!window)
return;
@@ -331,11 +326,11 @@ static void pointerHandleAxis(void* data,
axis == WL_POINTER_AXIS_VERTICAL_SCROLL);
if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL)
x = wl_fixed_to_double(value) * scrollFactor;
x = wl_fixed_to_double(value) * -1;
else if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL)
y = wl_fixed_to_double(value) * scrollFactor;
y = wl_fixed_to_double(value) * -1;
_glfwInputScroll(window, x, y);
_glfwInputScroll(window, x, y, 1);
}
static const struct wl_pointer_listener pointerListener = {
@@ -407,6 +402,15 @@ static void keyboardHandleLeave(void* data,
_glfwInputWindowFocus(window, GLFW_FALSE);
}
static void
dispatchPendingKeyRepeats(id_type timer_id, void *data) {
if (_glfw.wl.keyRepeatInfo.keyboardFocus != _glfw.wl.keyboardFocus || _glfw.wl.keyboardRepeatRate == 0) return;
glfw_xkb_handle_key_event(_glfw.wl.keyRepeatInfo.keyboardFocus, &_glfw.wl.xkb, _glfw.wl.keyRepeatInfo.key, GLFW_REPEAT);
changeTimerInterval(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, ((double)_glfw.wl.keyboardRepeatRate) / 1000.0);
toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, 1);
}
static void keyboardHandleKey(void* data,
struct wl_keyboard* keyboard,
uint32_t serial,
@@ -419,14 +423,18 @@ static void keyboardHandleKey(void* data,
return;
int action = state == WL_KEYBOARD_KEY_STATE_PRESSED ? GLFW_PRESS : GLFW_RELEASE;
glfw_xkb_handle_key_event(window, &_glfw.wl.xkb, key, action);
_glfw.wl.keyRepeatInfo.nextRepeatAt = 0;
GLFWbool repeatable = GLFW_FALSE;
if (action == GLFW_PRESS && _glfw.wl.keyboardRepeatRate > 0 && glfw_xkb_should_repeat(&_glfw.wl.xkb, key))
{
_glfw.wl.keyRepeatInfo.key = key;
_glfw.wl.keyRepeatInfo.nextRepeatAt = glfwGetTime() + (double)(_glfw.wl.keyboardRepeatDelay) / 1000.0;
repeatable = GLFW_TRUE;
_glfw.wl.keyRepeatInfo.keyboardFocus = window;
}
if (repeatable) {
changeTimerInterval(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, (double)(_glfw.wl.keyboardRepeatDelay) / 1000.0);
}
toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, repeatable ? 1 : 0);
}
static void keyboardHandleModifiers(void* data,
@@ -664,7 +672,9 @@ int _glfwPlatformInit(void)
"Wayland: Failed to connect to display");
return GLFW_FALSE;
}
initPollData(_glfw.wl.eventLoopData.fds, _glfw.wl.eventLoopData.wakeupFds[0], wl_display_get_fd(_glfw.wl.display));
initPollData(&_glfw.wl.eventLoopData, _glfw.wl.eventLoopData.wakeupFds[0], wl_display_get_fd(_glfw.wl.display));
glfw_dbus_init(&_glfw.wl.dbus, &_glfw.wl.eventLoopData);
_glfw.wl.keyRepeatInfo.keyRepeatTimer = addTimer(&_glfw.wl.eventLoopData, "wayland-keyrepeat", 0.5, 0, dispatchPendingKeyRepeats, NULL);
_glfw.wl.registry = wl_display_get_registry(_glfw.wl.display);
wl_registry_add_listener(_glfw.wl.registry, &registryListener, NULL);
@@ -678,8 +688,10 @@ int _glfwPlatformInit(void)
wl_display_roundtrip(_glfw.wl.display);
#ifdef __linux__
if (!_glfwInitJoysticksLinux())
return GLFW_FALSE;
if (_glfw.hints.init.enableJoysticks) {
if (!_glfwInitJoysticksLinux())
return GLFW_FALSE;
}
#endif
_glfwInitTimerPOSIX();
@@ -713,6 +725,7 @@ void _glfwPlatformTerminate(void)
}
glfw_xkb_release(&_glfw.wl.xkb);
glfw_dbus_terminate(&_glfw.wl.dbus);
if (_glfw.wl.cursorTheme)
wl_cursor_theme_destroy(_glfw.wl.cursorTheme);

9
glfw/wl_platform.h vendored
View File

@@ -49,6 +49,7 @@ typedef VkBool32 (APIENTRY *PFN_vkGetPhysicalDeviceWaylandPresentationSupportKHR
#else
#include "null_joystick.h"
#endif
#include "backend_utils.h"
#include "xkb_glfw.h"
#include "egl_context.h"
#include "osmesa_context.h"
@@ -204,10 +205,11 @@ typedef struct _GLFWlibraryWayland
int32_t keyboardRepeatDelay;
struct {
uint32_t key;
double nextRepeatAt;
id_type keyRepeatTimer;
_GLFWwindow* keyboardFocus;
} keyRepeatInfo;
_GLFWXKBData xkb;
_GLFWDBUSData dbus;
_GLFWwindow* pointerFocus;
_GLFWwindow* keyboardFocus;
@@ -229,10 +231,7 @@ typedef struct _GLFWlibraryWayland
PFN_wl_egl_window_resize window_resize;
} egl;
struct {
struct pollfd fds[2];
int wakeupFds[2];
} eventLoopData;
EventLoopData eventLoopData;
} _GLFWlibraryWayland;

66
glfw/wl_window.c vendored
View File

@@ -688,34 +688,14 @@ static GLFWbool createXdgSurface(_GLFWwindow* window)
return GLFW_TRUE;
}
static void
dispatchPendingKeyRepeats() {
if (_glfw.wl.keyRepeatInfo.nextRepeatAt <= 0 || _glfw.wl.keyRepeatInfo.keyboardFocus != _glfw.wl.keyboardFocus || _glfw.wl.keyboardRepeatRate == 0) return;
double now = glfwGetTime();
while (_glfw.wl.keyRepeatInfo.nextRepeatAt <= now) {
glfw_xkb_handle_key_event(_glfw.wl.keyRepeatInfo.keyboardFocus, &_glfw.wl.xkb, _glfw.wl.keyRepeatInfo.key, GLFW_REPEAT);
_glfw.wl.keyRepeatInfo.nextRepeatAt += 1.0 / _glfw.wl.keyboardRepeatRate;
now = glfwGetTime();
}
}
static double
adjustTimeoutForKeyRepeat(double timeout) {
if (_glfw.wl.keyRepeatInfo.nextRepeatAt <= 0 || _glfw.wl.keyRepeatInfo.keyboardFocus != _glfw.wl.keyboardFocus || _glfw.wl.keyboardRepeatRate == 0) return timeout;
double now = glfwGetTime();
if (timeout < 0 || now + timeout > _glfw.wl.keyRepeatInfo.nextRepeatAt) {
timeout = _glfw.wl.keyRepeatInfo.nextRepeatAt <= now ? 0 : ( (_glfw.wl.keyRepeatInfo.nextRepeatAt - now) + 0.001 );
}
return timeout;
}
static void
handleEvents(double timeout)
{
struct wl_display* display = _glfw.wl.display;
while (wl_display_prepare_read(display) != 0)
while (wl_display_prepare_read(display) != 0) {
wl_display_dispatch_pending(display);
}
// If an error different from EAGAIN happens, we have likely been
// disconnected from the Wayland session, try to handle that the best we
@@ -732,25 +712,8 @@ handleEvents(double timeout)
return;
}
dispatchPendingKeyRepeats();
timeout = adjustTimeoutForKeyRepeat(timeout);
GLFWbool read_ok = GLFW_FALSE;
for (nfds_t i = 0; i < 2; i++) _glfw.wl.eventLoopData.fds[i].revents = 0;
if (timeout >= 0) {
const int result = pollWithTimeout(_glfw.wl.eventLoopData.fds, 2, timeout);
if (result > 0)
{
if (_glfw.wl.eventLoopData.fds[0].revents & POLLIN) drainFd(_glfw.wl.eventLoopData.fds[0].fd);
read_ok = _glfw.wl.eventLoopData.fds[1].revents & POLLIN;
}
} else {
if (poll(_glfw.wl.eventLoopData.fds, 2, -1) > 0) {
if (_glfw.wl.eventLoopData.fds[0].revents & POLLIN) drainFd(_glfw.wl.eventLoopData.fds[0].fd);
read_ok = _glfw.wl.eventLoopData.fds[1].revents & POLLIN;
}
}
if (read_ok) {
GLFWbool display_read_ok = pollForEvents(&_glfw.wl.eventLoopData, timeout);
if (display_read_ok) {
wl_display_read_events(display);
wl_display_dispatch_pending(display);
}
@@ -758,7 +721,7 @@ handleEvents(double timeout)
{
wl_display_cancel_read(display);
}
dispatchPendingKeyRepeats();
glfw_ibus_dispatch(&_glfw.wl.xkb.ibus);
}
// Translates a GLFW standard cursor to a theme cursor name
@@ -1094,7 +1057,8 @@ void _glfwPlatformRequestWindowAttention(_GLFWwindow* window)
int _glfwPlatformWindowBell(_GLFWwindow* window)
{
// TODO: Use an actual Wayland API to implement this when one becomes available
int fd = open("/dev/tty", O_WRONLY | O_CLOEXEC);
static char tty[L_ctermid + 1];
int fd = open(ctermid(tty), O_WRONLY | O_CLOEXEC);
if (fd > -1) {
int ret = write(fd, "\x07", 1) == 1 ? GLFW_TRUE : GLFW_FALSE;
close(fd);
@@ -1200,16 +1164,19 @@ void _glfwPlatformSetWindowOpacity(_GLFWwindow* window, float opacity)
void _glfwPlatformPollEvents(void)
{
wl_display_dispatch_pending(_glfw.wl.display);
handleEvents(0);
}
void _glfwPlatformWaitEvents(void)
{
handleEvents(-1);
double timeout = wl_display_dispatch_pending(_glfw.wl.display) > 0 ? 0 : -1;
handleEvents(timeout);
}
void _glfwPlatformWaitEventsTimeout(double timeout)
{
if (wl_display_dispatch_pending(_glfw.wl.display) > 0) timeout = 0;
handleEvents(timeout);
}
@@ -1257,7 +1224,7 @@ int _glfwPlatformGetKeyScancode(int key)
int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
const GLFWimage* image,
int xhot, int yhot)
int xhot, int yhot, int count)
{
cursor->wl.buffer = createShmBuffer(image);
cursor->wl.width = image->width;
@@ -1546,6 +1513,11 @@ VkResult _glfwPlatformCreateWindowSurface(VkInstance instance,
return err;
}
void
_glfwPlatformUpdateIMEState(_GLFWwindow *w, int which, int a, int b, int c, int d) {
glfw_xkb_update_ime_state(w, &_glfw.wl.xkb, which, a, b, c, d);
}
//////////////////////////////////////////////////////////////////////////
////// GLFW native API //////
@@ -1563,3 +1535,7 @@ GLFWAPI struct wl_surface* glfwGetWaylandWindow(GLFWwindow* handle)
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
return window->wl.surface;
}
GLFWAPI int glfwGetXKBScancode(const char* keyName, GLFWbool caseSensitive) {
return glfw_xkb_keysym_from_name(keyName, caseSensitive);
}

14
glfw/x11_init.c vendored
View File

@@ -37,6 +37,7 @@
#include <stdio.h>
#include <locale.h>
#include <fcntl.h>
#include <unistd.h>
// Check whether the specified atom is supported
@@ -640,8 +641,8 @@ int _glfwPlatformInit(void)
return GLFW_FALSE;
}
initPollData(_glfw.x11.eventLoopData.fds, _glfw.x11.eventLoopData.wakeupFds[0], ConnectionNumber(_glfw.x11.display));
_glfw.x11.eventLoopData.fds[2].events = POLLIN;
initPollData(&_glfw.x11.eventLoopData, _glfw.x11.eventLoopData.wakeupFds[0], ConnectionNumber(_glfw.x11.display));
glfw_dbus_init(&_glfw.x11.dbus, &_glfw.x11.eventLoopData);
_glfw.x11.screen = DefaultScreen(_glfw.x11.display);
_glfw.x11.root = RootWindow(_glfw.x11.display, _glfw.x11.screen);
@@ -658,8 +659,12 @@ int _glfwPlatformInit(void)
_glfw.x11.hiddenCursorHandle = createHiddenCursor();
#if defined(__linux__)
if (!_glfwInitJoysticksLinux())
return GLFW_FALSE;
if (_glfw.hints.init.enableJoysticks) {
if (!_glfwInitJoysticksLinux())
return GLFW_FALSE;
if (_glfw.linjs.inotify > 0)
addWatch(&_glfw.x11.eventLoopData, "joystick", _glfw.linjs.inotify, POLLIN, 1, NULL, NULL);
}
#endif
_glfwInitTimerPOSIX();
@@ -689,6 +694,7 @@ void _glfwPlatformTerminate(void)
}
glfw_xkb_release(&_glfw.x11.xkb);
glfw_dbus_terminate(&_glfw.x11.dbus);
free(_glfw.x11.primarySelectionString);
free(_glfw.x11.clipboardString);

28
glfw/x11_monitor.c vendored
View File

@@ -152,6 +152,11 @@ void _glfwPollMonitorsX11(void)
}
ci = XRRGetCrtcInfo(_glfw.x11.display, sr, oi->crtc);
if (!ci)
{
XRRFreeOutputInfo(oi);
continue;
}
if (ci->rotation == RR_Rotate_90 || ci->rotation == RR_Rotate_270)
{
widthMM = oi->mm_height;
@@ -321,13 +326,15 @@ void _glfwPlatformGetMonitorPos(_GLFWmonitor* monitor, int* xpos, int* ypos)
sr = XRRGetScreenResourcesCurrent(_glfw.x11.display, _glfw.x11.root);
ci = XRRGetCrtcInfo(_glfw.x11.display, sr, monitor->x11.crtc);
if (ci)
{
if (xpos)
*xpos = ci->x;
if (ypos)
*ypos = ci->y;
if (xpos)
*xpos = ci->x;
if (ypos)
*ypos = ci->y;
XRRFreeCrtcInfo(ci);
XRRFreeCrtcInfo(ci);
}
XRRFreeScreenResources(sr);
}
}
@@ -406,9 +413,13 @@ void _glfwPlatformGetVideoMode(_GLFWmonitor* monitor, GLFWvidmode* mode)
sr = XRRGetScreenResourcesCurrent(_glfw.x11.display, _glfw.x11.root);
ci = XRRGetCrtcInfo(_glfw.x11.display, sr, monitor->x11.crtc);
*mode = vidmodeFromModeInfo(getModeInfo(sr, ci->mode), ci);
if (ci) {
const XRRModeInfo* mi = getModeInfo(sr, ci->mode);
if (mi) // mi can be NULL if the monitor has been disconnected
*mode = vidmodeFromModeInfo(mi, ci);
XRRFreeCrtcInfo(ci);
XRRFreeCrtcInfo(ci);
}
XRRFreeScreenResources(sr);
}
else
@@ -501,4 +512,3 @@ GLFWAPI RROutput glfwGetX11Monitor(GLFWmonitor* handle)
_GLFW_REQUIRE_INIT_OR_RETURN(None);
return monitor->x11.output;
}

9
glfw/x11_platform.h vendored
View File

@@ -53,6 +53,7 @@
// The libxkb library is used for improved keyboard support
#include "xkb_glfw.h"
#include "backend_utils.h"
typedef XRRCrtcGamma* (* PFN_XRRAllocGamma)(int);
typedef void (* PFN_XRRFreeCrtcInfo)(XRRCrtcInfo*);
@@ -103,8 +104,6 @@ typedef XineramaScreenInfo* (* PFN_XineramaQueryScreens)(Display*,int*);
#define XineramaQueryExtension _glfw.x11.xinerama.QueryExtension
#define XineramaQueryScreens _glfw.x11.xinerama.QueryScreens
typedef struct xcb_connection_t xcb_connection_t;
typedef Bool (* PFN_XF86VidModeQueryExtension)(Display*,int*,int*);
typedef Bool (* PFN_XF86VidModeGetGammaRamp)(Display*,int,int,unsigned short*,unsigned short*,unsigned short*);
typedef Bool (* PFN_XF86VidModeSetGammaRamp)(Display*,int,int,unsigned short*,unsigned short*,unsigned short*);
@@ -315,6 +314,7 @@ typedef struct _GLFWlibraryX11
} randr;
_GLFWXKBData xkb;
_GLFWDBUSData dbus;
struct {
int count;
@@ -382,10 +382,7 @@ typedef struct _GLFWlibraryX11
PFN_XRenderFindVisualFormat FindVisualFormat;
} xrender;
struct {
struct pollfd fds[3];
int wakeupFds[2];
} eventLoopData;
EventLoopData eventLoopData;
} _GLFWlibraryX11;

119
glfw/x11_window.c vendored
View File

@@ -54,47 +54,32 @@
// This avoids blocking other threads via the per-display Xlib lock that also
// covers GLX functions
//
static GLFWbool waitForEvent(double* timeout)
{
nfds_t count = 2;
GLFWbool _glfwDispatchX11Events(void);
#if defined(__linux__)
if (_glfw.linjs.inotify > 0)
{
count = 3;
_glfw.x11.eventLoopData.fds[2].fd = _glfw.linjs.inotify;
}
#endif
for (;;)
{
for (nfds_t i = 0; i < count; i++) _glfw.x11.eventLoopData.fds[i].revents = 0;
if (timeout)
{
const uint64_t base = _glfwPlatformGetTimerValue();
const int result = pollWithTimeout(_glfw.x11.eventLoopData.fds, count, *timeout);
*timeout -= (_glfwPlatformGetTimerValue() - base) /
(double) _glfwPlatformGetTimerFrequency();
static void
handleEvents(double timeout) {
int display_read_ok = pollForEvents(&_glfw.x11.eventLoopData, timeout);
if (display_read_ok) _glfwDispatchX11Events();
glfw_ibus_dispatch(&_glfw.x11.xkb.ibus);
}
if (result > 0)
{
if (_glfw.x11.eventLoopData.fds[0].revents & POLLIN) drainFd(_glfw.x11.eventLoopData.fds[0].fd);
return GLFW_TRUE;
}
if (result == 0)
return GLFW_FALSE;
if (*timeout > 0 && (errno == EINTR || errno == EAGAIN)) continue;
static GLFWbool
waitForX11Event(double timeout) {
// returns true iff there is X11 data waiting to be read, does not run watches and timers
double end_time = glfwGetTime() + timeout;
while(GLFW_TRUE) {
if (timeout >= 0) {
const int result = pollWithTimeout(_glfw.x11.eventLoopData.fds, 1, timeout);
if (result > 0) return GLFW_TRUE;
timeout = end_time - glfwGetTime();
if (timeout <= 0) return GLFW_FALSE;
if (result < 0 && (errno == EINTR || errno == EAGAIN)) continue;
return GLFW_FALSE;
} else {
const int result = poll(_glfw.x11.eventLoopData.fds, 1, -1);
if (result > 0) return GLFW_TRUE;
if (result < 0 && (errno == EINTR || errno == EAGAIN)) continue;
return GLFW_FALSE;
}
else {
const int result = poll(_glfw.x11.eventLoopData.fds, count, -1);
if (result > 0)
{
if (_glfw.x11.eventLoopData.fds[0].revents & POLLIN) drainFd(_glfw.x11.eventLoopData.fds[0].fd);
return GLFW_TRUE;
}
if (result == 0)
return GLFW_FALSE;
if (errno != EINTR && errno != EAGAIN) return GLFW_FALSE;
}
}
}
@@ -105,14 +90,13 @@ static GLFWbool waitForEvent(double* timeout)
static GLFWbool waitForVisibilityNotify(_GLFWwindow* window)
{
XEvent dummy;
double timeout = 0.1;
while (!XCheckTypedWindowEvent(_glfw.x11.display,
window->x11.handle,
VisibilityNotify,
&dummy))
{
if (!waitForEvent(&timeout))
if (!waitForX11Event(0.1))
return GLFW_FALSE;
}
@@ -984,7 +968,7 @@ static const char* getSelectionString(Atom selection)
SelectionNotify,
&notification))
{
waitForEvent(NULL);
waitForX11Event(-1);
}
if (notification.xselection.property == None)
@@ -1020,7 +1004,7 @@ static const char* getSelectionString(Atom selection)
isSelPropNewValueNotify,
(XPointer) &notification))
{
waitForEvent(NULL);
waitForX11Event(-1);
}
XFree(data);
@@ -1325,13 +1309,13 @@ static void processEvent(XEvent *event)
// Modern X provides scroll events as mouse button presses
else if (event->xbutton.button == Button4)
_glfwInputScroll(window, 0.0, 1.0);
_glfwInputScroll(window, 0.0, 1.0, 0);
else if (event->xbutton.button == Button5)
_glfwInputScroll(window, 0.0, -1.0);
_glfwInputScroll(window, 0.0, -1.0, 0);
else if (event->xbutton.button == Button6)
_glfwInputScroll(window, 1.0, 0.0);
_glfwInputScroll(window, 1.0, 0.0, 0);
else if (event->xbutton.button == Button7)
_glfwInputScroll(window, -1.0, 0.0);
_glfwInputScroll(window, -1.0, 0.0, 0);
else
{
@@ -1852,7 +1836,7 @@ void _glfwPushSelectionToManagerX11(void)
}
}
waitForEvent(NULL);
waitForX11Event(-1);
}
}
@@ -2142,7 +2126,6 @@ void _glfwPlatformGetWindowFrameSize(_GLFWwindow* window,
_glfw.x11.NET_REQUEST_FRAME_EXTENTS)
{
XEvent event;
double timeout = 0.5;
// Ensure _NET_FRAME_EXTENTS is set, allowing glfwGetWindowFrameSize to
// function before the window is mapped
@@ -2159,7 +2142,7 @@ void _glfwPlatformGetWindowFrameSize(_GLFWwindow* window,
isFrameExtentsEvent,
(XPointer) window))
{
if (!waitForEvent(&timeout))
if (!waitForX11Event(0.5))
{
_glfwInputError(GLFW_PLATFORM_ERROR,
"X11: The window manager has a broken _NET_REQUEST_FRAME_EXTENTS implementation; please report this issue");
@@ -2560,9 +2543,10 @@ void _glfwPlatformSetWindowOpacity(_GLFWwindow* window, float opacity)
PropModeReplace, (unsigned char*) &value, 1);
}
void _glfwPlatformPollEvents(void)
{
GLFWbool
_glfwDispatchX11Events(void) {
_GLFWwindow* window;
GLFWbool dispatched = GLFW_FALSE;
#if defined(__linux__)
_glfwDetectJoystickConnectionLinux();
@@ -2574,6 +2558,7 @@ void _glfwPlatformPollEvents(void)
XEvent event;
XNextEvent(_glfw.x11.display, &event);
processEvent(&event);
dispatched = GLFW_TRUE;
}
window = _glfw.x11.disabledCursorWindow;
@@ -2592,25 +2577,25 @@ void _glfwPlatformPollEvents(void)
}
XFlush(_glfw.x11.display);
return dispatched;
}
void _glfwPlatformPollEvents(void)
{
_glfwDispatchX11Events();
handleEvents(0);
}
void _glfwPlatformWaitEvents(void)
{
while (!XPending(_glfw.x11.display))
waitForEvent(NULL);
_glfwPlatformPollEvents();
double timeout = _glfwDispatchX11Events() ? 0 : -1;
handleEvents(timeout);
}
void _glfwPlatformWaitEventsTimeout(double timeout)
{
while (!XPending(_glfw.x11.display))
{
if (!waitForEvent(&timeout))
break;
}
_glfwPlatformPollEvents();
if (_glfwDispatchX11Events()) timeout = 0;
handleEvents(timeout);
}
void _glfwPlatformPostEmptyEvent(void)
@@ -2684,7 +2669,7 @@ int _glfwPlatformGetKeyScancode(int key)
int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
const GLFWimage* image,
int xhot, int yhot)
int xhot, int yhot, int count)
{
cursor->x11.handle = _glfwCreateCursorX11(image, xhot, yhot);
if (!cursor->x11.handle)
@@ -2894,6 +2879,10 @@ VkResult _glfwPlatformCreateWindowSurface(VkInstance instance,
}
}
void
_glfwPlatformUpdateIMEState(_GLFWwindow *w, int which, int a, int b, int c, int d) {
glfw_xkb_update_ime_state(w, &_glfw.x11.xkb, which, a, b, c, d);
}
//////////////////////////////////////////////////////////////////////////
////// GLFW native API //////
@@ -2937,3 +2926,7 @@ GLFWAPI const char* glfwGetX11SelectionString(void)
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
return getSelectionString(_glfw.x11.PRIMARY);
}
GLFWAPI int glfwGetXKBScancode(const char* keyName, GLFWbool caseSensitive) {
return glfw_xkb_keysym_from_name(keyName, caseSensitive);
}

322
glfw/xkb_glfw.c vendored
View File

@@ -30,8 +30,8 @@
#include "internal.h"
#include "xkb_glfw.h"
static GLFWbool debug_keyboard = GLFW_FALSE;
#define debug(...) if (debug_keyboard) printf(__VA_ARGS__);
#define debug(...) if (_glfw.hints.init.debugKeyboard) printf(__VA_ARGS__);
#define map_key(key) { \
switch(key) { \
@@ -171,157 +171,150 @@ glfw_xkb_update_x11_keyboard_id(_GLFWXKBData *xkb) {
#endif
static void
release_keyboard_data(_GLFWXKBData *xkb) {
#define US(group, state, unref) if (xkb->group.state) { unref(xkb->group.state); xkb->group.state = NULL; }
#define UK(keymap) if(xkb->keymap) { xkb_keymap_unref(xkb->keymap); xkb->keymap = NULL; }
US(states, composeState, xkb_compose_state_unref);
UK(keymap);
UK(default_keymap);
US(states, state, xkb_state_unref);
US(states, clean_state, xkb_state_unref);
US(states, default_state, xkb_state_unref);
#undef US
#undef UK
}
void
glfw_xkb_release(_GLFWXKBData *xkb) {
if (xkb->composeState) {
xkb_compose_state_unref(xkb->composeState);
xkb->composeState = NULL;
}
if (xkb->keymap) {
xkb_keymap_unref(xkb->keymap);
xkb->keymap = NULL;
}
if (xkb->default_keymap) {
xkb_keymap_unref(xkb->default_keymap);
xkb->default_keymap = NULL;
}
if (xkb->state) {
xkb_state_unref(xkb->state);
xkb->state = NULL;
}
if (xkb->clean_state) {
xkb_state_unref(xkb->clean_state);
xkb->clean_state = NULL;
}
if (xkb->default_state) {
xkb_state_unref(xkb->default_state);
xkb->default_state = NULL;
}
release_keyboard_data(xkb);
if (xkb->context) {
xkb_context_unref(xkb->context);
xkb->context = NULL;
}
glfw_ibus_terminate(&xkb->ibus);
}
GLFWbool
glfw_xkb_create_context(_GLFWXKBData *xkb) {
xkb->context = xkb_context_new(0);
debug_keyboard = getenv("GLFW_DEBUG_KEYBOARD") != NULL;
if (!xkb->context)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
"Failed to initialize XKB context");
return GLFW_FALSE;
}
glfw_connect_to_ibus(&xkb->ibus);
return GLFW_TRUE;
}
static const char*
load_keymaps(_GLFWXKBData *xkb, const char *map_str) {
(void)(map_str); // not needed on X11
xkb_glfw_load_keymap(xkb->keymap, map_str);
if (!xkb->keymap) return "Failed to compile XKB keymap";
// The system default keymap, can be overridden by the XKB_DEFAULT_RULES
// env var, see
// https://xkbcommon.org/doc/current/structxkb__rule__names.html
static struct xkb_rule_names default_rule_names = {0};
xkb->default_keymap = xkb_keymap_new_from_names(xkb->context, &default_rule_names, XKB_KEYMAP_COMPILE_NO_FLAGS);
if (!xkb->default_keymap) return "Failed to create default XKB keymap";
return NULL;
}
static const char*
load_states(_GLFWXKBData *xkb) {
xkb_glfw_load_state(xkb->keymap, xkb->states.state);
xkb->states.clean_state = xkb_state_new(xkb->keymap);
xkb->states.default_state = xkb_state_new(xkb->default_keymap);
if (!xkb->states.state || !xkb->states.clean_state || !xkb->states.default_state) return "Failed to create XKB state";
return NULL;
}
static void
load_compose_tables(_GLFWXKBData *xkb) {
/* Look up the preferred locale, falling back to "C" as default. */
struct xkb_compose_table* compose_table = NULL;
const char *locale = getenv("LC_ALL");
if (!locale) locale = getenv("LC_CTYPE");
if (!locale) locale = getenv("LANG");
if (!locale) locale = "C";
compose_table = xkb_compose_table_new_from_locale(xkb->context, locale, XKB_COMPOSE_COMPILE_NO_FLAGS);
if (!compose_table) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Failed to create XKB compose table for locale %s", locale);
return;
}
xkb->states.composeState = xkb_compose_state_new(compose_table, XKB_COMPOSE_STATE_NO_FLAGS);
if (!xkb->states.composeState) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Failed to create XKB compose state");
}
}
GLFWbool
glfw_xkb_compile_keymap(_GLFWXKBData *xkb, const char *map_str) {
const char* locale = NULL;
struct xkb_state* state = NULL, *clean_state = NULL;
struct xkb_keymap* keymap = NULL;
struct xkb_compose_table* compose_table = NULL;
struct xkb_compose_state* compose_state = NULL;
(void)(map_str); // not needed on X11
GLFWbool ok = GLFW_FALSE;
xkb_glfw_load_keymap(keymap, map_str);
if (!keymap) _glfwInputError(GLFW_PLATFORM_ERROR, "Failed to compile XKB keymap");
else {
xkb_glfw_load_state(keymap, state);
clean_state = xkb_state_new(keymap);
if (!state || !clean_state) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Failed to create XKB state");
xkb_keymap_unref(keymap); keymap = NULL;
} else {
ok = GLFW_TRUE;
/* Look up the preferred locale, falling back to "C" as default. */
locale = getenv("LC_ALL");
if (!locale) locale = getenv("LC_CTYPE");
if (!locale) locale = getenv("LANG");
if (!locale) locale = "C";
compose_table = xkb_compose_table_new_from_locale(xkb->context, locale, XKB_COMPOSE_COMPILE_NO_FLAGS);
if (!compose_table) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Failed to create XKB compose table");
} else {
compose_state = xkb_compose_state_new(compose_table, XKB_COMPOSE_STATE_NO_FLAGS);
xkb_compose_table_unref(compose_table); compose_table = NULL;
if (!compose_state) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Failed to create XKB compose state");
}
}
}
const char *err;
release_keyboard_data(xkb);
err = load_keymaps(xkb, map_str);
if (err) {
_glfwInputError(GLFW_PLATFORM_ERROR, "%s", err);
release_keyboard_data(xkb);
return GLFW_FALSE;
}
if (keymap && state && clean_state) {
if (xkb->composeState) xkb_compose_state_unref(xkb->composeState);
xkb->composeState = compose_state;
if (xkb->keymap) xkb_keymap_unref(xkb->keymap);
xkb->keymap = keymap;
if (xkb->state) xkb_state_unref(xkb->state);
xkb->state = state;
if (xkb->clean_state) xkb_state_unref(xkb->clean_state);
xkb->clean_state = clean_state;
err = load_states(xkb);
if (err) {
_glfwInputError(GLFW_PLATFORM_ERROR, "%s", err);
release_keyboard_data(xkb);
return GLFW_FALSE;
}
if (xkb->keymap) {
load_compose_tables(xkb);
#define S(a, n) xkb->a##Idx = xkb_keymap_mod_get_index(xkb->keymap, n); xkb->a##Mask = 1 << xkb->a##Idx;
S(control, XKB_MOD_NAME_CTRL);
S(alt, XKB_MOD_NAME_ALT);
S(shift, XKB_MOD_NAME_SHIFT);
S(super, XKB_MOD_NAME_LOGO);
S(capsLock, XKB_MOD_NAME_CAPS);
S(numLock, XKB_MOD_NAME_NUM);
S(control, XKB_MOD_NAME_CTRL);
S(alt, XKB_MOD_NAME_ALT);
S(shift, XKB_MOD_NAME_SHIFT);
S(super, XKB_MOD_NAME_LOGO);
S(capsLock, XKB_MOD_NAME_CAPS);
S(numLock, XKB_MOD_NAME_NUM);
#undef S
size_t capacity = sizeof(xkb->unknownModifiers)/sizeof(xkb->unknownModifiers[0]), j = 0;
for (xkb_mod_index_t i = 0; i < capacity; i++) xkb->unknownModifiers[i] = XKB_MOD_INVALID;
for (xkb_mod_index_t i = 0; i < xkb_keymap_num_mods(xkb->keymap) && j < capacity - 1; i++) {
if (i != xkb->controlIdx && i != xkb->altIdx && i != xkb->shiftIdx && i != xkb->superIdx && i != xkb->capsLockIdx && i != xkb->numLockIdx) xkb->unknownModifiers[j++] = i;
}
xkb->modifiers = 0;
xkb->activeUnknownModifiers = 0;
size_t capacity = sizeof(xkb->unknownModifiers)/sizeof(xkb->unknownModifiers[0]), j = 0;
for (xkb_mod_index_t i = 0; i < capacity; i++) xkb->unknownModifiers[i] = XKB_MOD_INVALID;
for (xkb_mod_index_t i = 0; i < xkb_keymap_num_mods(xkb->keymap) && j < capacity - 1; i++) {
if (i != xkb->controlIdx && i != xkb->altIdx && i != xkb->shiftIdx && i != xkb->superIdx && i != xkb->capsLockIdx && i != xkb->numLockIdx) xkb->unknownModifiers[j++] = i;
}
if (!xkb->default_keymap && xkb->context) {
// The system default keymap, can be overridden by the XKB_DEFAULT_RULES
// env var, see
// https://xkbcommon.org/doc/current/structxkb__rule__names.html
static struct xkb_rule_names default_rule_names = {0};
xkb->default_keymap = xkb_keymap_new_from_names(xkb->context, &default_rule_names, XKB_KEYMAP_COMPILE_NO_FLAGS);
if (xkb->default_keymap) {
xkb->default_state = xkb_state_new(keymap);
if (!xkb->default_state) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Failed to create default XKB state, fallback key processing is unavailable");
}
} else {
_glfwInputError(GLFW_PLATFORM_ERROR, "Failed to create default XKB keymap, fallback key processing is unavailable");
}
}
return ok;
xkb->states.modifiers = 0;
xkb->states.activeUnknownModifiers = 0;
return GLFW_TRUE;
}
static inline xkb_mod_mask_t
active_unknown_modifiers(_GLFWXKBData *xkb) {
active_unknown_modifiers(_GLFWXKBData *xkb, struct xkb_state *state) {
size_t i = 0;
xkb_mod_mask_t ans = 0;
while (xkb->unknownModifiers[i] != XKB_MOD_INVALID) {
if (xkb_state_mod_index_is_active(xkb->state, xkb->unknownModifiers[i], XKB_STATE_MODS_EFFECTIVE)) ans |= (1 << xkb->unknownModifiers[i]);
if (xkb_state_mod_index_is_active(state, xkb->unknownModifiers[i], XKB_STATE_MODS_EFFECTIVE)) ans |= (1 << xkb->unknownModifiers[i]);
i++;
}
return ans;
}
static void
update_modifiers(_GLFWXKBData *xkb, XKBStateGroup *group) {
#define S(attr, name) if (xkb_state_mod_index_is_active(group->state, xkb->attr##Idx, XKB_STATE_MODS_EFFECTIVE)) group->modifiers |= GLFW_MOD_##name
S(control, CONTROL); S(alt, ALT); S(shift, SHIFT); S(super, SUPER); S(capsLock, CAPS_LOCK); S(numLock, NUM_LOCK);
#undef S
xkb->states.activeUnknownModifiers = active_unknown_modifiers(xkb, xkb->states.state);
}
void
glfw_xkb_update_modifiers(_GLFWXKBData *xkb, xkb_mod_mask_t depressed, xkb_mod_mask_t latched, xkb_mod_mask_t locked, xkb_layout_index_t base_group, xkb_layout_index_t latched_group, xkb_layout_index_t locked_group) {
if (!xkb->keymap) return;
xkb->modifiers = 0;
xkb_state_update_mask(xkb->state, depressed, latched, locked, base_group, latched_group, locked_group);
xkb->states.modifiers = 0;
xkb_state_update_mask(xkb->states.state, depressed, latched, locked, base_group, latched_group, locked_group);
// We have to update the groups in clean_state, as they change for
// different keyboard layouts, see https://github.com/kovidgoyal/kitty/issues/488
xkb_state_update_mask(xkb->clean_state, 0, 0, 0, base_group, latched_group, locked_group);
#define S(attr, name) if (xkb_state_mod_index_is_active(xkb->state, xkb->attr##Idx, XKB_STATE_MODS_EFFECTIVE)) xkb->modifiers |= GLFW_MOD_##name
S(control, CONTROL); S(alt, ALT); S(shift, SHIFT); S(super, SUPER); S(capsLock, CAPS_LOCK); S(numLock, NUM_LOCK);
#undef S
xkb->activeUnknownModifiers = active_unknown_modifiers(xkb);
xkb_state_update_mask(xkb->states.clean_state, 0, 0, 0, base_group, latched_group, locked_group);
update_modifiers(xkb, &xkb->states);
}
GLFWbool
@@ -333,16 +326,16 @@ glfw_xkb_should_repeat(_GLFWXKBData *xkb, xkb_keycode_t scancode) {
}
static char text[256];
static KeyEvent key_event = {};
static inline xkb_keysym_t
compose_symbol(_GLFWXKBData *xkb, xkb_keysym_t sym) {
if (sym == XKB_KEY_NoSymbol || !xkb->composeState) return sym;
if (xkb_compose_state_feed(xkb->composeState, sym) != XKB_COMPOSE_FEED_ACCEPTED) return sym;
switch (xkb_compose_state_get_status(xkb->composeState)) {
compose_symbol(struct xkb_compose_state *composeState, xkb_keysym_t sym) {
if (sym == XKB_KEY_NoSymbol || !composeState) return sym;
if (xkb_compose_state_feed(composeState, sym) != XKB_COMPOSE_FEED_ACCEPTED) return sym;
switch (xkb_compose_state_get_status(composeState)) {
case XKB_COMPOSE_COMPOSED:
xkb_compose_state_get_utf8(xkb->composeState, text, sizeof(text));
return xkb_compose_state_get_one_sym(xkb->composeState);
xkb_compose_state_get_utf8(composeState, key_event.text, sizeof(key_event.text));
return xkb_compose_state_get_one_sym(composeState);
case XKB_COMPOSE_COMPOSING:
case XKB_COMPOSE_CANCELLED:
return XKB_KEY_NoSymbol;
@@ -361,6 +354,10 @@ glfw_xkb_keysym_name(xkb_keysym_t sym) {
return name;
}
int
glfw_xkb_keysym_from_name(const char *name, GLFWbool case_sensitive) {
return (int)xkb_keysym_from_name(name, case_sensitive ? XKB_KEYSYM_NO_FLAGS : XKB_KEYSYM_CASE_INSENSITIVE);
}
static inline const char*
format_mods(unsigned int mods) {
@@ -400,29 +397,75 @@ format_xkb_mods(_GLFWXKBData *xkb, const char* name, xkb_mod_mask_t mods) {
return buf;
}
void
glfw_xkb_update_ime_state(_GLFWwindow *w, _GLFWXKBData *xkb, int which, int a, int b, int c, int d) {
int x = 0, y = 0;
switch(which) {
case 1:
glfw_ibus_set_focused(&xkb->ibus, a ? GLFW_TRUE : GLFW_FALSE);
break;
case 2:
_glfwPlatformGetWindowPos(w, &x, &y);
x += a; y += b;
glfw_ibus_set_cursor_geometry(&xkb->ibus, x, y, c, d);
break;
}
}
void
glfw_xkb_key_from_ime(KeyEvent *ev, GLFWbool handled_by_ime, GLFWbool failed) {
_GLFWwindow *window = _glfwWindowForId(ev->window_id);
if (failed && window && window->callbacks.keyboard) {
// notify application to remove any existing pre-edit text
window->callbacks.keyboard((GLFWwindow*) window, GLFW_KEY_UNKNOWN, 0, GLFW_PRESS, 0, "", 1);
}
static xkb_keycode_t last_handled_press_keycode = 0;
// We filter out release events that correspond to the last press event
// handled by the IME system. This wont fix the case of multiple key
// presses before a release, but is better than nothing. For that case
// you'd need to implement a ring buffer to store pending key presses.
xkb_keycode_t prev_handled_press = last_handled_press_keycode;
last_handled_press_keycode = 0;
GLFWbool is_release = ev->action == GLFW_RELEASE;
debug("From IBUS: scancode: 0x%x name: %s is_release: %d\n", ev->keycode, glfw_xkb_keysym_name(ev->keysym), is_release);
if (window && !handled_by_ime && !(is_release && ev->keycode == prev_handled_press)) {
debug("↳ to application: glfw_keycode: 0x%x (%s) keysym: 0x%x (%s) action: %s %s text: %s\n",
ev->glfw_keycode, _glfwGetKeyName(ev->glfw_keycode), ev->keysym, glfw_xkb_keysym_name(ev->keysym),
(ev->action == GLFW_RELEASE ? "RELEASE" : (ev->action == GLFW_PRESS ? "PRESS" : "REPEAT")),
format_mods(ev->glfw_modifiers), key_event.text
);
_glfwInputKeyboard(window, ev->glfw_keycode, ev->keysym, ev->action, ev->glfw_modifiers, key_event.text, 0);
} else debug("↳ discarded\n");
if (!is_release && handled_by_ime) last_handled_press_keycode = ev->keycode;
}
void
glfw_xkb_handle_key_event(_GLFWwindow *window, _GLFWXKBData *xkb, xkb_keycode_t scancode, int action) {
const xkb_keysym_t *syms, *clean_syms, *default_syms;
xkb_keysym_t glfw_sym;
xkb_keycode_t code_for_sym = scancode;
key_event.ibus_keycode = scancode;
#ifdef _GLFW_WAYLAND
code_for_sym += 8;
#else
key_event.ibus_keycode -= 8;
#endif
debug("scancode: 0x%x release: %d ", scancode, action == GLFW_RELEASE);
int num_syms = xkb_state_key_get_syms(xkb->state, code_for_sym, &syms);
int num_clean_syms = xkb_state_key_get_syms(xkb->clean_state, code_for_sym, &clean_syms);
text[0] = 0;
debug("%s scancode: 0x%x ", action == GLFW_RELEASE ? "Release" : "Press", scancode);
XKBStateGroup *sg = &xkb->states;
int num_syms = xkb_state_key_get_syms(sg->state, code_for_sym, &syms);
int num_clean_syms = xkb_state_key_get_syms(sg->clean_state, code_for_sym, &clean_syms);
key_event.text[0] = 0;
// According to the documentation of xkb_compose_state_feed it does not
// support multi-sym events, so we ignore them
if (num_syms != 1 || num_clean_syms != 1) {
debug("scancode: 0x%x num_syms: %d num_clean_syms: %d ignoring event\n", scancode, num_syms, num_clean_syms);
debug("num_syms: %d num_clean_syms: %d ignoring event\n", num_syms, num_clean_syms);
return;
}
glfw_sym = clean_syms[0];
debug("clean_sym: %s ", glfw_xkb_keysym_name(clean_syms[0]));
if (action == GLFW_PRESS || action == GLFW_REPEAT) {
const char *text_type = "composed_text";
glfw_sym = compose_symbol(xkb, syms[0]);
glfw_sym = compose_symbol(sg->composeState, syms[0]);
if (glfw_sym == XKB_KEY_NoSymbol) {
debug("compose not complete, ignoring.\n");
return;
@@ -433,27 +476,40 @@ glfw_xkb_handle_key_event(_GLFWwindow *window, _GLFWXKBData *xkb, xkb_keycode_t
// are active (for example if ISO_Shift_Level_* mods are active
// they are not reported by GLFW so the key should be the shifted
// key). See https://github.com/kovidgoyal/kitty/issues/171#issuecomment-377557053
xkb_mod_mask_t consumed_unknown_mods = xkb_state_key_get_consumed_mods(xkb->state, code_for_sym) & xkb->activeUnknownModifiers;
if (xkb->activeUnknownModifiers) debug("%s", format_xkb_mods(xkb, "active_unknown_mods", xkb->activeUnknownModifiers));
xkb_mod_mask_t consumed_unknown_mods = xkb_state_key_get_consumed_mods(sg->state, code_for_sym) & sg->activeUnknownModifiers;
if (sg->activeUnknownModifiers) debug("%s", format_xkb_mods(xkb, "active_unknown_mods", sg->activeUnknownModifiers));
if (consumed_unknown_mods) { debug("%s", format_xkb_mods(xkb, "consumed_unknown_mods", consumed_unknown_mods)); }
else glfw_sym = clean_syms[0];
// xkb returns text even if alt and/or super are pressed
if ( ((GLFW_MOD_CONTROL | GLFW_MOD_ALT | GLFW_MOD_SUPER) & xkb->modifiers) == 0) xkb_state_key_get_utf8(xkb->state, code_for_sym, text, sizeof(text));
if ( ((GLFW_MOD_CONTROL | GLFW_MOD_ALT | GLFW_MOD_SUPER) & sg->modifiers) == 0) xkb_state_key_get_utf8(sg->state, code_for_sym, key_event.text, sizeof(key_event.text));
text_type = "text";
}
if ((1 <= text[0] && text[0] <= 31) || text[0] == 127) text[0] = 0; // dont send text for ascii control codes
if (text[0]) { debug("%s: %s ", text_type, text); }
if ((1 <= key_event.text[0] && key_event.text[0] <= 31) || key_event.text[0] == 127) key_event.text[0] = 0; // don't send text for ascii control codes
if (key_event.text[0]) { debug("%s: %s ", text_type, key_event.text); }
}
int glfw_keycode = glfw_key_for_sym(glfw_sym);
if (glfw_keycode == GLFW_KEY_UNKNOWN && !text[0]) {
int num_default_syms = xkb_state_key_get_syms(xkb->default_state, code_for_sym, &default_syms);
GLFWbool is_fallback = GLFW_FALSE;
if (glfw_keycode == GLFW_KEY_UNKNOWN && !key_event.text[0]) {
int num_default_syms = xkb_state_key_get_syms(sg->default_state, code_for_sym, &default_syms);
if (num_default_syms > 0) {
glfw_sym = default_syms[0];
glfw_keycode = glfw_key_for_sym(glfw_sym);
is_fallback = GLFW_TRUE;
}
debug("%sglfw_fallback_key: %s\n", format_mods(xkb->modifiers), _glfwGetKeyName(glfw_keycode));
} else {
debug("%sglfw_key: %s\n", format_mods(xkb->modifiers), _glfwGetKeyName(glfw_keycode));
}
_glfwInputKeyboard(window, glfw_keycode, glfw_sym, action, xkb->modifiers, text, 0);
debug(
"%s%s: %d (%s) xkb_key: %d (%s)\n",
format_mods(sg->modifiers),
is_fallback ? "glfw_fallback_key" : "glfw_key", glfw_keycode, _glfwGetKeyName(glfw_keycode),
glfw_sym, glfw_xkb_keysym_name(glfw_sym)
);
key_event.action = action; key_event.glfw_modifiers = sg->modifiers;
key_event.keycode = scancode; key_event.keysym = glfw_sym;
key_event.window_id = window->id; key_event.glfw_keycode = glfw_keycode;
key_event.ibus_sym = syms[0];
if (ibus_process_key(&key_event, &xkb->ibus)) {
debug("↳ to IBUS: keycode: 0x%x keysym: 0x%x (%s) %s\n", key_event.ibus_keycode, key_event.ibus_sym, glfw_xkb_keysym_name(key_event.ibus_sym), format_mods(key_event.glfw_modifiers));
} else {
_glfwInputKeyboard(window, glfw_keycode, glfw_sym, action, sg->modifiers, key_event.text, 0);
}
}

21
glfw/xkb_glfw.h vendored
View File

@@ -32,14 +32,23 @@
#include <xkbcommon/xkbcommon-x11.h>
#endif
#include "ibus_glfw.h"
typedef struct {
struct xkb_context* context;
struct xkb_keymap* keymap;
struct xkb_keymap* default_keymap;
struct xkb_state* state;
struct xkb_state* clean_state;
struct xkb_state* default_state;
struct xkb_compose_state* composeState;
xkb_mod_mask_t activeUnknownModifiers;
unsigned int modifiers;
} XKBStateGroup;
typedef struct {
struct xkb_context* context;
struct xkb_keymap* keymap;
struct xkb_keymap* default_keymap;
XKBStateGroup states;
xkb_mod_index_t controlIdx;
xkb_mod_index_t altIdx;
@@ -53,9 +62,8 @@ typedef struct {
xkb_mod_mask_t superMask;
xkb_mod_mask_t capsLockMask;
xkb_mod_mask_t numLockMask;
xkb_mod_mask_t activeUnknownModifiers;
unsigned int modifiers;
xkb_mod_index_t unknownModifiers[256];
_GLFWIBUSData ibus;
#ifdef _GLFW_X11
int32_t keyboard_device_id;
@@ -83,3 +91,6 @@ GLFWbool glfw_xkb_should_repeat(_GLFWXKBData *xkb, xkb_keycode_t scancode);
const char* glfw_xkb_keysym_name(xkb_keysym_t sym);
xkb_keysym_t glfw_xkb_sym_for_key(int key);
void glfw_xkb_handle_key_event(_GLFWwindow *window, _GLFWXKBData *xkb, xkb_keycode_t scancode, int action);
int glfw_xkb_keysym_from_name(const char *name, GLFWbool case_sensitive);
void glfw_xkb_update_ime_state(_GLFWwindow *w, _GLFWXKBData *xkb, int which, int a, int b, int c, int d);
void glfw_xkb_key_from_ime(KeyEvent *ev, GLFWbool handled_by_ime, GLFWbool failed);

View File

@@ -1,129 +1 @@
= Key encoding for extended keyboard protocol
See link:protocol-extensions.asciidoc#keyboard-handling[Keyboard Handling protocol extension]
for more information and link:key_encoding.json[for this table in JSON format].
|===
| Name | Encoded representation (base64)
| 0 | `G`
| 1 | `H`
| 2 | `I`
| 3 | `J`
| 4 | `K`
| 5 | `L`
| 6 | `M`
| 7 | `N`
| 8 | `O`
| 9 | `P`
| A | `S`
| APOSTROPHE | `B`
| B | `T`
| BACKSLASH | `t`
| BACKSPACE | `1`
| C | `U`
| CAPS LOCK | `:`
| COMMA | `C`
| D | `V`
| DELETE | `3`
| DOWN | `6`
| E | `W`
| END | `-`
| ENTER | `z`
| EQUAL | `R`
| ESCAPE | `y`
| F | `X`
| F1 | `/`
| F10 | `]`
| F11 | `{`
| F12 | `}`
| F13 | `@`
| F14 | `%`
| F15 | `$`
| F16 | `#`
| F17 | `BA`
| F18 | `BB`
| F19 | `BC`
| F2 | `*`
| F20 | `BD`
| F21 | `BE`
| F22 | `BF`
| F23 | `BG`
| F24 | `BH`
| F25 | `BI`
| F3 | `?`
| F4 | `&`
| F5 | `<`
| F6 | `>`
| F7 | `(`
| F8 | `)`
| F9 | `[`
| G | `Y`
| GRAVE ACCENT | `v`
| H | `Z`
| HOME | `.`
| I | `a`
| INSERT | `2`
| J | `b`
| K | `c`
| KP 0 | `BJ`
| KP 1 | `BK`
| KP 2 | `BL`
| KP 3 | `BM`
| KP 4 | `BN`
| KP 5 | `BO`
| KP 6 | `BP`
| KP 7 | `BQ`
| KP 8 | `BR`
| KP 9 | `BS`
| KP ADD | `BX`
| KP DECIMAL | `BT`
| KP DIVIDE | `BU`
| KP ENTER | `BY`
| KP EQUAL | `BZ`
| KP MULTIPLY | `BV`
| KP SUBTRACT | `BW`
| L | `d`
| LEFT | `5`
| LEFT ALT | `Bc`
| LEFT BRACKET | `s`
| LEFT CONTROL | `Bb`
| LEFT SHIFT | `Ba`
| LEFT SUPER | `Bd`
| M | `e`
| MINUS | `D`
| N | `f`
| NUM LOCK | `=`
| O | `g`
| P | `h`
| PAGE DOWN | `9`
| PAGE UP | `8`
| PAUSE | `!`
| PERIOD | `E`
| PRINT SCREEN | `^`
| Q | `i`
| R | `j`
| RIGHT | `4`
| RIGHT ALT | `Bg`
| RIGHT BRACKET | `u`
| RIGHT CONTROL | `Bf`
| RIGHT SHIFT | `Be`
| RIGHT SUPER | `Bh`
| S | `k`
| SCROLL LOCK | `+`
| SEMICOLON | `Q`
| SLASH | `F`
| SPACE | `A`
| T | `l`
| TAB | `0`
| U | `m`
| UP | `7`
| V | `n`
| W | `o`
| WORLD 1 | `w`
| WORLD 2 | `x`
| X | `p`
| Y | `q`
| Z | `r`
|===
See https://sw.kovidgoyal.net/kitty/key-encoding.html

View File

@@ -50,8 +50,10 @@ class HistoryCompleter:
def __enter__(self):
if self.history_path:
if os.path.exists(self.history_path):
try:
readline.read_history_file(self.history_path)
except Exception:
pass
readline.set_completer(self.complete)
return self

View File

@@ -2,7 +2,9 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import os
import sys
from kitty.cli import parse_args
from ..tui.handler import Handler
@@ -13,7 +15,6 @@ class Clipboard(Handler):
def __init__(self, data_to_send, args):
self.args = args
self.print_on_fail = None
self.clipboard_contents = None
self.data_to_send = data_to_send
@@ -63,16 +64,13 @@ def main(args):
data = None
if not sys.stdin.isatty():
data = sys.stdin.buffer.read()
sys.stdin = open('/dev/tty', 'r')
sys.stdin = open(os.ctermid(), 'r')
loop = Loop()
handler = Clipboard(data, args)
loop.loop(handler)
if loop.return_code == 0 and handler.clipboard_contents:
sys.stdout.write(handler.clipboard_contents)
sys.stdout.flush()
if handler.print_on_fail:
print(handler.print_on_fail, file=sys.stderr)
input('Press Enter to quit')
raise SystemExit(loop.return_code)

View File

@@ -116,12 +116,9 @@ def collect_files(collection, left, right):
sanitize_pat = re.compile('[\x00-\x09\x0b-\x1f\x7f\x80-\x9f]')
def sanitize_sub(m):
return '<{:x}>'.format(ord(m.group()[0]))
def sanitize(text):
return sanitize_pat.sub(sanitize_sub, text)
ntext = text.replace('\r\n', '\n')
return sanitize_pat.sub('', ntext)
@lru_cache(maxsize=1024)

View File

@@ -52,7 +52,7 @@ def parse_scroll_by(func, rest):
@func_with_args('scroll_to')
def parse_scroll_to(func, rest):
rest = rest.lower()
if rest not in {'start', 'end', 'next-change', 'prev-change', 'next-page', 'prev-page'}:
if rest not in {'start', 'end', 'next-change', 'prev-change', 'next-page', 'prev-page', 'next-match', 'prev-match'}:
rest = 'start'
return func, rest
@@ -69,6 +69,14 @@ def parse_change_context(func, rest):
return func, amount
@func_with_args('start_search')
def parse_start_search(func, rest):
rest = rest.lower().split()
is_regex = rest and rest[0] == 'regex'
is_backward = len(rest) > 1 and rest[1] == 'backward'
return func, (is_regex, is_backward)
def special_handling(key, val, ans):
if key == 'map':
action, *key_def = parse_kittens_key(val, args_funcs)

View File

@@ -83,6 +83,10 @@ c('filler_bg', '#fafbfc', long_text=_('Filler (empty) line background'))
c('hunk_margin_bg', '#dbedff', long_text=_('Hunk header colors'))
c('hunk_bg', '#f1f8ff')
c('search_bg', '#444', long_text=_('Highlighting'))
c('search_fg', 'white')
c('select_bg', '#B4D5FE')
c('select_fg', 'black')
g('shortcuts')
k('quit', 'q', 'quit', _('Quit'))
@@ -108,4 +112,13 @@ k('default_context', '=', 'change_context default', _('Show default context'))
k('increase_context', '+', 'change_context 5', _('Increase context'))
k('decrease_context', '-', 'change_context -5', _('Decrease context'))
k('search_forward', '/', 'start_search regex forward', _('Search forward'))
k('search_backward', '?', 'start_search regex backward', _('Search backward'))
k('next_match', '.', 'scroll_to next-match', _('Scroll to next search match'))
k('prev_match', ',', 'scroll_to prev-match', _('Scroll to previous search match'))
k('next_match', '>', 'scroll_to next-match', _('Scroll to next search match'))
k('prev_match', '<', 'scroll_to prev-match', _('Scroll to previous search match'))
k('search_forward_simple', 'f', 'start_search substring forward', _('Search forward (no regex)'))
k('search_backward_simple', 'b', 'start_search substring backward', _('Search backward (no regex)'))
type_map = {o.name: o.option_type for o in all_options.values() if hasattr(o, 'option_type')}

View File

@@ -133,6 +133,7 @@ def highlight_collection(collection, aliases=None):
jobs = {}
ans = {}
with concurrent.futures.ProcessPoolExecutor(max_workers=os.cpu_count()) as executor:
highlight_collection.processes = executor._processes
for path, item_type, other_path in collection:
if item_type != 'rename':
for p in (path, other_path):

View File

@@ -3,26 +3,31 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import os
import signal
import sys
import warnings
from collections import defaultdict
from functools import partial
from gettext import gettext as _
from kitty.cli import CONFIG_HELP, appname, parse_args
from kitty.cli import CONFIG_HELP, parse_args
from kitty.constants import appname
from kitty.fast_data_types import wcswidth
from kitty.key_encoding import RELEASE
from kitty.key_encoding import ESCAPE, RELEASE, enter_key
from ..tui.handler import Handler
from ..tui.images import ImageManager
from ..tui.line_edit import LineEdit
from ..tui.loop import Loop
from ..tui.operations import styled
from .collect import (
create_collection, data_for_path, lines_for_path, set_highlight_data
create_collection, data_for_path, lines_for_path, sanitize,
set_highlight_data
)
from .config import init_config
from .patch import Differ, set_diff_command
from .patch import Differ, set_diff_command, worker_processes
from .render import ImageSupportWarning, LineRef, render_diff
from .search import BadRegex, Search
try:
from .highlight import initialize_highlighter, highlight_collection
@@ -30,7 +35,7 @@ except ImportError:
initialize_highlighter = None
INITIALIZING, COLLECTED, DIFFED = range(3)
INITIALIZING, COLLECTED, DIFFED, COMMAND, MESSAGE = range(5)
def generate_diff(collection, context):
@@ -38,7 +43,7 @@ def generate_diff(collection, context):
for path, item_type, changed_path in collection:
if item_type == 'diff':
is_binary = isinstance(data_for_path(path), bytes)
is_binary = isinstance(data_for_path(path), bytes) or isinstance(data_for_path(changed_path), bytes)
if not is_binary:
d.add_diff(path, changed_path)
@@ -51,6 +56,10 @@ class DiffHandler(Handler):
def __init__(self, args, opts, left, right):
self.state = INITIALIZING
self.message = ''
self.current_search_is_regex = True
self.current_search = None
self.line_edit = LineEdit()
self.opts = opts
self.left, self.right = left, right
self.report_traceback_on_exit = None
@@ -76,6 +85,8 @@ class DiffHandler(Handler):
where = args[0]
if 'change' in where:
return self.scroll_to_next_change(backwards='prev' in where)
if 'match' in where:
return self.scroll_to_next_match(backwards='prev' in where)
if 'page' in where:
amt = self.num_lines * (1 if 'next' in where else -1)
else:
@@ -91,12 +102,72 @@ class DiffHandler(Handler):
else:
new_ctx += to
return self.change_context_count(new_ctx)
if func == 'start_search':
self.start_search(*args)
return
def create_collection(self):
self.start_job('collect', create_collection, self.left, self.right)
def collect_done(collection):
self.collection = collection
self.state = COLLECTED
self.generate_diff()
def collect(left, right):
collection = create_collection(left, right)
self.asyncio_loop.call_soon_threadsafe(collect_done, collection)
self.asyncio_loop.run_in_executor(None, collect, self.left, self.right)
def generate_diff(self):
self.start_job('diff', generate_diff, self.collection, self.current_context_count)
def diff_done(diff_map):
if isinstance(diff_map, str):
self.report_traceback_on_exit = diff_map
self.quit_loop(1)
return
self.state = DIFFED
self.diff_map = diff_map
self.calculate_statistics()
self.render_diff()
self.scroll_pos = 0
if self.restore_position is not None:
self.current_position = self.restore_position
self.restore_position = None
self.draw_screen()
if initialize_highlighter is not None and not self.highlighting_done:
from .highlight import StyleNotFound
self.highlighting_done = True
try:
initialize_highlighter(self.opts.pygments_style)
except StyleNotFound as e:
self.report_traceback_on_exit = str(e)
self.quit_loop(1)
return
self.syntax_highlight()
def diff(collection, current_context_count):
diff_map = generate_diff(collection, current_context_count)
self.asyncio_loop.call_soon_threadsafe(diff_done, diff_map)
self.asyncio_loop.run_in_executor(None, diff, self.collection, self.current_context_count)
def syntax_highlight(self):
def highlighting_done(hdata):
if isinstance(hdata, str):
self.report_traceback_on_exit = hdata
self.quit_loop(1)
return
set_highlight_data(hdata)
self.render_diff()
self.draw_screen()
def highlight(*a):
result = highlight_collection(*a)
self.asyncio_loop.call_soon_threadsafe(highlighting_done, result)
self.asyncio_loop.run_in_executor(None, highlight, self.collection, self.opts.syntax_aliases)
def calculate_statistics(self):
self.added_count = self.collection.added_count
@@ -107,10 +178,13 @@ class DiffHandler(Handler):
def render_diff(self):
self.diff_lines = tuple(render_diff(self.collection, self.diff_map, self.args, self.screen_size.cols, self.image_manager))
self.margin_size = render_diff.margin_size
self.ref_path_map = defaultdict(list)
for i, l in enumerate(self.diff_lines):
self.ref_path_map[l.ref.path].append((i, l.ref))
self.max_scroll_pos = len(self.diff_lines) - self.num_lines
if self.current_search is not None:
self.current_search(self.diff_lines, self.margin_size, self.screen_size.cols)
@property
def current_position(self):
@@ -152,6 +226,19 @@ class DiffHandler(Handler):
return
self.cmd.bell()
def scroll_to_next_match(self, backwards=False, include_current=False):
if self.current_search is not None:
offset = 0 if include_current else 1
if backwards:
r = range(self.scroll_pos - offset, -1, -1)
else:
r = range(self.scroll_pos + offset, len(self.diff_lines))
for i in r:
if i in self.current_search:
self.scroll_lines(i - self.scroll_pos)
return
self.cmd.bell()
def set_scrolling_region(self):
self.cmd.set_scrolling_region(self.screen_size, 0, self.num_lines - 2)
@@ -178,7 +265,16 @@ class DiffHandler(Handler):
def init_terminal_state(self):
self.cmd.set_line_wrapping(False)
self.cmd.set_window_title(main.title)
self.cmd.set_default_colors(self.opts.foreground, self.opts.background)
self.cmd.set_default_colors(
fg=self.opts.foreground, bg=self.opts.background,
cursor=self.opts.foreground, select_fg=self.opts.select_fg,
select_bg=self.opts.select_bg)
self.cmd.set_cursor_shape('bar')
def finalize(self):
self.cmd.set_default_colors()
self.cmd.set_cursor_visible(True)
self.cmd.set_scrolling_region()
def initialize(self):
self.init_terminal_state()
@@ -187,12 +283,7 @@ class DiffHandler(Handler):
self.create_collection()
def enforce_cursor_state(self):
self.cmd.set_cursor_visible(self.state > DIFFED)
def finalize(self):
self.cmd.set_cursor_visible(True)
self.cmd.set_default_colors()
self.cmd.set_scrolling_region()
self.cmd.set_cursor_visible(self.state == COMMAND)
def draw_lines(self, num, offset=0):
offset += self.scroll_pos
@@ -208,6 +299,8 @@ class DiffHandler(Handler):
if line.image_data is not None:
image_involved = True
self.write('\r\x1b[K' + text + '\x1b[0m')
if self.current_search is not None:
self.current_search.highlight_line(self.write, lpos)
if i < num - 1:
self.write('\n')
if image_involved:
@@ -262,19 +355,29 @@ class DiffHandler(Handler):
def draw_status_line(self):
if self.state < DIFFED:
return
self.enforce_cursor_state()
self.cmd.set_cursor_position(0, self.num_lines)
self.cmd.clear_to_eol()
scroll_frac = styled('{:.0%}'.format(self.scroll_pos / (self.max_scroll_pos or 1)), fg=self.opts.margin_fg)
counts = '{}{}{}'.format(
styled(str(self.added_count), fg=self.opts.highlight_added_bg),
styled(',', fg=self.opts.margin_fg),
styled(str(self.removed_count), fg=self.opts.highlight_removed_bg)
)
suffix = counts + ' ' + scroll_frac
prefix = styled(':', fg=self.opts.margin_fg)
filler = self.screen_size.cols - wcswidth(prefix) - wcswidth(suffix)
text = '{}{}{}'.format(prefix, ' ' * filler, suffix)
self.write(text)
if self.state is COMMAND:
self.line_edit.write(self.write)
elif self.state is MESSAGE:
self.cmd.styled(self.message, reverse=True)
else:
sp = '{:.0%}'.format(self.scroll_pos/self.max_scroll_pos) if self.scroll_pos and self.max_scroll_pos else '0%'
scroll_frac = styled(sp, fg=self.opts.margin_fg)
if self.current_search is None:
counts = '{}{}{}'.format(
styled(str(self.added_count), fg=self.opts.highlight_added_bg),
styled(',', fg=self.opts.margin_fg),
styled(str(self.removed_count), fg=self.opts.highlight_removed_bg)
)
else:
counts = styled('{} matches'.format(len(self.current_search)), fg=self.opts.margin_fg)
suffix = counts + ' ' + scroll_frac
prefix = styled(':', fg=self.opts.margin_fg)
filler = self.screen_size.cols - wcswidth(prefix) - wcswidth(suffix)
text = '{}{}{}'.format(prefix, ' ' * filler, suffix)
self.write(text)
def change_context_count(self, new_ctx):
new_ctx = max(0, new_ctx)
@@ -285,14 +388,77 @@ class DiffHandler(Handler):
self.restore_position = self.current_position
self.draw_screen()
def start_search(self, is_regex, is_backward):
if self.state != DIFFED:
self.cmd.bell()
return
self.state = COMMAND
self.line_edit.clear()
self.line_edit.add_text('?' if is_backward else '/')
self.current_search_is_regex = is_regex
self.draw_status_line()
def do_search(self):
self.current_search = None
query = self.line_edit.current_input
if len(query) < 2:
return
try:
self.current_search = Search(self.opts, query[1:], self.current_search_is_regex, query[0] == '?')
except BadRegex:
self.state = MESSAGE
self.message = sanitize(_('Bad regex: {}').format(query[1:]))
self.cmd.bell()
else:
if self.current_search(self.diff_lines, self.margin_size, self.screen_size.cols):
self.scroll_to_next_match(include_current=True)
else:
self.state = MESSAGE
self.message = sanitize(_('No matches found'))
self.cmd.bell()
def on_text(self, text, in_bracketed_paste=False):
if self.state is COMMAND:
self.line_edit.on_text(text, in_bracketed_paste)
self.draw_status_line()
return
if self.state is MESSAGE:
self.state = DIFFED
self.draw_status_line()
return
action = self.shortcut_action(text)
if action is not None:
return self.perform_action(action)
def on_key(self, key_event):
if self.state is MESSAGE:
if key_event.type is not RELEASE:
self.state = DIFFED
self.draw_status_line()
return
if self.state is COMMAND:
if self.line_edit.on_key(key_event):
if not self.line_edit.current_input:
self.state = DIFFED
self.draw_status_line()
return
if key_event.type is RELEASE:
return
if self.state is COMMAND:
if key_event.key is ESCAPE:
self.state = DIFFED
self.draw_status_line()
return
if key_event is enter_key:
self.state = DIFFED
self.do_search()
self.line_edit.clear()
self.draw_screen()
return
if self.state >= DIFFED and self.current_search is not None and key_event.key is ESCAPE:
self.current_search = None
self.draw_screen()
return
action = self.shortcut_action(key_event)
if action is not None:
return self.perform_action(action)
@@ -305,50 +471,6 @@ class DiffHandler(Handler):
self.render_diff()
self.draw_screen()
def on_job_done(self, job_id, job_result):
if 'tb' in job_result:
self.report_traceback_on_exit = job_result['tb']
self.quit_loop(1)
return
if job_id == 'collect':
self.collection = job_result['result']
self.state = COLLECTED
self.generate_diff()
elif job_id == 'diff':
diff_map = job_result['result']
if isinstance(diff_map, str):
self.report_traceback_on_exit = diff_map
self.quit_loop(1)
return
self.state = DIFFED
self.diff_map = diff_map
self.calculate_statistics()
self.render_diff()
self.scroll_pos = 0
if self.restore_position is not None:
self.current_position = self.restore_position
self.restore_position = None
self.draw_screen()
if initialize_highlighter is not None and not self.highlighting_done:
from .highlight import StyleNotFound
self.highlighting_done = True
try:
initialize_highlighter(self.opts.pygments_style)
except StyleNotFound as e:
self.report_traceback_on_exit = str(e)
self.quit_loop(1)
return
self.start_job('highlight', highlight_collection, self.collection, self.opts.syntax_aliases)
elif job_id == 'highlight':
hdata = job_result['result']
if isinstance(hdata, str):
self.report_traceback_on_exit = diff_map
self.quit_loop(1)
return
set_highlight_data(hdata)
self.render_diff()
self.draw_screen()
def on_interrupt(self):
self.quit_loop(1)
@@ -387,6 +509,14 @@ help_text = 'Show a side-by-side diff of the specified files/directories'
usage = 'file_or_directory_left file_or_directory_right'
def terminate_processes(processes):
for pid in processes:
try:
os.kill(pid, signal.SIGKILL)
except Exception:
pass
def main(args):
warnings.showwarning = showwarning
args, items = parse_args(args[1:], OPTIONS, usage, help_text, 'kitty +kitten diff')
@@ -399,11 +529,9 @@ def main(args):
opts = init_config(args)
set_diff_command(opts.diff_cmd)
lines_for_path.replace_tab_by = opts.replace_tab_by
# Ensure the kitten works even when stdin/stdout are redirected
if not sys.stdin.isatty():
sys.stdin = open('/dev/tty')
if not sys.stdout.isatty():
sys.stdout = open('/dev/tty', 'w')
for f in left, right:
if not os.path.exists(f):
raise SystemExit('{} does not exist'.format(f))
loop = Loop()
handler = DiffHandler(args, opts, left, right)
@@ -411,6 +539,9 @@ def main(args):
for message in showwarning.warnings:
from kitty.utils import safe_print
safe_print(message, file=sys.stderr)
highlight_processes = getattr(highlight_collection, 'processes', ())
terminate_processes(tuple(highlight_processes))
terminate_processes(tuple(worker_processes))
if loop.return_code != 0:
if handler.report_traceback_on_exit:
print(handler.report_traceback_on_exit, file=sys.stderr)

View File

@@ -14,6 +14,7 @@ from .diff_speedup import changed_center
left_lines = right_lines = None
GIT_DIFF = 'git diff --no-color --no-ext-diff --exit-code -U_CONTEXT_ --no-index --'
DIFF_DIFF = 'diff -p -U _CONTEXT_ --'
worker_processes = []
def find_differ():
@@ -43,8 +44,10 @@ def run_diff(file1, file2, context=3):
p = subprocess.Popen(
cmd + [path1, path2],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL)
worker_processes.append(p.pid)
stdout, stderr = p.communicate()
returncode = p.wait()
worker_processes.remove(p.pid)
if returncode in (0, 1):
return True, returncode == 1, stdout.decode('utf-8')
return False, returncode, stderr.decode('utf-8')
@@ -205,9 +208,13 @@ def parse_patch(raw):
class Differ:
diff_executor = None
def __init__(self):
self.jmap = {}
self.jobs = []
if Differ.diff_executor is None:
Differ.diff_executor = self.diff_executor = concurrent.futures.ThreadPoolExecutor(max_workers=os.cpu_count())
def add_diff(self, file1, file2):
self.jmap[file1] = file2
@@ -216,26 +223,26 @@ class Differ:
def __call__(self, context=3):
global left_lines, right_lines
ans = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=os.cpu_count()) as executor:
jobs = {executor.submit(run_diff, key, self.jmap[key], context): key for key in self.jobs}
for future in concurrent.futures.as_completed(jobs):
key = jobs[future]
left_path, right_path = key, self.jmap[key]
try:
ok, returncode, output = future.result()
except FileNotFoundError as err:
return 'Could not find the {} executable. Is it in your PATH?'.format(err.filename)
except Exception as e:
return 'Running git diff for {} vs. {} generated an exception: {}'.format(left_path, right_path, e)
if not ok:
return output + '\nRunning git diff for {} vs. {} failed'.format(left_path, right_path)
left_lines = lines_for_path(left_path)
right_lines = lines_for_path(right_path)
try:
patch = parse_patch(output)
except Exception:
import traceback
return traceback.format_exc() + '\nParsing diff for {} vs. {} failed'.format(left_path, right_path)
else:
ans[key] = patch
executor = Differ.diff_executor
jobs = {executor.submit(run_diff, key, self.jmap[key], context): key for key in self.jobs}
for future in concurrent.futures.as_completed(jobs):
key = jobs[future]
left_path, right_path = key, self.jmap[key]
try:
ok, returncode, output = future.result()
except FileNotFoundError as err:
return 'Could not find the {} executable. Is it in your PATH?'.format(err.filename)
except Exception as e:
return 'Running git diff for {} vs. {} generated an exception: {}'.format(left_path, right_path, e)
if not ok:
return output + '\nRunning git diff for {} vs. {} failed'.format(left_path, right_path)
left_lines = lines_for_path(left_path)
right_lines = lines_for_path(right_path)
try:
patch = parse_patch(output)
except Exception:
import traceback
return traceback.format_exc() + '\nParsing diff for {} vs. {} failed'.format(left_path, right_path)
else:
ans[key] = patch
return ans

View File

@@ -447,13 +447,15 @@ def render_diff(collection, diff_map, args, columns, image_manager):
if patch is not None:
largest_line_number = max(largest_line_number, patch.largest_line_number)
margin_size = max(3, len(str(largest_line_number)) + 1)
margin_size = render_diff.margin_size = max(3, len(str(largest_line_number)) + 1)
last_item_num = len(collection) - 1
for i, (path, item_type, other_path) in enumerate(collection):
item_ref = Reference(path)
is_binary = isinstance(data_for_path(path), bytes)
is_img = is_binary and is_image(path) and images_supported()
if not is_binary and item_type == 'diff' and isinstance(data_for_path(other_path), bytes):
is_binary = True
is_img = is_binary and (is_image(path) or is_image(other_path)) and images_supported()
yield from yield_lines_from(title_lines(path, other_path, args, columns, margin_size), item_ref, False)
if item_type == 'diff':
if is_binary:

67
kittens/diff/search.py Normal file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import re
from kitty.fast_data_types import wcswidth
from ..tui.operations import styled
class BadRegex(ValueError):
pass
class Search:
def __init__(self, opts, query, is_regex, is_backward):
self.matches = {}
self.count = 0
self.style = styled('|', fg=opts.search_fg, bg=opts.search_bg).split('|', 1)[0]
if not is_regex:
query = re.escape(query)
try:
self.pat = re.compile(query, flags=re.UNICODE | re.IGNORECASE)
except Exception:
raise BadRegex('Not a valid regex: {}'.format(query))
def __call__(self, diff_lines, margin_size, cols):
self.matches = {}
self.count = 0
half_width = cols // 2
strip_pat = re.compile('\033[[].*?m')
right_offset = half_width + 1 + margin_size
find = self.pat.finditer
for i, line in enumerate(diff_lines):
text = strip_pat.sub('', line.text)
left, right = text[margin_size:half_width + 1], text[right_offset:]
matches = []
def add(which, offset):
for m in find(which):
before = which[:m.start()]
matches.append((wcswidth(before) + offset, m.group()))
self.count += 1
add(left, margin_size)
add(right, right_offset)
if matches:
self.matches[i] = matches
return bool(self.matches)
def __contains__(self, i):
return i in self.matches
def __len__(self):
return self.count
def highlight_line(self, write, line_num):
highlights = self.matches.get(line_num)
if not highlights:
return False
write(self.style)
for start, text in highlights:
write('\r\x1b[{}C{}'.format(start, text))
write('\x1b[m')
return True

View File

@@ -24,7 +24,7 @@ HINT_ALPHABET = string.digits + string.ascii_lowercase
screen_size = screen_size_function()
class Mark(object):
class Mark:
__slots__ = ('index', 'start', 'end', 'text')
@@ -63,8 +63,10 @@ def highlight_mark(m, text, current_input):
)
def render(text, current_input, all_marks):
def render(text, current_input, all_marks, ignore_mark_indices):
for mark in reversed(all_marks):
if mark.index in ignore_mark_indices:
continue
mtext = highlight_mark(mark, text[mark.start:mark.end], current_input)
text = text[:mark.start] + mtext + text[mark.end:]
@@ -78,11 +80,16 @@ class Hints(Handler):
def __init__(self, text, all_marks, index_map, args):
self.text, self.index_map = text, index_map
self.all_marks = all_marks
self.current_input = ''
self.current_text = None
self.ignore_mark_indices = set()
self.args = args
self.window_title = _('Choose URL') if args.type == 'url' else _('Choose text')
self.chosen = None
self.multiple = args.multiple
self.chosen = []
self.reset()
def reset(self):
self.current_input = ''
self.current_text = None
def init_terminal_state(self):
self.cmd.set_cursor_visible(False)
@@ -101,13 +108,17 @@ class Hints(Handler):
changed = True
if changed:
matches = [
m.text for idx, m in self.index_map.items()
m for idx, m in self.index_map.items()
if encode_hint(idx).startswith(self.current_input)
]
if len(matches) == 1:
self.chosen = matches[0]
self.quit_loop(0)
return
self.chosen.append(matches[0].text)
if self.multiple:
self.ignore_mark_indices.add(matches[0].index)
self.reset()
else:
self.quit_loop(0)
return
self.current_text = None
self.draw_screen()
@@ -119,15 +130,20 @@ class Hints(Handler):
elif key_event is enter_key and self.current_input:
try:
idx = decode_hint(self.current_input)
self.chosen = self.index_map[idx].text
self.chosen.append(self.index_map[idx].text)
self.ignore_mark_indices.add(idx)
except Exception:
self.current_input = ''
self.current_text = None
self.draw_screen()
else:
self.quit_loop(0)
if self.multiple:
self.reset()
self.draw_screen()
else:
self.quit_loop(0)
elif key_event.key is ESCAPE:
self.quit_loop(1)
self.quit_loop(0 if self.multiple else 1)
def on_interrupt(self):
self.quit_loop(1)
@@ -136,12 +152,11 @@ class Hints(Handler):
self.quit_loop(1)
def on_resize(self, new_size):
Handler.on_resize(self, new_size)
self.draw_screen()
def draw_screen(self):
if self.current_text is None:
self.current_text = render(self.text, self.current_input, self.all_marks)
self.current_text = render(self.text, self.current_input, self.all_marks, self.ignore_mark_indices)
self.cmd.clear_screen()
self.write(self.current_text)
@@ -155,7 +170,7 @@ def regex_finditer(pat, minimum_match_length, text):
yield s, e
closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>'}
closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>', '*': '*', '"': '"', "'": "'"}
opening_brackets = ''.join(closing_bracket_map)
postprocessor_map = {}
@@ -174,15 +189,15 @@ def url(text, s, e):
e -= len(url) - idx
while text[e - 1] in '.,?!' and e > 1: # remove trailing punctuation
e -= 1
# truncate url at closing bracket/quote
if s > 0 and e <= len(text) and text[s-1] in opening_brackets:
q = closing_bracket_map[text[s-1]]
idx = text.find(q, s)
if idx > s:
e = idx
# Restructured Text URLs
if e > 3 and text[e-2:e] == '`_':
e -= 2
# Remove trailing bracket if matched by leading bracket
if s > 0 and e < len(text) and text[s-1] in opening_brackets and text[e-1] == closing_bracket_map[text[s-1]]:
e -= 1
# Remove trailing quote if matched by leading quote
if s > 0 and e < len(text) and text[s-1] in '\'"' and text[e-1] == text[s-1]:
e -= 1
return s, e
@@ -240,7 +255,7 @@ def functions_for(args):
)
post_processors.append(url)
elif args.type == 'path':
pattern = r'(?:\S*/\S+)|(?:\S+[.][a-zA-Z0-9]{2,5})'
pattern = r'(?:\S*/\S+)|(?:\S+[.][a-zA-Z0-9]{2,7})'
post_processors.extend((brackets, quotes))
elif args.type == 'line':
pattern = '(?m)^\\s*(.+)[\\s\0]*$'
@@ -342,6 +357,12 @@ Defaults to the select_by_word_characters setting from kitty.conf.
default=3
type=int
The minimum number of characters to consider a match.
--multiple
type=bool-set
Select multiple matches and perform the action on all of them together at the end.
In this mode, press :kbd:`Esc` to finish selecting.
'''.format(','.join(sorted(URL_PREFIXES))).format
help_text = 'Select text from the screen using the keyboard. Defaults to searching for URLs.'
usage = ''
@@ -360,7 +381,7 @@ def main(args):
return
else:
text = sys.stdin.buffer.read().decode('utf-8')
sys.stdin = open('/dev/tty')
sys.stdin = open(os.ctermid())
try:
args, items = parse_hints_args(args[1:])
except SystemExit as e:
@@ -377,18 +398,22 @@ def main(args):
def handle_result(args, data, target_window_id, boss):
program = data['program']
matches = tuple(filter(None, data['match']))
if program == '-':
w = boss.window_id_map.get(target_window_id)
if w is not None:
w.paste(data['match'])
for m in matches:
w.paste(m)
elif program == '@':
set_clipboard_string(data['match'])
set_clipboard_string(matches[-1])
else:
cwd = None
w = boss.window_id_map.get(target_window_id)
if w is not None:
cwd = w.cwd_of_child
boss.open_url(data['match'], None if program == 'default' else program, cwd=cwd)
program = None if program == 'default' else program
for m in matches:
boss.open_url(m, program, cwd=cwd)
handle_result.type_of_input = 'screen'

View File

@@ -1 +1 @@
url_delimiters = '\x00-\x09\x0b-\x20\x7f-\xa0\xad\u0600-\u0605\u061c\u06dd\u070f\u08e2\u1680\u180e\u2000-\u200f\u2028-\u202f\u205f-\u2064\u2066-\u206f\u3000\ud800-\uf8ff\ufeff\ufff9-\ufffb\U000110bd\U0001bca0-\U0001bca3\U0001d173-\U0001d17a\U000e0001\U000e0020-\U000e007f\U000f0000-\U000ffffd\U00100000-\U0010fffd' # noqa
url_delimiters = '\x00-\x09\x0b-\x20\x7f-\xa0\xad\u0600-\u0605\u061c\u06dd\u070f\u08e2\u1680\u180e\u2000-\u200f\u2028-\u202f\u205f-\u2064\u2066-\u206f\u3000\ud800-\uf8ff\ufeff\ufff9-\ufffb\U000110bd\U000110cd\U0001bca0-\U0001bca3\U0001d173-\U0001d17a\U000e0001\U000e0020-\U000e007f\U000f0000-\U000ffffd\U00100000-\U0010fffd' # noqa

View File

@@ -260,13 +260,13 @@ def main(args=sys.argv):
if args.print_window_size:
screen_size_function.ans = None
with open('/dev/tty') as tty:
with open(os.ctermid()) as tty:
ss = screen_size_function(tty)()
print('{}x{}'.format(ss.width, ss.height), end='')
raise SystemExit(0)
if not sys.stdout.isatty():
sys.stdout = open('/dev/tty', 'w')
sys.stdout = open(os.ctermid(), 'w')
screen_size = screen_size_function()
signal.signal(signal.SIGWINCH, lambda signum, frame: setattr(screen_size, 'changed', True))
if screen_size().width == 0:

View File

@@ -2,12 +2,12 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import shutil
import subprocess
import sys
from kitty.constants import is_macos, is_wayland
from kitty.cli import parse_args
from kitty.constants import is_macos, is_wayland
OPTIONS = r'''
--lines
@@ -123,7 +123,8 @@ def main(sys_args):
global args
if is_macos or is_wayland:
raise SystemExit('Currently the panel kitten is supported only on X11 desktops')
call_xprop('-version', silent=True) # ensure xprop is available
if not shutil.which('xprop'):
raise SystemExit('The xprop program is required for the panel kitten')
args, items = parse_panel_args(sys_args[1:])
if not items:
raise SystemExit('You must specify the program to run')

View File

@@ -74,7 +74,6 @@ class Resize(Handler):
self.do_window_resize(is_decrease=key_event.key in (N, S), is_horizontal=key_event.key in (W, N), multiplier=2)
def on_resize(self, new_size):
Handler.on_resize(self, new_size)
self.draw_screen()
def draw_screen(self):

View File

@@ -17,23 +17,27 @@ def resolved_kitten(k):
def import_kitten_main_module(config_dir, kitten):
if kitten.endswith('.py'):
path_modified = False
path = os.path.expanduser(kitten)
if not os.path.isabs(path):
path = os.path.join(config_dir, path)
path = os.path.abspath(path)
if os.path.dirname(path):
sys.path.insert(0, os.path.dirname(path))
path_modified = True
with open(path) as f:
src = f.read()
code = compile(src, path, 'exec')
g = {'__name__': 'kitten'}
exec(code, g)
hr = g.get('handle_result', lambda *a, **kw: None)
if path_modified:
del sys.path[0]
return {'start': g['main'], 'end': hr}
else:
kitten = resolved_kitten(kitten)
m = importlib.import_module('kittens.{}.main'.format(kitten))
return {'start': m.main, 'end': getattr(m, 'handle_result', lambda *a, **k: None)}
kitten = resolved_kitten(kitten)
m = importlib.import_module('kittens.{}.main'.format(kitten))
return {'start': m.main, 'end': getattr(m, 'handle_result', lambda *a, **k: None)}
def create_kitten_handler(kitten, orig_args):
@@ -76,8 +80,11 @@ def launch(args):
def deserialize(output):
import json
if output.startswith('OK: '):
prefix, sz, rest = output.split(' ', 2)
return json.loads(rest[:int(sz)])
try:
prefix, sz, rest = output.split(' ', 2)
return json.loads(rest[:int(sz)])
except Exception:
raise ValueError('Failed to parse kitten output: {!r}'.format(output))
def run_kitten(kitten, run_name='__main__'):

View File

@@ -2,11 +2,12 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import os
import sys
from kitty.cli import parse_args
from ..tui.operations import styled
from ..tui.operations import styled
OPTIONS = '''\
--title
@@ -17,7 +18,7 @@ The title for the error message.
def real_main(args):
error_message = sys.stdin.buffer.read().decode('utf-8')
sys.stdin = open('/dev/tty')
sys.stdin = open(os.ctermid())
msg = 'Show an error message'
args, items = parse_args(args, OPTIONS, '', msg, 'hints')
print(styled(args.title, fg_intense=True, fg='red', bold=True))

View File

@@ -102,7 +102,7 @@ def main(args):
sh_script = SHELL_SCRIPT.replace('TERMINFO', terminfo, 1)
if len(server_args) > 1:
command_to_execute = ["'{}'".format(c.replace("'", """'"'"'""")) for c in server_args[1:]]
command_to_execute = 'cmd=({}); exec "$cmd"'.format(' '.join(command_to_execute))
command_to_execute = 'exec ' + ' '.join(command_to_execute)
else:
command_to_execute = ''
sh_script = sh_script.replace('EXEC_CMD', command_to_execute)

View File

@@ -10,14 +10,19 @@ class Handler:
image_manager_class = None
def _initialize(self, screen_size, quit_loop, wakeup, start_job, debug, image_manager=None):
self.screen_size, self.quit_loop = screen_size, quit_loop
self.wakeup = wakeup
def _initialize(self, screen_size, term_manager, schedule_write, tui_loop, debug, image_manager=None):
self.screen_size = screen_size
self._term_manager = term_manager
self._tui_loop = tui_loop
self._schedule_write = schedule_write
self.debug = debug
self.start_job = start_job
self.cmd = commander(self)
self.image_manager = image_manager
@property
def asyncio_loop(self):
return self._tui_loop.asycio_loop
def add_shortcut(self, action, key, mods=None, is_text=False):
if not hasattr(self, '_text_shortcuts'):
self._text_shortcuts, self._key_shortcuts = {}, {}
@@ -38,7 +43,6 @@ class Handler:
self.initialize()
def __exit__(self, *a):
del self.write_buf[:]
del self.debug.fobj
self.finalize()
if self.image_manager is not None:
@@ -53,8 +57,11 @@ class Handler:
def on_resize(self, screen_size):
self.screen_size = screen_size
def quit_loop(self, return_code=None):
self._tui_loop.quit(return_code)
def on_term(self):
self.quit_loop(1)
self._tui_loop.quit(1)
def on_text(self, text, in_bracketed_paste=False):
pass
@@ -71,12 +78,6 @@ class Handler:
def on_eot(self):
pass
def on_wakeup(self):
pass
def on_job_done(self, job_id, job_result):
pass
def on_kitty_cmd_response(self, response):
pass
@@ -86,7 +87,7 @@ class Handler:
def write(self, data):
if isinstance(data, str):
data = data.encode('utf-8')
self.write_buf.append(data)
self._schedule_write(data)
def print(self, *args, sep=' ', end='\r\n'):
data = sep.join(map(str, args)) + end

View File

@@ -71,13 +71,15 @@ def convert(path, m, available_width, available_height, scale_up, tdir=None):
from tempfile import NamedTemporaryFile
width, height = m.width, m.height
cmd = ['convert', '-background', 'none', path]
scaled = False
if scale_up:
if width < available_width:
r = available_width / width
width, height = available_width, int(height * r)
if width > available_width or height > available_height:
scaled = True
if scaled or width > available_width or height > available_height:
width, height = fit_image(width, height, available_width, available_height)
cmd += ['-resize', '{}x{}'.format(width, height)]
cmd += ['-resize', '{}x{}!'.format(width, height)]
with NamedTemporaryFile(prefix='icat-', suffix='.' + m.mode, delete=False, dir=tdir) as outfile:
run_imagemagick(path, cmd + [outfile.name])
# ImageMagick sometimes generated rgba images smaller than the specified
@@ -88,7 +90,10 @@ def convert(path, m, available_width, available_height, scale_up, tdir=None):
if sz < expected_size:
missing = expected_size - sz
if missing % (bytes_per_pixel * width) != 0:
raise ConvertFailed(path, 'ImageMagick failed to convert {} correctly, it generated {} < {} of data'.format(path, sz, expected_size))
raise ConvertFailed(
path, 'ImageMagick failed to convert {} correctly,'
' it generated {} < {} of data (w={}, h={}, bpp={})'.format(
path, sz, expected_size, width, height, bytes_per_pixel))
height -= missing // (bytes_per_pixel * width)
return outfile.name, width, height

128
kittens/tui/line_edit.py Normal file
View File

@@ -0,0 +1,128 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
from kitty.fast_data_types import truncate_point_for_length, wcswidth
from kitty.key_encoding import RELEASE, HOME, END, BACKSPACE, DELETE, LEFT, RIGHT
class LineEdit:
def __init__(self):
self.clear()
def clear(self):
self.current_input = ''
self.cursor_pos = 0
self.pending_bell = False
def split_at_cursor(self, delta=0):
pos = max(0, self.cursor_pos + delta)
x = truncate_point_for_length(self.current_input, pos) if pos else 0
before, after = self.current_input[:x], self.current_input[x:]
return before, after
def write(self, write, prompt=''):
if self.pending_bell:
write('\a')
self.pending_bell = False
write(prompt)
write(self.current_input)
write('\r\x1b[{}C'.format(self.cursor_pos + wcswidth(prompt)))
def add_text(self, text):
if self.current_input:
x = truncate_point_for_length(self.current_input, self.cursor_pos) if self.cursor_pos else 0
self.current_input = self.current_input[:x] + text + self.current_input[x:]
else:
self.current_input = text
self.cursor_pos += wcswidth(text)
def on_text(self, text, in_bracketed_paste):
self.add_text(text)
def backspace(self, num=1):
before, after = self.split_at_cursor()
nbefore = before[:-num]
if nbefore != before:
self.current_input = nbefore + after
self.cursor_pos = wcswidth(nbefore)
return True
self.pending_bell = True
return False
def delete(self, num=1):
before, after = self.split_at_cursor()
nafter = after[num:]
if nafter != after:
self.current_input = before + nafter
self.cursor_pos = wcswidth(before)
return True
self.pending_bell = True
return False
def _left(self):
if not self.current_input:
self.cursor_pos = 0
return
if self.cursor_pos:
before, after = self.split_at_cursor(-1)
self.cursor_pos = wcswidth(before)
def _right(self):
if not self.current_input:
self.cursor_pos = 0
return
max_pos = wcswidth(self.current_input)
if self.cursor_pos >= max_pos:
self.cursor_pos = max_pos
return
before, after = self.split_at_cursor(1)
self.cursor_pos += 1 + int(wcswidth(before) == self.cursor_pos)
def _move_loop(self, func, num):
before = self.cursor_pos
while func() and num > 0:
num -= 1
changed = self.cursor_pos != before
if not changed:
self.pending_bell = True
return changed
def left(self, num=1):
return self._move_loop(self._left, num)
def right(self, num=1):
return self._move_loop(self._right, num)
def home(self):
if self.cursor_pos:
self.cursor_pos = 0
return True
return False
def end(self):
orig = self.cursor_pos
self.cursor_pos = wcswidth(self.current_input) + 1
return self.cursor_pos != orig
def on_key(self, key_event):
if key_event.type is RELEASE:
return False
elif key_event.key is HOME:
return self.home()
elif key_event.key is END:
return self.end()
elif key_event.key is BACKSPACE:
self.backspace()
return True
elif key_event.key is DELETE:
self.delete()
return True
elif key_event.key is LEFT:
self.left()
return True
elif key_event.key is RIGHT:
self.right()
return True
return False

View File

@@ -2,6 +2,7 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import asyncio
import codecs
import io
import os
@@ -10,13 +11,12 @@ import selectors
import signal
import sys
from collections import namedtuple
from contextlib import closing, contextmanager
from contextlib import contextmanager
from functools import partial
from queue import Empty, Queue
from kitty.constants import is_macos
from kitty.fast_data_types import (
close_tty, normal_tty, open_tty, parse_input_from_terminal, raw_tty,
safe_pipe
close_tty, normal_tty, open_tty, parse_input_from_terminal, raw_tty
)
from kitty.key_encoding import (
ALT, CTRL, PRESS, RELEASE, REPEAT, SHIFT, C, D, backspace_key,
@@ -74,29 +74,6 @@ class TermManager:
del self.tty_fd, self.original_termios
class SignalManager:
def __init__(self, on_sigwinch, on_sigterm, on_sigint):
self.on_sigwinch = on_sigwinch
self.on_sigterm = on_sigterm
self.on_sigint = on_sigint
def __enter__(self):
for x in ('winch', 'term', 'int'):
attr = 'on_sig' + x
handler = getattr(self, attr)
old_handler = signal.signal(getattr(signal, 'SIG' + x.upper()), handler)
setattr(self, attr, old_handler)
def __exit__(self, *a):
for x in ('winch', 'term', 'int'):
attr = 'on_sig' + x
val = getattr(self, attr)
if val is None:
val = signal.SIG_DFL
signal.signal(getattr(signal, 'SIG' + x.upper()), val)
LEFT, MIDDLE, RIGHT, FOURTH, FIFTH = 1, 2, 4, 8, 16
DRAG = REPEAT
MouseEvent = namedtuple('MouseEvent', 'x y type buttons mods')
@@ -150,22 +127,38 @@ class UnhandledException(Handler):
def on_interrupt(self):
self.quit_loop(1)
on_eot = on_term = on_interrupt
def on_eot(self):
self.quit_loop(1)
class SignalManager:
def __init__(self, loop, on_winch, on_interrupt, on_term):
self.asycio_loop = loop
self.on_winch, self.on_interrupt, self.on_term = on_winch, on_interrupt, on_term
def __enter__(self):
tuple(map(lambda x: self.asycio_loop.add_signal_handler(*x), (
(signal.SIGWINCH, self.on_winch),
(signal.SIGINT, self.on_interrupt),
(signal.SIGTERM, self.on_term)
)))
def __exit__(self, *a):
tuple(map(self.asycio_loop.remove_signal_handler, (
signal.SIGWINCH, signal.SIGINT, signal.SIGTERM)))
class Loop:
def __init__(self,
sanitize_bracketed_paste='[\x03\x04\x0e\x0f\r\x07\x7f\x8d\x8e\x8f\x90\x9b\x9d\x9e\x9f]'):
self.wakeup_read_fd, self.wakeup_write_fd = safe_pipe()
# For some reason on macOS the DefaultSelector fails when tty_fd is
# open('/dev/tty')
self.sel = s = selectors.PollSelector()
s.register(self.wakeup_read_fd, selectors.EVENT_READ)
if is_macos:
# On macOS PTY devices are not supported by the KqueueSelector
self.asycio_loop = asyncio.SelectorEventLoop(selectors.PollSelector())
asyncio.set_event_loop(self.asycio_loop)
else:
self.asycio_loop = asyncio.get_event_loop()
self.return_code = 0
self.read_allowed = True
self.read_buf = ''
self.decoder = codecs.getincrementaldecoder('utf-8')('ignore')
try:
@@ -178,28 +171,8 @@ class Loop:
self.sanitize_bracketed_paste = bool(sanitize_bracketed_paste)
if self.sanitize_bracketed_paste:
self.sanitize_ibp_pat = re.compile(sanitize_bracketed_paste)
self.jobs_queue = Queue()
def start_job(self, job_id, func, *args, **kw):
from threading import Thread
t = Thread(target=partial(self._run_job, job_id, func), args=args, kwargs=kw, name='LoopJob')
t.daemon = True
t.start()
def _run_job(self, job_id, func, *args, **kw):
try:
result = func(*args, **kw)
except Exception as err:
import traceback
entry = {'id': job_id, 'exception': err, 'tb': traceback.format_exc()}
else:
entry = {'id': job_id, 'result': result}
self.jobs_queue.put(entry)
self._wakeup_write(b'j')
def _read_ready(self, handler, fd):
if not self.read_allowed:
return
try:
data = os.read(fd, io.DEFAULT_BUFFER_SIZE)
except BlockingIOError:
@@ -219,6 +192,7 @@ class Loop:
finally:
del self.handler
# terminal input callbacks {{{
def _on_text(self, text):
if self.in_bracketed_paste and self.sanitize_bracketed_paste:
text = self.sanitize_ibp_pat.sub('', text)
@@ -235,7 +209,7 @@ class Loop:
self.handler.on_eot()
else:
self.handler.on_text(chunk, self.in_bracketed_paste)
else:
elif chunk:
self.handler.on_text(chunk, self.in_bracketed_paste)
def _on_dcs(self, dcs):
@@ -292,127 +266,99 @@ class Loop:
elif apc.startswith('G'):
if self.handler.image_manager is not None:
self.handler.image_manager.handle_response(apc)
# }}}
def _write_ready(self, handler, fd):
if len(handler.write_buf) > self.iov_limit:
handler.write_buf[self.iov_limit - 1] = b''.join(handler.write_buf[self.iov_limit - 1:])
del handler.write_buf[self.iov_limit:]
sizes = tuple(map(len, handler.write_buf))
if len(self.write_buf) > self.iov_limit:
self.write_buf[self.iov_limit - 1] = b''.join(self.write_buf[self.iov_limit - 1:])
del self.write_buf[self.iov_limit:]
sizes = tuple(map(len, self.write_buf))
try:
written = os.writev(fd, handler.write_buf)
written = os.writev(fd, self.write_buf)
except BlockingIOError:
return
if not written:
raise EOFError('The output stream is closed')
if written >= sum(sizes):
handler.write_buf = []
self.write_buf = []
self.asycio_loop.remove_writer(fd)
self.waiting_for_writes = False
else:
consumed = 0
for i, buf in enumerate(handler.write_buf):
for i, buf in enumerate(self.write_buf):
if not written:
break
if len(buf) <= written:
written -= len(buf)
consumed += 1
continue
handler.write_buf[i] = buf[written:]
self.write_buf[i] = buf[written:]
break
del handler.write_buf[:consumed]
def _wakeup_ready(self, handler, fd):
data = os.read(fd, 1024)
if b'r' in data:
self._get_screen_size.changed = True
handler.on_resize(self._get_screen_size())
if b't' in data:
handler.on_term()
if b'i' in data:
handler.on_interrupt()
if b'j' in data:
while True:
try:
entry = self.jobs_queue.get_nowait()
except Empty:
break
else:
job_id = entry.pop('id')
handler.on_job_done(job_id, entry)
if b'1' in data:
handler.on_wakeup()
def _wakeup_write(self, val):
while not os.write(self.wakeup_write_fd, val):
pass
def _on_sigwinch(self, signum, frame):
self._wakeup_write(b'r')
def _on_sigterm(self, signum, frame):
self._wakeup_write(b't')
def _on_sigint(self, signum, frame):
self._wakeup_write(b'i')
del self.write_buf[:consumed]
def quit(self, return_code=None):
self.read_allowed = False
if return_code is not None:
self.return_code = return_code
self.asycio_loop.stop()
def wakeup(self):
self._wakeup_write(b'1')
def loop_impl(self, handler, term_manager, image_manager=None):
self.write_buf = []
tty_fd = term_manager.tty_fd
tb = None
self.waiting_for_writes = True
def _modify_output_selector(self, tty_fd, waiting_for_write):
events = selectors.EVENT_READ
if waiting_for_write:
events |= selectors.EVENT_WRITE
self.sel.modify(tty_fd, events)
def schedule_write(data):
self.write_buf.append(data)
if not self.waiting_for_writes:
self.asycio_loop.add_writer(tty_fd, self._write_ready, handler, tty_fd)
self.waiting_for_writes = True
def loop_impl(self, handler, tty_fd, image_manager=None, waiting_for_write=True):
read_ready, write_ready, wakeup_ready = self._read_ready, self._write_ready, self._wakeup_ready
select = self.sel.select
handler._initialize(self._get_screen_size(), self.quit, self.wakeup, self.start_job, debug, image_manager)
def handle_exception(loop, context):
nonlocal tb
loop.stop()
tb = context['message']
exc = context.get('exception')
if exc is not None:
import traceback
tb += '\n' + ''.join(traceback.format_exception(exc.__class__, exc, exc.__traceback__))
self.asycio_loop.set_exception_handler(handle_exception)
handler._initialize(self._get_screen_size(), term_manager, schedule_write, self, debug, image_manager)
with handler:
while True:
has_data_to_write = bool(handler.write_buf)
if not has_data_to_write and not self.read_allowed:
break
if has_data_to_write != waiting_for_write:
waiting_for_write = has_data_to_write
self._modify_output_selector(tty_fd, waiting_for_write)
for key, mask in select():
fd = key.fd
if fd == tty_fd:
if mask & selectors.EVENT_READ:
read_ready(handler, fd)
if mask & selectors.EVENT_WRITE:
write_ready(handler, fd)
else:
wakeup_ready(handler, fd)
self.asycio_loop.add_reader(
tty_fd, self._read_ready, handler, tty_fd)
self.asycio_loop.add_writer(
tty_fd, self._write_ready, handler, tty_fd)
self.asycio_loop.run_forever()
self.asycio_loop.remove_reader(tty_fd)
if self.waiting_for_writes:
self.asycio_loop.remove_writer(tty_fd)
return tb
def loop(self, handler):
tb = None
signal_manager = SignalManager(self._on_sigwinch, self._on_sigterm, self._on_sigint)
with closing(self.sel), TermManager() as term_manager, signal_manager:
self.sel.register(term_manager.tty_fd, selectors.EVENT_READ | selectors.EVENT_WRITE)
def _on_sigwinch():
self._get_screen_size.changed = True
handler.screen_size = self._get_screen_size()
handler.on_resize(handler.screen_size)
signal_manager = SignalManager(self.asycio_loop, _on_sigwinch, handler.on_interrupt, handler.on_term)
with TermManager() as term_manager, signal_manager:
self._get_screen_size = screen_size_function(term_manager.tty_fd)
handler.write_buf = []
handler._term_manager = term_manager
image_manager = None
if handler.image_manager_class is not None:
image_manager = handler.image_manager_class(handler)
try:
self.loop_impl(handler, term_manager.tty_fd, image_manager)
tb = self.loop_impl(handler, term_manager, image_manager)
except Exception:
import traceback
tb = traceback.format_exc()
self.return_code = 1
term_manager.extra_finalize = b''.join(handler.write_buf).decode('utf-8')
term_manager.extra_finalize = b''.join(self.write_buf).decode('utf-8')
if tb is not None:
self.return_code = 1
self._report_error_loop(tb, term_manager)
def _report_error_loop(self, tb, term_manager):
handler = UnhandledException(tb)
handler.write_buf = []
handler._term_manager = term_manager
self.loop_impl(handler, term_manager.tty_fd, waiting_for_write=False)
self.loop_impl(UnhandledException(tb), term_manager)

View File

@@ -78,6 +78,13 @@ def set_cursor_position(x, y) -> str: # (0, 0) is top left
return '\033[{};{}H'.format(y + 1, x + 1)
def set_cursor_shape(shape='block', blink=True) -> str:
val = {'block': 1, 'underline': 3, 'bar': 5}.get(shape, 1)
if not blink:
val += 1
return '\033[{} q'.format(val)
def set_scrolling_region(screen_size=None, top=None, bottom=None) -> str:
if screen_size is None:
return '\033[r'
@@ -182,10 +189,11 @@ def init_state(alternate_screen=True):
reset_mode('IRM') + reset_mode('DECKM') + reset_mode('DECSCNM') +
set_mode('DECARM') + reset_mode('DECOM') + set_mode('DECAWM') +
set_mode('DECTCEM') + reset_mode('MOUSE_BUTTON_TRACKING') +
reset_mode('MOUSE_MOTION_TRACKING') + reset_mode('MOUSE_MOVE_TRACKING')
+ reset_mode('FOCUS_TRACKING') + reset_mode('MOUSE_UTF8_MODE') +
reset_mode('MOUSE_MOTION_TRACKING') + reset_mode('MOUSE_MOVE_TRACKING') +
reset_mode('FOCUS_TRACKING') + reset_mode('MOUSE_UTF8_MODE') +
reset_mode('MOUSE_SGR_MODE') + reset_mode('MOUSE_UTF8_MODE') +
set_mode('BRACKETED_PASTE') + set_mode('EXTENDED_KEYBOARD') +
'\033]30001\033\\' +
'\033[*x' # reset DECSACE to default region select
)
if alternate_screen:
@@ -200,6 +208,7 @@ def reset_state(normal_screen=True):
ans += reset_mode('ALTERNATE_SCREEN')
ans += RESTORE_PRIVATE_MODE_VALUES
ans += RESTORE_CURSOR
ans += '\033]30101\033\\'
return ans
@@ -218,16 +227,21 @@ def alternate_screen(f=None):
print(reset_mode('ALTERNATE_SCREEN'), end='', file=f)
def set_default_colors(fg=None, bg=None) -> str:
def set_default_colors(fg=None, bg=None, cursor=None, select_bg=None, select_fg=None) -> str:
ans = ''
if fg is None:
ans += '\x1b]110\x1b\\'
else:
ans += '\x1b]10;{}\x1b\\'.format(color_as_sharp(fg if isinstance(fg, Color) else to_color(fg)))
if bg is None:
ans += '\x1b]111\x1b\\'
else:
ans += '\x1b]11;{}\x1b\\'.format(color_as_sharp(bg if isinstance(bg, Color) else to_color(bg)))
def item(which, num):
nonlocal ans
if which is None:
ans += '\x1b]1{}\x1b\\'.format(num)
else:
ans += '\x1b]{};{}\x1b\\'.format(num, color_as_sharp(which if isinstance(which, Color) else to_color(which)))
item(fg, 10)
item(bg, 11)
item(cursor, 12)
item(select_bg, 17)
item(select_fg, 19)
return ans

View File

@@ -14,9 +14,10 @@ from kitty.utils import get_editor
from kitty.fast_data_types import wcswidth
from kitty.key_encoding import (
DOWN, ESCAPE, F1, F2, F3, F4, F12, LEFT, RELEASE, RIGHT, SHIFT, TAB, UP,
backspace_key, enter_key
enter_key
)
from ..tui.line_edit import LineEdit
from ..tui.handler import Handler
from ..tui.loop import Loop
from ..tui.operations import (
@@ -247,8 +248,8 @@ class UnicodeInput(Handler):
def __init__(self, cached_values):
self.cached_values = cached_values
self.line_edit = LineEdit()
self.recent = list(self.cached_values.get('recent', DEFAULT_SET))
self.current_input = ''
self.current_char = None
self.prompt_template = '{}> '
self.last_updated_code_point_at = None
@@ -269,9 +270,9 @@ class UnicodeInput(Handler):
codepoints = load_favorites()
q = self.mode, tuple(codepoints)
elif self.mode is NAME:
q = self.mode, self.current_input
q = self.mode, self.line_edit.current_input
if q != self.last_updated_code_point_at:
words = self.current_input.split()
words = self.line_edit.current_input.split()
words = [w for w in words if w != INDEX_CHAR]
index_words = [i for i, w in enumerate(words) if i > 0 and is_index(w)]
if index_words:
@@ -291,10 +292,10 @@ class UnicodeInput(Handler):
self.current_char = None
if self.mode is HEX:
try:
if self.current_input.startswith(INDEX_CHAR) and len(self.current_input) > 1:
self.current_char = chr(self.table.codepoint_at_hint(self.current_input[1:]))
if self.line_edit.current_input.startswith(INDEX_CHAR) and len(self.line_edit.current_input) > 1:
self.current_char = chr(self.table.codepoint_at_hint(self.line_edit.current_input[1:]))
else:
code = int(self.current_input, 16)
code = int(self.line_edit.current_input, 16)
self.current_char = chr(code)
except Exception:
pass
@@ -304,8 +305,8 @@ class UnicodeInput(Handler):
self.current_char = chr(cc)
else:
try:
if self.current_input:
self.current_char = chr(self.table.codepoint_at_hint(self.current_input.lstrip(INDEX_CHAR)))
if self.line_edit.current_input:
self.current_char = chr(self.table.codepoint_at_hint(self.line_edit.current_input.lstrip(INDEX_CHAR)))
except Exception:
pass
if self.current_char is not None:
@@ -366,8 +367,7 @@ class UnicodeInput(Handler):
writeln(_('Enter the hex code for the character'))
else:
writeln(_('Enter the index for the character you want from the list below'))
self.write(self.prompt)
self.write(self.current_input)
self.line_edit.write(self.write, self.prompt)
with cursor(self.write):
writeln()
writeln(self.choice_line)
@@ -385,14 +385,34 @@ class UnicodeInput(Handler):
self.draw_screen()
def on_text(self, text, in_bracketed_paste):
self.current_input += text
self.line_edit.on_text(text, in_bracketed_paste)
self.refresh()
def on_key(self, key_event):
if key_event is backspace_key:
self.current_input = self.current_input[:-1]
if self.mode is NAME and key_event.type is not RELEASE and not key_event.mods:
if key_event.key is TAB:
if key_event.mods == SHIFT:
self.table.move_current(cols=-1), self.refresh()
elif not key_event.mods:
self.table.move_current(cols=1), self.refresh()
return
elif key_event.key is LEFT and not key_event.mods:
self.table.move_current(cols=-1), self.refresh()
return
elif key_event.key is RIGHT and not key_event.mods:
self.table.move_current(cols=1), self.refresh()
return
elif key_event.key is UP and not key_event.mods:
self.table.move_current(rows=-1), self.refresh()
return
elif key_event.key is DOWN and not key_event.mods:
self.table.move_current(rows=1), self.refresh()
return
if self.line_edit.on_key(key_event):
self.refresh()
elif key_event is enter_key:
return
if key_event is enter_key:
self.quit_loop(0)
elif key_event.type is RELEASE and not key_event.mods:
if key_event.key is ESCAPE:
@@ -407,20 +427,6 @@ class UnicodeInput(Handler):
self.switch_mode(FAVORITES)
elif key_event.key is F12 and self.mode is FAVORITES:
self.edit_favorites()
elif self.mode is NAME:
if key_event.key is TAB:
if key_event.mods == SHIFT:
self.table.move_current(cols=-1), self.refresh()
elif not key_event.mods:
self.table.move_current(cols=1), self.refresh()
elif key_event.key is LEFT and not key_event.mods:
self.table.move_current(cols=-1), self.refresh()
elif key_event.key is RIGHT and not key_event.mods:
self.table.move_current(cols=1), self.refresh()
elif key_event.key is UP and not key_event.mods:
self.table.move_current(rows=-1), self.refresh()
elif key_event.key is DOWN and not key_event.mods:
self.table.move_current(rows=1), self.refresh()
def edit_favorites(self):
if not os.path.exists(favorites_path):
@@ -437,7 +443,7 @@ class UnicodeInput(Handler):
if mode is not self.mode:
self.mode = mode
self.cached_values['mode'] = mode
self.current_input = ''
self.line_edit.clear()
self.current_char = None
self.choice_line = ''
self.refresh()
@@ -449,7 +455,6 @@ class UnicodeInput(Handler):
self.quit_loop(1)
def on_resize(self, new_size):
Handler.on_resize(self, new_size)
self.refresh()

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,6 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from functools import partial
from itertools import chain
from .fast_data_types import (
@@ -10,6 +9,16 @@ from .fast_data_types import (
)
from .utils import load_shaders
try:
from enum import IntFlag
except Exception:
from enum import IntEnum as IntFlag
class BorderColor(IntFlag):
# See the border vertex shader for how these flags become actual colors
default_bg, active, inactive, window_bg, bell = ((1 << i) for i in range(5))
def vertical_edge(os_window_id, tab_id, color, width, top, bottom, left):
add_borders_rect(os_window_id, tab_id, left, top, left + width, bottom, color)
@@ -19,15 +28,15 @@ def horizontal_edge(os_window_id, tab_id, color, height, left, right, top):
add_borders_rect(os_window_id, tab_id, left, top, right, top + height, color)
def edge(func, os_window_id, tab_id, color, sz, a, b):
return partial(func, os_window_id, tab_id, color, sz, a, b)
def border(os_window_id, tab_id, color, sz, left, top, right, bottom):
horz = edge(horizontal_edge, os_window_id, tab_id, color, sz, left, right)
horz(top), horz(bottom - sz) # top, bottom edges
vert = edge(vertical_edge, os_window_id, tab_id, color, sz, top, bottom)
vert(left), vert(right - sz) # left, right edges
def draw_edges(os_window_id, tab_id, colors, width, geometry, base_width=0):
left = geometry.left - (width + base_width)
top = geometry.top - (width + base_width)
right = geometry.right + (width + base_width)
bottom = geometry.bottom + (width + base_width)
horizontal_edge(os_window_id, tab_id, colors[1], width, left, right, top)
horizontal_edge(os_window_id, tab_id, colors[3], width, left, right, geometry.bottom + base_width)
vertical_edge(os_window_id, tab_id, colors[0], width, top, bottom, left)
vertical_edge(os_window_id, tab_id, colors[2], width, top, bottom, geometry.right + base_width)
def load_borders_program():
@@ -52,28 +61,31 @@ class Borders:
extra_blank_rects,
draw_window_borders=True
):
add_borders_rect(self.os_window_id, self.tab_id, 0, 0, 0, 0, 1)
add_borders_rect(self.os_window_id, self.tab_id, 0, 0, 0, 0, BorderColor.default_bg)
for br in chain(current_layout.blank_rects, extra_blank_rects):
add_borders_rect(self.os_window_id, self.tab_id, *br, 1)
add_borders_rect(self.os_window_id, self.tab_id, *br, BorderColor.default_bg)
bw, pw = self.border_width, self.padding_width
fw = bw + pw
if bw + pw <= 0:
return
draw_borders = bw > 0 and draw_window_borders and len(windows) > 1
if draw_borders:
border_data = current_layout.resolve_borders(windows, active_window)
if fw > 0:
for w in windows:
g = w.geometry
if bw > 0 and draw_window_borders:
# Draw the border rectangles
color = 2 if w is active_window else (16 if w.needs_attention else 4)
border(
self.os_window_id, self.tab_id,
color, bw, g.left - fw, g.top - fw, g.right + fw,
g.bottom + fw
)
if pw > 0:
# Draw the background rectangles over the padding region
color = w.screen.color_profile.default_bg
border(
self.os_window_id, self.tab_id,
(color << 8) | 8, pw, g.left - pw, g.top - pw, g.right + pw,
g.bottom + pw
)
for i, w in enumerate(windows):
g = w.geometry
window_bg = w.screen.color_profile.default_bg
window_bg = (window_bg << 8) | BorderColor.window_bg
if draw_borders:
# Draw the border rectangles
if w is active_window:
color = BorderColor.active
else:
color = BorderColor.bell if w.needs_attention else BorderColor.inactive
colors = tuple(color if needed else window_bg for needed in next(border_data))
draw_edges(
self.os_window_id, self.tab_id, colors, bw, g, base_width=pw)
if pw > 0:
# Draw the background rectangles over the padding region
colors = (window_bg, window_bg, window_bg, window_bg)
draw_edges(
self.os_window_id, self.tab_id, colors, pw, g)

View File

@@ -15,7 +15,7 @@ from .conf.utils import to_cmdline
from .config import initial_window_size_func, prepare_config_file_for_editing
from .config_data import MINIMUM_FONT_SIZE
from .constants import (
appname, config_dir, set_boss, supports_primary_selection
appname, config_dir, kitty_exe, set_boss, supports_primary_selection
)
from .fast_data_types import (
ChildMonitor, background_opacity_of, change_background_opacity,
@@ -25,6 +25,7 @@ from .fast_data_types import (
set_clipboard_string, set_in_sequence_mode, toggle_fullscreen
)
from .keys import get_shortcut, shortcut_matches
from .layout import set_draw_minimal_borders
from .remote_control import handle_cmd
from .rgb import Color, color_from_int
from .session import create_session
@@ -76,11 +77,13 @@ class DumpCommands: # {{{
class Boss:
def __init__(self, os_window_id, opts, args, cached_values, new_os_window_trigger):
set_draw_minimal_borders(opts)
self.window_id_map = WeakValueDictionary()
self.startup_colors = {k: opts[k] for k in opts if isinstance(opts[k], Color)}
self.pending_sequences = None
self.cached_values = cached_values
self.os_window_map = {}
self.os_window_death_actions = {}
self.cursor_blinking = True
self.shutting_down = False
talk_fd = getattr(single_instance, 'socket', None)
@@ -95,7 +98,7 @@ class Boss:
)
set_boss(self)
self.opts, self.args = opts, args
startup_session = create_session(opts, args)
startup_session = create_session(opts, args, default_session=opts.startup_session)
self.keymap = self.opts.keymap.copy()
if new_os_window_trigger is not None:
self.keymap.pop(new_os_window_trigger, None)
@@ -115,10 +118,13 @@ class Boss:
return os_window_id
def list_os_windows(self):
active_tab, active_window = self.active_tab, self.active_window
active_tab_manager = self.active_tab_manager
for os_window_id, tm in self.os_window_map.items():
yield {
'id': os_window_id,
'tabs': list(tm.list_tabs()),
'is_focused': tm is active_tab_manager,
'tabs': list(tm.list_tabs(active_tab, active_window)),
}
@property
@@ -149,11 +155,18 @@ class Boss:
return
if w is not None:
yield w
return
if field == 'env':
kp, vp = exp.partition('=')[::2]
if vp:
pat = tuple(map(re.compile, (kp, vp)))
else:
pat = re.compile(kp), None
else:
pat = re.compile(exp)
for window in self.all_windows:
if window.matches(field, pat):
yield window
for window in self.all_windows:
if window.matches(field, pat):
yield window
def tab_for_window(self, window):
for tab in self.all_tabs:
@@ -180,17 +193,20 @@ class Boss:
yield tab
def set_active_window(self, window):
for tm in self.os_window_map.values():
for os_window_id, tm in self.os_window_map.items():
for tab in tm:
for w in tab:
if w.id == window.id:
if tab is not self.active_tab:
tm.set_active_tab(tab)
tab.set_active_window(w)
return
return os_window_id
def _new_os_window(self, args, cwd_from=None):
sw = self.args_to_special_window(args, cwd_from) if args else None
if isinstance(args, SpecialWindowInstance):
sw = args
else:
sw = self.args_to_special_window(args, cwd_from) if args else None
startup_session = create_session(self.opts, special_window=sw, cwd_from=cwd_from)
return self.add_os_window(startup_session)
@@ -239,7 +255,9 @@ class Boss:
if not os.path.isabs(args.directory):
args.directory = os.path.join(msg['cwd'], args.directory)
session = create_session(opts, args, respect_cwd=True)
self.add_os_window(session, wclass=args.cls, wname=args.name, opts_for_size=opts, startup_id=startup_id)
os_window_id = self.add_os_window(session, wclass=args.cls, wname=args.name, opts_for_size=opts, startup_id=startup_id)
if msg.get('notify_on_os_window_death'):
self.os_window_death_actions[os_window_id] = partial(self.notify_on_os_window_death, msg['notify_on_os_window_death'])
else:
log_error('Unknown message received from peer, ignoring')
@@ -310,11 +328,30 @@ class Boss:
if tm is not None:
tm.resize()
def clear_terminal(self, action, only_active):
if only_active:
windows = []
w = self.active_window
if w is not None:
windows.append(w)
else:
windows = self.all_windows
reset = action == 'reset'
how = 3 if action == 'scrollback' else 2
for w in windows:
w.screen.cursor.x = w.screen.cursor.y = 0
if reset:
w.screen.reset()
else:
w.screen.erase_in_display(how, False)
def increase_font_size(self): # legacy
self.set_font_size(min(self.opts.font_size * 5, self.current_font_size + 2.0))
cfs = global_font_size()
self.set_font_size(min(self.opts.font_size * 5, cfs + 2.0))
def decrease_font_size(self): # legacy
self.set_font_size(self.current_font_size - self.opts.font_size_delta)
cfs = global_font_size()
self.set_font_size(max(MINIMUM_FONT_SIZE, cfs - 2.0))
def restore_font_size(self): # legacy
self.set_font_size(self.opts.font_size)
@@ -471,6 +508,8 @@ class Boss:
if w is not None:
tm = self.os_window_map.get(w.os_window_id)
if tm is not None:
tm.update_tab_bar_data()
tm.mark_tab_bar_dirty()
t = tm.tab_for_id(w.tab_id)
if t is not None:
t.relayout_borders()
@@ -527,6 +566,23 @@ class Boss:
tm.destroy()
for window_id in tuple(w.id for w in self.window_id_map.values() if getattr(w, 'os_window_id', None) == os_window_id):
self.window_id_map.pop(window_id, None)
action = self.os_window_death_actions.pop(os_window_id, None)
if action is not None:
action()
def notify_on_os_window_death(self, address):
import socket
s = socket.socket(family=socket.AF_UNIX)
try:
s.connect(address)
s.sendall(b'c')
try:
s.shutdown(socket.SHUT_RDWR)
except EnvironmentError:
pass
s.close()
except Exception:
pass
def display_scrollback(self, window, data, cmd):
tab = self.active_tab
@@ -539,7 +595,7 @@ class Boss:
confpath = prepare_config_file_for_editing()
# On macOS vim fails to handle SIGWINCH if it occurs early, so add a
# small delay.
cmd = ['kitty', '+runpy', 'import os, sys, time; time.sleep(0.05); os.execvp(sys.argv[1], sys.argv[1:])'] + get_editor() + [confpath]
cmd = [kitty_exe(), '+runpy', 'import os, sys, time; time.sleep(0.05); os.execvp(sys.argv[1], sys.argv[1:])'] + get_editor() + [confpath]
self.new_os_window(*cmd)
def get_output(self, source_window, num_lines=1):
@@ -551,9 +607,14 @@ class Boss:
output += str(s.linebuf.line(i))
return output
def _run_kitten(self, kitten, args=(), input_data=None):
w = self.active_window
tab = self.active_tab
def _run_kitten(self, kitten, args=(), input_data=None, window=None):
if window is None:
w = self.active_window
tab = self.active_tab
else:
w = window
tab = w.tabref()
if w is not None and tab is not None and w.overlay_for is None:
orig_args, args = list(args), list(args)
from kittens.runner import create_kitten_handler
@@ -578,15 +639,18 @@ class Boss:
copts = {k: self.opts[k] for k in ('select_by_word_characters', 'open_url_with')}
overlay_window = tab.new_special_window(
SpecialWindow(
['kitty', '+runpy', 'from kittens.runner import main; main()'] + args,
[kitty_exe(), '+runpy', 'from kittens.runner import main; main()'] + args,
stdin=data,
env={
'KITTY_COMMON_OPTS': json.dumps(copts),
'KITTY_CHILD_PID': w.child.pid,
'PYTHONWARNINGS': 'ignore',
'OVERLAID_WINDOW_LINES': str(w.screen.lines),
'OVERLAID_WINDOW_COLS': str(w.screen.columns),
},
overlay_for=w.id))
cwd=w.cwd_of_child,
overlay_for=w.id
))
overlay_window.action_on_close = partial(self.on_kitten_finish, w.id, end_kitten)
return overlay_window
@@ -625,23 +689,19 @@ class Boss:
break
def kitty_shell(self, window_type):
cmd = ['kitty', '@']
cmd = ['@', kitty_exe(), '@']
if window_type == 'tab':
window = self._new_tab(cmd).active_window
self._new_tab(cmd).active_window
elif window_type == 'os_window':
os_window_id = self._new_os_window(cmd)
window = self.os_window_map[os_window_id].active_window
self.os_window_map[os_window_id].active_window
elif window_type == 'overlay':
w = self.active_window
tab = self.active_tab
if w is not None and tab is not None and w.overlay_for is None:
window = tab.new_special_window(SpecialWindow(cmd, overlay_for=w.id))
else:
window = None
tab.new_special_window(SpecialWindow(cmd, overlay_for=w.id))
else:
window = self._new_window(cmd)
if window is not None:
window.allow_remote_control = True
self._new_window(cmd)
def switch_focus_to(self, window_idx):
tab = self.active_tab
@@ -725,7 +785,7 @@ class Boss:
if arg == '@ansi_screen':
return w.as_text(as_ansi=True)
if args[0].startswith('@'):
if args[0].startswith('@') and args[0] != '@':
stdin = data_for_at(args[0]) or None
if stdin is not None:
stdin = stdin.encode('utf-8')

View File

@@ -22,8 +22,10 @@ uniform sampler2DArray sprites;
in float effective_text_alpha;
in vec3 sprite_pos;
in vec3 underline_pos;
in vec3 cursor_pos;
in vec3 strike_pos;
in vec3 foreground;
in vec4 cursor_color_vec;
in vec3 decoration_fg;
in float colored_sprite;
#endif
@@ -58,10 +60,12 @@ vec4 calculate_foreground() {
float text_alpha = text_fg.a;
float underline_alpha = texture(sprites, underline_pos).a;
float strike_alpha = texture(sprites, strike_pos).a;
float cursor_alpha = texture(sprites, cursor_pos).a;
// Since strike and text are the same color, we simply add the alpha values
float combined_alpha = min(text_alpha + strike_alpha, 1.0f);
// Underline color might be different, so alpha blend
return alpha_blend(decoration_fg, underline_alpha * effective_text_alpha, fg, combined_alpha * effective_text_alpha);
vec4 ans = alpha_blend(decoration_fg, underline_alpha * effective_text_alpha, fg, combined_alpha * effective_text_alpha);
return mix(ans, cursor_color_vec, cursor_alpha);
}
#endif

View File

@@ -5,6 +5,7 @@
#define REVERSE_SHIFT {REVERSE_SHIFT}
#define STRIKE_SHIFT {STRIKE_SHIFT}
#define DIM_SHIFT {DIM_SHIFT}
#define CURSOR_TEXT_COLOR {CURSOR_TEXT_COLOR}
// Inputs {{{
layout(std140) uniform CellRenderData {
@@ -12,7 +13,8 @@ layout(std140) uniform CellRenderData {
uint default_fg, default_bg, highlight_fg, highlight_bg, cursor_color, url_color, url_style, inverted;
uint xnum, ynum, cursor_x, cursor_y, cursor_w;
uint xnum, ynum, cursor_fg_sprite_idx;
float cursor_x, cursor_y, cursor_w;
uint color_table[256];
};
@@ -53,6 +55,8 @@ uniform float inactive_text_alpha;
uniform float dim_opacity;
out vec3 sprite_pos;
out vec3 underline_pos;
out vec3 cursor_pos;
out vec4 cursor_color_vec;
out vec3 strike_pos;
out vec3 foreground;
out vec3 decoration_fg;
@@ -110,9 +114,18 @@ vec3 choose_color(float q, vec3 a, vec3 b) {
return mix(b, a, q);
}
float is_cursor(uint x, uint y) {
if (y == cursor_y && (x == cursor_x || x == cursor_w)) return 1.0;
return 0.0;
float are_integers_equal(float a, float b) { // return 1 iff equal otherwise 0
float delta = abs(a - b); // delta can be 0, 1 or larger
return step(delta, 0.5); // 0 if 0.5 < delta else 1
}
float is_cursor(uint xi, uint y) {
float x = float(xi);
float y_equal = are_integers_equal(float(y), cursor_y);
float x1_equal = are_integers_equal(x, cursor_x);
float x2_equal = are_integers_equal(x, cursor_w);
float x_equal = step(0.5, x1_equal + x2_equal);
return step(2.0, x_equal + y_equal);
}
// }}}
@@ -141,7 +154,9 @@ void main() {
uint is_inverted = ((text_attrs >> REVERSE_SHIFT) & ONE) + inverted;
int fg_index = fg_index_map[is_inverted];
int bg_index = 1 - fg_index;
float cursor = is_cursor(c, r);
float cell_has_cursor = is_cursor(c, r);
float is_block_cursor = step(float(cursor_fg_sprite_idx), 0.5);
float cell_has_block_cursor = cell_has_cursor * is_block_cursor;
vec3 bg = to_color(colors[bg_index], default_colors[bg_index]);
// }}}
@@ -166,8 +181,10 @@ void main() {
strike_pos = to_sprite_pos(pos, ((text_attrs >> STRIKE_SHIFT) & ONE) * FOUR, ZERO, ZERO);
// Cursor
foreground = choose_color(cursor, bg, foreground);
decoration_fg = choose_color(cursor, bg, decoration_fg);
cursor_color_vec = vec4(color_to_vec(cursor_color), 1.0);
foreground = choose_color(cell_has_block_cursor, CURSOR_TEXT_COLOR, foreground);
decoration_fg = choose_color(cell_has_block_cursor, CURSOR_TEXT_COLOR, decoration_fg);
cursor_pos = to_sprite_pos(pos, cursor_fg_sprite_idx * uint(cell_has_cursor), ZERO, ZERO);
#endif
// }}}
@@ -182,17 +199,17 @@ void main() {
// If the background color is default, set its opacity to background_opacity, otherwise it should be opaque
bg_alpha = step(0.5, float(colors[bg_index] & BYTE_MASK));
// Cursor must not be affected by background_opacity
bg_alpha = mix(bg_alpha, 1.0, cursor);
bg_alpha = mix(bg_alpha, 1.0, cell_has_block_cursor);
bg_alpha = bg_alpha + (1.0f - bg_alpha) * background_opacity;
#endif
#if defined(SPECIAL) || defined(SIMPLE)
// Selection and cursor
bg = choose_color(float(is_selected & ONE), color_to_vec(highlight_bg), bg);
background = choose_color(cursor, color_to_vec(cursor_color), bg);
background = choose_color(cell_has_block_cursor, color_to_vec(cursor_color), bg);
#ifdef SPECIAL
// bg_alpha should be 1 if cursor/selection otherwise 0
bg_alpha = mix(0.0, 1.0, step(0.5, float(is_selected & ONE) + cursor));
bg_alpha = mix(0.0, 1.0, step(0.5, float(is_selected & ONE) + cell_has_block_cursor));
#endif
#endif

View File

@@ -25,7 +25,7 @@ extern PyTypeObject Screen_Type;
#define MSG_NOSIGNAL 0
#endif
static void (*parse_func)(Screen*, PyObject*);
static void (*parse_func)(Screen*, PyObject*, double);
typedef struct {
char *data;
@@ -246,9 +246,18 @@ add_child(ChildMonitor *self, PyObject *args) {
}
bool
schedule_write_to_child(unsigned long id, const char *data, size_t sz) {
schedule_write_to_child(unsigned long id, unsigned int num, ...) {
ChildMonitor *self = the_monitor;
bool found = false;
const char *data;
size_t sz = 0;
va_list ap;
va_start(ap, num);
for (unsigned int i = 0; i < num; i++) {
data = va_arg(ap, const char*);
sz += va_arg(ap, size_t);
}
va_end(ap);
children_mutex(lock);
for (size_t i = 0; i < self->count; i++) {
if (children[i].id == id) {
@@ -266,8 +275,14 @@ schedule_write_to_child(unsigned long id, const char *data, size_t sz) {
screen->write_buf = PyMem_RawRealloc(screen->write_buf, screen->write_buf_sz);
if (screen->write_buf == NULL) { fatal("Out of memory."); }
}
memcpy(screen->write_buf + screen->write_buf_used, data, sz);
screen->write_buf_used += sz;
va_start(ap, num);
for (unsigned int i = 0; i < num; i++) {
data = va_arg(ap, const char*);
size_t dsz = va_arg(ap, size_t);
memcpy(screen->write_buf + screen->write_buf_used, data, dsz);
screen->write_buf_used += dsz;
}
va_end(ap);
if (screen->write_buf_sz > BUFSIZ && screen->write_buf_used < BUFSIZ) {
screen->write_buf_sz = BUFSIZ;
screen->write_buf = PyMem_RawRealloc(screen->write_buf, screen->write_buf_sz);
@@ -288,7 +303,7 @@ needs_write(ChildMonitor UNUSED *self, PyObject *args) {
unsigned long id, sz;
const char *data;
if (!PyArg_ParseTuple(args, "ks#", &id, &data, &sz)) return NULL;
if (schedule_write_to_child(id, data, sz)) { Py_RETURN_TRUE; }
if (schedule_write_to_child(id, 1, data, (size_t)sz)) { Py_RETURN_TRUE; }
Py_RETURN_FALSE;
}
@@ -314,13 +329,17 @@ shutdown_monitor(ChildMonitor *self, PyObject *a UNUSED) {
static inline void
do_parse(ChildMonitor *self, Screen *screen, double now) {
screen_mutex(lock, read);
if (screen->read_buf_sz) {
if (screen->read_buf_sz || screen->pending_mode.used) {
double time_since_new_input = now - screen->new_input_at;
if (time_since_new_input >= OPT(input_delay)) {
parse_func(screen, self->dump_callback);
if (screen->read_buf_sz >= READ_BUF_SZ) wakeup_io_loop(false); // Ensure the read fd has POLLIN set
screen->read_buf_sz = 0;
bool read_buf_full = screen->read_buf_sz >= READ_BUF_SZ;
parse_func(screen, self->dump_callback, now);
if (read_buf_full) wakeup_io_loop(false); // Ensure the read fd has POLLIN set
screen->new_input_at = 0;
if (screen->pending_mode.activated_at) {
double time_since_pending = MAX(0, now - screen->pending_mode.activated_at);
set_maximum_wait(screen->pending_mode.wait_time - time_since_pending);
}
} else set_maximum_wait(OPT(input_delay) - time_since_new_input);
}
screen_mutex(unlock, read);
@@ -491,14 +510,6 @@ pyset_iutf8(ChildMonitor *self, PyObject *args) {
static double last_render_at = -DBL_MAX;
static inline double
cursor_width(double w, bool vert, OSWindow *os_window) {
double dpi = vert ? os_window->fonts_data->logical_dpi_x : os_window->fonts_data->logical_dpi_y;
double ans = w * dpi / 72.0; // as pixels
double factor = 2.0 / (vert ? os_window->viewport_width : os_window->viewport_height);
return ans * factor;
}
extern void cocoa_update_title(PyObject*);
static inline void
@@ -526,23 +537,6 @@ collect_cursor_info(CursorRenderInfo *ans, Window *w, double now, OSWindow *os_w
ans->shape = cursor->shape ? cursor->shape : OPT(cursor_shape);
ans->color = colorprofile_to_color(cp, cp->overridden.cursor_color, cp->configured.cursor_color);
ans->is_focused = os_window->is_focused;
if (ans->shape == CURSOR_BLOCK && ans->is_focused) return;
double left = rd->xstart + cursor->x * rd->dx;
double top = rd->ystart - cursor->y * rd->dy;
unsigned long mult = MAX(1, screen_current_char_width(rd->screen));
double right = left + (ans->shape == CURSOR_BEAM ? cursor_width(1.5, true, os_window) : rd->dx * mult);
double bottom = top - rd->dy;
switch (ans->shape) {
case CURSOR_UNDERLINE:
top = bottom + cursor_width(2.0, false, os_window);
break;
case CURSOR_BLOCK:
top -= 2.0 / os_window->viewport_height; // 1px adjustment for width of line
break;
default:
break;
}
ans->left = left; ans->right = right; ans->top = top; ans->bottom = bottom;
}
static inline bool
@@ -614,9 +608,6 @@ render_os_window(OSWindow *os_window, double now, unsigned int active_window_id,
if (w->visible && WD.screen) {
bool is_active_window = i == tab->active_window;
draw_cells(WD.vao_idx, WD.gvao_idx, WD.xstart, WD.ystart, WD.dx, WD.dy, WD.screen, os_window, is_active_window, true);
if (is_active_window && WD.screen->cursor_render_info.is_visible && (!WD.screen->cursor_render_info.is_focused || WD.screen->cursor_render_info.shape != CURSOR_BLOCK)) {
draw_cursor(&WD.screen->cursor_render_info, os_window->is_focused);
}
if (WD.screen->start_visual_bell_at != 0) {
double bell_left = global_state.opts.visual_bell_duration - (now - WD.screen->start_visual_bell_at);
set_maximum_wait(bell_left);
@@ -976,7 +967,12 @@ write_to_child(int fd, Screen *screen) {
written = screen->write_buf_used;
}
}
screen->write_buf_used -= written;
if (written) {
screen->write_buf_used -= written;
if (screen->write_buf_used) {
memmove(screen->write_buf, screen->write_buf + written, screen->write_buf_used);
}
}
screen_mutex(unlock, write);
}
@@ -1060,9 +1056,6 @@ io_loop(void *data) {
// {{{ Talk thread functions
#define MAX_PEERS 256
#define MAX_LISTENERS 2
typedef struct {
char *data;
size_t capacity, used;
@@ -1081,15 +1074,18 @@ static PeerWriteData empty_pwd = {.fd = -1, 0};
typedef struct {
size_t num_listen_fds, num_talk_fds, num_reads, num_writes, num_queued_writes;
struct pollfd fds[MAX_PEERS + MAX_LISTENERS + 1];
PeerReadData reads[MAX_LISTENERS];
PeerWriteData writes[MAX_LISTENERS];
PeerWriteData queued_writes[MAX_LISTENERS];
size_t fds_capacity, reads_capacity, writes_capacity, queued_writes_capacity;
struct pollfd *fds;
PeerReadData *reads;
PeerWriteData *writes;
PeerWriteData *queued_writes;
int wakeup_fds[2];
pthread_mutex_t peer_lock;
} TalkData;
static TalkData talk_data = {0};
typedef struct pollfd PollFD;
#define PEER_LIMIT 256
#define nuke_socket(s) { shutdown(s, SHUT_RDWR); close(s); }
static inline bool
@@ -1101,11 +1097,16 @@ accept_peer(int listen_fd, bool shutting_down) {
return false;
}
size_t fd_idx = talk_data.num_listen_fds + talk_data.num_talk_fds;
if (fd_idx < arraysz(talk_data.fds) && talk_data.num_reads < arraysz(talk_data.reads)) {
if (fd_idx < PEER_LIMIT && talk_data.reads_capacity < PEER_LIMIT) {
ensure_space_for(&talk_data, fds, PollFD, fd_idx + 1, fds_capacity, 8, false);
talk_data.fds[fd_idx].fd = peer; talk_data.fds[fd_idx].events = POLLIN;
ensure_space_for(&talk_data, reads, PeerReadData, talk_data.num_reads + 1, reads_capacity, 8, false);
talk_data.reads[talk_data.num_reads] = empty_prd; talk_data.reads[talk_data.num_reads++].fd = peer;
talk_data.num_talk_fds++;
} else nuke_socket(peer);
} else {
log_error("Too many peers want to talk, ignoring one.");
nuke_socket(peer);
}
return true;
}
@@ -1234,8 +1235,10 @@ move_queued_writes() {
while (talk_data.num_queued_writes) {
PeerWriteData *src = talk_data.queued_writes + --talk_data.num_queued_writes;
size_t fd_idx = talk_data.num_listen_fds + talk_data.num_talk_fds;
if (fd_idx < arraysz(talk_data.fds) && talk_data.num_writes < arraysz(talk_data.writes)) {
if (fd_idx < PEER_LIMIT && talk_data.num_writes < PEER_LIMIT) {
ensure_space_for(&talk_data, fds, PollFD, fd_idx + 1, fds_capacity, 8, false);
talk_data.fds[fd_idx].fd = src->fd; talk_data.fds[fd_idx].events = POLLOUT;
ensure_space_for(&talk_data, writes, PeerWriteData, talk_data.num_writes + 1, writes_capacity, 8, false);
talk_data.writes[talk_data.num_writes++] = *src;
talk_data.num_talk_fds++;
} else {
@@ -1254,6 +1257,7 @@ talk_loop(void *data) {
set_thread_name("KittyPeerMon");
if ((pthread_mutex_init(&talk_data.peer_lock, NULL)) != 0) { perror("Failed to create peer mutex"); return 0; }
if (!self_pipe(talk_data.wakeup_fds)) { perror("Failed to create wakeup fds for talk thread"); return 0; }
ensure_space_for(&talk_data, fds, PollFD, 8, fds_capacity, 8, false);
#define add_listener(which) \
if (self->which > -1) { \
talk_data.fds[talk_data.num_listen_fds].fd = self->which; talk_data.fds[talk_data.num_listen_fds++].events = POLLIN; \
@@ -1284,6 +1288,7 @@ talk_loop(void *data) {
}
end:
close(talk_data.wakeup_fds[0]); close(talk_data.wakeup_fds[1]);
free(talk_data.fds); free(talk_data.reads); free(talk_data.writes); free(talk_data.queued_writes);
return 0;
}
@@ -1291,7 +1296,8 @@ static inline bool
add_peer_writer(int fd, const char* msg, size_t msg_sz) {
bool ok = false;
peer_mutex(lock);
if (talk_data.num_queued_writes < arraysz(talk_data.queued_writes)) {
if (talk_data.num_queued_writes < PEER_LIMIT) {
ensure_space_for(&talk_data, queued_writes, PeerWriteData, talk_data.num_queued_writes + 1, queued_writes_capacity, 8, false);
talk_data.queued_writes[talk_data.num_queued_writes] = empty_pwd;
talk_data.queued_writes[talk_data.num_queued_writes].data = malloc(msg_sz);
if (talk_data.queued_writes[talk_data.num_queued_writes].data) {

View File

@@ -11,13 +11,20 @@
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
static inline char**
serialize_string_tuple(PyObject *src) {
Py_ssize_t sz = PyTuple_GET_SIZE(src);
char **ans = calloc(sz + 1, sizeof(char*));
if (!ans) fatal("Out of memory");
for (Py_ssize_t i = 0; i < sz; i++) ans[i] = PyUnicode_AsUTF8(PyTuple_GET_ITEM(src, i));
for (Py_ssize_t i = 0; i < sz; i++) {
const char *pysrc = PyUnicode_AsUTF8(PyTuple_GET_ITEM(src, i));
size_t len = strlen(pysrc);
ans[i] = calloc(len + 1, sizeof(char));
if (ans[i] == NULL) fatal("Out of memory");
memcpy(ans[i], pysrc, len);
}
return ans;
}
@@ -38,18 +45,29 @@ write_to_stderr(const char *text) {
}
}
#define exit_on_err(m) { write_to_stderr(m); write_to_stderr(": "); write_to_stderr(strerror(errno)); exit(EXIT_FAILURE); }
static inline void
wait_for_terminal_ready(int fd) {
char data;
while(1) {
int ret = read(fd, &data, 1);
if (ret == -1 && (errno == EINTR || errno == EAGAIN)) continue;
break;
}
}
static PyObject*
spawn(PyObject *self UNUSED, PyObject *args) {
PyObject *argv_p, *env_p;
int master, slave, stdin_read_fd, stdin_write_fd;
int master, slave, stdin_read_fd, stdin_write_fd, ready_read_fd, ready_write_fd;
char *cwd, *exe;
if (!PyArg_ParseTuple(args, "ssO!O!iiii", &exe, &cwd, &PyTuple_Type, &argv_p, &PyTuple_Type, &env_p, &master, &slave, &stdin_read_fd, &stdin_write_fd)) return NULL;
if (!PyArg_ParseTuple(args, "ssO!O!iiiiii", &exe, &cwd, &PyTuple_Type, &argv_p, &PyTuple_Type, &env_p, &master, &slave, &stdin_read_fd, &stdin_write_fd, &ready_read_fd, &ready_write_fd)) return NULL;
char name[2048] = {0};
if (ttyname_r(slave, name, sizeof(name) - 1) != 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; }
char **argv = serialize_string_tuple(argv_p);
char **env = serialize_string_tuple(env_p);
#define exit_on_err(m) { write_to_stderr(m); write_to_stderr(": "); write_to_stderr(strerror(errno)); exit(EXIT_FAILURE); }
pid_t pid = fork();
switch(pid) {
case 0:
@@ -57,6 +75,17 @@ spawn(PyObject *self UNUSED, PyObject *args) {
// Use only signal-safe functions (man 7 signal-safety)
if (chdir(cwd) != 0) { if (chdir("/") != 0) {} }; // ignore failure to chdir to /
if (setsid() == -1) exit_on_err("setsid() in child process failed");
// Establish the controlling terminal (see man 7 credentials)
int tfd = open(name, O_RDWR);
if (tfd == -1) exit_on_err("Failed to open controlling terminal");
#ifdef TIOCSTTY
// On BSD open() does not establish the controlling terminal
if (ioctl(tfd, TIOCSCTTY, 0) == -1) exit_on_err("Failed to set controlling terminal with TIOCSCTTY");
#endif
close(tfd);
// Redirect stdin/stdout/stderr to the pty
if (dup2(slave, 1) == -1) exit_on_err("dup2() failed for fd number 1");
if (dup2(slave, 2) == -1) exit_on_err("dup2() failed for fd number 2");
if (stdin_read_fd > -1) {
@@ -68,12 +97,14 @@ spawn(PyObject *self UNUSED, PyObject *args) {
}
close(slave);
close(master);
for (int c = 3; c < 201; c++) close(c);
// Establish the controlling terminal (see man 7 credentials)
int tfd = open(name, O_RDWR);
if (tfd == -1) exit_on_err("Failed to open controlling terminal");
close(tfd);
// Wait for READY_SIGNAL which indicates kitty has setup the screen object
close(ready_write_fd);
wait_for_terminal_ready(ready_read_fd);
close(ready_read_fd);
// Close any extra fds inherited from parent
for (int c = 3; c < 201; c++) close(c);
environ = env;
execvp(exe, argv);

View File

@@ -9,33 +9,81 @@ import kitty.fast_data_types as fast_data_types
from .constants import is_macos, shell_path, terminfo_dir
if is_macos:
from kitty.fast_data_types import cmdline_of_process, cwd_of_process as _cwd, environ_of_process as _environ_of_process
def cwd_of_process(pid):
if is_macos:
from kitty.fast_data_types import cwd_of_process
ans = cwd_of_process(pid)
else:
def cwd_of_process(pid):
return os.path.realpath(_cwd(pid))
else:
def cmdline_of_process(pid):
return list(filter(None, open('/proc/{}/cmdline'.format(pid), 'rb').read().decode('utf-8').split('\0')))
def cwd_of_process(pid):
ans = '/proc/{}/cwd'.format(pid)
return os.path.realpath(ans)
return os.path.realpath(ans)
def _environ_of_process(pid):
return open('/proc/{}/environ'.format(pid), 'rb').read().decode('utf-8')
def cmdline_of_process(pid):
if is_macos:
# TODO: macOS implementation, see DarwinProcess.c in htop for inspiration
raise NotImplementedError()
return list(filter(None, open('/proc/{}/cmdline'.format(pid), 'rb').read().decode('utf-8').split('\0')))
def parse_environ_block(data):
"""Parse a C environ block of environment variables into a dictionary."""
# The block is usually raw data from the target process. It might contain
# trailing garbage and lines that do not look like assignments.
ret = {}
pos = 0
while True:
next_pos = data.find("\0", pos)
# nul byte at the beginning or double nul byte means finish
if next_pos <= pos:
break
# there might not be an equals sign
equal_pos = data.find("=", pos, next_pos)
if equal_pos > pos:
key = data[pos:equal_pos]
value = data[equal_pos + 1:next_pos]
ret[key] = value
pos = next_pos + 1
return ret
def environ_of_process(pid):
return parse_environ_block(_environ_of_process(pid))
def remove_cloexec(fd):
fcntl.fcntl(fd, fcntl.F_SETFD, fcntl.fcntl(fd, fcntl.F_GETFD) & ~fcntl.FD_CLOEXEC)
def default_env():
try:
return default_env.env
except AttributeError:
return os.environ
def set_default_env(val=None):
env = os.environ.copy()
if val:
env.update(val)
default_env.env = env
class Child:
child_fd = pid = None
forked = False
def __init__(self, argv, cwd, opts, stdin=None, env=None, cwd_from=None):
self.allow_remote_control = False
if argv and argv[0] == '@':
self.allow_remote_control = True
if len(argv) > 1:
argv = argv[1:]
self.argv = argv
if cwd_from is not None:
try:
@@ -58,12 +106,14 @@ class Child:
remove_cloexec(slave)
fast_data_types.set_iutf8(master, True)
stdin, self.stdin = self.stdin, None
ready_read_fd, ready_write_fd = os.pipe()
remove_cloexec(ready_read_fd)
if stdin is not None:
stdin_read_fd, stdin_write_fd = os.pipe()
remove_cloexec(stdin_read_fd)
else:
stdin_read_fd = stdin_write_fd = -1
env = os.environ.copy()
env = default_env().copy()
env.update(self.env)
env['TERM'] = self.opts.term
env['COLORTERM'] = 'truecolor'
@@ -76,15 +126,22 @@ class Child:
# Some macOS machines need the shell to have argv[0] prefixed by
# hyphen, see https://github.com/kovidgoyal/kitty/issues/247
argv[0] = ('-' + exe.split('/')[-1])
pid = fast_data_types.spawn(exe, self.cwd, tuple(argv), env, master, slave, stdin_read_fd, stdin_write_fd)
pid = fast_data_types.spawn(exe, self.cwd, tuple(argv), env, master, slave, stdin_read_fd, stdin_write_fd, ready_read_fd, ready_write_fd)
os.close(slave)
self.pid = pid
self.child_fd = master
if stdin is not None:
os.close(stdin_read_fd)
fast_data_types.thread_write(stdin_write_fd, stdin)
os.close(ready_read_fd)
self.terminal_ready_fd = ready_write_fd
fcntl.fcntl(self.child_fd, fcntl.F_SETFL, fcntl.fcntl(self.child_fd, fcntl.F_GETFL) | os.O_NONBLOCK)
return pid
def mark_terminal_ready(self):
os.close(self.terminal_ready_fd)
self.terminal_ready_fd = -1
@property
def cmdline(self):
try:
@@ -92,6 +149,13 @@ class Child:
except Exception:
return list(self.argv)
@property
def environ(self):
try:
return environ_of_process(self.pid)
except Exception:
return {}
@property
def current_cwd(self):
try:

View File

@@ -7,7 +7,6 @@ import re
import sys
from collections import deque
from .config import defaults, load_config
from .conf.utils import resolve_config
from .constants import appname, defconf, is_macos, is_wayland, str_version
@@ -29,126 +28,10 @@ directory is always used and the above searching does not happen.
If :file:`/etc/xdg/{appname}/{conf_name}.conf` exists it is merged before (i.e. with lower
priority) than any user config files. It can be used to specify system-wide
defaults for all users.
'''.replace('{macos_confpath}', ':file:`~/Library/Preferences/{appname}/{conf_name}.conf`,' if is_macos else '')
OPTIONS = '''
--class
dest=cls
default={appname}
condition=not is_macos
Set the class part of the :italic:`WM_CLASS` window property
--name
condition=not is_macos
Set the name part of the :italic:`WM_CLASS` property (defaults to using the value from :option:`{appname} --class`)
--title
Set the window title. This will override any title set by the program running inside kitty. So
only use this if you are running a program that does not set titles.
--config -c
type=list
{config_help}
--override -o
type=list
Override individual configuration options, can be specified multiple times.
Syntax: :italic:`name=value`. For example: :option:`kitty -o` font_size=20
--directory -d
default=.
Change to the specified directory when launching
--detach
type=bool-set
condition=not is_macos
Detach from the controlling terminal, if any
--session
Path to a file containing the startup :italic:`session` (tabs, windows, layout, programs).
See the README file for details and an example.
--single-instance -1
type=bool-set
If specified only a single instance of :italic:`{appname}` will run. New invocations will
instead create a new top-level window in the existing :italic:`{appname}` instance. This
allows :italic:`{appname}` to share a single sprite cache on the GPU and also reduces
startup time. You can also have separate groups of :italic:`{appname}` instances by using the
:option:`kitty --instance-group` option
--instance-group
Used in combination with the :option:`kitty --single-instance` option. All :italic:`{appname}` invocations
with the same :option:`kitty --instance-group` will result in new windows being created
in the first :italic:`{appname}` instance within that group
--listen-on
Tell kitty to listen on the specified address for control
messages. For example, :option:`{appname} --listen-on`=unix:/tmp/mykitty or
:option:`{appname} --listen-on`=tcp:localhost:12345. On Linux systems, you can also use abstract
UNIX sockets, not associated with a file, like this: :option:`{appname} --listen-on`=unix:@mykitty.
To control kitty, you can send it commands with :italic:`kitty @` using the :option:`kitty @ --to` option
to specify this address. Note that this option will be ignored, unless you set
:opt:`allow_remote_control` to yes in :file:`kitty.conf`.
# Debugging options
--version -v
type=bool-set
The current {appname} version
--dump-commands
type=bool-set
Output commands received from child process to stdout
--replay-commands
Replay previously dumped commands. Specify the path to a dump file previously created by --dump-commands. You
can open a new kitty window to replay the commands with::
kitty sh -c "kitty --replay-commands /path/to/dump/file; read"
--dump-bytes
Path to file in which to store the raw bytes received from the child process
--debug-gl
type=bool-set
Debug OpenGL commands. This will cause all OpenGL calls to check for errors
instead of ignoring them. Useful when debugging rendering problems
--debug-keyboard
type=bool-set
This option will cause kitty to print out key events as they are received
--debug-font-fallback
type=bool-set
Print out information about the selection of fallback fonts for characters not present in the main font.
--debug-config
type=bool-set
Print out information about the system and kitty configuration.
--execute -e
type=bool-set
!
'''
'''.replace(
'{macos_confpath}',
(':file:`~/Library/Preferences/{appname}/{conf_name}.conf`,' if is_macos else ''), 1
)
def surround(x, start, end):
@@ -205,6 +88,10 @@ def code(x):
return x
def kbd(x):
return x
def env(x):
return italic(x)
@@ -213,7 +100,9 @@ def file(x):
return italic(x)
def parse_option_spec(spec=OPTIONS):
def parse_option_spec(spec=None):
if spec is None:
spec = options_spec()
NORMAL, METADATA, HELP = 'NORMAL', 'METADATA', 'HELP'
state = NORMAL
lines = spec.splitlines()
@@ -435,7 +324,7 @@ def seq_as_rst(seq, usage, message, appname, heading_char='-'):
if defval is not None:
a(textwrap.indent('Default: :code:`{}`'.format(defval), ' ' * 4))
if 'choices' in opt:
a(textwrap.indent('Choices: :code:`{}`'.format(', '.join(opt['choices'])), ' ' * 4))
a(textwrap.indent('Choices: :code:`{}`'.format(', '.join(sorted(opt['choices']))), ' ' * 4))
a('')
text = '\n'.join(blocks)
@@ -570,6 +459,141 @@ def parse_cmdline(oc, disabled, args=None):
def options_spec():
if not hasattr(options_spec, 'ans'):
OPTIONS = '''
--class
dest=cls
default={appname}
condition=not is_macos
Set the class part of the :italic:`WM_CLASS` window property
--name
condition=not is_macos
Set the name part of the :italic:`WM_CLASS` property (defaults to using the value from :option:`{appname} --class`)
--title -T
Set the window title. This will override any title set by the program running inside kitty. So
only use this if you are running a program that does not set titles.
--config -c
type=list
{config_help}
--override -o
type=list
Override individual configuration options, can be specified multiple times.
Syntax: :italic:`name=value`. For example: :option:`kitty -o` font_size=20
--directory -d
default=.
Change to the specified directory when launching
--detach
type=bool-set
condition=not is_macos
Detach from the controlling terminal, if any
--session
Path to a file containing the startup :italic:`session` (tabs, windows, layout, programs).
See the README file for details and an example.
--hold
type=bool-set
Remain open after child process exits. Note that this only affects the first
window. You can quit by either using the close window shortcut or :kbd:`Ctrl+d`.
--single-instance -1
type=bool-set
If specified only a single instance of :italic:`{appname}` will run. New invocations will
instead create a new top-level window in the existing :italic:`{appname}` instance. This
allows :italic:`{appname}` to share a single sprite cache on the GPU and also reduces
startup time. You can also have separate groups of :italic:`{appname}` instances by using the
:option:`kitty --instance-group` option
--instance-group
Used in combination with the :option:`kitty --single-instance` option. All :italic:`{appname}` invocations
with the same :option:`kitty --instance-group` will result in new windows being created
in the first :italic:`{appname}` instance within that group
--wait-for-single-instance-window-close
type=bool-set
Normally, when using :option:`--single-instance`, :italic:`{appname}` will open a new window in an existing
instance and quit immediately. With this option, it will not quit till the newly opened
window is closed. Note that if no previous instance is found, then :italic:`{appname}` will wait anyway,
regardless of this option.
--listen-on
Tell kitty to listen on the specified address for control
messages. For example, :option:`{appname} --listen-on`=unix:/tmp/mykitty or
:option:`{appname} --listen-on`=tcp:localhost:12345. On Linux systems, you can
also use abstract UNIX sockets, not associated with a file, like this:
:option:`{appname} --listen-on`=unix:@mykitty. To control kitty, you can send
it commands with :italic:`kitty @` using the :option:`kitty @ --to` option to
specify this address. This option will be ignored, unless you set
:opt:`allow_remote_control` to yes in :file:`kitty.conf`. Note that if you run
:italic:`kitty @` within a kitty window, there is no need to specify the :italic:`--to`
option as it is read automatically from the environment.
# Debugging options
--version -v
type=bool-set
The current {appname} version
--dump-commands
type=bool-set
Output commands received from child process to stdout
--replay-commands
Replay previously dumped commands. Specify the path to a dump file previously created by --dump-commands. You
can open a new kitty window to replay the commands with::
kitty sh -c "kitty --replay-commands /path/to/dump/file; read"
--dump-bytes
Path to file in which to store the raw bytes received from the child process
--debug-gl
type=bool-set
Debug OpenGL commands. This will cause all OpenGL calls to check for errors
instead of ignoring them. Useful when debugging rendering problems
--debug-keyboard
type=bool-set
This option will cause kitty to print out key events as they are received
--debug-font-fallback
type=bool-set
Print out information about the selection of fallback fonts for characters not present in the main font.
--debug-config
type=bool-set
Print out information about the system and kitty configuration.
--execute -e
type=bool-set
!
'''
options_spec.ans = OPTIONS.format(
appname=appname, config_help=CONFIG_HELP.format(appname=appname, conf_name=appname)
@@ -577,6 +601,10 @@ def options_spec():
return options_spec.ans
def options_for_completion():
return parse_option_spec(options_spec())[0]
def option_spec_as_rst(ospec=options_spec, usage=None, message=None, appname=None, heading_char='-'):
options = parse_option_spec(ospec())
seq, disabled = options
@@ -606,12 +634,16 @@ def print_shortcut(key_sequence, action):
keys = []
for key in key_sequence:
names = []
mods, key = key
mods, is_native, key = key
for name, val in mmap.items():
if mods & val:
names.append(name)
if key:
names.append(krmap[key])
if is_native:
from .fast_data_types import GLFW_KEY_UNKNOWN, glfw_get_key_name
names.append(glfw_get_key_name(GLFW_KEY_UNKNOWN, key))
else:
names.append(krmap[key])
keys.append('+'.join(names))
print('\t', ' > '.join(keys), action)
@@ -643,6 +675,7 @@ def flatten_sequence_map(m):
def compare_opts(opts):
from .config import defaults, load_config
print('\nConfig options different from defaults:')
default_opts = load_config()
changed_opts = [
@@ -664,6 +697,7 @@ def compare_opts(opts):
def create_opts(args, debug_config=False):
from .config import load_config
config = tuple(resolve_config(SYSTEM_CONF, defconf, args.config))
if debug_config:
print(version(add_rev=True))

View File

@@ -122,6 +122,10 @@ def draw(*a):
write(' '.join(a))
def screen_manipulate_title_stack(op, which):
write(CSI + '%d;%dt' % (op, which))
def report_device_attributes(mode, char):
x = CSI
if char:

View File

@@ -9,7 +9,9 @@ import sys
from .cli import parse_args
from .config import parse_config, parse_send_text_bytes
from .constants import appname
from .fast_data_types import focus_os_window
from .tabs import SpecialWindow
from .utils import natsort_ints
class MatchError(ValueError):
@@ -25,6 +27,11 @@ class OpacityError(ValueError):
hide_traceback = True
class UnknownLayout(ValueError):
hide_traceback = True
def cmd(short_desc, desc=None, options_spec=None, no_response=False, argspec='...', string_return_is_error=False, args_count=None):
def w(func):
@@ -45,18 +52,20 @@ def cmd(short_desc, desc=None, options_spec=None, no_response=False, argspec='..
MATCH_WINDOW_OPTION = '''\
--match -m
The window to match. Match specifications are of the form:
:italic:`field:regexp`. Where field can be one of: id, title, pid, cwd, cmdline, num.
:italic:`field:regexp`. Where field can be one of: id, title, pid, cwd, cmdline, num, env.
You can use the :italic:`ls` command to get a list of windows. Note that for
numeric fields such as id, pid and num the expression is interpreted as a number,
not a regular expression. The field num refers to the window position in the current tab,
starting from zero and counting clockwise (this is the same as the order in which the
windows are reported by the :italic:`ls` command). The window id of the current window
is available as the KITTY_WINDOW_ID environment variable.
is available as the KITTY_WINDOW_ID environment variable. When using the :italic:`env` field
to match on environment variables you can specify only the environment variable name or a name
and value, for example, :italic:`env:MY_ENV_VAR=2`
'''
MATCH_TAB_OPTION = '''\
--match -m
The tab to match. Match specifications are of the form:
:italic:`field:regexp`. Where field can be one of: id, title, pid, cwd, cmdline.
:italic:`field:regexp`. Where field can be one of: id, title, pid, cwd, env, cmdline.
You can use the :italic:`ls` command to get a list of tabs. Note that for
numeric fields such as id and pid the expression is interpreted as a number,
not a regular expression. When using title or id, first a matching tab is
@@ -71,8 +80,8 @@ for that window is used.
'List all windows. The list is returned as JSON tree. The top-level is a list of'
' operating system {appname} windows. Each OS window has an :italic:`id` and a list'
' of :italic:`tabs`. Each tab has its own :italic:`id`, a :italic:`title` and a list of :italic:`windows`.'
' Each window has an :italic:`id`, :italic:`title`, :italic:`current working directory`, :italic:`process id (PID)` and'
' :italic:`command-line` of the process running in the window.\n\n'
' Each window has an :italic:`id`, :italic:`title`, :italic:`current working directory`, :italic:`process id (PID)`, '
' :italic:`command-line` and :italic:`environment` of the process running in the window.\n\n'
'You can use these criteria to select windows/tabs for the other commands.'.format(appname=appname),
argspec=''
)
@@ -269,6 +278,41 @@ def set_tab_title(boss, window, payload):
# }}}
# set_layout {{{
@cmd(
'Set the window layout',
'Set the window layout in the specified tab (or the active tab if not specified).'
' You can use special match value :italic:`all` to set the layout in all tabs.',
options_spec=MATCH_TAB_OPTION,
argspec='LAYOUT_NAME'
)
def cmd_goto_layout(global_opts, opts, args):
try:
return {'layout': args[0], 'match': opts.match}
except IndexError:
raise SystemExit('No layout specified')
def goto_layout(boss, window, payload):
match = payload['match']
if match:
if match == 'all':
tabs = tuple(boss.all_tabs)
else:
tabs = tuple(boss.match_tabs(match))
if not tabs:
raise MatchError(match, 'tabs')
else:
tabs = [boss.tab_for_window(window) if window else boss.active_tab]
for tab in tabs:
if tab:
try:
tab.goto_layout(payload['layout'], raise_exception=True)
except ValueError:
raise UnknownLayout('The layout {} is unknown or disabled'.format(payload['layout']))
# }}}
# close_window {{{
@cmd(
'Close the specified window(s)',
@@ -380,7 +424,7 @@ def close_tab(boss, window, payload):
'Open new window',
'Open a new window in the specified tab. If you use the :option:`kitty @ new-window --match` option'
' the first matching tab is used. Otherwise the currently active tab is used.'
' Prints out the id of the newly opened window. Any command line arguments'
' Prints out the id of the newly opened window (unless :option:`--no-response` is used). Any command line arguments'
' are assumed to be the command line used to run in the new window, if none'
' are provided, the default shell is run. For example:\n'
':italic:`kitty @ new-window --title Email mutt`',
@@ -399,6 +443,12 @@ type=bool-set
Keep the current window focused instead of switching to the newly opened window
--window-type
default=kitty
choices=kitty,os
What kind of window to open. A kitty window or a top-level OS window.
--new-tab
type=bool-set
Open a new tab
@@ -406,12 +456,23 @@ Open a new tab
--tab-title
When using --new-tab set the title of the tab.
--no-response
type=bool-set
default=false
Don't wait for a response giving the id of the newly opened window. Note that
using this option means that you will not be notified of failures and that
the id of the new window will not be printed out.
''',
argspec='[CMD ...]'
)
def cmd_new_window(global_opts, opts, args):
if opts.no_response:
global_opts.no_command_response = True
return {'match': opts.match, 'title': opts.title, 'cwd': opts.cwd,
'new_tab': opts.new_tab, 'tab_title': opts.tab_title,
'window_type': opts.window_type, 'no_response': opts.no_response,
'keep_focus': opts.keep_focus, 'args': args or []}
@@ -426,7 +487,16 @@ def new_window(boss, window, payload):
wid = boss.active_window.id
if payload['keep_focus'] and old_window:
boss.set_active_window(old_window)
return str(wid)
return None if payload['no_response'] else str(wid)
if payload['window_type'] == 'os':
boss._new_os_window(w)
wid = boss.active_window.id
if payload['keep_focus'] and old_window:
os_window_id = boss.set_active_window(old_window)
if os_window_id:
focus_os_window(os_window_id)
return None if payload['no_response'] else str(wid)
match = payload['match']
if match:
@@ -439,7 +509,7 @@ def new_window(boss, window, payload):
w = tab.new_special_window(w)
if payload['keep_focus'] and old_window:
boss.set_active_window(old_window)
return str(w.id)
return None if payload['no_response'] else str(w.id)
# }}}
@@ -447,11 +517,19 @@ def new_window(boss, window, payload):
@cmd(
'Focus the specified window',
'Focus the specified window, if no window is specified, focus the window this command is run inside.',
options_spec=MATCH_WINDOW_OPTION,
argspec='',
options_spec=MATCH_WINDOW_OPTION + '''\n\n
--no-response
type=bool-set
default=false
Don't wait for a response from kitty. This means that even if no matching window is found,
the command will exit with a success code.
'''
)
def cmd_focus_window(global_opts, opts, args):
return {'match': opts.match}
if opts.no_response:
global_opts.no_command_response = True
return {'match': opts.match, 'no_response': opts.no_response}
def focus_window(boss, window, payload):
@@ -529,7 +607,7 @@ def get_text(boss, window, payload):
if payload['extent'] == 'selection':
ans = window.text_for_selection()
else:
ans = window.as_text(as_ansi=bool(payload['ansi']), add_history=True)
ans = window.as_text(as_ansi=bool(payload['ansi']), add_history=payload['extent'] == 'all')
return ans
# }}}
@@ -607,6 +685,38 @@ def set_colors(boss, window, payload):
# }}}
# get_colors {{{
@cmd(
'Get terminal colors',
'Get the terminal colors for the specified window (defaults to active window). Colors will be output to stdout in the same syntax as used for kitty.conf',
options_spec='''\
--configured -c
type=bool-set
Instead of outputting the colors for the specified window, output the currently
configured colors.
''' + '\n\n' + MATCH_WINDOW_OPTION
)
def cmd_get_colors(global_opts, opts, args):
return {'configured': opts.configured, 'match': opts.match}
def get_colors(boss, window, payload):
from .rgb import Color, color_as_sharp, color_from_int
ans = {k: getattr(boss.opts, k) for k in boss.opts if isinstance(getattr(boss.opts, k), Color)}
if not payload['configured']:
windows = (window or boss.active_window,)
if payload['match']:
windows = tuple(boss.match_windows(payload['match']))
if not windows:
raise MatchError(payload['match'])
ans.update({k: color_from_int(v) for k, v in windows[0].current_colors.items()})
all_keys = natsort_ints(ans)
maxlen = max(map(len, all_keys))
return '\n'.join(('{:%ds} {}' % maxlen).format(key, color_as_sharp(ans[key])) for key in all_keys)
# }}}
# set_background_opacity {{{
@cmd(
'Set the background_opacity',
@@ -647,6 +757,36 @@ def set_background_opacity(boss, window, payload):
# }}}
# kitten {{{
@cmd(
'Run a kitten',
'Run a kitten over the specified window (active window by default).'
' The :italic:`kitten_name` can be either the name of a builtin kitten'
' or the path to a python file containing a custom kitten. If a relative path'
' is used it is searched for in the kitty config directory.',
options_spec=MATCH_WINDOW_OPTION,
argspec='kitten_name',
)
def cmd_kitten(global_opts, opts, args):
if len(args) < 1:
raise SystemExit('Must specify kitten name')
return {'match': opts.match, 'args': list(args)[1:], 'kitten': args[0]}
def kitten(boss, window, payload):
windows = [window or boss.active_window]
match = payload['match']
if match:
windows = tuple(boss.match_windows(match))
if not windows:
raise MatchError(match)
for window in windows:
if window:
boss._run_kitten(payload['kitten'], args=tuple(payload['args']), window=window)
break
# }}}
cmap = {v.name: v for v in globals().values() if hasattr(v, 'is_cmd')}

View File

@@ -12,8 +12,6 @@
#include <AvailabilityMacros.h>
// Needed for _NSGetProgname
#include <crt_externs.h>
typedef void* rusage_info_t; // needed for libproc.h
#include <libproc.h>
#if (MAC_OS_X_VERSION_MAX_ALLOWED < 101200)
#define NSWindowStyleMaskResizable NSResizableWindowMask
@@ -202,19 +200,30 @@ cocoa_update_title(PyObject *pytitle) {
}
bool
cocoa_make_window_resizable(void *w) {
cocoa_make_window_resizable(void *w, bool resizable) {
NSWindow *window = (NSWindow*)w;
@try {
[window setStyleMask:
[window styleMask] | NSWindowStyleMaskResizable];
if (resizable) {
[window setStyleMask:
[window styleMask] | NSWindowStyleMaskResizable];
} else {
[window setStyleMask:
[window styleMask] & ~NSWindowStyleMaskResizable];
}
} @catch (NSException *e) {
return PyErr_Format(PyExc_ValueError, "Failed to set style mask: %s: %s", [[e name] UTF8String], [[e reason] UTF8String]);
log_error("Failed to set style mask: %s: %s", [[e name] UTF8String], [[e reason] UTF8String]);
return false;
}
return true;
}
void
cocoa_focus_window(void *w) {
NSWindow *window = (NSWindow*)w;
[window makeKeyWindow];
}
static PyObject*
cocoa_get_lang(PyObject UNUSED *self) {
NSString* locale = nil;
@@ -229,15 +238,6 @@ cocoa_get_lang(PyObject UNUSED *self) {
return Py_BuildValue("s", [locale UTF8String]);
}
static PyObject*
cwd_of_process(PyObject *self UNUSED, PyObject *pid_) {
long pid = PyLong_AsLong(pid_);
struct proc_vnodepathinfo vpi;
int ret = proc_pidinfo(pid, PROC_PIDVNODEPATHINFO, 0, &vpi, sizeof(vpi));
if (ret < 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; }
return PyUnicode_FromString(vpi.pvi_cdir.vip_path);
}
void
cocoa_set_hide_from_tasks(void) {
[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
@@ -271,7 +271,6 @@ cocoa_set_titlebar_color(void *w, color_type titlebar_color)
static PyMethodDef module_methods[] = {
{"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""},
{"cwd_of_process", (PyCFunction)cwd_of_process, METH_O, ""},
{"cocoa_set_new_window_trigger", (PyCFunction)cocoa_set_new_window_trigger, METH_VARARGS, ""},
{NULL, NULL, 0, NULL} /* Sentinel */
};

View File

@@ -108,8 +108,8 @@ patch_color_profiles(PyObject *module UNUSED, PyObject *args) {
v = PyDict_GetItemString(spec, key);
if (v) {
color_type color = PyLong_AsUnsignedLong(v);
for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(profiles); i++) {
self = (ColorProfile*)PyTuple_GET_ITEM(profiles, i);
for (Py_ssize_t j = 0; j < PyTuple_GET_SIZE(profiles); j++) {
self = (ColorProfile*)PyTuple_GET_ITEM(profiles, j);
self->color_table[i] = color;
if (change_configured) self->orig_color_table[i] = color;
self->dirty = true;
@@ -150,6 +150,37 @@ colorprofile_to_color(ColorProfile *self, color_type entry, color_type defval) {
}
static PyObject*
as_dict(ColorProfile *self, PyObject *args UNUSED) {
#define as_dict_doc "Return all colors as a dictionary of color_name to integer (names are the same as used in kitty.conf)"
PyObject *ans = PyDict_New();
if (ans == NULL) return PyErr_NoMemory();
for (unsigned i = 0; i < arraysz(self->color_table); i++) {
static char buf[32] = {0};
snprintf(buf, sizeof(buf) - 1, "color%u", i);
PyObject *val = PyLong_FromUnsignedLong(self->color_table[i]);
if (!val) { Py_CLEAR(ans); return PyErr_NoMemory(); }
int ret = PyDict_SetItemString(ans, buf, val);
Py_CLEAR(val);
if (ret != 0) { Py_CLEAR(ans); return NULL; }
}
#define D(attr, name) { \
color_type c = colorprofile_to_color(self, self->overridden.attr, 0xffffffff); \
if (c != 0xffffffff) { \
PyObject *val = PyLong_FromUnsignedLong(c); \
if (!val) { Py_CLEAR(ans); return PyErr_NoMemory(); } \
int ret = PyDict_SetItemString(ans, #name, val); \
Py_CLEAR(val); \
if (ret != 0) { Py_CLEAR(ans); return NULL; } \
}}
D(default_fg, foreground); D(default_bg, background);
D(cursor_color, cursor); D(highlight_fg, selection_foreground);
D(highlight_bg, selection_background);
#undef D
return ans;
}
static PyObject*
as_color(ColorProfile *self, PyObject *val) {
#define as_color_doc "Convert the specified terminal color into an (r, g, b) tuple based on the current profile values"
@@ -220,6 +251,21 @@ copy_color_table_to_buffer(ColorProfile *self, color_type *buf, int offset, size
self->dirty = false;
}
void
colorprofile_push_dynamic_colors(ColorProfile *self) {
if (self->dynamic_color_stack_idx >= arraysz(self->dynamic_color_stack)) {
memmove(self->dynamic_color_stack, self->dynamic_color_stack + 1, sizeof(self->dynamic_color_stack) - sizeof(self->dynamic_color_stack[0]));
self->dynamic_color_stack_idx = arraysz(self->dynamic_color_stack) - 1;
}
self->dynamic_color_stack[self->dynamic_color_stack_idx++] = self->overridden;
}
void
colorprofile_pop_dynamic_colors(ColorProfile *self) {
if (!self->dynamic_color_stack_idx) return;
self->overridden = self->dynamic_color_stack[--(self->dynamic_color_stack_idx)];
}
static PyObject*
color_table_address(ColorProfile *self, PyObject *a UNUSED) {
#define color_table_address_doc "Pointer address to start of color table"
@@ -260,6 +306,7 @@ static PyMemberDef members[] = {
static PyMethodDef methods[] = {
METHOD(update_ansi_color_table, METH_O)
METHOD(reset_color_table, METH_NOARGS)
METHOD(as_dict, METH_NOARGS)
METHOD(color_table_address, METH_NOARGS)
METHOD(as_color, METH_O)
METHOD(reset_color, METH_O)

326
kitty/complete.py Normal file
View File

@@ -0,0 +1,326 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import os
import shlex
import sys
from kittens.runner import get_kitten_cli_docs, all_kitten_names
from .cli import options_for_completion, parse_option_spec
from .cmds import cmap
from .shell import options_for_cmd
parsers, serializers = {}, {}
def debug(*a, **kw):
kw['file'] = sys.stderr
print(*a, **kw)
class Completions:
def __init__(self):
self.match_groups = {}
self.no_space_groups = set()
self.files_groups = set()
# Shell specific code {{{
completion_scripts = {
'zsh': '''
_kitty() {
local src
# Send all words upto the word the cursor is currently on
src=$(printf "%s\n" "${(@)words[1,$CURRENT]}" | kitty +complete zsh)
if [[ $? == 0 ]]; then
eval ${src}
fi
}
compdef _kitty kitty
''',
'bash': '''
kitty_completions() {
local src
local limit
# Send all words up to the word the cursor is currently on
let limit=1+$COMP_CWORD
src=$(printf "%s\n" "${COMP_WORDS[@]: 0:$limit}" | kitty +complete bash)
if [[ $? == 0 ]]; then
eval ${src}
fi
}
complete -o nospace -F kitty_completions kitty
''',
}
def input_parser(func):
name = func.__name__.split('_')[0]
parsers[name] = func
return func
def output_serializer(func):
name = func.__name__.split('_')[0]
serializers[name] = func
return func
@input_parser
def zsh_input_parser(data):
new_word = data.endswith('\n\n')
words = data.rstrip().splitlines()
return words, new_word
@input_parser
def bash_input_parser(data):
new_word = data.endswith('\n\n')
words = data.rstrip().splitlines()
return words, new_word
@output_serializer
def zsh_output_serializer(ans):
lines = []
for description, matches in ans.match_groups.items():
cmd = ['compadd', '-U', '-J', shlex.quote(description), '-X', shlex.quote(description)]
if description in ans.no_space_groups:
cmd += ['-S', '""']
if description in ans.files_groups:
cmd.append('-f')
cmd.append('--')
for word, description in matches.items():
cmd.append(shlex.quote(word))
lines.append(' '.join(cmd) + ';')
# debug('\n'.join(lines))
return '\n'.join(lines)
@output_serializer
def bash_output_serializer(ans):
lines = []
for description, matches in ans.match_groups.items():
needs_space = description not in ans.no_space_groups
for word in matches:
if needs_space:
word += ' '
lines.append('COMPREPLY+=({})'.format(shlex.quote(word)))
# debug('\n'.join(lines))
return '\n'.join(lines)
# }}}
def completions_for_first_word(ans, prefix, entry_points, namespaced_entry_points):
cmds = ['@' + c for c in cmap]
ans.match_groups['Entry points'] = {
k: None for k in
list(entry_points) + cmds + ['+' + k for k in namespaced_entry_points]
if not prefix or k.startswith(prefix)
}
def kitty_cli_opts(ans, prefix=None):
matches = {}
for opt in options_for_completion():
if isinstance(opt, str):
continue
aliases = frozenset(x for x in opt['aliases'] if x.startswith(prefix)) if prefix else opt['aliases']
for alias in aliases:
matches[alias] = opt['help'].strip()
ans.match_groups['Options'] = matches
def complete_kitty_cli_arg(ans, opt, prefix):
prefix = prefix or ''
if opt and opt['dest'] == 'override':
from kitty.config import option_names_for_completion
k = 'Config directives'
ans.match_groups[k] = {k+'=': None for k in option_names_for_completion() if k.startswith(prefix)}
ans.no_space_groups.add(k)
def complete_alias_map(ans, words, new_word, option_map, complete_args=None):
expecting_arg = False
opt = None
last_word = words[-1] if words else ''
for w in words:
if expecting_arg:
if w is last_word and not new_word:
if opt is not None:
complete_args(ans, opt, w)
return
expecting_arg = False
continue
opt = option_map.get(w)
if w is last_word and not new_word:
if w.startswith('-'):
ans.match_groups['Options'] = {k: opt['help'] for k, opt in option_map.items() if k.startswith(last_word)}
else:
if complete_args is not None:
complete_args(ans, None, last_word)
return
if opt is None:
if complete_args is not None:
complete_args(ans, None, '' if new_word else last_word)
return # some non-option word encountered
expecting_arg = not opt.get('type', '').startswith('bool-')
if expecting_arg:
if opt is not None and complete_args is not None:
complete_args(ans, opt, '' if new_word else last_word)
else:
prefix = '' if new_word else last_word
if complete_args is not None:
complete_args(ans, None, prefix)
ans.match_groups['Options'] = {k: opt['help'] for k, opt in option_map.items() if k.startswith(prefix)}
def complete_cli(ans, words, new_word, seq, complete_args=lambda *a: None):
option_map = {}
for opt in seq:
if not isinstance(opt, str):
for alias in opt['aliases']:
option_map[alias] = opt
complete_alias_map(ans, words, new_word, option_map, complete_args)
def complete_remote_command(ans, cmd_name, words, new_word):
aliases, alias_map = options_for_cmd(cmd_name)
if not alias_map:
return
complete_alias_map(ans, words, new_word, alias_map)
def complete_files_and_dirs(ans, prefix, files_group_name='Files', predicate=lambda filename: True):
dirs, files = [], []
base = '.'
if prefix.endswith('/'):
base = prefix
elif '/' in prefix:
base = os.path.dirname(prefix)
for x in os.scandir(base):
q = os.path.relpath(x.path)
if x.is_dir():
if q.startswith(prefix):
dirs.append(q.rstrip(os.sep) + os.sep)
else:
if q.startswith(prefix) and predicate(q):
files.append(q)
if dirs:
ans.match_groups['Directories'] = dict.fromkeys(dirs)
ans.files_groups.add('Directories'), ans.no_space_groups.add('Directories')
if files:
ans.match_groups[files_group_name] = dict.fromkeys(files)
ans.files_groups.add(files_group_name)
def complete_icat_args(ans, opt, prefix):
from mimetypes import guess_type
def icat_file_predicate(filename):
mt = guess_type(filename)[0]
if mt and mt.startswith('image/'):
return True
if opt is None:
complete_files_and_dirs(ans, prefix, 'Images', icat_file_predicate)
def complete_kitten(ans, kitten, words, new_word):
try:
cd = get_kitten_cli_docs(kitten)
except SystemExit:
cd = None
if cd is None:
return
options = cd['options']()
seq = parse_option_spec(options)[0]
option_map = {}
for opt in seq:
if not isinstance(opt, str):
for alias in opt['aliases']:
option_map[alias] = opt
complete_alias_map(ans, words, new_word, option_map, {
'icat': complete_icat_args
}.get(kitten))
def find_completions(words, new_word, entry_points, namespaced_entry_points):
ans = Completions()
if not words or words[0] != 'kitty':
return ans
words = words[1:]
if not words or (len(words) == 1 and not new_word):
prefix = words[0] if words else ''
completions_for_first_word(ans, prefix, entry_points, namespaced_entry_points)
kitty_cli_opts(ans, prefix)
return ans
if words[0] == '@':
if len(words) == 1 or (len(words) == 2 and not new_word):
prefix = words[1] if len(words) > 1 else ''
ans.match_groups['Remote control commands'] = {c: None for c in cmap if c.startswith(prefix)}
else:
complete_remote_command(ans, words[1], words[2:], new_word)
return ans
if words[0].startswith('@'):
if len(words) == 1 and not new_word:
prefix = words[0]
ans.match_groups['Remote control commands'] = {'@' + c: None for c in cmap if c.startswith(prefix)}
else:
complete_remote_command(ans, words[0][1:], words[1:], new_word)
if words[0] == '+':
if len(words) == 1 or (len(words) == 2 and not new_word):
prefix = words[1] if len(words) > 1 else ''
ans.match_groups['Entry points'] = {c: None for c in namespaced_entry_points if c.startswith(prefix)}
else:
if words[1] == 'kitten':
if len(words) == 2 or (len(words) == 3 and not new_word):
ans.match_groups['Kittens'] = dict.fromkeys(k for k in all_kitten_names() if k.startswith('' if len(words) == 2 else words[2]))
else:
complete_kitten(ans, words[2], words[3:], new_word)
return ans
if words[0].startswith('+'):
if len(words) == 1:
if new_word:
if words[0] == '+kitten':
ans.match_groups['Kittens'] = dict.fromkeys(all_kitten_names())
else:
prefix = words[0]
ans.match_groups['Entry points'] = {c: None for c in namespaced_entry_points if c.startswith(prefix)}
else:
if len(words) == 2 and not new_word:
ans.match_groups['Kittens'] = dict.fromkeys(k for k in all_kitten_names() if k.startswith(words[1]))
else:
if words[0] == '+kitten':
complete_kitten(ans, words[1], words[2:], new_word)
else:
complete_cli(ans, words, new_word, options_for_completion(), complete_kitty_cli_arg)
return ans
def setup(cstyle):
print(completion_scripts[cstyle])
def main(args, entry_points, namespaced_entry_points):
if not args:
raise SystemExit('Must specify completion style')
cstyle = args[0]
if cstyle == 'setup':
return setup(args[1])
data = sys.stdin.read()
try:
parser = parsers[cstyle]
serializer = serializers[cstyle]
except KeyError:
raise SystemExit('Unknown completion style: {}'.format(cstyle))
words, new_word = parser(data)
ans = find_completions(words, new_word, entry_points, namespaced_entry_points)
print(serializer(ans), end='')

View File

@@ -119,7 +119,16 @@ def merged_opts(all_options, opt, i):
def remove_markup(text):
return re.sub(r':([a-zA-Z0-9]+):`(.+?)`', r'\2', text, flags=re.DOTALL)
def sub(m):
if m.group(1) == 'ref':
return {
'layouts': 'https://sw.kovidgoyal.net/kitty/index.html#layouts',
'sessions': 'https://sw.kovidgoyal.net/kitty/index.html#sessions',
}[m.group(2)]
return m.group(2)
return re.sub(r':([a-zA-Z0-9]+):`(.+?)`', sub, text, flags=re.DOTALL)
def iter_blocks(lines):

View File

@@ -2,7 +2,6 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import ast
import os
import re
import shlex
@@ -40,6 +39,7 @@ def to_cmdline(x):
def python_string(text):
import ast
return ast.literal_eval("'''" + text.replace("'''", "'\\''") + "'''")
@@ -228,19 +228,28 @@ def parse_kittens_shortcut(sc):
def parse_kittens_func_args(action, args_funcs):
parts = action.split(' ', 1)
parts = action.strip().split(' ', 1)
func = parts[0]
if len(parts) == 1:
return func, ()
rest = parts[1]
parser = args_funcs.get(func)
if parser is not None:
try:
func, args = parser(func, rest)
except Exception:
raise ValueError('Unknown key action: {}'.format(action))
try:
parser = args_funcs[func]
except KeyError as e:
raise KeyError(
'Unknown action: {}. Check if map action: '
'{} is valid'.format(func, action)
) from e
try:
func, args = parser(func, rest)
except Exception:
raise ValueError('Unknown key action: {}'.format(action))
if not isinstance(args, (list, tuple)):
args = (args,)
return func, tuple(args)

View File

@@ -37,12 +37,14 @@ def parse_shortcut(sc):
parts = sc.split('+')
mods = parse_mods(parts[:-1], sc)
if mods is None:
return None, None
return None, None, None
key = parts[-1].upper()
key = getattr(defines, 'GLFW_KEY_' + named_keys.get(key, key), None)
if key is not None:
return mods, key
return mods, None
is_native = False
if key is None:
key = defines.key_for_native_key_name(parts[-1])
is_native = key is not None
return mods, is_native, key
KeyAction = namedtuple('KeyAction', 'func args')
@@ -119,6 +121,17 @@ def parse_change_font_size(func, rest):
return func, args
@func_with_args('clear_terminal')
def clear_terminal(func, rest):
vals = rest.split(' ', 1)
if len(vals) != 2:
log_error('clear_terminal needs two arguments, using defaults')
args = ['reset', 'active']
else:
args = [vals[0].lower(), vals[1].lower() == 'active']
return func, args
def parse_key_action(action):
parts = action.split(' ', 1)
func = parts[0]
@@ -140,15 +153,15 @@ sequence_sep = '>'
class KeyDefinition:
def __init__(self, is_sequence, action, mods, key, rest=()):
def __init__(self, is_sequence, action, mods, is_native, key, rest=()):
self.is_sequence = is_sequence
self.action = action
self.trigger = mods, key
self.trigger = mods, is_native, key
self.rest = rest
def resolve(self, kitty_mod):
self.trigger = defines.resolve_key_mods(kitty_mod, self.trigger[0]), self.trigger[1]
self.rest = tuple((defines.resolve_key_mods(kitty_mod, mods), key) for mods, key in self.rest)
self.trigger = defines.resolve_key_mods(kitty_mod, self.trigger[0]), self.trigger[1], self.trigger[2]
self.rest = tuple((defines.resolve_key_mods(kitty_mod, mods), is_native, key) for mods, is_native, key in self.rest)
def parse_key(val, key_definitions):
@@ -161,18 +174,18 @@ def parse_key(val, key_definitions):
trigger = None
rest = []
for part in sc.split(sequence_sep):
mods, key = parse_shortcut(part)
mods, is_native, key = parse_shortcut(part)
if key is None:
if mods is not None:
log_error('Shortcut: {} has unknown key, ignoring'.format(sc))
return
if trigger is None:
trigger = mods, key
trigger = mods, is_native, key
else:
rest.append((mods, key))
rest.append((mods, is_native, key))
rest = tuple(rest)
else:
mods, key = parse_shortcut(sc)
mods, is_native, key = parse_shortcut(sc)
if key is None:
if mods is not None:
log_error('Shortcut: {} has unknown key, ignoring'.format(sc))
@@ -186,9 +199,9 @@ def parse_key(val, key_definitions):
if paction is not None:
all_key_actions.add(paction.func)
if is_sequence:
key_definitions.append(KeyDefinition(True, paction, trigger[0], trigger[1], rest))
key_definitions.append(KeyDefinition(True, paction, trigger[0], trigger[1], trigger[2], rest))
else:
key_definitions.append(KeyDefinition(False, paction, mods, key))
key_definitions.append(KeyDefinition(False, paction, mods, is_native, key))
def parse_symbol_map(val):
@@ -243,28 +256,73 @@ def parse_send_text(val, key_definitions):
return parse_key(key_str, key_definitions)
special_handlers = {}
def special_handler(func):
special_handlers[func.__name__.partition('_')[2]] = func
@special_handler
def handle_map(key, val, ans):
parse_key(val, ans['key_definitions'])
@special_handler
def handle_symbol_map(key, val, ans):
ans['symbol_map'].update(parse_symbol_map(val))
@special_handler
def handle_send_text(key, val, ans):
# For legacy compatibility
parse_send_text(val, ans['key_definitions'])
@special_handler
def handle_clear_all_shortcuts(key, val, ans):
if to_bool(val):
ans['key_definitions'] = [None]
def expandvars(val, env):
def sub(m):
key = m.group(1)
result = env.get(key)
if result is None:
result = os.environ.get(key)
if result is None:
result = m.group()
return result
return re.sub(r'$\{(\S+)\}', sub, val)
@special_handler
def handle_env(key, val, ans):
key, val = val.partition('=')[::2]
key, val = key.strip(), val.strip()
ans['env'][key] = expandvars(val, ans['env'])
def special_handling(key, val, ans):
if key == 'map':
parse_key(val, ans['key_definitions'])
func = special_handlers.get(key)
if func is not None:
func(key, val, ans)
return True
if key == 'symbol_map':
ans['symbol_map'].update(parse_symbol_map(val))
return True
if key == 'send_text':
# For legacy compatibility
parse_send_text(val, ans['key_definitions'])
return True
if key == 'clear_all_shortcuts':
if to_bool(val):
ans['key_definitions'] = [None]
return
defaults = None
def option_names_for_completion():
yield from defaults
yield from special_handlers
def parse_config(lines, check_keys=True):
ans = {'symbol_map': {}, 'keymap': {}, 'sequence_map': {}, 'key_definitions': []}
ans = {'symbol_map': {}, 'keymap': {}, 'sequence_map': {}, 'key_definitions': [], 'env': {}}
parse_config_base(
lines,
defaults,

View File

@@ -3,6 +3,7 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
# Utils {{{
import os
from gettext import gettext as _
from . import fast_data_types as defines
@@ -10,7 +11,7 @@ from .conf.definition import option_func
from .conf.utils import (
choices, positive_float, positive_int, to_cmdline, to_color, unit_float
)
from .constants import is_macos
from .constants import config_dir, is_macos
from .fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE
from .layout import all_layouts
from .rgb import color_as_int, color_as_sharp, color_from_int
@@ -82,8 +83,17 @@ as color16 to color255.''')
'shortcuts': [
_('Keyboard shortcuts'),
_('''\
For a list of key names, see: :link:`GLFW keys <http://www.glfw.org/docs/latest/group__keys.html>`
For a list of modifier names, see: :link:`GLFW mods <http://www.glfw.org/docs/latest/group__mods.html>`
For a list of key names, see: :link:`GLFW keys
<http://www.glfw.org/docs/latest/group__keys.html>`. The name to use is the part
after the :code:`GLFW_KEY_` prefix. For a list of modifier names, see:
:link:`GLFW mods <http://www.glfw.org/docs/latest/group__mods.html>`
On Linux you can also use XKB key names to bind keys that are not supported by
GLFW. See :link:`XKB keys
<https://github.com/xkbcommon/libxkbcommon/blob/master/xkbcommon/xkbcommon-keysyms.h>`
for a list of key names. The name to use is the part after the :code:`XKB_KEY_`
prefix. Note that you should only use an XKB key name for keys that are not present
in the list of GLFW keys.
You can use the special action :code:`no_op` to unmap a keyboard shortcut that is
assigned in the default configuration.
@@ -256,7 +266,16 @@ def to_cursor_shape(x):
)
def cursor_text_color(x):
if x.lower() == 'background':
return
return to_color(x)
o('cursor', '#cccccc', _('Default cursor color'), option_type=to_color)
o('cursor_text_color', '#111111', option_type=cursor_text_color, long_text=_('''
Choose the color of text under the cursor. If you want it rendered with the
background color of the cell underneath instead, use the special keyword: background'''))
o('cursor_shape', 'block', option_type=to_cursor_shape, long_text=_(
'The cursor shape can be one of (block, beam, underline)'))
o('cursor_blink_interval', 0.5, option_type=positive_float, long_text=_('''
@@ -283,8 +302,9 @@ INPUT_LINE_NUMBER in the command line above will be replaced by an integer
representing which line should be at the top of the screen.'''))
o('wheel_scroll_multiplier', 5.0, long_text=_('''
Modify the amount scrolled by the mouse wheel or touchpad. Use
negative numbers to change scroll direction.'''))
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.'''))
# }}}
g('mouse') # {{{
@@ -439,9 +459,20 @@ o('window_border_width', 1.0, option_type=positive_float, long_text=_('''
The width (in pts) of window borders. Will be rounded to the nearest number of pixels based on screen resolution.
Note that borders are displayed only when more than one window is visible. They are meant to separate multiple windows.'''))
o('draw_minimal_borders', True, long_text=_('''
Draw only the minimum borders needed. This means that only the minimum
needed borders for inactive windows are drawn. That is only the borders
that separate the inactive window from a neighbor. Note that setting
a non-zero window margin overrides this and causes all borders to be drawn.
'''))
o('window_margin_width', 0.0, option_type=positive_float, long_text=_('''
The window margin (in pts) (blank area outside the border)'''))
o('single_window_margin_width', -1000.0, option_type=float, long_text=_('''
The window margin (in pts) to use when only a single window is visible.
Negative values will cause the value of :opt:`window_margin_width` to be used instead.'''))
o('window_padding_width', 0.0, option_type=positive_float, long_text=_('''
The window padding (in pts) (blank area between the text and the window border)'''))
@@ -616,6 +647,38 @@ opening new windows, closing windows, reading the content of windows, etc.
Note that this even works over ssh connections.
'''))
o(
'+env', '',
add_to_default=False,
long_text=_('''
Specify environment variables to set in all child processes. Note that
environment variables are expanded recursively, so if you use::
env MYVAR1=a
env MYVAR2=${MYVAR}/${HOME}/b
The value of MYVAR2 will be :code:`a/<path to home directory>/b`.
'''))
def startup_session(x):
if x.lower() == 'none':
return
x = os.path.expanduser(x)
x = os.path.expandvars(x)
if not os.path.isabs(x):
x = os.path.join(config_dir, x)
return x
o('startup_session', 'none', option_type=startup_session, long_text=_('''
Path to a session file to use for all kitty instances. Can be overridden
by using the :option:`kitty --session` command line option for individual
instances. See :ref:`sessions` in the kitty documentation for details. Note
that relative paths are interpreted with respect to the kitty config directory.
Environment variables in the path are expanded.
'''))
o('clipboard_control', 'write-clipboard write-primary', option_type=lambda x: frozenset(x.lower().split()), long_text=_('''
Allow programs running in kitty to read and write from the clipboard. You can
control exactly which actions are allowed. The set of possible actions is:
@@ -682,7 +745,10 @@ kitty will stay running, even with no open windows, as is the expected
behavior on macOS.
'''))
o('macos_window_resizable', True, long_text=_('''
Disable this if you want kitty top-level (OS) windows to not be resizable
on macOS.
'''))
# }}}
g('shortcuts') # {{{
@@ -750,6 +816,13 @@ You can open a new window with the current working directory set to the
working directory of the current window using::
map ctrl+alt+enter new_window_with_cwd
You can open a new window that is allowed to control kitty via
the kitty remote control facility by prefixing the command line with @.
Any programs running in that window will be allowed to control kitty.
For example::
map ctrl+enter new_window @ some_program
'''))
if is_macos:
k('new_os_window', 'cmd+n', 'new_os_window', _('New OS window'))
@@ -774,6 +847,8 @@ k('tenth_window', 'kitty_mod+0', 'tenth_window', _('Tenth window'))
# }}}
g('shortcuts.tab') # {{{
if is_macos:
k('next_tab', 'ctrl+tab', 'next_tab', _('Next tab'))
k('next_tab', 'kitty_mod+right', 'next_tab', _('Next tab'))
k('previous_tab', 'kitty_mod+left', 'previous_tab', _('Previous tab'))
k('new_tab', 'kitty_mod+t', 'new_tab', _('New tab'))
@@ -820,7 +895,7 @@ Useful with git, which uses sha1 hashes to identify commits'''))
g('shortcuts.misc') # {{{
k('toggle_fullscreen', 'kitty_mod+f11', 'toggle_fullscreen', _('Toggle fullscreen'))
k('input_unicode_character', 'kitty_mod+u', 'input_unicode_character', _('Unicode input'))
k('input_unicode_character', 'kitty_mod+u', 'kitten unicode_input', _('Unicode input'))
k('edit_config_file', 'kitty_mod+f2', 'edit_config_file', _('Edit config file'))
k('kitty_shell', 'kitty_mod+escape', 'kitty_shell window', _('Open the kitty command shell'), long_text=_('''
Open the kitty shell in a new window/tab/overlay/os_window to control kitty using commands.'''))
@@ -828,6 +903,17 @@ k('increase_background_opacity', 'kitty_mod+a>m', 'set_background_opacity +0.1',
k('decrease_background_opacity', 'kitty_mod+a>l', 'set_background_opacity -0.1', _('Decrease background opacity'))
k('full_background_opacity', 'kitty_mod+a>1', 'set_background_opacity 1', _('Make background fully opaque'))
k('reset_background_opacity', 'kitty_mod+a>d', 'set_background_opacity default', _('Reset background opacity'))
k('reset_terminal', 'kitty_mod+delete', 'clear_terminal reset active', _('Reset the terminal'),
long_text=_('''
You can create shortcuts to clear/reset the terminal. For example::
map kitty_mod+f9 clear_terminal reset active
map kitty_mod+f10 clear_terminal clear active
map kitty_mod+f11 clear_terminal scrollback active
These will reset screen/clear screen/clear screen+scrollback respectively. If you want to
operate on all windows instead of just the current one, use :italic:`all` instead of :italic`active`.
'''))
k('send_text', 'ctrl+shift+alt+h', 'send_text all Hello World', _('Send arbitrary text on key presses'),
add_to_default=False, long_text=_('''
You can tell kitty to send arbitrary (UTF-8) encoded text to

View File

@@ -7,10 +7,9 @@ import pwd
import sys
from collections import namedtuple
from .fast_data_types import set_boss as set_c_boss
appname = 'kitty'
version = (0, 11, 0)
version = (0, 12, 0)
str_version = '.'.join(map(str, version))
_plat = sys.platform.lower()
is_macos = 'darwin' in _plat
@@ -21,6 +20,22 @@ ScreenGeometry = namedtuple('ScreenGeometry', 'xstart ystart xnum ynum dx dy')
WindowGeometry = namedtuple('WindowGeometry', 'left top right bottom xnum ynum')
def kitty_exe():
ans = getattr(kitty_exe, 'ans', None)
if ans is None:
rpath = getattr(sys, 'bundle_exe_dir', None)
if not rpath:
items = frozenset(os.environ['PATH'].split(os.pathsep))
for candidate in items:
if os.access(os.path.join(candidate, 'kitty'), os.X_OK):
rpath = candidate
break
else:
rpath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'launcher')
ans = kitty_exe.ans = os.path.join(rpath, 'kitty')
return ans
def _get_config_dir():
if 'KITTY_CONFIG_DIRECTORY' in os.environ:
return os.path.abspath(os.path.expanduser(os.environ['KITTY_CONFIG_DIRECTORY']))
@@ -42,7 +57,10 @@ def _get_config_dir():
candidate = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME') or '~/.config'))
ans = os.path.join(candidate, appname)
os.makedirs(ans, exist_ok=True)
try:
os.makedirs(ans, exist_ok=True)
except FileExistsError:
raise SystemExit('A file {} already exists. It must be a directory, not a file.'.format(ans))
return ans
@@ -75,6 +93,7 @@ def get_boss():
def set_boss(m):
from .fast_data_types import set_boss as set_c_boss
get_boss.boss = m
set_c_boss(m)
@@ -86,6 +105,7 @@ def wakeup():
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
terminfo_dir = os.path.join(base_dir, 'terminfo')
logo_data_file = os.path.join(base_dir, 'logo', 'kitty.rgba')
beam_cursor_data_file = os.path.join(base_dir, 'logo', 'beam-cursor.png')
try:
shell_path = pwd.getpwuid(os.geteuid()).pw_shell or '/bin/sh'
except KeyError:

View File

@@ -1,7 +0,0 @@
#version GLSL_VERSION
uniform vec3 color;
out vec4 final_color;
void main() {
final_color = vec4(color, 1);
}

View File

@@ -1,18 +0,0 @@
#version GLSL_VERSION
uniform vec4 pos;
const uvec2 pos_map[] = uvec2[4](
uvec2(1, 0), // right, top
uvec2(1, 1), // right, bottom
uvec2(0, 1), // left, bottom
uvec2(0, 0) // left, top
);
void main() {
vec2 xpos = vec2(pos[0], pos[2]);
vec2 ypos = vec2(pos[1], pos[3]);
uvec2 pos = pos_map[gl_VertexID];
gl_Position = vec4(xpos[pos[0]], ypos[pos[1]], 0, 1);
}

View File

@@ -18,6 +18,7 @@
#include <termios.h>
#include <signal.h>
#include <fcntl.h>
#include <stdio.h>
#ifdef WITH_PROFILER
#include <gperftools/profiler.h>
#endif
@@ -98,28 +99,37 @@ start_profiler(PyObject UNUSED *self, PyObject *args) {
}
static PyObject*
stop_profiler(PyObject UNUSED *self) {
stop_profiler(PyObject UNUSED *self, PyObject *args UNUSED) {
ProfilerStop();
Py_RETURN_NONE;
}
#endif
static inline bool
put_tty_in_raw_mode(int fd, const struct termios* termios_p) {
put_tty_in_raw_mode(int fd, const struct termios* termios_p, bool read_with_timeout) {
struct termios raw_termios = *termios_p;
cfmakeraw(&raw_termios);
raw_termios.c_cc[VMIN] = 1; raw_termios.c_cc[VTIME] = 0;
if (read_with_timeout) {
raw_termios.c_cc[VMIN] = 0; raw_termios.c_cc[VTIME] = 1;
} else {
raw_termios.c_cc[VMIN] = 1; raw_termios.c_cc[VTIME] = 0;
}
if (tcsetattr(fd, TCSAFLUSH, &raw_termios) != 0) { PyErr_SetFromErrno(PyExc_OSError); return false; }
return true;
}
static PyObject*
open_tty(PyObject *self UNUSED, PyObject *args UNUSED) {
int fd = open("/dev/tty", O_RDWR | O_NONBLOCK | O_CLOEXEC | O_NOCTTY);
open_tty(PyObject *self UNUSED, PyObject *args) {
int read_with_timeout = 0;
if (!PyArg_ParseTuple(args, "|p", &read_with_timeout)) return NULL;
int flags = O_RDWR | O_CLOEXEC | O_NOCTTY;
if (!read_with_timeout) flags |= O_NONBLOCK;
static char ctty[L_ctermid+1];
int fd = open(ctermid(ctty), flags);
struct termios *termios_p = calloc(1, sizeof(struct termios));
if (!termios_p) return PyErr_NoMemory();
if (tcgetattr(fd, termios_p) != 0) { free(termios_p); PyErr_SetFromErrno(PyExc_OSError); return NULL; }
if (!put_tty_in_raw_mode(fd, termios_p)) { free(termios_p); return NULL; }
if (!put_tty_in_raw_mode(fd, termios_p, read_with_timeout != 0)) { free(termios_p); return NULL; }
return Py_BuildValue("iN", fd, PyLong_FromVoidPtr(termios_p));
}
@@ -138,7 +148,7 @@ normal_tty(PyObject *self UNUSED, PyObject *args) {
static PyObject*
raw_tty(PyObject *self UNUSED, PyObject *args) {
TTY_ARGS
if (!put_tty_in_raw_mode(fd, termios_p)) return NULL;
if (!put_tty_in_raw_mode(fd, termios_p, false)) return NULL;
Py_RETURN_NONE;
}
@@ -155,7 +165,7 @@ close_tty(PyObject *self UNUSED, PyObject *args) {
#undef TTY_ARGS
static PyMethodDef module_methods[] = {
{"open_tty", open_tty, METH_NOARGS, ""},
{"open_tty", open_tty, METH_VARARGS, ""},
{"normal_tty", normal_tty, METH_VARARGS, ""},
{"raw_tty", raw_tty, METH_VARARGS, ""},
{"close_tty", close_tty, METH_VARARGS, ""},
@@ -203,9 +213,11 @@ extern bool init_shaders(PyObject *module);
extern bool init_mouse(PyObject *module);
extern bool init_kittens(PyObject *module);
extern bool init_logging(PyObject *module);
extern bool init_png_reader(PyObject *module);
#ifdef __APPLE__
extern int init_CoreText(PyObject *);
extern bool init_cocoa(PyObject *module);
extern bool init_macos_process_info(PyObject *module);
#else
extern bool init_freetype_library(PyObject*);
#endif
@@ -238,7 +250,9 @@ PyInit_fast_data_types(void) {
if (!init_shaders(m)) return NULL;
if (!init_mouse(m)) return NULL;
if (!init_kittens(m)) return NULL;
if (!init_png_reader(m)) return NULL;
#ifdef __APPLE__
if (!init_macos_process_info(m)) return NULL;
if (!init_CoreText(m)) return NULL;
if (!init_cocoa(m)) return NULL;
#else

View File

@@ -27,7 +27,7 @@
#define xstr(s) str(s)
#define str(s) #s
#define arraysz(x) (sizeof(x)/sizeof(x[0]))
void log_error(const char *fmt, ...);
void log_error(const char *fmt, ...) __attribute__ ((format (printf, 1, 2)));
#define fatal(...) { log_error(__VA_ARGS__); exit(EXIT_FAILURE); }
typedef unsigned long long id_type;
@@ -197,7 +197,6 @@ typedef struct {
bool is_visible, is_focused;
CursorShape shape;
unsigned int x, y;
double left, right, top, bottom;
color_type color;
} CursorRenderInfo;
@@ -211,6 +210,8 @@ typedef struct {
bool dirty;
uint32_t color_table[256];
uint32_t orig_color_table[256];
DynamicColor dynamic_color_stack[10];
size_t dynamic_color_stack_idx;
DynamicColor configured, overridden;
} ColorProfile;
@@ -270,19 +271,22 @@ const char* cursor_as_sgr(Cursor*, Cursor*);
double monotonic();
PyObject* cm_thread_write(PyObject *self, PyObject *args);
bool schedule_write_to_child(unsigned long id, const char *data, size_t sz);
bool schedule_write_to_child(unsigned long id, unsigned int num, ...);
bool set_iutf8(int, bool);
color_type colorprofile_to_color(ColorProfile *self, color_type entry, color_type defval);
void copy_color_table_to_buffer(ColorProfile *self, color_type *address, int offset, size_t stride);
void colorprofile_push_dynamic_colors(ColorProfile*);
void colorprofile_pop_dynamic_colors(ColorProfile*);
void set_mouse_cursor(MouseShape);
void enter_event();
void mouse_event(int, int);
void focus_in_event();
void wakeup_io_loop(bool);
void scroll_event(double, double);
void scroll_event(double, double, int);
void fake_scroll(int, bool);
void set_special_key_combo(int glfw_key, int mods);
void set_special_key_combo(int glfw_key, int mods, bool is_native);
void on_key_input(int key, int scancode, int action, int mods, const char*, int);
void request_window_attention(id_type, bool);
SPRITE_MAP_HANDLE alloc_sprite_map(unsigned int, unsigned int);

2
kitty/emoji.h generated
View File

@@ -1,4 +1,4 @@
// unicode data, built from the unicode standard on: 2018-05-01
// unicode data, built from the unicode standard on: 2018-08-04
// see gen-wcwidth.py
#pragma once
#include "data-types.h"

View File

@@ -1076,10 +1076,11 @@ send_prerendered_sprites(FontGroup *fg) {
current_send_sprite_to_gpu((FONTS_DATA_HANDLE)fg, x, y, z, fg->canvas);
do_increment(fg, &error);
if (error != 0) { sprite_map_set_error(error); PyErr_Print(); fatal("failed"); }
PyObject *args = PyObject_CallFunction(prerender_function, "IIIII", fg->cell_width, fg->cell_height, fg->baseline, fg->underline_position, fg->underline_thickness);
PyObject *args = PyObject_CallFunction(prerender_function, "IIIIIdd", fg->cell_width, fg->cell_height, fg->baseline, fg->underline_position, fg->underline_thickness, fg->logical_dpi_x, fg->logical_dpi_y);
if (args == NULL) { PyErr_Print(); fatal("Failed to prerender cells"); }
for (ssize_t i = 0; i < PyTuple_GET_SIZE(args) - 1; i++) {
x = fg->sprite_tracker.x; y = fg->sprite_tracker.y; z = fg->sprite_tracker.z;
if (y > 0) { fatal("too many prerendered sprites for your GPU"); }
do_increment(fg, &error);
if (error != 0) { sprite_map_set_error(error); PyErr_Print(); fatal("failed"); }
uint8_t *alpha_mask = PyLong_AsVoidPtr(PyTuple_GET_ITEM(args, i));
@@ -1105,7 +1106,7 @@ initialize_font(FontGroup *fg, unsigned int desc_idx, const char *ftype) {
Py_CLEAR(face);
if (!ok) {
if (PyErr_Occurred()) { PyErr_Print(); }
fatal("Failed to initialize %s font: %d", ftype, idx);
fatal("Failed to initialize %s font: %zu", ftype, idx);
}
return idx;
}

View File

@@ -15,6 +15,7 @@ from kitty.fast_data_types import (
test_render_line, test_shape
)
from kitty.fonts.box_drawing import render_box_char, render_missing_glyph
from kitty.utils import log_error
if is_macos:
from .core_text import get_font_files, font_for_family
@@ -42,7 +43,26 @@ def descriptor_for_idx(idx):
return current_faces[idx]
def set_font_family(opts=None, override_font_size=None):
def dump_faces(ftypes, indices):
def face_str(f):
f = f[0]
if is_macos:
return f
return '{}:{}'.format(f['path'], f['index'])
log_error('Preloaded font faces:')
log_error('normal face:', face_str(current_faces[0]))
for ftype in ftypes:
if indices[ftype]:
log_error(ftype, 'face:', face_str(current_faces[indices[ftype]]))
si_faces = current_faces[max(indices.values())+1:]
if si_faces:
log_error('Symbol map faces:')
for face in si_faces:
log_error(face_str(face))
def set_font_family(opts=None, override_font_size=None, debug_font_matching=False):
global current_faces
opts = opts or defaults
sz = override_font_size or opts.font_size
@@ -57,6 +77,8 @@ def set_font_family(opts=None, override_font_size=None):
before = len(current_faces)
sm = create_symbol_map(opts)
num_symbol_fonts = len(current_faces) - before
if debug_font_matching:
dump_faces(ftypes, indices)
set_font_data(
render_box_drawing, prerender_function, descriptor_for_idx,
indices['bold'], indices['italic'], indices['bi'], num_symbol_fonts,
@@ -144,12 +166,43 @@ def render_special(
return ans
def prerender_function(cell_width, cell_height, baseline, underline_position, underline_thickness):
# Pre-render the special underline, strikethrough and missing cells
def render_cursor(which, cell_width=0, cell_height=0, dpi_x=0, dpi_y=0):
CharTexture = ctypes.c_ubyte * (cell_width * cell_height)
ans = CharTexture()
def vert(edge, width_pt=1):
width = max(1, int(round(width_pt * dpi_x / 72.0)))
left = 0 if edge == 'left' else max(0, cell_width - width)
for y in range(cell_height):
offset = y * cell_width + left
for x in range(offset, offset + width):
ans[x] = 255
def horz(edge, height_pt=1):
height = max(1, int(round(height_pt * dpi_y / 72.0)))
top = 0 if edge == 'top' else max(0, cell_height - height)
for y in range(top, top + height):
offset = y * cell_width
for x in range(cell_width):
ans[offset + x] = 255
if which == 1: # beam
vert('left', 1.5)
elif which == 2: # underline
horz('bottom', 2.0)
elif which == 3: # hollow
vert('left'), vert('right'), horz('top'), horz('bottom')
return ans
def prerender_function(cell_width, cell_height, baseline, underline_position, underline_thickness, dpi_x, dpi_y):
# Pre-render the special underline, strikethrough and missing and cursor cells
f = partial(
render_special, cell_width=cell_width, cell_height=cell_height, baseline=baseline,
underline_position=underline_position, underline_thickness=underline_thickness)
cells = f(1), f(2), f(3), f(0, True), f(missing=True)
c = partial(
render_cursor, cell_width=cell_width, cell_height=cell_height, dpi_x=dpi_x, dpi_y=dpi_y)
cells = f(1), f(2), f(3), f(0, True), f(missing=True), c(1), c(2), c(3)
return tuple(map(ctypes.addressof, cells)) + (cells,)

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