Compare commits

...

568 Commits

Author SHA1 Message Date
Kovid Goyal
be6e41c400 version 0.4.0 2017-10-22 09:48:43 +05:30
Kovid Goyal
a9e26952e9 ... 2017-10-22 09:29:19 +05:30
Kovid Goyal
348cdf5d5d Expose wcwidth to the python api 2017-10-22 09:22:32 +05:30
Kovid Goyal
a527cc88f3 ... 2017-10-21 14:03:48 +05:30
Kovid Goyal
17cfd98545 ... 2017-10-21 13:46:19 +05:30
Kovid Goyal
09c723380c Refactoring to allow using kitty in other projects 2017-10-21 12:16:46 +05:30
Kovid Goyal
ff35bd61a8 API to easily get primary monitor size from python 2017-10-21 10:10:57 +05:30
Kovid Goyal
eb5fe234f4 API to easily render a Screen object from python 2017-10-21 10:04:56 +05:30
Kovid Goyal
113494b4c6 Re-organize a few blurbs 2017-10-20 18:24:13 +05:30
Kovid Goyal
b7e8bd2faf Fix error when using @arg for stdin and no match found 2017-10-20 18:02:39 +05:30
Kovid Goyal
aaf94b61ed ... 2017-10-20 18:00:28 +05:30
Kovid Goyal
9a1b85081e Allow creating shortcuts to run arbitrary programs in new windows/tabs 2017-10-20 17:49:52 +05:30
Kovid Goyal
626847f2f5 Document specifying a different program for pass_selection_to_program 2017-10-20 15:43:09 +05:30
Kovid Goyal
6b8d38e4b9 ... 2017-10-20 13:46:50 +05:30
Kovid Goyal
227d21184a Add a new shortcut to pass the current selection to an external program 2017-10-20 13:30:23 +05:30
Kovid Goyal
f947c2eecb Redirect std streams to devnull when opening URL 2017-10-20 13:07:48 +05:30
Kovid Goyal
6a42a510bf ... 2017-10-20 12:38:26 +05:30
Kovid Goyal
efc8b898f8 Allow combining multiple independent actions into a single shortcut
Fixes #128
2017-10-20 12:31:36 +05:30
Kovid Goyal
e8a1ada5bf Allow passing arguments to functions mapped to shortcuts 2017-10-20 11:22:43 +05:30
Kovid Goyal
73b6cc433b ... 2017-10-20 11:15:33 +05:30
Kovid Goyal
4f6084c9f2 Use /proc/sef instead of /proc/getpid() 2017-10-20 10:57:06 +05:30
Kovid Goyal
6c9615df0d ... 2017-10-18 14:03:05 +05:30
Kovid Goyal
073c330800 Better wayland detection 2017-10-18 14:02:12 +05:30
Kovid Goyal
e558880e8f Load glfw primary selection functions dynamically
Allows kitty to run on wayland. Also dont try to set the window
icon on wayland.
2017-10-18 12:46:32 +05:30
Kovid Goyal
d4e5bc4141 Fix compilation with glfw 3.2 2017-10-18 09:13:19 +05:30
Kovid Goyal
7f3c808e66 Use glfw 3.3 functions for the primary selection
Allows us to drop the dependency on xsel for systems with glfw 3.3
2017-10-17 21:44:56 +05:30
Kovid Goyal
9322b210ff typo 2017-10-17 21:41:33 +05:30
Kovid Goyal
3d7e8e3e84 typo 2017-10-17 20:43:22 +05:30
Kovid Goyal
40d80c7833 Remove note about macOS versions + graphics 2017-10-17 17:59:45 +05:30
Kovid Goyal
e908484491 Also output error number when glewInit() fails
glewGetErrorString() is incomplete/not maintained
2017-10-17 16:47:38 +05:30
Kovid Goyal
25913f77e0 Add a clean action to setup.py 2017-10-17 13:35:54 +05:30
Kovid Goyal
66803e6873 Workaround to get graphics working on Apple's buggy products
Apparently some macOS OpenGL drivers cannot handle using a VAO with
attributes that have different divisors/apply to different shaders. So
use a separate VAO for graphics rendering. This is a small performance hit,
but is the price of supporting substandard computers.
2017-10-17 12:15:52 +05:30
Kovid Goyal
40722f42d3 ... 2017-10-17 10:18:03 +05:30
Kovid Goyal
192b16e9ff Add Lenna to the test dir so the gr.py script becomes self-sufficient 2017-10-17 10:15:38 +05:30
Kovid Goyal
02dc637ed0 Use the correct blend function for text 2017-10-17 09:51:21 +05:30
Kovid Goyal
2aa29f1873 typo 2017-10-16 22:05:29 +05:30
Kovid Goyal
199d5b34a4 Add note about needing High Sierra for graphics 2017-10-16 22:04:05 +05:30
Kovid Goyal
48b98c583e Fix incorrect scrolling of images with specified number of rows 2017-10-16 22:02:55 +05:30
Kovid Goyal
090ee5b846 Disable graphics on older macOS machines
I give up, trying to work around Apple's buggy drivers without even
having a machine to test my workarounds on is too hard. Simply disable
graphics on macOS < 10.13

Fixes #141
2017-10-16 21:23:29 +05:30
Kovid Goyal
55db07c79f Only display Lenna if she exists in the gr script 2017-10-16 18:52:17 +05:30
Kovid Goyal
75e84a4519 Let the gr.pt script run with python2 as well 2017-10-16 18:50:57 +05:30
Kovid Goyal
e679fa9a1c Possible fix for #141
Presumably on older macOS versions the Apple OpenGL drivers cannot handle both
non-sequential and non-zero based binding locations. Sigh.
2017-10-16 18:39:49 +05:30
Kovid Goyal
6f0922d5e3 Merge branch 'master' of https://github.com/eldentyrell/kitty
Fix #143
2017-10-16 16:57:51 +05:30
Elden Tyrell
4663133871 correct off-by-one error in initial tabstops so emacs renders properly 2017-10-16 03:50:11 -07:00
Kovid Goyal
3f8b2184a6 Add a sanity check to ensure the attribute location binding worked 2017-10-13 10:06:12 +05:30
Kovid Goyal
2f7e517b5b Update comment to reflect code 2017-10-12 16:51:35 +05:30
Kovid Goyal
f2e8877032 ... 2017-10-12 16:10:19 +05:30
Kovid Goyal
555409ea73 Apple's OpenGL drivers are sad
Apparently they cant handle non-sequential binding locations. Sigh.
2017-10-12 15:36:38 +05:30
Kovid Goyal
30baf81858 Fix #140 2017-10-12 12:58:38 +05:30
Kovid Goyal
76a591e7d1 ... 2017-10-10 11:34:19 +05:30
Kovid Goyal
57d16a02c0 Update changelog and add some performance comparisons 2017-10-10 09:21:42 +05:30
Kovid Goyal
5c55502946 ... 2017-10-09 15:22:38 +05:30
Kovid Goyal
ef7fa507fb ... 2017-10-09 15:21:33 +05:30
Kovid Goyal
1ceefde2d0 Graphics rendering is now merged into master!
Fix #33
2017-10-09 15:20:15 +05:30
Kovid Goyal
94eaaae4a6 Handle font size changes, rescaling images 2017-10-09 15:15:13 +05:30
Kovid Goyal
3cce238b2b Relayout images on screen resizes
The strategy is very simple, images are not rescaled or re-positioned in
any way.
2017-10-09 12:43:37 +05:30
Kovid Goyal
f9b52249d1 Remove unused code 2017-10-09 11:58:25 +05:30
Kovid Goyal
f396fe59ca Implement a quota for image data 2017-10-09 11:31:57 +05:30
Kovid Goyal
b4b6968e07 Clear the alt screen graphics buffer when switching away from it as well as to it 2017-10-08 23:14:56 +05:30
Kovid Goyal
747f7b076d Implement control code for deleting images 2017-10-08 22:51:00 +05:30
Kovid Goyal
3f9f5ae076 DRYer 2017-10-08 11:48:23 +05:30
Kovid Goyal
2058682d1f ... 2017-10-08 11:46:10 +05:30
Kovid Goyal
201ac973e3 Make clear as also delete images 2017-10-07 16:43:17 +05:30
Kovid Goyal
df87c051bd Add a note about the optional dep on ImageMagick for icat 2017-10-07 11:55:45 +05:30
Kovid Goyal
f76077a45c icat: switch to ImageMagick and also add support for local client speedup by using a file to transmit the data 2017-10-07 11:46:00 +05:30
Kovid Goyal
836e9d98b4 Forgot to actually switch grman on buffer switch 2017-10-07 10:12:16 +05:30
Kovid Goyal
d5088a3e5b Do image clipping using OpenGL rather than on the CPU 2017-10-07 10:08:26 +05:30
Kovid Goyal
3a344d86fe Add graphics protocol detection to icat.py 2017-10-07 09:01:31 +05:30
Kovid Goyal
6a65f23f98 Add a a=q mode for querying without storing image data 2017-10-06 23:43:42 +05:30
Kovid Goyal
9e284442e8 ... 2017-10-06 23:11:14 +05:30
Kovid Goyal
135358423c Remove use of random data in the tests 2017-10-06 22:52:50 +05:30
Kovid Goyal
31173245f8 Send ACKs to client when displaying images by id 2017-10-06 22:39:17 +05:30
Kovid Goyal
ad5a276ddb Fix test broken by clipping of images to screen area 2017-10-06 22:34:21 +05:30
Kovid Goyal
4bc852e615 A spot of refacatoring 2017-10-06 21:36:54 +05:30
Kovid Goyal
d4e493727e Clip all rendered images to screen boundaries 2017-10-06 21:22:52 +05:30
Kovid Goyal
8d3ec7451b typo 2017-10-06 21:14:39 +05:30
Kovid Goyal
652e1084bc ... 2017-10-06 21:10:45 +05:30
Kovid Goyal
a9d31541c1 Implement clipped scrolling of images when in page area mode 2017-10-06 21:08:10 +05:30
Kovid Goyal
480645877a Clear graphics when switching to the alternate screen buffer 2017-10-05 23:01:47 +05:30
Kovid Goyal
600bff73d6 Off-by-one for image visibility testing
Also delete all images that overlap the screen on reset
2017-10-05 22:54:52 +05:30
Kovid Goyal
1c9612bd55 Turn on LTO for release builds 2017-10-05 21:18:21 +05:30
Kovid Goyal
259e01e879 Add test for deleting images after they scroll off 2017-10-05 21:14:56 +05:30
Kovid Goyal
240c9b5d8b A spot of refactoring 2017-10-05 20:54:45 +05:30
Kovid Goyal
689227dce4 Scroll images with text 2017-10-05 20:23:32 +05:30
Kovid Goyal
a4e7985fb0 Condition on negative refs as well as in the future we will use interleaved drawing for background opacity as well 2017-10-05 18:21:29 +05:30
Kovid Goyal
9bd231c4f9 Note that integers in the protocol are 32-bit 2017-10-05 18:15:10 +05:30
Kovid Goyal
7bf8b3c3c1 ... 2017-10-05 18:10:11 +05:30
Kovid Goyal
57a6d53a45 Document the image display protocol 2017-10-05 18:08:12 +05:30
Kovid Goyal
49aeae4aad Fix invocation of the image cat script as kitty icat 2017-10-05 17:19:17 +05:30
Kovid Goyal
664975b707 Show off kitty's graphics capabilities 2017-10-05 16:49:34 +05:30
Kovid Goyal
c3ab6ff780 Fix vertically inverted rendering of PNG 2017-10-04 23:38:36 +05:30
Kovid Goyal
6ad7260871 Dont compress PNG files 2017-10-04 23:26:15 +05:30
Kovid Goyal
e8f72c90f3 Add note about needing S key for PNG + compression 2017-10-04 23:25:27 +05:30
Kovid Goyal
75244b1416 ... 2017-10-04 23:07:59 +05:30
Kovid Goyal
952f0ad98c Accommodate the underpowered Apple lineup
macOS is missing glDrawArraysInstancedBaseInstance
2017-10-04 22:44:03 +05:30
Kovid Goyal
c726a15ed4 ... 2017-10-04 21:08:28 +05:30
Kovid Goyal
5d6af442c1 Correctly query num of cols/rows in icat 2017-10-04 20:06:07 +05:30
Kovid Goyal
596a392e68 micro optimization 2017-10-04 19:46:19 +05:30
Kovid Goyal
46bb482e96 Fix y-axis mapping incorrect for images 2017-10-04 19:34:34 +05:30
Kovid Goyal
e9bc64205e Code (not working) to draw graphics 2017-10-04 18:29:48 +05:30
Kovid Goyal
8f05f752af Code to update graphics buffer 2017-10-04 17:41:11 +05:30
Kovid Goyal
124a41643f Add a comment about the use of layout locations 2017-10-04 17:29:30 +05:30
Kovid Goyal
138a37ccac Start work on the graphics program 2017-10-04 17:22:00 +05:30
Kovid Goyal
e1650d5df7 Implement interleaved drawing of cells 2017-10-04 16:11:20 +05:30
Kovid Goyal
3f615666df Call grman_update_layers 2017-10-04 15:39:31 +05:30
Kovid Goyal
96a9d64bcb A spot of refactoring 2017-10-04 15:34:54 +05:30
Kovid Goyal
bd09e2ebf1 Allow re-using VAO objects between different types of cell program 2017-10-04 15:33:15 +05:30
Kovid Goyal
ad040271b9 Initialize uniforms for all cell program variants 2017-10-04 14:46:36 +05:30
Kovid Goyal
ead73c8209 Prepare for two stage rendering of cells 2017-10-04 14:15:51 +05:30
Kovid Goyal
b032130a20 A spot of refactoring 2017-10-04 10:56:44 +05:30
Kovid Goyal
c5837d4577 More work on the iamge cat utility. Make it useable as a standalone script 2017-10-04 10:22:31 +05:30
Kovid Goyal
1a8d9314fe Fix transmission of chunked compressed data 2017-10-04 10:19:25 +05:30
Kovid Goyal
9ccf9636ce Allow running icat script outside of kitty 2017-10-03 21:37:55 +05:30
Kovid Goyal
e56042c41a Graceful handling of errors from rsvg-convert 2017-10-03 21:31:47 +05:30
Kovid Goyal
72626279c2 Start work on image cat utility 2017-10-03 21:31:42 +05:30
Kovid Goyal
acc43ce9dc Handle T action with chunked transfer correctly 2017-10-03 20:34:48 +05:30
Kovid Goyal
c87dcdbe3c Tests for displaying graphics 2017-10-03 16:10:04 +05:30
Kovid Goyal
aec1612de2 API to enable image placement tests
Also fix cursor movement handling after graphics command
2017-10-02 10:35:51 +05:30
Kovid Goyal
7c488888a0 more work on displaying graphics 2017-10-01 20:17:48 +05:30
Kovid Goyal
6d960e4130 ... 2017-10-01 09:12:14 +05:30
Kovid Goyal
3cbc20005b Implement sending of image data to GPU 2017-10-01 09:04:46 +05:30
Kovid Goyal
e2a8115328 ... 2017-09-30 18:36:25 +05:30
Kovid Goyal
28ae99ed37 More work on displaying images 2017-09-30 14:46:17 +05:30
Kovid Goyal
e5898ad4b0 More work on displaying images 2017-09-30 13:51:52 +05:30
Kovid Goyal
b722bc809c More work on displaying images 2017-09-30 13:51:52 +05:30
Kovid Goyal
710d00674a Export the free texture function 2017-09-30 13:51:52 +05:30
Kovid Goyal
9078ca975e A spot of refactoring 2017-09-30 13:51:52 +05:30
Kovid Goyal
4f98d10caa Remove the requirement to specify sizes for PNG files 2017-09-30 13:51:52 +05:30
Kovid Goyal
9e5dbb45d4 Move the graphics protocol documentation to a separate file 2017-09-30 13:51:52 +05:30
Kovid Goyal
5e5065715e ... 2017-09-30 13:51:52 +05:30
Kovid Goyal
8be7679a38 grrr 2017-09-30 13:51:52 +05:30
Kovid Goyal
6297c9a719 ... 2017-09-30 13:51:52 +05:30
Kovid Goyal
933621b038 Better libpng error handling 2017-09-30 13:51:51 +05:30
Kovid Goyal
c31e608ff9 Tests for loading PNG images 2017-09-30 13:51:51 +05:30
Kovid Goyal
671bcafee5 Dont carry the fd around 2017-09-30 13:51:51 +05:30
Kovid Goyal
c01f009e42 Remove sz requirement and implement proper fix for failure to mmap shm objects on OS X. Apparently they have to be mapped shared. 2017-09-30 13:51:51 +05:30
Kovid Goyal
dbd7ec5b27 Allow specifying the offset and size for reading data from files
Also require size to be specified for SHM objects to support platforms
such as macOS that have no way to get the size from the fd.
2017-09-30 13:51:51 +05:30
Kovid Goyal
898136dbdc Free the mapped file early if possible 2017-09-30 13:51:51 +05:30
Kovid Goyal
ef91f9dc8f Use fstat to get SHM file size instead of seeking 2017-09-30 13:51:51 +05:30
Kovid Goyal
bfd7675639 ... 2017-09-30 13:51:51 +05:30
Kovid Goyal
b644d4620a OS X does not support write() with shared memory 2017-09-30 13:51:51 +05:30
Kovid Goyal
03dd6e0490 Test for image loading from shared memory 2017-09-30 13:51:50 +05:30
Kovid Goyal
dfca991173 Test for loading image data from file 2017-09-30 13:51:50 +05:30
Kovid Goyal
13c4c605b3 Prevent loading of too large PNG images 2017-09-30 13:51:50 +05:30
Kovid Goyal
8e5c4b6f7a Require data size to be sent for PNG format 2017-09-30 13:51:50 +05:30
Kovid Goyal
d0d769f26b Test for zlib de-compression 2017-09-30 13:51:50 +05:30
Kovid Goyal
558b208edd Fix compressed key not being parsed 2017-09-30 13:51:50 +05:30
Kovid Goyal
35acb1497a Fix chunked loading of images 2017-09-30 13:51:50 +05:30
Kovid Goyal
8cd1f76d2b Framework for testing loading of images 2017-09-30 13:51:50 +05:30
Kovid Goyal
f0ee728b94 Fix failing flake 2017-09-30 13:51:50 +05:30
Kovid Goyal
b4a96428f3 DRYer 2017-09-30 13:51:50 +05:30
Kovid Goyal
7d6d8efc26 ... 2017-09-30 13:51:49 +05:30
Kovid Goyal
f3ee3784f7 Fix longjmp handling when calling libpng 2017-09-30 13:51:49 +05:30
Kovid Goyal
7da08d37d2 File to host graphics tests 2017-09-30 13:51:49 +05:30
Kovid Goyal
23c2bdb6f0 Dont silently wrap too large ids, instead ignore the graphics command 2017-09-30 13:51:49 +05:30
Kovid Goyal
7736629bc4 Fix graphics id not going upto UINT32_MAX 2017-09-30 13:51:49 +05:30
Kovid Goyal
204bd97198 Nicer error responses on bad graphics data 2017-09-30 13:51:49 +05:30
Kovid Goyal
2786976c63 ... 2017-09-30 13:51:49 +05:30
Kovid Goyal
7567f96a34 Python API to get image data from gr. manager for tests 2017-09-30 13:51:49 +05:30
Kovid Goyal
cf33ef8666 ... 2017-09-30 13:51:49 +05:30
Kovid Goyal
9860ed544f Respond to load commands when an id is present 2017-09-30 13:51:49 +05:30
Kovid Goyal
9601ed79f1 Travis build fixes 2017-09-30 13:51:48 +05:30
Kovid Goyal
e5b5fb2773 fmt defaults to RGBA and ignore add commands for images with no width/height 2017-09-30 13:51:48 +05:30
Kovid Goyal
01fb9436dd Code to read compressed and PNG encoded image data 2017-09-30 13:51:48 +05:30
Kovid Goyal
df1e06ec2c DRYer 2017-09-30 13:51:48 +05:30
Kovid Goyal
c973a7326e Dont link against rt on macOS 2017-09-30 13:51:48 +05:30
Kovid Goyal
9facc5f7d3 DRYer 2017-09-30 13:51:48 +05:30
Kovid Goyal
66bce4b8cd Code to read image data 2017-09-30 13:51:48 +05:30
Kovid Goyal
c567acb4e5 Work on loading images 2017-09-30 13:51:48 +05:30
Kovid Goyal
8418cc04c1 Start work on the graphics manager 2017-09-30 13:51:48 +05:30
Kovid Goyal
e52818d54c Move the screen datatype decls into screen.h 2017-09-30 13:51:47 +05:30
Kovid Goyal
15e03f74f4 Also report the payload for graphics commands 2017-09-30 13:51:47 +05:30
Kovid Goyal
56c2e0c26a Report incomplete graphics commands as parser errors 2017-09-30 13:51:47 +05:30
Kovid Goyal
83de392b39 ... 2017-09-30 13:51:47 +05:30
Kovid Goyal
3c89f80603 More tests for the graphics control code parser 2017-09-30 13:51:47 +05:30
Kovid Goyal
8e64895c23 Framework for testing graphics command parsing 2017-09-30 13:51:47 +05:30
Kovid Goyal
967e65e150 Use a static b64 decoding table 2017-09-30 13:51:47 +05:30
Kovid Goyal
9f5d731d8d ... 2017-09-30 13:51:47 +05:30
Kovid Goyal
ef312a3d1c ... 2017-09-30 13:51:47 +05:30
Kovid Goyal
705298737a Fix off by one 2017-09-30 13:51:47 +05:30
Kovid Goyal
a7b95f73be Cleanup handling of ,; in the GR control code parser 2017-09-30 13:51:46 +05:30
Kovid Goyal
a9ed44eecc Implement decoding of graphics command payload 2017-09-30 13:51:46 +05:30
Kovid Goyal
b8d9629ee4 Start work on parsing of graphics escape code 2017-09-30 13:51:46 +05:30
Kovid Goyal
5b24d51fcd Fix failing screen resize test
Take into account the index that now happens to ensure that
the cursor is on a new line.
2017-09-30 13:51:35 +05:30
Kovid Goyal
e832524bfc Ensure cursor is on a blank line after a resize 2017-09-30 13:25:31 +05:30
Kovid Goyal
a783325464 Nicer parsing of dependencies file 2017-09-30 12:25:03 +05:30
Kovid Goyal
af99c172fe Use dependency mapping when doing incremental builds 2017-09-30 12:18:08 +05:30
Kovid Goyal
b5b9a7ba6d Fix compilation with clang 2017-09-30 11:56:30 +05:30
Kovid Goyal
2909fddaa2 Split out GL API wrapping code into its own header 2017-09-30 11:48:52 +05:30
Kovid Goyal
7863857b2c Use a UBO for all cell program uniforms 2017-09-30 11:44:44 +05:30
Kovid Goyal
b28cec3236 Give Face objects a nice __repr__ 2017-09-30 08:01:04 +05:30
Kovid Goyal
55498b5a44 Avoid unnecessary calls to glBindTexture 2017-09-30 07:00:46 +05:30
Kovid Goyal
8d03d7216f Do not crash when failing to render a glyph, instead render the missing glyph and print an error msg to stderr 2017-09-29 11:57:17 +05:30
Kovid Goyal
7ddaa70498 Cleanup freetype library on exit 2017-09-28 11:22:18 +05:30
Kovid Goyal
150ffab72d finalize the fontconfig library on exit 2017-09-28 11:06:56 +05:30
Kovid Goyal
cda385b37c Fix #132 2017-09-28 10:01:08 +05:30
Kovid Goyal
28146a4e9c Proper fix for sending constant uniform to cell program 2017-09-28 09:55:17 +05:30
Kovid Goyal
0ad9149ca8 Avoid one more call to set uniforms 2017-09-26 11:27:04 +05:30
Kovid Goyal
f9972bc33c ... 2017-09-26 09:13:50 +05:30
Kovid Goyal
e6cff1e9b6 Fix #130 2017-09-26 09:10:24 +05:30
Kovid Goyal
c64527c333 Fix #129 2017-09-26 08:22:26 +05:30
Kovid Goyal
9cfc19e010 Dont shadow the glfw global mouse state variable 2017-09-25 22:00:00 +05:30
Kovid Goyal
af971af4ca Remove unused code 2017-09-25 21:59:52 +05:30
Kovid Goyal
3471887817 ... 2017-09-25 18:05:25 +05:30
Kovid Goyal
cd88505ca7 Fix #120 2017-09-25 17:47:30 +05:30
Kovid Goyal
3ca45ab241 Fix cursor moving one line up when resizing 2017-09-20 10:59:31 +05:30
Kovid Goyal
564f19d248 ... 2017-09-19 11:17:17 +05:30
Kovid Goyal
5622efb710 Add a CHANGELOG 2017-09-19 10:33:23 +05:30
Kovid Goyal
a857a35348 Unhide mouse cursor on focus in 2017-09-18 20:28:55 +05:30
Kovid Goyal
93ca469d85 Only set the glfw error callback immediately before initializing glfw
Avoids spurious glfw not initialized error messages in case fo
exceptions that occur before the call to init
2017-09-18 08:46:25 +05:30
Kovid Goyal
1e01c2a07c Remove unused code 2017-09-18 08:40:37 +05:30
Kovid Goyal
efaee69486 Fix detection of glfinithintstring 2017-09-18 08:40:02 +05:30
Kovid Goyal
558e4af3c6 Make the curly underline a bit thicker 2017-09-17 21:09:30 +05:30
Kovid Goyal
8490457cab ... 2017-09-17 21:01:50 +05:30
Kovid Goyal
6585c11808 Make the curly underline an actual curl 2017-09-17 20:30:36 +05:30
Kovid Goyal
142cba54b3 DRYer 2017-09-17 19:36:47 +05:30
Kovid Goyal
19b065b6a4 Fix underline not being set to background color when cursor is over it 2017-09-17 19:28:01 +05:30
Kovid Goyal
826a64c8dc Fix wide cursor not being rendered 2017-09-17 19:23:01 +05:30
Kovid Goyal
14103db2ee Coalesce another couple of uniforms 2017-09-17 18:10:32 +05:30
Kovid Goyal
51d037fb70 Coalesce a couple of set uniform calls into a single one 2017-09-17 17:48:42 +05:30
Kovid Goyal
b433380e1d Make the cell shader a little more readable 2017-09-17 17:38:03 +05:30
Kovid Goyal
df4df76d24 Switch to using an opaque block cursor
The text is rendered in the background color on top of the opaque
block cursor. We use the background color since applications can change
both cursor and background colors, so it is up to the application to
ensure the colors have good contrast. Fix #126
2017-09-17 13:03:02 +05:30
Kovid Goyal
2d0c6a43c5 Fix broken reverse video 2017-09-16 17:39:07 +05:30
Kovid Goyal
0307497e06 Fix broken scrolling 2017-09-16 17:11:30 +05:30
Kovid Goyal
bc1de92534 Fix the broken tests 2017-09-16 17:05:58 +05:30
Kovid Goyal
37feb65204 Move wakeup calls into child-monitor 2017-09-16 16:46:23 +05:30
Kovid Goyal
9cd146fe3e Remove the buf_toggled callback 2017-09-16 16:31:29 +05:30
Kovid Goyal
7b1576d960 Remove unneccessary python callbacks 2017-09-16 15:03:18 +05:30
Kovid Goyal
6361da9e0b ... 2017-09-16 14:34:47 +05:30
Kovid Goyal
50a1bf528c Get rid of the bell callback, just handle it all in C 2017-09-16 14:34:23 +05:30
Kovid Goyal
2ddbac073a DRYer 2017-09-16 12:56:41 +05:30
Kovid Goyal
33f48e8268 Fix rendering occasionally not occurring after input received 2017-09-16 12:51:30 +05:30
Kovid Goyal
ee581ebdf4 Remove unused code 2017-09-16 12:26:35 +05:30
Kovid Goyal
81879db2db Fix new_input_at not protected by read lock 2017-09-16 12:24:31 +05:30
Kovid Goyal
3675c27268 Fix the new_window keyboard shortcut being passed through 2017-09-16 11:22:58 +05:30
Kovid Goyal
a92b3c605f Move the text attributes out of the char type
This means that more text attributes can be added in the future (there
are 8 bits available) and there is no need to keep bit twiddling when
accessing the chars themselves. Also means less data needs to be sent to
the GPU. Cell size does not change since there were currently 2 unused
bytes because of alignment.
2017-09-16 11:02:42 +05:30
Kovid Goyal
85ed5c1515 Remove unnecessary conditional operators 2017-09-16 09:43:43 +05:30
Kovid Goyal
b7a5c064c2 Fix some Alt based shortcuts not working 2017-09-16 09:32:17 +05:30
Kovid Goyal
34084e7ab5 Neaten up keys.h 2017-09-16 09:16:30 +05:30
Kovid Goyal
728f33700a Fine tune the artificial delays in the render loop
There are now two numbers, repaint_delay and input_delay
that control how often the screen is repainted and how frequently
input received from the child process is processed.

This halves the CPU usage in intensive cases such as scrolling
a file in less. The CPU usage of kitty + X when scrolling is now
significantly lower than all the other terminals on my system.

MROAWR!

...
2017-09-16 08:20:23 +05:30
Kovid Goyal
43ebddc28f Fix writing to child causing excessive CPU usage
Couple of bugs combined.
2017-09-15 21:56:20 +05:30
Kovid Goyal
c5e989bc94 Move the key handler to C 2017-09-15 21:46:00 +05:30
Kovid Goyal
a4d71bcf5c Move the text input callback to C 2017-09-15 18:33:45 +05:30
Kovid Goyal
ddbe0703a3 Remove unused code 2017-09-15 18:10:17 +05:30
Kovid Goyal
b2c7272af1 Store key combos that need special handling in a table for fast lookup 2017-09-15 18:06:26 +05:30
Kovid Goyal
0423c55dc1 Move a few externs into appropriate header files 2017-09-15 11:56:08 +05:30
Kovid Goyal
0b656246fd Fix cursor width incorrect after scrolling 2017-09-15 11:44:16 +05:30
Kovid Goyal
28430a4104 Fix wide chars not being rendered 2017-09-15 11:43:05 +05:30
Kovid Goyal
f0a9f32d0a Fix cursor not being rendered on empty cells 2017-09-15 11:08:42 +05:30
Kovid Goyal
44c897a61a Translate key presses to bytes using a lookup table 2017-09-15 10:45:27 +05:30
Kovid Goyal
b5af266b87 A spot of refactoring 2017-09-15 10:45:27 +05:30
Kovid Goyal
9989363ecd Make the URL highlight color configurable 2017-09-15 10:45:27 +05:30
Kovid Goyal
53ea5c678d Highlight links with a underline on mouseover 2017-09-15 10:45:27 +05:30
Kovid Goyal
464291bbb1 Port click on URL code to C 2017-09-15 10:45:27 +05:30
Kovid Goyal
ed3427f349 Dont use the python unicodedata module as we use libunistring
No sense in loading two hude unicode datasets into memory
2017-09-15 10:45:27 +05:30
Kovid Goyal
21accfe114 Send mouse events to child directly in C 2017-09-15 10:45:26 +05:30
Kovid Goyal
ee0a539e01 Reclaim write buffer space once no longer needed 2017-09-15 10:45:26 +05:30
Kovid Goyal
d236955dfb ... 2017-09-15 10:45:26 +05:30
Kovid Goyal
32a11d9d21 Avoid mallocs when writing small amounts of data to the child 2017-09-15 10:45:26 +05:30
Kovid Goyal
43c4c71abd Port mouse event encoding code to C 2017-09-15 10:45:26 +05:30
Kovid Goyal
b9ac13c379 Move the scroll event handler to C 2017-09-15 10:45:26 +05:30
Kovid Goyal
11bb21e8a4 Remove unused code 2017-09-15 10:45:26 +05:30
Kovid Goyal
4bfe7e1069 Fix incorrect cell co-ordinates when more than one window visible in the layout 2017-09-15 10:45:26 +05:30
Kovid Goyal
4feaf13556 Implement drag scrolling in C 2017-09-15 10:45:26 +05:30
Kovid Goyal
a27004da35 Port drag select to C 2017-09-15 10:45:26 +05:30
Kovid Goyal
cf6894d74f Port multi-click handler to C 2017-09-15 10:45:25 +05:30
Kovid Goyal
a921180224 Port middle-click-to-paste to C 2017-09-15 10:45:25 +05:30
Kovid Goyal
7e4948be10 Port switch focus on click code to C 2017-09-15 10:45:25 +05:30
Kovid Goyal
04a45babfb DRYer 2017-09-15 10:45:25 +05:30
Kovid Goyal
1c1d0a4e91 Port mouse cursor change over hyperlinks to C 2017-09-15 10:45:25 +05:30
Kovid Goyal
271b623f82 More URL detection tests 2017-09-15 10:45:25 +05:30
Kovid Goyal
88d896e745 Move function to detect URLs into C code 2017-09-15 10:45:25 +05:30
Kovid Goyal
745b6222c2 Start work on migrating the mouse handlers to C 2017-09-15 10:45:25 +05:30
Kovid Goyal
42329e5d46 Merge the char_grid and window modules 2017-09-15 10:45:25 +05:30
Kovid Goyal
240c683504 DRYer 2017-09-15 10:45:24 +05:30
Kovid Goyal
c683725434 Get rid of the timers infrastructure
It is not needed with a pure state machine.
Note that drag scrolling still has to be ported from using
timers.
2017-09-15 10:45:24 +05:30
Kovid Goyal
f3cd0a4966 ... 2017-09-15 10:45:24 +05:30
Kovid Goyal
573dd8c99e ... 2017-09-15 10:45:24 +05:30
Kovid Goyal
6d8b59cb61 Track window and application titles in C
Allows removing of the last bit fo python from the render loop
2017-09-15 10:45:24 +05:30
Kovid Goyal
c41ede4d20 Dont use a timer for mouse hiding 2017-09-15 10:45:24 +05:30
Kovid Goyal
13ac050bf8 Move cursor render call to C 2017-09-15 10:45:24 +05:30
Kovid Goyal
6e4b977128 Track application focus state in global state 2017-09-15 10:45:24 +05:30
Kovid Goyal
e6df82b255 Move the char grid render call into C 2017-09-15 10:45:24 +05:30
Kovid Goyal
943a1575ad Move render call for tab bar to C code 2017-09-15 10:45:24 +05:30
Kovid Goyal
2e0cbb88bb Call draw_borders() directly from C 2017-09-15 10:45:23 +05:30
Kovid Goyal
8e379df24f Ensure the visual bell is cleared
UI thread must wakeup after visual bell duration
2017-09-15 10:45:23 +05:30
Kovid Goyal
ccf5391b45 Move visual bell tracking into the Screen object 2017-09-15 10:45:23 +05:30
Kovid Goyal
ca5eb4feb5 Start working on central object to track state 2017-09-15 10:45:23 +05:30
Kovid Goyal
c1cb4df9d2 Avoid unnecessary use of timers for resize_pty 2017-09-15 10:45:23 +05:30
Kovid Goyal
367ffb602c Color table buffer has to be bound before each render
Other wise the last created child's buffer is always used
2017-09-15 10:45:23 +05:30
Kovid Goyal
4e8ade53d8 Fix compilation on apple 2017-09-15 10:45:23 +05:30
Kovid Goyal
bc8a8d83fc Restore a couple fo python bindings needed for the test suite 2017-09-15 10:45:23 +05:30
Kovid Goyal
7f6befe4f9 Remove the python OpenGL wrapper as it is no longer needed 2017-09-15 10:45:23 +05:30
Kovid Goyal
86893db46b Fix re-alloc of sprite texture 2017-09-15 10:45:22 +05:30
Kovid Goyal
3c3eeb1835 ... 2017-09-15 10:45:22 +05:30
Kovid Goyal
3a883ad436 Move the sprites GPU code to shaders.c 2017-09-15 10:45:22 +05:30
Kovid Goyal
cc8271b766 Fix compilation on macOS 2017-09-15 10:45:22 +05:30
Kovid Goyal
1246032b67 Remove unused code 2017-09-15 10:45:22 +05:30
Kovid Goyal
9eea178890 Make --debug-gl have zero runtime cost when absent 2017-09-15 10:45:22 +05:30
Kovid Goyal
44f456089b Migrate the cell program 2017-09-15 10:45:22 +05:30
Kovid Goyal
2fff6e1cb9 Directly rende r borders instead of going through the active tab 2017-09-15 10:45:22 +05:30
Kovid Goyal
ff6c366b8a Remove unused code 2017-09-15 10:45:22 +05:30
Kovid Goyal
6701d52e26 Simplify error handling of open gl code 2017-09-15 10:45:22 +05:30
Kovid Goyal
11e147c718 ... 2017-09-15 10:45:21 +05:30
Kovid Goyal
634cd53454 Remove unused code 2017-09-15 10:45:21 +05:30
Kovid Goyal
084f31292b ... 2017-09-15 10:45:21 +05:30
Kovid Goyal
1e37041b50 Fix rendering of horizontal borders 2017-09-15 10:45:21 +05:30
Kovid Goyal
2a24199c90 Migrate the borders program 2017-09-15 10:45:21 +05:30
Kovid Goyal
5905216f96 Use instanced rendering for the borders 2017-09-15 10:45:21 +05:30
Kovid Goyal
3ddf963c5c API to map VAO buffers 2017-09-15 10:45:21 +05:30
Kovid Goyal
778c3c075a Migrate the cursor program 2017-09-15 10:45:21 +05:30
Kovid Goyal
d2b3052918 Code to get uniform locations 2017-09-15 10:45:21 +05:30
Kovid Goyal
8ce2794a8a More work on migrating shader code 2017-09-15 10:45:20 +05:30
Kovid Goyal
c1dc4d0575 ... 2017-09-15 10:45:20 +05:30
Kovid Goyal
9c184c794a Migrate function to compile programs 2017-09-15 10:45:20 +05:30
Kovid Goyal
d3f2620caf Migrate GL setup and error handling code 2017-09-15 10:45:20 +05:30
Kovid Goyal
a1c9c90fc8 Start work on moving shaders.py to C 2017-09-15 10:45:20 +05:30
Kovid Goyal
83f4b6e391 Remove unused code 2017-09-15 10:45:20 +05:30
Kovid Goyal
cc9ed734cf Get rid of prepare_for_render as it is not needed anymore 2017-09-15 10:45:20 +05:30
Kovid Goyal
98daddc39a Move selection render tracking into the Screen class 2017-09-15 10:45:20 +05:30
Kovid Goyal
408c719c5f Move selection tracking into the Screen class 2017-09-15 10:45:20 +05:30
Kovid Goyal
5caea75115 ... 2017-09-15 10:45:20 +05:30
Kovid Goyal
19bd068599 Raise an error on negative scroll amounts 2017-09-15 10:45:19 +05:30
Kovid Goyal
2490199e52 Move scroll tracking into the screen object 2017-09-15 10:45:19 +05:30
Kovid Goyal
726a6370ad ... 2017-09-15 10:45:19 +05:30
Kovid Goyal
00b8db26fc Add libunistring on travis 2017-09-15 10:45:19 +05:30
Kovid Goyal
544c402c9f Move word selection code into C, avoiding the need for the python regex module 2017-09-15 10:45:19 +05:30
Kovid Goyal
9bea1001f9 Speedup unicode character property lookup
Use libunistring instead of building predicates from the unicode
database
2017-09-15 10:45:19 +05:30
Kovid Goyal
63882e1fdc Make selection smarter
It now does not add trailing blank cells on selected lines to the
selection.
2017-09-15 10:45:19 +05:30
Kovid Goyal
5103381c27 DRYer 2017-09-15 10:45:19 +05:30
Kovid Goyal
56e55ddbc3 Fix incorrect handling of auto for bold and italic fonts on macOS 2017-09-15 10:45:19 +05:30
Kovid Goyal
a261e1f57d Fix erro on python built without threading 2017-09-15 10:45:19 +05:30
Kovid Goyal
5ceec08d32 Use buffer mapping for the tabbar selbuf 2017-09-15 10:45:18 +05:30
Kovid Goyal
92d53daf22 Dont spam resize_pty events, include a small timeout 2017-09-15 10:45:18 +05:30
Kovid Goyal
99acc4adcb Fix a couple of recursive timer adds 2017-09-15 10:45:18 +05:30
Kovid Goyal
827d086c54 Allow re-adding of timers during timers_call() 2017-09-15 10:45:18 +05:30
Kovid Goyal
fb820faf11 Prevent timers being added in a call 2017-09-15 10:45:18 +05:30
Kovid Goyal
0aecae288f Avoid extra function call when waking up for cursor blink rendering 2017-09-15 10:45:18 +05:30
Kovid Goyal
3274327fe7 Speedup as_unicode by avoiding an unnecessary malloc/free 2017-09-15 10:45:18 +05:30
Kovid Goyal
f97127f587 ... 2017-09-15 10:45:18 +05:30
Kovid Goyal
b8c34c3ee2 Move implementation of screen_line() into C 2017-09-15 10:45:18 +05:30
Kovid Goyal
4ae3abb3cb Fix selection being cleared when drag scrolling 2017-09-15 10:45:18 +05:30
Kovid Goyal
5c1747b2c8 Use buffer mapping for the selection buffer 2017-09-15 10:45:17 +05:30
Kovid Goyal
2611678855 ... 2017-09-15 10:45:17 +05:30
Kovid Goyal
746a20a11d Cleanup data-types.h 2017-09-15 10:45:17 +05:30
Kovid Goyal
3072eac222 DRYer sprites 2017-09-15 10:45:17 +05:30
Kovid Goyal
a4580f2126 DRYer glfw 2017-09-15 10:45:17 +05:30
Kovid Goyal
4a27793651 DRYer CoreText 2017-09-15 10:45:17 +05:30
Kovid Goyal
5ed4b29e4b DRYer fontconfig module 2017-09-15 10:45:17 +05:30
Kovid Goyal
b8678871df Remove some unnecessary declarations from data-types.h 2017-09-15 10:45:17 +05:30
Kovid Goyal
457bf8c289 Move gl binding code into its own module 2017-09-15 10:45:17 +05:30
Kovid Goyal
dd8be35922 ... 2017-09-15 10:45:17 +05:30
Kovid Goyal
b4d1cfea50 ... 2017-09-15 10:45:16 +05:30
Kovid Goyal
7113d5c293 Use a mapped buffer to send color table to the GPU 2017-09-15 10:45:16 +05:30
Kovid Goyal
bc97cfa024 Use a null to represent a blank rather than a space
This has performance benefits when clearing (can use a single
memset). Also allows detecting trailing whitespace on lines correctly.
2017-09-15 10:45:16 +05:30
Kovid Goyal
382daacb73 Remove the support for multiple top-level glfw windows 2017-09-15 10:45:16 +05:30
Kovid Goyal
aa526db772 Another place that was using python threading APIs 2017-09-15 10:45:16 +05:30
Kovid Goyal
1f9510ab9e Fix incorrect decoration colors 2017-09-15 10:45:16 +05:30
Kovid Goyal
b8acb9b133 Pass cell data to GPU directly
Converts update_cell_range() to a simple memcpy(). The GPU has to do
roughly the same amount of work (there is one extra attribute, and one
extra calculation for reverse video).
2017-09-15 10:45:16 +05:30
Kovid Goyal
51231b7eb2 Migrate tabs to also use the new buffer mapping API 2017-09-15 10:45:16 +05:30
Kovid Goyal
029ff84f09 Remove the change tracker related tests
The change tracker no longer exists
2017-09-15 10:45:16 +05:30
Kovid Goyal
f5dbe36cf3 Remove the now useless change tracking code 2017-09-15 10:45:16 +05:30
Kovid Goyal
906937149e Copy cell data to the GPU in a single pass 2017-09-15 10:45:15 +05:30
Kovid Goyal
f324d8ec4f Fix a couple of bugs in the buffer map API 2017-09-15 10:45:15 +05:30
Kovid Goyal
2089d2bfdf Fix main_loop() not returning NULL when an error has occurred 2017-09-15 10:45:15 +05:30
Kovid Goyal
281cb50780 API to map OpenGL buffers 2017-09-15 10:45:15 +05:30
Kovid Goyal
012b209a56 Update CELL data structure 2017-09-15 10:45:15 +05:30
Kovid Goyal
c0593d9f1f Get rid of update_screen
No longer needed.
2017-09-15 10:45:15 +05:30
Kovid Goyal
f655112bda Directly copy the sprite positions into the GPU buffer 2017-09-15 10:45:15 +05:30
Kovid Goyal
c4680aae2c Track sprite positions in the Cell structure 2017-09-15 10:45:15 +05:30
Kovid Goyal
a8d5ad2c1a ... 2017-09-15 10:45:15 +05:30
Kovid Goyal
333eb3534a ... 2017-09-15 10:45:14 +05:30
Kovid Goyal
1a42356f68 Forgot to close stdin_read_fd in the master process 2017-09-15 10:45:14 +05:30
Kovid Goyal
1197549e5b Remove the last use of the python threading module 2017-09-15 10:45:14 +05:30
Kovid Goyal
d2749f5ccc Sigh 2017-09-15 10:45:14 +05:30
Kovid Goyal
6379b3f898 Forgot to modify the fds array in remove children 2017-09-15 10:45:14 +05:30
Kovid Goyal
c97afd7b44 Handle unexpected closing of child fds gracefully 2017-09-15 10:45:14 +05:30
Kovid Goyal
9b9003ded0 Simplify code to set thread names 2017-09-15 10:45:14 +05:30
Kovid Goyal
5550c7a95f _GNU_SOURCE needed on apple as well? 2017-09-15 10:45:14 +05:30
Kovid Goyal
fe7f3233ba Fix initial tab bar visibility when starting a session with multiple tabs 2017-09-15 10:45:14 +05:30
Kovid Goyal
a52f0939df Make using the timer for wakeups more efficient 2017-09-15 10:45:14 +05:30
Kovid Goyal
ff3ddfd369 Simply use of repaint_delay in parse_input()
Dont maintain per child parse times. Instead just gate it globally.
This could result in slowdowns with lots of very active children, but
that seems unlikely.
2017-09-15 10:45:13 +05:30
Kovid Goyal
161161f840 Dont include stropts.h on macs 2017-09-15 10:45:13 +05:30
Kovid Goyal
b0e1c74fdd Note about interaction of cursor_blink_time and repaint_delay 2017-09-15 10:45:13 +05:30
Kovid Goyal
1bd46b0636 Avoid extra wakeup when rendering blinking cursor 2017-09-15 10:45:13 +05:30
Kovid Goyal
5e676090fa Report failures to write to child 2017-09-15 10:45:13 +05:30
Kovid Goyal
4d31624a69 Better handling of failure to execute child
exec() a shell instead so that we are not left with a forked but not
execed process
2017-09-15 10:45:13 +05:30
Kovid Goyal
c9695efd18 ... 2017-09-15 10:45:13 +05:30
Kovid Goyal
7dc51d7d9e Handle ioctl failures when resizing the tty more gracefully 2017-09-15 10:45:13 +05:30
Kovid Goyal
d5d812ccfe Fix for session with empty enabled_layouts 2017-09-15 10:45:13 +05:30
Kovid Goyal
af02b735e7 Move child process management into child-monitor.c 2017-09-15 10:45:13 +05:30
Kovid Goyal
52ab534e22 Move setting of IUTF8 into child-monitor.c 2017-09-15 10:45:12 +05:30
Kovid Goyal
895eedeb9b Apparently pthread_mutex_t is a scalar on macs 2017-09-15 10:45:12 +05:30
Kovid Goyal
5972a37550 Work on centralising lifecycle management in the child monitor 2017-09-15 10:45:12 +05:30
Kovid Goyal
079054f798 Fix tab bar rendering 2017-09-15 10:45:12 +05:30
Kovid Goyal
914f7ddbd5 Fix zero division 2017-09-15 10:45:12 +05:30
Kovid Goyal
5357876128 Ensure screen objects are de-allocated immediately on close instead of waiting for garbage collection 2017-09-15 10:45:12 +05:30
Kovid Goyal
b20367b888 Avoid dict lookups when calling python callbacks 2017-09-15 10:45:12 +05:30
Kovid Goyal
6698712b44 Respect repaint_delay when calling render() as well 2017-09-15 10:45:12 +05:30
Kovid Goyal
21b799905d Move the main loop into the C module 2017-09-15 10:45:12 +05:30
Kovid Goyal
02870d7221 Move the signal received handler into the C module 2017-09-15 10:45:12 +05:30
Kovid Goyal
d4991424f9 Set the python thread switch interval to forever 2017-09-15 10:45:11 +05:30
Kovid Goyal
1684bda1e9 Better thread name 2017-09-15 10:45:11 +05:30
Kovid Goyal
e8b5a72c96 Remove GIL management code since it is no longer needed 2017-09-15 10:45:11 +05:30
Kovid Goyal
7f180ad3d9 Do not use the python threading module
Threading in python imposes significant overhead.
Instead create the thread using pthreads directly
2017-09-15 10:45:11 +05:30
Kovid Goyal
94c4c00859 Move signal handling code into the C module 2017-09-15 10:45:11 +05:30
Kovid Goyal
1308bbac2d Move creation of wakeup fds to c module 2017-09-15 10:45:11 +05:30
Kovid Goyal
76890a2b71 Do not perform any python API calls in the I/O thread 2017-09-15 10:45:11 +05:30
Kovid Goyal
2c8f3b6d98 Remove unnecessary extra lock/unlock of children_mutex 2017-09-15 10:45:11 +05:30
Kovid Goyal
0e46994d0a Restore repaint_delay 2017-09-15 10:45:11 +05:30
Kovid Goyal
44650c5723 Make reading into the screen read buffer more efficient 2017-09-15 10:45:11 +05:30
Kovid Goyal
234bb07b67 Initial attempt at moving all threading code into a single file 2017-09-15 10:45:10 +05:30
Kovid Goyal
fea79878fa Make the monotonic() function available everywhere 2017-09-15 10:45:10 +05:30
Kovid Goyal
0365828a5b Remove all python threading related code 2017-09-15 10:45:10 +05:30
Kovid Goyal
fa30a95b46 ... 2017-09-15 10:45:10 +05:30
Kovid Goyal
f44d56b116 Ensure GIL is not released when rendering sprites 2017-09-15 10:45:10 +05:30
Kovid Goyal
6127d2d122 Make the sprite map globally accessible from C code as well as python code
The lock was removed as the Python GIL is sufficient to serialize access
to the SpriteMap structure.
2017-09-15 10:45:10 +05:30
Kovid Goyal
4feff2e2da Fix rendering of strikethrough not working 2017-09-15 10:45:10 +05:30
Kovid Goyal
8624dbac2c Sigh, stupid OpenGL
Apparently glGetUniformIndices has different declarations on different
systems.
2017-09-15 10:45:10 +05:30
Kovid Goyal
4c0cf93562 Fix tab bar not rendering because of selection handling changes 2017-09-15 10:45:10 +05:30
Kovid Goyal
d12573a67d Dont send the color table to the GPU on every frame 2017-09-15 10:45:09 +05:30
Kovid Goyal
d7b433f6b7 API for Uniform Block Objects 2017-09-15 10:45:09 +05:30
Kovid Goyal
b1168f6c6c Handle zero timeout correctly 2017-09-15 10:45:09 +05:30
Kovid Goyal
91e263cabb Fix sub-millisecond and no event waits not working 2017-09-15 10:45:09 +05:30
Kovid Goyal
ed15f2f97e Get the layout of the Cell structure 2017-09-15 10:45:09 +05:30
Kovid Goyal
6689595277 Dont use Py_UCS4 for char_type 2017-09-15 10:45:09 +05:30
Kovid Goyal
34f946fd98 Move rendering of selection to GPU 2017-09-15 10:45:09 +05:30
Kovid Goyal
a5bec84554 ... 2017-09-15 10:45:09 +05:30
Kovid Goyal
0fae3e1b10 ... 2017-09-15 10:45:09 +05:30
Kovid Goyal
a56e1296fd Refactor the tab bar into its own class
Also ensure that Screen APIs are only used in the child thread in the
tab bar
2017-09-15 10:45:09 +05:30
Kovid Goyal
acdeaaeda5 Move the border shaders into their own files 2017-09-15 10:45:08 +05:30
Kovid Goyal
a44e3a3a50 Ensure line numbers reported by the GLSL compiler are correct 2017-09-15 10:45:08 +05:30
Kovid Goyal
db40445572 Do color mapping on the GPU 2017-09-15 10:45:08 +05:30
Kovid Goyal
1d888c9194 Centralise color management into ColorProfile 2017-09-15 10:45:08 +05:30
Kovid Goyal
b50eaa9cbb ColorProfile should belong to Screen 2017-09-15 10:45:08 +05:30
Kovid Goyal
fe764ab6b6 oops 2017-09-15 10:45:08 +05:30
Kovid Goyal
bab4680828 ... 2017-09-15 10:45:08 +05:30
Kovid Goyal
564010c7cd Migrate history.c 2017-09-15 10:45:08 +05:30
Kovid Goyal
052fd46ff9 Migrate sprites.c 2017-09-15 10:45:08 +05:30
Kovid Goyal
4fd8c7cfaa DRYer 2017-09-15 10:45:08 +05:30
Kovid Goyal
37f0cf86d0 Migrated line.c 2017-09-15 10:45:07 +05:30
Kovid Goyal
0cbd3a2820 Start work on using a struct for the cell
Should eventually allow the screen buf to be sent directly to the GPU
without a serialization step.

Migrates line-buf.c
2017-09-15 10:45:07 +05:30
Kovid Goyal
af3c89d79f oops 2017-09-15 10:45:07 +05:30
Kovid Goyal
a1d75eb00d Use a function pointer to avoid a branch in loop() 2017-09-15 10:45:07 +05:30
Kovid Goyal
6176607ac4 Initial stab at switching to poll() instead of select()
Also avoid python code in the child monitoring inner loop
2017-09-15 10:45:07 +05:30
Kovid Goyal
40254625d9 Fix timers not being replaced 2017-09-15 10:45:07 +05:30
Kovid Goyal
29b16d0856 Fix timers not being re-sorted on replace 2017-09-15 10:45:07 +05:30
Kovid Goyal
ff72b1f96f Limit sprite co-ordinates to fit in uint16_t 2017-09-15 10:45:07 +05:30
Kovid Goyal
a429bcbb22 Dont use the glfw timer function as it requires glfwInit 2017-09-15 10:45:07 +05:30
Kovid Goyal
3c7305b93c ... 2017-09-15 10:45:07 +05:30
Kovid Goyal
f9b1a71edd Move the timers implementation to C
It is used int he hot loop for monitoring the child process
2017-09-15 10:45:06 +05:30
Kovid Goyal
2de0c82f18 Fix incorrect full keyboard mode escape codes 2017-09-15 10:44:56 +05:30
Kovid Goyal
cdb12f2049 Fix #122 2017-09-13 14:20:20 +05:30
Kovid Goyal
ca310fc2f2 Add a note about border visibility 2017-09-12 06:52:33 +05:30
Kovid Goyal
273210cb97 Ignore resize requests for very small sizes. Fixes #119 2017-08-28 20:31:01 +05:30
Kovid Goyal
b957ef441c Avoid function name conflict with index from string.h
Hopefully Fixes #118
2017-08-27 21:17:18 +05:30
Kovid Goyal
31775c8b50 ... 2017-08-26 15:19:33 +05:30
Kovid Goyal
8199802b44 Remove unused code 2017-08-26 15:10:55 +05:30
Kovid Goyal
941c553172 Implement an option to adjust the line height
Fixes #116
2017-08-26 14:14:51 +05:30
Kovid Goyal
8ee1c851a1 Optimize the bit twiddling in sprite_position_for 2017-08-26 13:05:18 +05:30
Kovid Goyal
4ce9f550ac Inline to_color for an easy performance boost 2017-08-26 10:52:24 +05:30
Kovid Goyal
7041616837 More semantic handling of profile arg when compiling 2017-08-26 10:36:04 +05:30
Kovid Goyal
6a3c812470 Forgot to remove python profiling from one place 2017-08-26 10:35:11 +05:30
Kovid Goyal
778b090467 ... 2017-08-26 10:25:50 +05:30
Kovid Goyal
a922b4c789 Add instrumentation to analyse kitty performance 2017-08-26 10:24:12 +05:30
Kovid Goyal
e43bf891e4 Print a warning when the specified font family does not exist 2017-08-25 09:55:46 +05:30
Kovid Goyal
fa8203ff31 CoreText: Use a scoring function to find the best matching font
Also if the specified family does not exist, fallback to Menlo
2017-08-25 09:53:11 +05:30
Kovid Goyal
212207d667 Test function for family matching 2017-08-24 22:27:28 +05:30
Kovid Goyal
95a3c51a4b Remove unused code 2017-08-24 22:22:55 +05:30
Kovid Goyal
697a7b78b3 macOS: Fix selecting fonts using full names (including sub-family) not working
Fixes #83
2017-08-24 19:16:12 +05:30
Kovid Goyal
58644e2b37 Function to get all fonts on macOS 2017-08-24 15:05:42 +05:30
Kovid Goyal
ddcd4daede macOS: Fix wide characters having their right edges slightly truncated 2017-08-24 12:46:41 +05:30
Kovid Goyal
678bd97e1f Merge branch 'patch-1' of https://github.com/leonardt/kitty 2017-08-24 08:01:52 +05:30
Leonard (Lenny) Truong
2eb4bf7a1d Typo fix in default kitty.conf 2017-08-23 14:28:51 -07:00
Kovid Goyal
e0e517d6ec Modify VAO API to allow using multiple buffers 2017-08-23 17:32:46 +05:30
Kovid Goyal
b6679ff7b5 version 0.3.0 2017-08-23 07:25:37 +05:30
Kovid Goyal
de80003103 macOS: Show the current window title in the global menu bar 2017-08-22 22:46:46 +05:30
Kovid Goyal
2b0596c7f9 Add a basic global menubar on macOS
Fixes #111
2017-08-22 20:59:56 +05:30
Kovid Goyal
3d15a1d786 Fix textures not being rendered in a render call that also uploads textures
The active texture should not be unbound in the upload texture functions
2017-08-22 18:34:25 +05:30
Kovid Goyal
1fcb865202 ... 2017-08-22 18:19:46 +05:30
Kovid Goyal
1c56de5605 Change default usage mode to GL_STREAM_DRAW 2017-08-22 18:10:28 +05:30
Kovid Goyal
adbce71fb4 Remove unused code 2017-08-22 18:04:40 +05:30
Kovid Goyal
00e3ea8c08 Remove unused code 2017-08-22 17:32:40 +05:30
Kovid Goyal
36432c8616 API to conveniently download data from a vertex buffer 2017-08-22 12:51:30 +05:30
Kovid Goyal
03e0b9de04 Use vertex buffers instead of texture buffers
Apparently, integer samplers for textFetch() are broken on some macOS
Intel drivers. Fixes #101
2017-08-22 12:08:35 +05:30
Kovid Goyal
4364163ceb Use a single buffer for multiple vertex arrays 2017-08-22 07:59:51 +05:30
Kovid Goyal
ffd9ec653d Allow programs to be used with multiple vertex array objects 2017-08-22 00:24:45 +05:30
Kovid Goyal
ee3c3e4cb4 Binding for DeleteVertexArray 2017-08-22 00:18:03 +05:30
Kovid Goyal
277c46908d Remove unused code 2017-08-21 23:55:39 +05:30
Kovid Goyal
8e27c80e1f A spot of refactoring 2017-08-21 23:26:30 +05:30
Kovid Goyal
afa767c3a8 Support vertex attrib divisors 2017-08-21 23:24:05 +05:30
Kovid Goyal
67ec04fba3 Also use the buffer manager for vertex arrays 2017-08-21 23:10:25 +05:30
Kovid Goyal
f35bf7f1ba Move buffer management into its own class 2017-08-21 22:53:13 +05:30
Kovid Goyal
4fdb55e260 Ensure window with hidden chrome is resizable on macOS 2017-08-21 21:06:34 +05:30
Kovid Goyal
be06669e8f Ensure we dont exceed GL_MAX_TEXTURE_BUFFER_SIZE 2017-08-21 20:53:02 +05:30
Kovid Goyal
313253cd95 Add glFlush() and glFinish() bindings 2017-08-21 19:55:28 +05:30
Kovid Goyal
8c16be2ccd Remove unused code 2017-08-21 19:38:52 +05:30
Kovid Goyal
e1b276786b Fix #112 2017-08-21 19:10:06 +05:30
Kovid Goyal
05f5a05c20 Ensure sprite map size tracking is always correct 2017-08-21 17:39:30 +05:30
Kovid Goyal
7e79aac275 Fix incorrect tracking of previous buffer sizes to avoid re-allocs 2017-08-21 17:12:46 +05:30
Kovid Goyal
0a21819e16 DRYer 2017-08-21 16:53:04 +05:30
Kovid Goyal
fe3e51a00d Move the GLSL shaders into their own files 2017-08-21 16:39:34 +05:30
Kovid Goyal
21874339f1 Remove un-needed binding of the sprite_map buffer 2017-08-21 15:51:55 +05:30
Kovid Goyal
4f22fcdaac Code to verify buffer uploads are working 2017-08-21 15:30:50 +05:30
Kovid Goyal
0424d665d5 Function to read data from OpenGL buffers 2017-08-21 15:29:05 +05:30
Kovid Goyal
0bbea6812e Avoid unnecessary reallocs for cell data
Only re-allocate when the buffer size changes
2017-08-21 14:56:29 +05:30
Kovid Goyal
101a50b0ff Do not use GL_ARB_texture_buffer_object_rgb32
This is not available on macOS using Intel graphics cards
2017-08-21 12:46:09 +05:30
Kovid Goyal
a9f3a698d2 Also check for required OpenGL extensions on OS X 2017-08-21 11:21:57 +05:30
Kovid Goyal
5117a2c17a version 0.2.8 2017-08-05 08:21:20 +05:30
Kovid Goyal
ebce1d7c61 Fix incorrect handling of escape codes to change default foreground and background colors. Fixes #104 2017-08-04 21:57:22 +05:30
Kovid Goyal
e4668c1aff Replay set_dynamic_color and set_color_table_color 2017-08-04 20:47:59 +05:30
Kovid Goyal
2519c49c02 Fix incorrect escape codes generated when using the obsolete "normal" mouse protocol.
Fixes #105
2017-08-04 20:09:18 +05:30
Kovid Goyal
9e512ff58a Ensure queue_action is only called after the Tab object is fully initialized
Fix #103
2017-08-04 07:52:25 +05:30
Kovid Goyal
ed50595aca Add a --detach option
Allows kitty to detach itself from the controlling terminal. Useful
when launching kitty from a GUI environment with broken stdout/stderr or
when launching from a terminal that you want to close later without
affecting the launched kitty instance.
2017-08-03 22:35:47 +05:30
Kovid Goyal
9d62d087e0 Redirect xsel std streams to /dev/null
Prevents hanging if the streams are blocked. Fixes #102
2017-08-03 21:55:09 +05:30
Kovid Goyal
6b933e33f5 If the saved initial window size fails, retry creating the window with a standard size. Fixes #98 2017-07-30 08:32:51 +05:30
Kovid Goyal
a86931f401 Use the new glfw API to set WM_CLASS if available 2017-07-27 18:11:23 +05:30
Kovid Goyal
fc4e1595e8 Print a warning about unknown config keys 2017-07-23 22:59:30 +05:30
Kovid Goyal
b36c3f3425 Clean up config merging 2017-07-23 22:45:57 +05:30
Kovid Goyal
e427fd1233 Fix #95 2017-07-23 21:36:43 +05:30
Kovid Goyal
dd3af45043 Implement a send_text action to allow using keyboard shortcuts to send arbitrary text
Fixes #94
2017-07-23 14:37:15 +05:30
Kovid Goyal
304d42d4c2 Fix #93
Ignore invalid language names returned by the coca framework on macOS
2017-07-16 22:49:05 +05:30
Kovid Goyal
abca9280e7 Fix #92 2017-07-15 08:22:40 +05:30
Kovid Goyal
9a103f3979 version 0.2.7 2017-07-14 10:46:35 +05:30
Kovid Goyal
270dde7020 Use erase in line instead as it is more efficient 2017-07-13 19:32:18 +05:30
Kovid Goyal
11de18e737 Fix #91 2017-07-13 19:24:26 +05:30
Kovid Goyal
62db44c71e macOS: Ensure the LANG environment variable is set
Fixes #90
2017-06-25 20:44:16 +05:30
Kovid Goyal
3d5c65eaea Fix #89 2017-06-21 10:19:45 +05:30
Kovid Goyal
ea298f95f2 Another place we can use exist_ok 2017-06-09 00:00:02 +05:30
Kovid Goyal
2f21e0e341 Merge branch 'patch-2' of https://github.com/mimi1vx/kitty 2017-06-08 23:59:14 +05:30
Ondřej Súkup
ae62d36a4a Remove unneeded try except block
os.makedirs has from python-3.2 option `exist_ok`
2017-06-08 20:25:34 +02:00
Kovid Goyal
76e3101d9b Hide all symbols 2017-06-07 11:19:53 +05:30
Kovid Goyal
c3442545a8 Add a note about changing mouse wheel scroll direction 2017-06-06 00:06:43 +05:30
Kovid Goyal
3a9b0faa06 Be more positive ;) 2017-06-06 00:03:43 +05:30
Kovid Goyal
4989b1f8bb ... 2017-06-05 23:58:36 +05:30
Kovid Goyal
348fe4ada4 Option for window padding
Fixes #85
2017-06-05 23:57:17 +05:30
Kovid Goyal
bbc6b2d86a Option to size window margin (blank area outside window borders)
Defaults to zero
2017-06-05 22:27:47 +05:30
Kovid Goyal
b4d4ed718f version 0.2.6 2017-06-03 09:58:38 +05:30
Kovid Goyal
836724709e Add an option to hide the window title bar on macOS
Fixes #84
2017-06-03 09:57:29 +05:30
Kovid Goyal
96d2567815 Fix compilation with gcc >= 7
Requires explicit fallthrough comments in switch statements
2017-06-03 08:59:09 +05:30
Kovid Goyal
419f43ceed Fix deprecation warning on macOS 10.12 2017-06-03 08:58:11 +05:30
Kovid Goyal
47851ebb1b Fix conversion of type 2 terminal colors not working because of missing break 2017-06-03 08:51:00 +05:30
Kovid Goyal
24a4fbd987 Add a function to hide the title bar on OS X 2017-06-03 08:45:27 +05:30
Kovid Goyal
8047743882 0.2.5 2017-05-26 14:02:14 +05:30
Kovid Goyal
e9b5963610 Fix incorrect replay of screen cursor down 2017-05-26 13:21:16 +05:30
Kovid Goyal
a1d4630a25 Fix incorrect implementation of the Vertical Position Adjust (VPA) CSI code
Fixes #80
2017-05-26 13:11:27 +05:30
Kovid Goyal
fafd710ce3 Clean exit for client on EOF/interrupt 2017-05-26 12:36:36 +05:30
Kovid Goyal
a79bb3add2 Correct the DEBUG define 2017-05-24 07:47:11 +05:30
Kovid Goyal
b3a718b1e4 ... 2017-05-23 20:28:50 +05:30
Kovid Goyal
952aa7ad4a A tribute to Thomas E. Dickey 2017-05-23 20:27:51 +05:30
Kovid Goyal
149d606154 Add note about impossibility of using multiple modifier keys 2017-05-23 08:25:41 +05:30
Kovid Goyal
ad21c7ed0f Mimic behavior of xterm when pressing Ctrl+<key>
Where <key> is one of ,.;'-=

These dont map to control codes, xterm and libvte just ignore the Ctrl
and echo the key as if control was not pressed. Mimic that behavior
2017-05-22 21:09:20 +05:30
Kovid Goyal
38d2839206 Fix Ctrl+6 and Ctrl+/ not working
Fixes #79
2017-05-22 21:01:14 +05:30
Kovid Goyal
24d0bb8bd5 Allow IME to generate unicode characters using Alt+key which is used on OSX, I think 2017-05-20 12:01:50 +05:30
Kovid Goyal
08f336769f Add tests for key mapping
Also fix Alt+Special keys no generating correct codes
2017-05-20 11:41:21 +05:30
Kovid Goyal
5525d4db49 Fix alt+key resulting in the upper case version of key even when shift is not pressed
Fixes #78
2017-05-20 08:47:02 +05:30
Kovid Goyal
448ba26257 DRYer 2017-05-20 00:58:40 +05:30
Kovid Goyal
1d1138ca31 Be a little more stringent with Xft parsing 2017-05-20 00:26:26 +05:30
Kovid Goyal
357a415386 ... 2017-05-20 00:23:43 +05:30
Kovid Goyal
a65856ec98 Use Xlib to directly query the xrm system.Only if it fails do we try to shell out to xrd 2017-05-20 00:21:09 +05:30
Kovid Goyal
83855e16ce On linux query xrdb for Xft.dpi and use that, if set as the logical DPI.
Fall back to the actual physical dpi as returned by GLFW if that fails.
2017-05-19 23:26:09 +05:30
Kovid Goyal
ccf66fc621 A method on the window object to get the current monitor for that window 2017-05-19 20:37:54 +05:30
Kovid Goyal
c27b597951 Fix incorrect implementation of the CSI scroll commands
I was lazy and just assumed they were n indexes, but they actually
scroll the screen without moving the cursor. Fixes #76
2017-05-19 19:25:41 +05:30
Kovid Goyal
85dbae1de4 Ensure that dump_bytes truncates the file it is dumping to 2017-05-19 18:09:37 +05:30
Kovid Goyal
cd1ba334c1 Forgot to change test 2017-05-19 15:54:34 +05:30
Kovid Goyal
1cff4f9d29 ... 2017-05-19 15:45:07 +05:30
Kovid Goyal
d180601711 More command replaying 2017-05-19 15:43:15 +05:30
Kovid Goyal
01d0e7474f Change reported version to >= 4000 so that vim autodetects SGR mouse support correctly 2017-05-19 14:23:11 +05:30
84 changed files with 43156 additions and 4783 deletions

9
.gitignore vendored
View File

@@ -1,10 +1,11 @@
*.js
*.pyj-cached
*.so
.build-cache
*.pyc
*.pyo
tags
build
README.html
linux-package
logo/*.iconset
test-launcher
kitty-profile
dev
__pycache__

View File

@@ -18,6 +18,9 @@ matrix:
- libxrandr-dev
- libxinerama-dev
- libxcursor-dev
- libunistring-dev
- libpng-dev
- python3-pil
- os: linux
dist: trusty
@@ -36,6 +39,9 @@ matrix:
- libxrandr-dev
- libxinerama-dev
- libxcursor-dev
- libunistring-dev
- libpng-dev
- python3-pil
- os: linux
dist: trusty
@@ -54,6 +60,9 @@ matrix:
- libxrandr-dev
- libxinerama-dev
- libxcursor-dev
- libunistring-dev
- libpng-dev
- python3-pil
- clang
- os: osx
language: generic
@@ -78,6 +87,7 @@ install: |
brew update;
brew install python3;
brew install glfw;
brew install libunistring;
else
mkdir -p $SW;
curl https://download.calibre-ebook.com/travis/kitty/osx.tar.xz | tar xJ -C $SW;

41
CHANGELOG.rst Normal file
View File

@@ -0,0 +1,41 @@
Changelog
==============
kitty is a feature full, cross-platform, *fast*, OpenGL based terminal emulator.
version 0.4.0 [2017-10-22]
---------------------------
- Support for drawing arbitrary raster graphics (images) in the terminal via a
new graphics protocol. kitty can draw images with full 32-bit color using both
ssh connections and files/shared memory (when available) for better
performance. The drawing primitives support alpha blending and z-index.
Images can be drawn both above and below text. See
https://github.com/kovidgoyal/kitty/blob/master/graphics-protocol.asciidoc
for details.
- Refactor kitty's internals to make it even faster and more efficient. The CPU
usage of kitty + X server while doing intensive tasks such as scrolling a
file continuously in less has been reduced by 50%. There are now two
configuration options ``repaint_delay`` and ``input_delay`` you can use to
fine tune kitty's performance vs CPU usage profile. The CPU usage of kitty +
X when scrolling in less is now significantly better than most (all?) other
terminals. See https://github.com/kovidgoyal/kitty#performance
- Hovering over URLs with the mouse now underlines them to indicate they
can be clicked. Hold down Ctrl+Shift while clicking to open the URL.
- Selection using the mouse is now more intelligent. It does not add
blank cells (i.e. cells that have no content) after the end of text in a
line to the selection.
- The block cursor in now fully opaque but renders the character under it in
the background color, for enhanced visibility.
- Allow combining multiple independent actions into a single shortcut
- Add a new shortcut to pass the current selection to an external program
- Allow creating shortcuts to open new windows running arbitrary commands. You
can also pass the current selection to the command as an arguments and the
contents of the screen + scrollback buffer as stdin to the command.

View File

@@ -3,3 +3,6 @@ all:
test:
python3 setup.py test
clean:
python3 setup.py clean

View File

@@ -48,23 +48,25 @@ image::https://travis-ci.org/kovidgoyal/kitty.svg?branch=master[Build status, li
* Supports tiling multiple terminal windows side by side in different
layouts without needing to use an extra program like tmux
* Supports all modern terminal features: unicode, true-color, mouse
protocol, focus tracking, bracketed paste and so on.
* Supports all modern terminal features: graphics (images), unicode,
true-color, mouse protocol, focus tracking, bracketed paste and so on.
* Allows you to view the scrollback buffer in a separate window
using your favorite pager program such as less
* Support startup sessions which allow you to specify the window/tab layout,
* Supports startup sessions which allow you to specify the window/tab layout,
working directories and programs to run on startup.
* Uses OpenGL for rendering, offloads rendering to the GPU for lower system
load and buttery smooth scrolling. Uses threaded rendering to minimize input
latency.
* Cross-platform support: kitty currently works on Linux and macOS, but because
it uses only OpenGL for rendering, it should be trivial to port to other
platforms.
* Allows you to view the scrollback buffer in a separate window using your
favorite pager program such as less
* Easily hackable (UI layer written in python, inner loops in C for
speed). Less than ten thousand lines of code.
* Uses OpenGL for rendering, offloads rendering work to the GPU for lower system load.
* Cross-platform support: kitty currently works on Linux and macOS, but because it
uses only OpenGL for rendering, it should be trivial to port to
other platforms.
speed). Less than fifteen thousand lines of code.
image::screenshot.png?raw=true[Screenshot, showing three programs in the "Tall" layout]
@@ -80,9 +82,14 @@ the following dependencies are installed first.
* python >= 3.5
* glfw >= 3.2
* libunistring
* zlib
* libpng
* glew >= 2.0 (not needed on macOS)
* fontconfig (not needed on macOS)
* xdpyinfo and xsel (only on X11 based systems)
* xrdb (only on X11 based systems)
* xsel (only on X11 systems with glfw < 3.3)
* ImageMagick (optional, needed to use the `kitty icat` tool to display images in the terminal)
* gcc or clang (required only for building)
* pkg-config (required only for building)
@@ -123,7 +130,7 @@ or a similar package manager)
kitty is designed for power keyboard users. To that end all its controls
work with the keyboard (although it fully supports mouse interactions as
well). Its configuration is a simple, human editable, single file for
easy reproducability (I like to store config files in source control).
easy reproducibility (I like to store config files in source control).
The code in kitty is designed to be simple, modular and hackable. It is
written in a mix of C (for performance sensitive parts) and Python (for
@@ -292,6 +299,48 @@ come in handy for applications like powerline, without the need to use patched
fonts. See the various font related configuration directives in the
link:kitty/kitty.conf[config file].
== Performance
The main goals for kitty performance are user perceived latency while typing
and "smoothness" while scrolling as well as CPU usage. kitty tries hard to find
an optimum balance for these. To that end it keeps a cache of each rendered
glyph in video RAM so that font rendering is not a bottleneck. Interaction
with child programs takes place in a separate thread from rendering, to improve
smoothness.
There are two parameters you can tune to adjust the performance. ``repaint_delay``
and ``input_delay``. These control the artificial delays introduced into the
render loop to reduce CPU usage. See the link:kitty/kitty.conf[config file] for details.
You can generate detailed per-function performance data using
link:https://github.com/gperftools/gperftools[gperftools]. Build kitty with the
`--profile` flag which will create an executable called `kitty-profile`. Run
that and perform the task you want to analyse, for example, scrolling a large
file with `less`. After you quit, function call statistics will be printed to
`stdout` and you can use tools like *kcachegrind* for more detailed analysis.
Here are some CPU usage numbers for the task of scrolling a file continuously in less.
The CPU usage is for the terminal process and X together and is measured using htop.
The measurements are taken at the same font and window size for all terminals on a
`Intel(R) Core(TM) i7-4820K CPU @ 3.70GHz` CPU with a
`Advanced Micro Devices, Inc. [AMD/ATI] Cape Verde XT [Radeon HD 7770/8760 / R7 250X]` GPU.
|===
| Terminal | CPU usage (X + terminal)
| kitty | 6 - 8%
| xterm | 5 - 7% (but scrolling was extremely janky)
| termite | 10 - 13%
| urxvt | 12 - 14%
| gnome-terminal | 15 - 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).
== Note for Linux/macOS packagers
While kitty does use python, it is not a traditional python package, so please do not install it in site-packages.
@@ -317,6 +366,25 @@ without needing to install all of kitty.
This applies to creating packages for kitty for macOS package managers such as
brew or MacPorts as well.
== A tribute
While over the decades I am sure many people have contributed to the
development of the terminal emulator space, there is one individual in
particular I would like to thank. link:http://invisible-island.net[Thomas E.
Dickey], the creator of xterm. xterm is the most comprehensive and
feature-rich terminal emulator I have had the pleasure of using. As I worked on
kitty, I ran headlong into more and more gnarly corners of terminal behavior.
On all those occasions, either the excellent documentation at
link:http://invisible-island.net/xterm/ctlseqs/ctlseqs.html[xterm-ctlseqs] or
investigating the behavior and code of xterm or vttest were invaluable tools to
aid my understanding.
My achievements, if any, in developing kitty would not have been possible without
his prior work and the generous sharing of knowledge accumulated over decades.
Thank you.
== Resources on terminal behavior
http://invisible-island.net/xterm/ctlseqs/ctlseqs.html

View File

@@ -2,6 +2,12 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
import sys
sys.debug_gl = '--debug-kitty-gl' in sys.argv
from kitty.main import main
main()
if len(sys.argv) > 1 and sys.argv[1] == 'icat':
from kitty.icat import main
main(sys.argv[1:])
else:
from kitty.main import main
main()

View File

@@ -1,2 +1,2 @@
#!/bin/bash
cloc --exclude-list-file <(echo -e 'kitty/wcwidth9.h\nkitty/unicode-data.h\nkitty/gl.h\nkitty/glfw.c\nkitty/glfw.h\nkitty/charsets.c\nkitty/key_encoding.py\nkitty/rgb.py') kitty
cloc --exclude-list-file <(echo -e 'kitty/wcwidth9.h\nkitty/glfw.c\nkitty/keys.h\nkitty/charsets.c\nkitty/key_encoding.py\nkitty/rgb.py\nkitty/gl.h') kitty

View File

@@ -1,57 +0,0 @@
#!/usr/bin/env python3
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import unicodedata
import itertools
import sys
IGNORED_CATEGORIES = ('Cc', 'Cf', 'Cs')
def ranges(i):
for a, b in itertools.groupby(enumerate(i), lambda r: r[1] - r[0]):
b = list(b)
yield b[0][1], b[-1][1]
def generate_data(chars):
points, cranges = [], []
for l, r in ranges(chars):
if l == r:
points.append(l)
else:
cranges.append((l, r))
return points, cranges
def generate_predicate(name, chars):
points, cranges = generate_data(chars)
cranges = ['(0x%x %s ch && ch <= 0x%x)' % (l, '<' if l == 0 else '<=', r) for l, r in cranges]
points = ['(ch == 0x%x)' % p for p in points]
return '''
static inline bool %s(uint32_t ch) {
return %s || %s;
}
''' % (name, '||'.join(cranges), '||'.join(points))
def main():
combining_chars = []
igchars = []
for c in map(chr, range(sys.maxunicode + 1)):
if unicodedata.category(c) in IGNORED_CATEGORIES:
igchars.append(ord(c))
if unicodedata.combining(c):
combining_chars.append(ord(c))
cc = generate_predicate('is_combining_char', combining_chars)
ig = generate_predicate('is_ignored_char', igchars)
with open('kitty/unicode-data.h', 'w') as f:
print('#pragma once', file=f)
print(cc, file=f)
print(ig, file=f)
if __name__ == '__main__':
main()

371
graphics-protocol.asciidoc Normal file
View File

@@ -0,0 +1,371 @@
= The terminal graphics protocol
:toc:
:toc-placement!:
The goal of this specification is to create a flexible and performant protocol
that allows the program running in the terminal, hereafter called the _client_,
to render arbitrary pixel (raster) graphics to the screen of the terminal
emulator. The major design goals are
* Should not require terminal emulators to understand image formats.
* Should allow specifying graphics to be drawn at individual pixel positions.
* The graphics should integrate with the text, in particular it should be possible to draw graphics
below as well as above the text, with alpha blending. The graphics should also scroll with the text, automatically.
* Should use optimizations when the client is running on the same computer as the terminal emulator.
For some discussion regarding the design choices, see link:../../issues/33[#33].
To see a quick demo, inside a kitty terminal run:
```
kitty icat path/to/some/image.png
```
You can also see a screenshot with more sophisticated features such as alpha-blending and text over graphics
link:https://github.com/kovidgoyal/kitty/issues/33#issuecomment-334436100[here].
toc::[]
== Getting the window size
In order to know what size of images to display and how to position them, the client must be able to get the
window size in pixels and the number of cells per row and column. This can be done by using the `TIOCGWINSZ` ioctl.
Some C code to demonstrate its use
```C
struct ttysize ts;
ioctl(0, TIOCGWINSZ, &ts);
printf("number of columns: %i, number of rows: %i, screen width: %i, screen height: %i\n", sz.ws_col, sz.ws_row, sz.ws_xpixel, sz.ws_ypixel);
```
Note that some terminals return `0` for the width and height values. Such terminals should be modified to return the correct values.
Examples of terminals that return correct values: `kitty, xterm`
== The graphics escape code
All graphics escape codes are of the form:
```
<ESC>_G<control data>;<payload><ESC>\
```
This is a so-called _Application Programming Command (APC)_. Most terminal
emulators ignore APC codes, making it safe to use.
The control data is a comma-separated list of `key=value` pairs. The payload
is arbitrary binary data, base64-encoded to prevent interoperation problems
with legacy terminals that get confused by control codes within an APC code.
The meaning of the payload is interpreted based on the control data.
The first step is to transmit the actual image data.
== Transferring pixel data
The first consideration when transferring data between the client and the
terminal emulator is the format in which to do so. Since there is a vast and
growing number of image formats in existence, it does not make sense to have
every terminal emulator implement support for them. Instead, the client should
send simple pixel data to the terminal emulator. The obvious downside to this
is performance, especially when the client is running on a remote machine.
Techniques for remedying this limitation are discussed later. The terminal
emulator must understand pixel data in three formats, 24-bit RGB, 32-bit RGBA and
PNG. This is specified using the `f` key in the control data. `f=32` (which is the
default) indicates 32-bit RGBA data and `f=24` indicates 24-bit RGB data and `f=100`
indicates PNG data. The PNG format is supported for convenience and a compact way
of transmitting paletted images.
=== RGB and RGBA data
In these formats the pixel data is stored directly as 3 or 4 bytes per pixel, respectively.
When specifying images in this format, the image dimensions **must** be sent in the control data.
For example:
```
<ESC>_Gf=24,s=10,v=20;<payload><ESC>\
```
Here the width and height are specified using the `s` and `v` keys respectively. Since
`f=24` there are three bytes per pixel and therefore the pixel data must be `3 * 10 * 20 = 600`
bytes.
=== PNG data
In this format any PNG image can be transmitted directly. For example:
```
<ESC>_Gf=100;<payload><ESC>\
```
The PNG format is specified using the `f=100` key. The width and height of
the image will be read from the PNG data itself. Note that if you use both PNG and
compression, then you must provide the `S` key with the size of the PNG data.
=== Compression
The client can send compressed image data to the terminal emulator, by specifying the
`o` key. Currently, only zlib based deflate compression is supported, which is specified using
`o=z`. For example,
```
<ESC>_Gf=24,s=10,v=20,o=z;<payload><ESC>\
```
This is the same as the example from the RGB data section, except that the
payload is now compressed using deflate. The terminal emulator will decompress
it before rendering. You can specify compression for any format. The terminal
emulator will decompress before interpreting the pixel data.
=== The transmission medium
The transmission medium is specified using the `t` key. The `t` key defaults to `d`
and can take the values:
|===
| Value of `t` | Meaning
| d | Direct (the data is transmitted within the escape code itself)
| f | A simple file
| t | A temporary file, the terminal emulator will delete the file after reading the pixel data
| s | A http://man7.org/linux/man-pages/man7/shm_overview.7.html[POSIX shared memory object]. The terminal emulator will delete it after reading the pixel data
|===
==== Local client
First let us consider the local client techniques (files and shared memory). Some examples:
```
<ESC>_Gf=100,t=f;<encoded /path/to/file.png><ESC>\
```
Here we tell the terminal emulator to read PNG data from the specified file of
the specified size.
```
<ESC>_Gs=10,v=2,t=s,o=z;<encoded /some-shared-memory-name><ESC>\
```
Here we tell the terminal emulator to read compressed image data from
the specified shared memory object.
The client can also specify a size and offset to tell the terminal emulator
to only read a part of the specified file. The is done using the `S` and `O`
keys respectively. For example:
```
<ESC>_Gs=10,v=2,t=s,S=80,O=10;<encoded /some-shared-memory-name><ESC>\
```
This tells the terminal emulator to read `80` bytes starting from the offset `10`
inside the specified shared memory buffer.
==== Remote client
Remote clients, those that are unable to use the filesystem/shared memory to
transmit data, must send the pixel data directly using escape codes. Since
escape codes are of limited maximum length, the data will need to be chunked up
for transfer. This is done using the `m` key. The pixel data must first be
base64 encoded then chunked up into chunks no larger than `4096` bytes. The client
then sends the graphics escape code as usual, with the addition of an `m` key that
must have the value `1` for all but the last chunk, where it must be `0`. For example,
if the data is split into three chunks, the client would send the following
sequence of escape codes to the terminal emulator:
```
<ESC>_Gs=100,v=30,m=1;<encoded pixel data first chunk><ESC>\
<ESC>_Gm=1;<encoded pixel data second chunk><ESC>\
<ESC>_Gm=0;<encoded pixel data last chunk><ESC>\
```
Note that only the first escape code needs to have the full set of control
codes such as width, height, format etc. Subsequent chunks must have
only the `m` key. The client **must** finish sending all chunks for a single image
before sending any other graphics related escape codes.
=== Detecting available transmission mediums
Since a client has no a-priori knowledge of whether it shares a filesystem/shared memory
with the terminal emulator, it can send an id with the control data, using the `i` key
(which can be an arbitrary positive integer up to 4294967295, it must not be zero).
If it does so, the terminal emulator will reply after trying to load the image, saying
whether loading was successful or not. For example:
```
<ESC>_Gi=31,s=10,v=2,t=s;<encoded /some-shared-memory-name><ESC>\
```
to which the terminal emulator will reply (after trying to load the data):
```
<ESC>_Gi=31;error message or OK<ESC>\
```
Here the `i` value will be the same as was sent by the client in the original
request. The message data will be a ASCII encoded string containing only
printable characters and spaces. The string will be `OK` if reading the pixel
data succeeded or an error message.
Sometimes, using an id is not appropriate, for example, if you do not want to
replace a previously sent image with the same id, or if you are sending a dummy
image and do not want it stored by the terminal emulator. In that case, you can
use the *query action*, set `a=q`. Then the terminal emulator will try to load
the image and respond with either OK or an error, as above, but it will not
replace an existing image with the same id, nor will it store the image.
== Display images on screen
Every transmitted image can be displayed an arbitrary number of times on the
screen, in different locations, using different parts of the source image, as
needed. You can either simultaneously transmit and display an image using the
action `a=T`, or first transmit the image with a id, such as `i=10` and then display
it with `a=p,i=10` which will display the previously transmitted image at the current
cursor position. When specifying an image id, the terminal emulator will reply with an
acknowledgement code, which will be either:
```
<ESC>_Gi=<id>;OK<ESC>\
```
when the image referred to by id was found, or
```
<ESC>_Gi=<id>;ENOENT:<some detailed error msg><ESC>\
```
when the image with the specified id was not found. This is similar to the
scheme described above for querying available transmission media, except that
here we are querying if the image with the specified id is available or needs to
be re-transmitted.
=== Controlling displayed image layout
The image is rendered at the current cursor position, from the upper left corner of
the current cell. You can also specify extra `X=3` and `Y=4` pixel offsets to display from
a different origin within the cell. Note that the offsets must be smaller that the size of the cell.
By default, the entire image will be displayed (images wider than the available
width will be truncated on the right edge). You can choose a source rectangle (in pixels)
as the part of the image to display. This is done with the keys: `x, y, w, h` which specify
the top-left corner, width and height of the source rectangle.
You can also ask the terminal emulator to display the image in a specified rectangle
(num of columns / num of lines), using the control codes `c,r`. `c` is the number of columns
and `r` the number of rows. The image will be scaled (enlarged/shrunk) as needed to fit
the specified area. Note that if you specify a start cell offset via the `X,Y` keys, it is not
added to the number of rows/columns.
Finally, you can specify the image *z-index*, i.e. the vertical stacking order. Images
placed in the same location with different z-index values will be blended if
they are semi-transparent. You can specify z-index values using the `z` key.
Negative z-index values mean that the images will be drawn under the text. This
allows rendering of text on top of images.
== Deleting images
Images can be deleted by using the delete action `a=d`. If specified without any
other keys, it will delete all images visible on screen. To delete specific images,
use the `d` key as described in the table below. Note that each value of d has
both a lowercase and an uppercase variant. The lowercase variant only deletes the
images without necessarily freeing up the stored image data, so that the images can be
re-displayed without needing to resend the data. The uppercase variants will delete
the image data as well, provided that the image is not referenced elsewhere, such as in the
scrollback buffer. The values of the `x` and `y` keys are the same as cursor positions (i.e.
x=1, y=1 is the top left cell).
|===
| Value of `d` | Meaning
| `a` or `A` | Delete all images visible on screen
| `i` or `I` | Delete all images with the specified id, specified using the `i` key.
| `c` or `C` | Delete all images that intersect with the current cursor position.
| `p` or `P` | Delete all images that intersect a specific cell, the cell is specified using the `x` and `y` keys
| `q` or `Q` | Delete all images that intersect a specific cell having a specific z-index. The cell and z-index is specified using the `x`, `y` and `z` keys.
| `x` or `X` | Delete all images that intersect the specified column, specified using the `x` key.
| `y` or `Y` | Delete all images that intersect the specified row, specified using the `y` key.
| `z` or `Z` | Delete all images that have the specified z-index, specified using the `z` key.
|===
Some examples:
```
<ESC>_Ga=d<ESC>\ # delete all visible images
<ESC>_Ga=d,i=10<ESC>\ # delete the image with id=10
<ESC>_Ga=Z,z=-1<ESC>\ # delete the images with z-index -1, also freeing up image data
<ESC>_Ga=P,x=3,y=4<ESC>\ # delete all images that intersect the cell at (3, 4)
```
=== Image persistence and storage quotas
In order to avoid *Denial-of-Service* attacks, terminal emulators should have a
maximum storage quota for image data. It should allow at least a few full
screen images. For example the quota in kitty is 320MB per buffer. When adding
a new image, if the total size exceeds the quota, the terminal emulator should
delete older images to make space for the new one.
== Control data reference
The table below shows all the control data keys as well as what values they can
take, and the default value they take when missing. All integers are 32-bit.
[cols="^1,<3,^1,<6"]
|===
|Key | Value | Default | Description
| `a` | Single character. `(t, T, q, p, d)` | `t` | The overall action this graphics command is performing.
4+^.^h| Keys for image transmission
| `f` | Positive integer. `(24, 32, 100)`. | `32` | The format in which the image data is sent.
| `t` | Single character. `(d, f, t, s)`. | `d` | The transmission medium used.
| `s` | Positive integer. | `0` | The width of the image being sent.
| `v` | Positive integer. | `0` | The height of the image being sent.
| `S` | Positive integer. | `0` | The size of data to read from a file.
| `O` | Positive integer. | `0` | The offset from which to read data from a file.
| `i` | Positive integer. `(0 - 4294967295)` | `0` | The image id
| `o` | Single character. `only z` | `null` | The type of data compression.
| `m` | zero or one | `0` | Whether there is more chunked data available.
4+^.^h| Keys for image display
| `x` | Positive integer | `0` | The left edge (in pixels) of the image area to display
| `y` | Positive integer | `0` | The top edge (in pixels) of the image area to display
| `w` | Positive integer | `0` | The width (in pixels) of the image area to display. By default, the entire width is used.
| `h` | Positive integer | `0` | The height (in pixels) of the image area to display. By default, the entire height is used
| `X` | Positive integer | `0` | The x-offset within the first cell at which to start displaying the image
| `Y` | Positive integer | `0` | The y-offset within the first cell at which to start displaying the image
| `c` | Positive integer | `0` | The number of columns to display the image over
| `r` | Positive integer | `0` | The number of rows to display the image over
| `z` | Integer | `0` | The *z-index* vertical stacking order of the image
4+^.^h| Keys for deleting images
| `d` | Single character. `(a, A, c, C, p, P, q, Q, x, X, y, Y, z, Z)`. | `a` | What to delete.
|===
== Interaction with other terminal actions
When resetting the terminal, all images that are visible on the screen must be
cleared. When switching from the main screen to the alternate screen buffer
(1049 private mode) all images in the alternate screen must be cleared, just as
all text is cleared. The clear screen escape code (usually `<ESC>[2J`) should also
clear all images. This is so that the clear command works.
The other commands to erase text must have no effect on graphics.
The dedicated delete graphics commands must be used for those.
When scrolling the screen (such as when using index cursor movement commands,
or scrolling through the history buffer), images must be scrolled along with
text. When page margins are defined and the index commands are used, only
images that are entirely within the page area (between the margins) must be
scrolled. When scrolling them would cause them to extend outside the page area,
they must be clipped.

View File

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

35
kitty/border_vertex.glsl Normal file
View File

@@ -0,0 +1,35 @@
#version GLSL_VERSION
uniform uvec2 viewport;
in uvec4 rect; // left, top, right, bottom
in uint rect_color;
out vec3 color;
// indices into the rect vector
const int LEFT = 0;
const int TOP = 1;
const int RIGHT = 2;
const int BOTTOM = 3;
const uvec2 pos_map[] = uvec2[4](
uvec2(RIGHT, TOP),
uvec2(RIGHT, BOTTOM),
uvec2(LEFT, BOTTOM),
uvec2(LEFT, TOP)
);
vec2 to_opengl(uint x, uint y) {
return vec2(
-1.0 + 2.0 * (float(x) / float(viewport.x)),
1.0 - 2.0 * (float(y) / float(viewport.y))
);
}
float to_color(uint c) {
return float(c & uint(0xff)) / 255.0;
}
void main() {
uvec2 pos = pos_map[gl_VertexID];
gl_Position = vec4(to_opengl(rect[pos.x], rect[pos.y]), 0, 1);
color = vec3(to_color(rect_color >> 16), to_color(rect_color >> 8), to_color(rect_color));
}

View File

@@ -2,110 +2,76 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from ctypes import addressof
from functools import partial
from itertools import chain
from threading import Lock
from .constants import GLfloat, GLint, GLuint, viewport_size
from .fast_data_types import GL_TRIANGLE_FAN, glMultiDrawArrays, glUniform3fv
from .shaders import ShaderProgram
from .utils import get_dpi
from .constants import viewport_size
from .fast_data_types import (
BORDERS_PROGRAM, add_borders_rect, compile_program, init_borders_program,
send_borders_rects
)
from .utils import color_as_int, pt_to_px, load_shaders
def as_color(c):
return c[0] / 255, c[1] / 255, c[2] / 255
def vertical_edge(color, width, top, bottom, left):
add_borders_rect(left, top, left + width, bottom, color)
def to_opengl(x, y):
return -1 + 2 * x / viewport_size.width, 1 - 2 * y / viewport_size.height
def horizontal_edge(color, height, left, right, top):
add_borders_rect(left, top, right, top + height, color)
def as_rect(left, top, right, bottom, color=0):
for (x, y) in ((right, top), (right, bottom), (left, bottom), (left, top)):
x, y = to_opengl(x, y)
yield x
yield y
yield color
def edge(func, color, sz, a, b):
return partial(func, color, sz, a, b)
class BordersProgram(ShaderProgram):
def __init__(self):
ShaderProgram.__init__(self, '''\
uniform vec3 colors[3];
in vec3 rect;
out vec3 color;
void main() {
gl_Position = vec4(rect[0], rect[1], 0, 1);
color = colors[uint(rect[2])];
}
''', '''\
in vec3 color;
out vec4 final_color;
void main() {
final_color = vec4(color, 1);
}
''')
self.add_vertex_array('rect')
def send_data(self, data):
self.send_vertex_data('rect', data)
def set_colors(self, color_buf):
glUniform3fv(self.uniform_location('colors'), 3, addressof(color_buf))
def border(color, sz, left, top, right, bottom):
horz = edge(horizontal_edge, color, sz, left, right)
horz(top), horz(bottom - sz) # top, bottom edges
vert = edge(vertical_edge, color, sz, top, bottom)
vert(left), vert(right - sz) # left, right edges
class Borders:
def __init__(self, opts):
self.is_dirty = False
self.lock = Lock()
self.can_render = False
dpix, dpiy = get_dpi()['logical']
dpi = (dpix + dpiy) / 2
self.border_width = round(opts.window_border_width * dpi / 72)
self.color_buf = (GLfloat * 9)(
*as_color(opts.background),
*as_color(opts.active_border_color),
*as_color(opts.inactive_border_color)
)
self.border_width = pt_to_px(opts.window_border_width)
self.padding_width = pt_to_px(opts.window_padding_width)
compile_program(BORDERS_PROGRAM, *load_shaders('border'))
init_borders_program()
self.background = color_as_int(opts.background)
self.active_border = color_as_int(opts.active_border_color)
self.inactive_border = color_as_int(opts.inactive_border_color)
def __call__(self, windows, active_window, current_layout, extra_blank_rects, draw_window_borders=True):
rects = []
def __call__(
self,
windows,
active_window,
current_layout,
extra_blank_rects,
draw_window_borders=True
):
add_borders_rect(0, 0, 0, 0, 0)
for br in chain(current_layout.blank_rects, extra_blank_rects):
rects.extend(as_rect(*br))
if draw_window_borders and self.border_width > 0:
bw = self.border_width
add_borders_rect(*br, self.background)
bw, pw = self.border_width, self.padding_width
fw = bw + pw
if fw > 0:
for w in windows:
g = w.geometry
color = 1 if w is active_window else 2
rects.extend(as_rect(g.left - bw, g.top - bw, g.left, g.bottom + bw, color))
rects.extend(as_rect(g.left - bw, g.top - bw, g.right + bw, g.top, color))
rects.extend(as_rect(g.right, g.top - bw, g.right + bw, g.bottom + bw, color))
rects.extend(as_rect(g.left - bw, g.bottom, g.right + bw, g.bottom + bw, color))
with self.lock:
self.num_of_rects = len(rects) // 12
self.rects = (GLfloat * len(rects))()
self.starts = (GLint * self.num_of_rects)()
self.counts = (GLuint * self.num_of_rects)()
for i, x in enumerate(rects):
self.rects[i] = x
if i % 12 == 0:
idx = i // 12
self.starts[idx] = i // 3
self.counts[idx] = 4
self.is_dirty = True
self.can_render = True
def render(self, program):
with self.lock:
if not self.can_render:
return
with program:
if self.is_dirty:
program.send_data(self.rects)
program.set_colors(self.color_buf)
self.is_dirty = False
glMultiDrawArrays(GL_TRIANGLE_FAN, addressof(self.starts), addressof(self.counts), self.num_of_rects)
if bw > 0 and draw_window_borders:
# Draw the border rectangles
color = self.active_border if w is active_window else self.inactive_border
border(
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 = self.background
border(
color, pw, g.left - pw, g.top - pw, g.right + pw,
g.bottom + pw
)
send_borders_rects(viewport_size.width, viewport_size.height)

View File

@@ -2,124 +2,81 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import os
import io
import select
import signal
import struct
import inspect
from functools import wraps
from threading import Thread, current_thread
from time import monotonic
from queue import Queue, Empty
from gettext import gettext as _
from weakref import WeakValueDictionary
from .config import MINIMUM_FONT_SIZE
from .constants import (
viewport_size, set_boss, wakeup, cell_size, MODIFIER_KEYS,
main_thread, mouse_button_pressed, mouse_cursor_pos
)
from .constants import cell_size, set_boss, viewport_size, wakeup
from .fast_data_types import (
glViewport, glBlendFunc, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GLFW_PRESS,
GLFW_REPEAT, GLFW_MOUSE_BUTTON_1, glfw_post_empty_event,
GLFW_CURSOR_NORMAL, GLFW_CURSOR, GLFW_CURSOR_HIDDEN, drain_read
GLFW_KEY_DOWN, GLFW_KEY_UP, ChildMonitor, destroy_global_data,
destroy_sprite_map, glfw_post_empty_event, layout_sprite_map
)
from .fonts.render import set_font_family
from .borders import BordersProgram
from .char_grid import cursor_shader, cell_shader
from .constants import is_key_pressed
from .keys import interpret_text_event, interpret_key_event, get_shortcut
from .fonts.render import render_cell_wrapper, set_font_family
from .keys import get_key_map, get_sent_data, get_shortcut
from .session import create_session
from .shaders import Sprites, ShaderProgram
from .tabs import TabManager, SpecialWindow
from .timers import Timers
from .utils import handle_unix_signals, safe_print, pipe2
from .tabs import SpecialWindow, TabManager
from .utils import (
get_primary_selection, open_url, safe_print, set_primary_selection
)
from .window import load_shader_programs
def conditional_run(w, i):
if w is None or not w.destroyed:
next(i, None)
def initialize_renderer():
load_shader_programs()
layout_sprite_map(cell_size.width, cell_size.height, render_cell_wrapper)
def callback(func):
''' Wrapper for function that executes first half (up to a yield statement)
in the UI thread and the rest in the child thread. If the function yields
something, the destroyed attribute of that something is checked before
running the second half. If the function returns before the yield, the
second half is not run. '''
class DumpCommands: # {{{
assert inspect.isgeneratorfunction(func)
def __init__(self, args):
self.draw_dump_buf = []
if args.dump_bytes:
self.dump_bytes_to = open(args.dump_bytes, 'wb')
@wraps(func)
def f(self, *a):
i = func(self, *a)
try:
w = next(i)
except StopIteration:
pass
else:
self.queue_action(conditional_run, w, i)
return f
def __call__(self, *a):
if a:
if a[0] == 'draw':
if a[1] is None:
if self.draw_dump_buf:
safe_print('draw', ''.join(self.draw_dump_buf))
self.draw_dump_buf = []
else:
self.draw_dump_buf.append(a[1])
elif a[0] == 'bytes':
self.dump_bytes_to.write(a[1])
self.dump_bytes_to.flush()
else:
if self.draw_dump_buf:
safe_print('draw', ''.join(self.draw_dump_buf))
self.draw_dump_buf = []
safe_print(*a)
# }}}
class Boss(Thread):
daemon = True
class Boss:
def __init__(self, glfw_window, opts, args):
Thread.__init__(self, name='ChildMonitor')
self.window_id_map = WeakValueDictionary()
startup_session = create_session(opts, args)
self.cursor_blink_zero_time = monotonic()
self.cursor_blinking = True
self.window_is_focused = True
self.glfw_window_title = None
self.action_queue = Queue()
self.pending_resize = False
self.resize_gl_viewport = False
self.shutting_down = False
self.screen_update_delay = opts.repaint_delay / 1000.0
self.signal_fd = handle_unix_signals()
self.read_wakeup_fd, self.write_wakeup_fd = pipe2()
self.read_dispatch_map = {self.signal_fd: self.signal_received, self.read_wakeup_fd: self.on_wakeup}
self.all_writers = []
self.timers = Timers()
self.ui_timers = Timers()
self.pending_ui_thread_calls = Queue()
self.write_dispatch_map = {}
self.child_monitor = ChildMonitor(
glfw_window.window_id(),
self.on_child_death,
DumpCommands(args) if args.dump_commands or args.dump_bytes else None)
set_boss(self)
self.current_font_size = opts.font_size
cell_size.width, cell_size.height = set_font_family(opts)
self.opts, self.args = opts, args
self.glfw_window = glfw_window
glfw_window.framebuffer_size_callback = self.on_window_resize
glfw_window.char_mods_callback = self.on_text_input
glfw_window.key_callback = self.on_key
glfw_window.mouse_button_callback = self.on_mouse_button
glfw_window.scroll_callback = self.on_mouse_scroll
glfw_window.cursor_pos_callback = self.on_mouse_move
glfw_window.window_focus_callback = self.on_focus
self.tab_manager = TabManager(opts, args, startup_session)
self.sprites = Sprites()
self.cell_program = ShaderProgram(*cell_shader)
self.cursor_program = ShaderProgram(*cursor_shader)
self.borders_program = BordersProgram()
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
self.sprites.do_layout(cell_size.width, cell_size.height)
self.glfw_window.set_click_cursor(False)
self.show_mouse_cursor()
self.start_cursor_blink()
def signal_received(self):
try:
data = os.read(self.signal_fd, io.DEFAULT_BUFFER_SIZE)
except BlockingIOError:
return
if data:
signals = struct.unpack('%uB' % len(data), data)
if signal.SIGINT in signals or signal.SIGTERM in signals:
if not self.shutting_down:
self.glfw_window.set_should_close(True)
glfw_post_empty_event()
initialize_renderer()
self.tab_manager = TabManager(opts, args)
self.tab_manager.init(startup_session)
self.activate_tab_at = self.tab_manager.activate_tab_at
@property
def current_tab_bar_height(self):
@@ -132,109 +89,46 @@ class Boss(Thread):
for t in self:
yield from t
def queue_action(self, func, *args):
self.action_queue.put((func, args))
wakeup()
def add_child(self, window):
self.child_monitor.add_child(window.id, window.child.pid, window.child.child_fd, window.screen)
self.window_id_map[window.id] = window
def on_wakeup(self):
if not self.shutting_down:
drain_read(self.read_wakeup_fd)
while True:
try:
func, args = self.action_queue.get_nowait()
except Empty:
break
try:
func(*args)
except Exception:
import traceback
safe_print(traceback.format_exc())
def add_child_fd(self, child_fd, read_ready, write_ready):
self.read_dispatch_map[child_fd] = read_ready
self.write_dispatch_map[child_fd] = write_ready
def remove_child_fd(self, child_fd):
self.read_dispatch_map.pop(child_fd, None)
self.write_dispatch_map.pop(child_fd, None)
try:
self.all_writers.remove(child_fd)
except Exception:
pass
def queue_ui_action(self, func, *args):
self.pending_ui_thread_calls.put((func, args))
glfw_post_empty_event()
def on_child_death(self, window_id):
w = self.window_id_map.pop(window_id, None)
if w is not None:
w.on_child_death()
def close_window(self, window=None):
' Can be called in either thread, will first kill the child, then remove the window from the gui '
if window is None:
window = self.active_window
if current_thread() is main_thread:
self.queue_action(self.close_window, window)
else:
self.remove_child_fd(window.child_fd)
window.destroy()
self.queue_ui_action(self.gui_close_window, window)
self.child_monitor.mark_for_close(window.id)
def close_tab(self, tab=None):
' Can be called in either thread, will first kill all children, then remove the tab from the gui '
if tab is None:
tab = self.active_tab
if current_thread() is main_thread:
self.queue_action(self.close_tab, tab)
else:
for window in tab:
self.remove_child_fd(window.child_fd)
window.destroy()
self.queue_ui_action(self.gui_close_window, window)
for window in tab:
self.close_window(window)
def run(self):
if self.args.profile:
import cProfile
import pstats
pr = cProfile.Profile()
pr.enable()
self.loop()
if self.args.profile:
pr.disable()
pr.create_stats()
s = pstats.Stats(pr)
s.dump_stats(self.args.profile)
def start(self):
if not getattr(self, 'io_thread_started', False):
self.child_monitor.start()
self.io_thread_started = True
def loop(self):
while not self.shutting_down:
all_readers = list(self.read_dispatch_map)
all_writers = [w.child_fd for w in self.iterwindows() if w.write_buf]
readers, writers, _ = select.select(all_readers, all_writers, [], self.timers.timeout())
for r in readers:
self.read_dispatch_map[r]()
for w in writers:
self.write_dispatch_map[w]()
self.timers()
for w in self.iterwindows():
if w.screen.is_dirty():
self.timers.add_if_missing(self.screen_update_delay, w.update_screen)
@callback
def on_window_resize(self, window, w, h):
# debounce resize events
self.pending_resize = True
yield
self.timers.add(0.02, self.apply_pending_resize, w, h)
def apply_pending_resize(self, w, h):
viewport_size.width, viewport_size.height = w, h
self.tab_manager.resize()
self.resize_gl_viewport = True
self.pending_resize = False
glfw_post_empty_event()
def increase_font_size(self):
self.change_font_size(min(self.opts.font_size * 5, self.current_font_size + self.opts.font_size_delta))
self.change_font_size(
min(
self.opts.font_size * 5, self.current_font_size +
self.opts.font_size_delta))
def decrease_font_size(self):
self.change_font_size(max(MINIMUM_FONT_SIZE, self.current_font_size - self.opts.font_size_delta))
self.change_font_size(
max(
MINIMUM_FONT_SIZE, self.current_font_size -
self.opts.font_size_delta))
def restore_font_size(self):
self.change_font_size(self.opts.font_size)
@@ -243,10 +137,17 @@ class Boss(Thread):
if new_size == self.current_font_size:
return
self.current_font_size = new_size
w, h = cell_size.width, cell_size.height
windows = tuple(filter(None, self.window_id_map.values()))
cell_size.width, cell_size.height = set_font_family(
self.opts, override_font_size=self.current_font_size)
self.sprites.do_layout(cell_size.width, cell_size.height)
self.queue_action(self.resize_windows_after_font_size_change)
layout_sprite_map(cell_size.width, cell_size.height, render_cell_wrapper)
for window in windows:
window.screen.rescale_images(w, h)
self.resize_windows_after_font_size_change()
for window in windows:
window.screen.refresh_sprite_positions()
self.tab_manager.refresh_sprite_positions()
def resize_windows_after_font_size_change(self):
self.tab_manager.resize()
@@ -269,184 +170,80 @@ class Boss(Thread):
if t is not None:
return t.active_window
@callback
def on_text_input(self, window, codepoint, mods):
data = interpret_text_event(codepoint, mods)
if data:
w = self.active_window
if w is not None:
yield w
w.write_to_child(data)
def dispatch_special_key(self, key, scancode, action, mods):
# Handles shortcuts, return True if the key was consumed
key_action = get_shortcut(self.opts.keymap, mods, key, scancode)
self.current_key_press_info = key, scancode, action, mods
return self.dispatch_action(key_action)
@callback
def on_key(self, window, key, scancode, action, mods):
is_key_pressed[key] = action == GLFW_PRESS
self.start_cursor_blink()
self.cursor_blink_zero_time = monotonic()
func = None
if action == GLFW_PRESS or action == GLFW_REPEAT:
func = get_shortcut(self.opts.keymap, mods, key, scancode)
if func is not None:
f = getattr(self, func, None)
if f is not None:
passthrough = f()
if not passthrough:
return
def dispatch_action(self, key_action):
if key_action is not None:
f = getattr(self, key_action.func, None)
if f is not None:
passthrough = f(*key_action.args)
if passthrough is not True:
return True
tab = self.active_tab
if tab is None:
return
return False
window = self.active_window
if window is None:
return
yield window
if func is not None:
f = getattr(tab, func, getattr(window, func, None))
return False
if key_action is not None:
f = getattr(tab, key_action.func, getattr(window, key_action.func, None))
if f is not None:
passthrough = f()
if not passthrough:
return
if window.char_grid.scrolled_by and key not in MODIFIER_KEYS and action == GLFW_PRESS:
window.scroll_end()
data = interpret_key_event(key, scancode, mods, window, action)
passthrough = f(*key_action.args)
if passthrough is not True:
return True
key, scancode, action, mods = self.current_key_press_info
data = get_sent_data(
self.opts.send_text_map, key, scancode, mods, window, action
)
if data:
window.write_to_child(data)
return True
return False
def combine(self, *actions):
for key_action in actions:
self.dispatch_action(key_action)
@callback
def on_focus(self, window, focused):
self.window_is_focused = focused
w = self.active_window
if w is not None:
yield w
w.focus_changed(focused)
def display_scrollback(self, data):
if self.opts.scrollback_in_new_tab:
self.queue_ui_action(self.display_scrollback_in_new_tab, data)
self.display_scrollback_in_new_tab(data)
else:
tab = self.active_tab
if tab is not None:
tab.new_special_window(SpecialWindow(self.opts.scrollback_pager, data, _('History')))
tab.new_special_window(
SpecialWindow(
self.opts.scrollback_pager, data, _('History')))
def window_for_pos(self, x, y):
def switch_focus_to(self, window_idx):
tab = self.active_tab
if tab is not None:
for w in tab:
if w.is_visible_in_layout and w.contains(x, y):
return w
tab.set_active_window_idx(window_idx)
old_focus = tab.active_window
if not old_focus.destroyed:
old_focus.focus_changed(False)
tab.active_window.focus_changed(True)
def in_tab_bar(self, y):
th = self.current_tab_bar_height
return th > 0 and y >= viewport_size.height - th
@callback
def on_mouse_button(self, window, button, action, mods):
mouse_button_pressed[button] = action == GLFW_PRESS
self.show_mouse_cursor()
x, y = mouse_cursor_pos
w = self.window_for_pos(x, y)
if w is None:
if self.in_tab_bar(y):
if button == GLFW_MOUSE_BUTTON_1 and action == GLFW_PRESS:
self.tab_manager.activate_tab_at(x)
return
focus_moved = False
old_focus = self.active_window
def send_fake_scroll(self, window_idx, amt, upwards):
tab = self.active_tab
yield
if button == GLFW_MOUSE_BUTTON_1 and w is not old_focus:
tab.set_active_window(w)
focus_moved = True
if focus_moved:
if old_focus is not None and not old_focus.destroyed:
old_focus.focus_changed(False)
w.focus_changed(True)
w.on_mouse_button(button, action, mods)
w = tab.windows[window_idx]
k = get_key_map(w.screen)[GLFW_KEY_UP if upwards else GLFW_KEY_DOWN]
w.write_to_child(k * amt)
@callback
def on_mouse_move(self, window, xpos, ypos):
mouse_cursor_pos[:2] = xpos, ypos = int(xpos * viewport_size.x_ratio), int(ypos * viewport_size.y_ratio)
self.show_mouse_cursor()
w = self.window_for_pos(xpos, ypos)
if w is not None:
yield w
w.on_mouse_move(xpos, ypos)
else:
self.change_mouse_cursor(self.in_tab_bar(ypos))
@callback
def on_mouse_scroll(self, window, x, y):
self.show_mouse_cursor()
w = self.window_for_pos(*mouse_cursor_pos)
if w is not None:
yield w
w.on_mouse_scroll(x, y)
# GUI thread API {{{
def show_mouse_cursor(self):
self.glfw_window.set_input_mode(GLFW_CURSOR, GLFW_CURSOR_NORMAL)
if self.opts.mouse_hide_wait > 0:
self.ui_timers.add(self.opts.mouse_hide_wait, self.hide_mouse_cursor)
def hide_mouse_cursor(self):
self.glfw_window.set_input_mode(GLFW_CURSOR, GLFW_CURSOR_HIDDEN)
def change_mouse_cursor(self, click=False):
self.glfw_window.set_click_cursor(click)
def request_attention(self):
try:
self.glfw_window.request_window_attention()
except AttributeError:
pass # needs glfw 3.3
def start_cursor_blink(self):
self.cursor_blinking = True
if self.opts.cursor_stop_blinking_after > 0:
self.ui_timers.add(self.opts.cursor_stop_blinking_after, self.stop_cursor_blinking)
def stop_cursor_blinking(self):
self.cursor_blinking = False
def render(self):
if self.pending_resize:
return
if self.resize_gl_viewport:
glViewport(0, 0, viewport_size.width, viewport_size.height)
self.resize_gl_viewport = False
tab = self.active_tab
if tab is not None:
if tab.title != self.glfw_window_title:
self.glfw_window_title = tab.title
self.glfw_window.set_title(self.glfw_window_title)
with self.sprites:
self.sprites.render_dirty_cells()
tab.render()
render_data = {window: window.char_grid.prepare_for_render(self.sprites) for window in tab.visible_windows() if not window.needs_layout}
with self.cell_program:
self.tab_manager.render(self.cell_program, self.sprites)
for window, rd in render_data.items():
if rd is not None:
window.render_cells(rd, self.cell_program, self.sprites)
active = self.active_window
rd = render_data.get(active)
if rd is not None:
draw_cursor = True
if self.cursor_blinking and self.opts.cursor_blink_interval > 0 and self.window_is_focused:
now = monotonic() - self.cursor_blink_zero_time
t = int(now * 1000)
d = int(self.opts.cursor_blink_interval * 1000)
n = t // d
draw_cursor = n % 2 == 0
self.ui_timers.add_if_missing(((n + 1) * d / 1000) - now, glfw_post_empty_event)
if draw_cursor:
with self.cursor_program:
active.char_grid.render_cursor(rd, self.cursor_program, self.window_is_focused)
def open_url(self, url):
if url:
open_url(url, self.opts.open_url_with)
def gui_close_window(self, window):
if window.char_grid.buffer_id is not None:
self.sprites.destroy_sprite_map(window.char_grid.buffer_id)
window.char_grid.buffer_id = None
window.destroy()
for tab in self.tab_manager:
if window in tab:
break
@@ -462,41 +259,95 @@ class Boss(Thread):
glfw_post_empty_event()
def destroy(self):
# Must be called in the main thread as it manipulates signal handlers
signal.signal(signal.SIGINT, signal.SIG_DFL)
signal.signal(signal.SIGTERM, signal.SIG_DFL)
self.shutting_down = True
self.child_monitor.shutdown()
wakeup()
self.join()
self.child_monitor.join()
for t in self.tab_manager:
t.destroy()
del self.tab_manager
self.sprites.destroy()
del self.sprites
destroy_sprite_map()
destroy_global_data()
del self.glfw_window
def paste_from_clipboard(self):
text = self.glfw_window.get_clipboard_string()
def paste_to_active_window(self, text):
if text:
w = self.active_window
if w is not None:
self.queue_action(w.paste, text)
w.paste(text)
def paste_from_clipboard(self):
text = self.glfw_window.get_clipboard_string()
self.paste_to_active_window(text)
def paste_from_selection(self):
text = get_primary_selection()
self.paste_to_active_window(text)
def set_primary_selection(self):
w = self.active_window
if w is not None and not w.destroyed:
text = w.text_for_selection()
if text:
set_primary_selection(text)
def next_tab(self):
self.queue_action(self.tab_manager.next_tab)
self.tab_manager.next_tab()
def previous_tab(self):
self.queue_action(self.tab_manager.next_tab, -1)
self.tab_manager.next_tab(-1)
def new_tab(self):
self.tab_manager.new_tab()
def args_to_special_window(self, args):
args = list(args)
stdin = None
w = self.active_window
def data_for_at(arg):
if arg == '@selection':
return w.text_for_selection()
if arg == '@ansi':
return w.buffer_as_ansi()
if arg == '@text':
return w.buffer_as_text()
if args[0].startswith('@'):
stdin = data_for_at(args[0]) or None
if stdin is not None:
stdin = stdin.encode('utf-8')
del args[0]
cmd = []
for arg in args:
if arg == '@selection':
arg = data_for_at(arg)
if not arg:
continue
cmd.append(arg)
return SpecialWindow(cmd, stdin)
def new_tab(self, *args):
special_window = None
if args:
special_window = self.args_to_special_window(args)
self.tab_manager.new_tab(special_window=special_window)
def new_window(self, *args):
tab = self.active_tab
if tab is not None:
if args:
tab.new_special_window(self.args_to_special_window(args))
else:
tab.new_window()
def move_tab_forward(self):
self.queue_action(self.tab_manager.move_tab, 1)
self.tab_manager.move_tab(1)
def move_tab_backward(self):
self.queue_action(self.tab_manager.move_tab, -1)
self.tab_manager.move_tab(-1)
def display_scrollback_in_new_tab(self, data):
self.tab_manager.new_tab(special_window=SpecialWindow(self.opts.scrollback_pager, data, _('History')))
self.tab_manager.new_tab(
special_window=SpecialWindow(
self.opts.scrollback_pager, data, _('History')))
# }}}

49
kitty/cell_fragment.glsl Normal file
View File

@@ -0,0 +1,49 @@
#version GLSL_VERSION
#define WHICH_PROGRAM
#if defined(FOREGROUND) || defined(ALL)
uniform sampler2DArray sprites;
in vec3 sprite_pos;
in vec3 underline_pos;
in vec3 strike_pos;
in vec3 foreground;
in vec3 decoration_fg;
#endif
in vec3 background;
#ifdef SPECIAL
in vec4 special_bg;
#endif
out vec4 final_color;
vec3 blend(float alpha, vec3 over, vec3 under) {
return over + (1 - alpha) * under;
}
void main() {
#if defined(FOREGROUND) || defined(ALL)
float text_alpha = texture(sprites, sprite_pos).r;
float underline_alpha = texture(sprites, underline_pos).r;
float strike_alpha = texture(sprites, strike_pos).r;
vec3 underline = underline_alpha * decoration_fg;
vec3 strike = strike_alpha * foreground;
vec3 fg = text_alpha * foreground;
vec3 decoration = blend(underline_alpha, underline, strike);
vec3 combined_fg = blend(text_alpha, fg, decoration);
float combined_alpha = max(max(underline_alpha, strike_alpha), text_alpha);
#ifdef ALL
final_color = vec4(blend(combined_alpha, combined_fg, background), 1);
#else
final_color = vec4(combined_fg, combined_alpha);
#endif
#else
#ifdef SPECIAL
final_color = special_bg;
#else
final_color = vec4(background, 1);
#endif
#endif
}

152
kitty/cell_vertex.glsl Normal file
View File

@@ -0,0 +1,152 @@
#version GLSL_VERSION
#define WHICH_PROGRAM
layout(std140) uniform CellRenderData {
float xstart, ystart, dx, dy, sprite_dx, sprite_dy;
uint default_fg, default_bg, highlight_fg, highlight_bg, cursor_color, url_color;
int color1, color2;
uint xnum, ynum, cursor_x, cursor_y, cursor_w, url_xl, url_yl, url_xr, url_yr;
uint color_table[256];
};
// Have to use fixed locations here as all variants of the cell program share the same VAO
layout(location=0) in uvec3 colors;
layout(location=1) in uvec4 sprite_coords;
layout(location=2) in float is_selected;
#if defined(FOREGROUND) || defined(ALL)
out vec3 sprite_pos;
out vec3 underline_pos;
out vec3 strike_pos;
out vec3 foreground;
out vec3 decoration_fg;
#endif
out vec3 background;
#ifdef SPECIAL
out vec4 special_bg;
#endif
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
);
const uint BYTE_MASK = uint(0xFF);
const uint SHORT_MASK = uint(0xFFFF);
const uint ZERO = uint(0);
const uint ONE = uint(1);
const uint TWO = uint(2);
const uint THREE = uint(3);
const uint DECORATION_MASK = uint(3);
const uint STRIKE_MASK = uint(1);
const uint REVERSE_MASK = uint(1);
vec3 color_to_vec(uint c) {
uint r, g, b;
r = (c >> 16) & BYTE_MASK;
g = (c >> 8) & BYTE_MASK;
b = c & BYTE_MASK;
return vec3(float(r) / 255.0, float(g) / 255.0, float(b) / 255.0);
}
uint resolve_color(uint c, uint defval) {
// Convert a cell color to an actual color based on the color table
int t = int(c & BYTE_MASK);
uint r;
switch(t) {
case 1:
r = color_table[(c >> 8) & BYTE_MASK];
break;
case 2:
r = c >> 8;
break;
default:
r = defval;
}
return r;
}
vec3 to_color(uint c, uint defval) {
return color_to_vec(resolve_color(c, defval));
}
vec3 to_sprite_pos(uvec2 pos, uint x, uint y, uint z) {
vec2 s_xpos = vec2(x, float(x) + 1.0) * sprite_dx;
vec2 s_ypos = vec2(y, float(y) + 1.0) * sprite_dy;
return vec3(s_xpos[pos.x], s_ypos[pos.y], z);
}
vec3 choose_color(float q, vec3 a, vec3 b) {
return q * a + (1.0 - q) * b;
}
float in_range(uint x, uint y) {
if (url_yl == y && url_xl <= x && x <= url_xr) return 1.0;
return 0.0;
}
float is_cursor(uint x, uint y) {
if (y == cursor_y && (x == cursor_x || x == cursor_w)) return 1.0;
return 0.0;
}
void main() {
float cursor;
uint instance_id = uint(gl_InstanceID);
// The current cell being rendered
uint r = instance_id / xnum;
uint c = instance_id - r * xnum;
// The position of this vertex, at a corner of the cell
float left = xstart + c * dx;
float top = ystart - r * dy;
vec2 xpos = vec2(left, left + dx);
vec2 ypos = vec2(top, top - dy);
uvec2 pos = pos_map[gl_VertexID];
gl_Position = vec4(xpos[pos.x], ypos[pos.y], 0, 1);
uvec2 default_colors = uvec2(default_fg, default_bg);
ivec2 color_indices = ivec2(color1, color2);
uint text_attrs = sprite_coords[3];
int fg_index = color_indices[(text_attrs >> 6) & REVERSE_MASK];
int bg_index = color_indices[1 - fg_index];
background = to_color(colors[bg_index], default_colors[bg_index]);
#if defined(FOREGROUND) || defined(ALL)
// The character sprite being rendered
sprite_pos = to_sprite_pos(pos, sprite_coords.x, sprite_coords.y, sprite_coords.z & SHORT_MASK);
// Foreground and background colors
uint resolved_fg = resolve_color(colors[fg_index], default_colors[fg_index]);
foreground = color_to_vec(resolved_fg);
// Selection
foreground = choose_color(is_selected, color_to_vec(highlight_fg), foreground);
// Underline and strike through (rendered via sprites)
float in_url = in_range(c, r);
decoration_fg = choose_color(in_url, color_to_vec(url_color), to_color(colors[2], resolved_fg));
underline_pos = choose_color(in_url, to_sprite_pos(pos, TWO, ZERO, ZERO), to_sprite_pos(pos, (text_attrs >> 2) & DECORATION_MASK, ZERO, ZERO));
strike_pos = to_sprite_pos(pos, ((text_attrs >> 7) & STRIKE_MASK) * THREE, ZERO, ZERO);
// Block cursor rendering
cursor = is_cursor(c, r);
foreground = choose_color(cursor, background, foreground);
decoration_fg = choose_color(cursor, background, decoration_fg);
#endif
#if defined(ALL)
// Selection and cursor
background = choose_color(is_selected, color_to_vec(highlight_bg), background);
background = choose_color(cursor, color_to_vec(cursor_color), background);
#elif defined(SPECIAL)
cursor = is_cursor(c, r);
background = choose_color(is_selected, color_to_vec(highlight_bg), background);
background = choose_color(cursor, color_to_vec(cursor_color), background);
special_bg = vec4(background, max(cursor, is_selected));
#endif
}

View File

@@ -1,520 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import re
import sys
from enum import Enum
from collections import namedtuple
from ctypes import addressof, memmove, sizeof
from threading import Lock
from .config import build_ansi_color_table, defaults
from .constants import (
GLuint, ScreenGeometry, cell_size, get_boss, viewport_size
)
from .fast_data_types import (
CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE, DATA_CELL_SIZE, GL_BLEND,
GL_LINE_LOOP, GL_TRIANGLE_FAN, ColorProfile, glDisable, glDrawArrays,
glDrawArraysInstanced, glEnable, glUniform1i, glUniform2f, glUniform2ui,
glUniform4f
)
from .rgb import to_color
from .utils import (
color_as_int, color_from_int, get_logical_dpi, open_url, safe_print,
set_primary_selection
)
Cursor = namedtuple('Cursor', 'x y shape blink')
class DynamicColor(Enum):
default_fg, default_bg, cursor_color, highlight_fg, highlight_bg = range(1, 6)
if DATA_CELL_SIZE % 3:
raise ValueError('Incorrect data cell size, must be a multiple of 3')
# cell shader {{{
cell_shader = (
'''\
uniform uvec2 dimensions; // xnum, ynum
uniform vec4 steps; // xstart, ystart, dx, dy
uniform vec2 sprite_layout; // dx, dy
uniform usamplerBuffer sprite_map; // gl_InstanceID -> x, y, z
uniform uvec2 color_indices; // which color to use as fg and which as bg
out vec3 sprite_pos;
out vec3 underline_pos;
out vec3 strike_pos;
out vec3 foreground;
out vec3 background;
out vec3 decoration_fg;
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
);
const uint BYTE_MASK = uint(255);
const uint ZERO = uint(0);
const uint SMASK = uint(3);
vec3 to_color(uint c) {
uint r, g, b;
r = (c >> 16) & BYTE_MASK;
g = (c >> 8) & BYTE_MASK;
b = c & BYTE_MASK;
return vec3(float(r) / 255.0, float(g) / 255.0, float(b) / 255.0);
}
vec3 to_sprite_pos(uvec2 pos, uint x, uint y, uint z) {
vec2 s_xpos = vec2(x, float(x) + 1.0) * sprite_layout[0];
vec2 s_ypos = vec2(y, float(y) + 1.0) * sprite_layout[1];
return vec3(s_xpos[pos[0]], s_ypos[pos[1]], z);
}
void main() {
uint instance_id = uint(gl_InstanceID);
uint r = instance_id / dimensions[0];
uint c = instance_id - r * dimensions[0];
float left = steps[0] + c * steps[2];
float top = steps[1] - r * steps[3];
vec2 xpos = vec2(left, left + steps[2]);
vec2 ypos = vec2(top, top - steps[3]);
uvec2 pos = pos_map[gl_VertexID];
gl_Position = vec4(xpos[pos[0]], ypos[pos[1]], 0, 1);
int sprite_id = gl_InstanceID * STRIDE;
uvec4 spos = texelFetch(sprite_map, sprite_id);
uvec4 colors = texelFetch(sprite_map, sprite_id + 1);
sprite_pos = to_sprite_pos(pos, spos[0], spos[1], spos[2]);
foreground = to_color(colors[color_indices[0]]);
background = to_color(colors[color_indices[1]]);
uint decoration = colors[2];
decoration_fg = to_color(decoration);
underline_pos = to_sprite_pos(pos, (decoration >> 24) & SMASK, ZERO, ZERO);
strike_pos = to_sprite_pos(pos, (decoration >> 26) & SMASK, ZERO, ZERO);
}
'''.replace('STRIDE', str(DATA_CELL_SIZE // 3)),
'''\
uniform sampler2DArray sprites;
in vec3 sprite_pos;
in vec3 underline_pos;
in vec3 strike_pos;
in vec3 foreground;
in vec3 background;
in vec3 decoration_fg;
out vec4 final_color;
vec3 blend(float alpha, vec3 over, vec3 under) {
return over + (1 - alpha) * under;
}
void main() {
float text_alpha = texture(sprites, sprite_pos).r;
float underline_alpha = texture(sprites, underline_pos).r;
float strike_alpha = texture(sprites, strike_pos).r;
vec3 underline = underline_alpha * decoration_fg;
vec3 strike = strike_alpha * foreground;
vec3 fg = text_alpha * foreground;
vec3 decoration = blend(underline_alpha, underline, strike);
vec3 combined_fg = blend(text_alpha, fg, decoration);
float combined_alpha = max(max(underline_alpha, strike_alpha), text_alpha);
final_color = vec4(blend(combined_alpha, combined_fg, background), 1);
}
''')
# }}}
# cursor shader {{{
cursor_shader = (
'''\
uniform vec2 xpos;
uniform vec2 ypos;
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() {
uvec2 pos = pos_map[gl_VertexID];
gl_Position = vec4(xpos[pos[0]], ypos[pos[1]], 0, 1);
}
''',
'''\
uniform vec4 color;
out vec4 final_color;
void main() {
final_color = color;
}
''')
# }}}
class Selection: # {{{
__slots__ = tuple('in_progress start_x start_y start_scrolled_by end_x end_y end_scrolled_by'.split())
def __init__(self):
self.clear()
def clear(self):
self.in_progress = False
self.start_x = self.start_y = self.end_x = self.end_y = 0
self.start_scrolled_by = self.end_scrolled_by = 0
def limits(self, scrolled_by, lines, columns):
def coord(x, y, ydelta):
y = y - ydelta + scrolled_by
if y < 0:
x, y = 0, 0
elif y >= lines:
x, y = columns - 1, lines - 1
return x, y
a = coord(self.start_x, self.start_y, self.start_scrolled_by)
b = coord(self.end_x, self.end_y, self.end_scrolled_by)
return (a, b) if a[1] < b[1] or (a[1] == b[1] and a[0] <= b[0]) else (b, a)
def text(self, linebuf, historybuf):
sy = self.start_y - self.start_scrolled_by
ey = self.end_y - self.end_scrolled_by
if sy == ey and self.start_x == self.end_x:
return ''
a, b = (sy, self.start_x), (ey, self.end_x)
if a > b:
a, b = b, a
def line(y):
if y < 0:
return historybuf.line(-1 - y)
return linebuf.line(y)
lines = []
for y in range(a[0], b[0] + 1):
startx, endx = 0, linebuf.xnum - 1
if y == a[0]:
startx = max(0, min(a[1], endx))
if y == b[0]:
endx = max(0, min(b[1], endx))
l = line(y)
is_continued = l.is_continued()
if endx - startx >= linebuf.xnum - 1:
l = str(l).rstrip(' ')
else:
l = ''.join(l[x] for x in range(startx, endx + 1))
if not is_continued and startx == 0 and len(lines) > 0:
l = '\n' + l
lines.append(l)
return ''.join(lines)
# }}}
def calculate_gl_geometry(window_geometry):
dx, dy = 2 * cell_size.width / viewport_size.width, 2 * cell_size.height / viewport_size.height
xmargin = window_geometry.left / viewport_size.width
ymargin = window_geometry.top / viewport_size.height
xstart = -1 + 2 * xmargin
ystart = 1 - 2 * ymargin
return ScreenGeometry(xstart, ystart, window_geometry.xnum, window_geometry.ynum, dx, dy)
def render_cells(buffer_id, sg, cell_program, sprites, invert_colors=False):
sprites.bind_sprite_map(buffer_id)
ul = cell_program.uniform_location
glUniform2ui(ul('dimensions'), sg.xnum, sg.ynum)
glUniform2ui(ul('color_indices'), 1 if invert_colors else 0, 0 if invert_colors else 1)
glUniform4f(ul('steps'), sg.xstart, sg.ystart, sg.dx, sg.dy)
glUniform1i(ul('sprites'), sprites.sampler_num)
glUniform1i(ul('sprite_map'), sprites.buffer_sampler_num)
glUniform2f(ul('sprite_layout'), *(sprites.layout))
glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, sg.xnum * sg.ynum)
class CharGrid:
url_pat = re.compile('(?:http|https|file|ftp)://\S+', re.IGNORECASE)
def __init__(self, screen, opts):
self.buffer_lock = Lock()
self.buffer_id = None
self.current_selection = Selection()
self.last_rendered_selection = self.current_selection.limits(0, screen.lines, screen.columns)
self.render_buf_is_dirty = True
self.render_data = None
self.scrolled_by = 0
self.color_profile = ColorProfile()
self.color_profile.update_ansi_color_table(build_ansi_color_table(opts))
self.screen = screen
self.opts = opts
self.default_bg = color_as_int(opts.background)
self.default_fg = color_as_int(opts.foreground)
self.dpix, self.dpiy = get_logical_dpi()
self.opts = opts
self.default_cursor = self.current_cursor = Cursor(0, 0, opts.cursor_shape, opts.cursor_blink_interval > 0)
self.opts = opts
self.highlight_fg, self.highlight_bg = map(color_as_int, (opts.selection_foreground, opts.selection_background))
self.sprite_map_type = self.main_sprite_map = self.scroll_sprite_map = self.render_buf = None
def escape(chars):
return ''.join(frozenset(chars)).replace('\\', r'\\').replace(']', r'\]').replace('-', r'\-')
try:
self.word_pat = re.compile(r'[\w{}]'.format(escape(self.opts.select_by_word_characters)), re.UNICODE)
except Exception:
safe_print('Invalid characters in select_by_word_characters, ignoring', file=sys.stderr)
self.word_pat = re.compile(r'[\w{}]'.format(escape(defaults.select_by_word_characters)), re.UNICODE)
def update_position(self, window_geometry):
self.screen_geometry = calculate_gl_geometry(window_geometry)
def resize(self, window_geometry):
self.update_position(window_geometry)
self.sprite_map_type = (GLuint * (self.screen_geometry.ynum * self.screen_geometry.xnum * DATA_CELL_SIZE))
self.main_sprite_map = self.sprite_map_type()
self.scroll_sprite_map = self.sprite_map_type()
with self.buffer_lock:
self.render_buf = self.sprite_map_type()
self.selection_buf = self.sprite_map_type()
self.render_buf_is_dirty = True
self.current_selection.clear()
def change_colors(self, changes):
dirtied = False
def item(raw):
if raw is None:
return 0
val = to_color(raw)
return None if val is None else (color_as_int(val) << 8) | 1
for which, val in changes.items():
val = item(val)
if val is None:
continue
dirtied = True
setattr(self.screen, which.name, val)
if dirtied:
self.screen.mark_as_dirty()
def scroll(self, amt, upwards=True):
if not isinstance(amt, int):
amt = {'line': 1, 'page': self.screen.lines - 1, 'full': self.screen.historybuf.count}[amt]
if not upwards:
amt *= -1
y = max(0, min(self.scrolled_by + amt, self.screen.historybuf.count))
if y != self.scrolled_by:
self.scrolled_by = y
self.update_cell_data()
def update_cell_data(self, force_full_refresh=False):
sprites = get_boss().sprites
is_dirty = self.screen.is_dirty()
with sprites.lock:
fg = self.screen.default_fg
fg = fg >> 8 if fg & 1 else self.default_fg
bg = self.screen.default_bg
bg = bg >> 8 if bg & 1 else self.default_bg
cursor_changed, history_line_added_count = self.screen.update_cell_data(
sprites.backend, self.color_profile, addressof(self.main_sprite_map), fg, bg, force_full_refresh)
if self.scrolled_by:
self.scrolled_by = min(self.scrolled_by + history_line_added_count, self.screen.historybuf.count)
self.screen.set_scroll_cell_data(
sprites.backend, self.color_profile, addressof(self.main_sprite_map), fg, bg,
self.scrolled_by, addressof(self.scroll_sprite_map))
data = self.scroll_sprite_map if self.scrolled_by else self.main_sprite_map
with self.buffer_lock:
if is_dirty:
self.current_selection.clear()
memmove(self.render_buf, data, sizeof(type(data)))
self.render_data = self.screen_geometry
self.render_buf_is_dirty = True
if cursor_changed:
c = self.screen.cursor
self.current_cursor = Cursor(c.x, c.y, c.shape, c.blink)
def cell_for_pos(self, x, y):
x, y = int(x // cell_size.width), int(y // cell_size.height)
if 0 <= x < self.screen.columns and 0 <= y < self.screen.lines:
return x, y
return None, None
def update_drag(self, is_press, mx, my):
x, y = self.cell_for_pos(mx, my)
if x is None:
x = 0 if mx <= cell_size.width else self.screen.columns - 1
y = 0 if my <= cell_size.height else self.screen.lines - 1
ps = None
with self.buffer_lock:
if is_press:
self.current_selection.start_x = self.current_selection.end_x = x
self.current_selection.start_y = self.current_selection.end_y = y
self.current_selection.start_scrolled_by = self.current_selection.end_scrolled_by = self.scrolled_by
self.current_selection.in_progress = True
elif self.current_selection.in_progress:
self.current_selection.end_x = x
self.current_selection.end_y = y
self.current_selection.end_scrolled_by = self.scrolled_by
if is_press is False:
self.current_selection.in_progress = False
ps = self.text_for_selection()
if ps and ps.strip():
set_primary_selection(ps)
def has_url_at(self, x, y):
x, y = self.cell_for_pos(x, y)
if x is not None:
l = self.screen_line(y)
if l is not None:
text = l.as_base_text()
for m in self.url_pat.finditer(text):
if m.start() <= x < m.end():
return True
return False
def click_url(self, x, y):
x, y = self.cell_for_pos(x, y)
if x is not None:
l = self.screen_line(y)
if l is not None:
text = l.as_base_text()
for m in self.url_pat.finditer(text):
if m.start() <= x < m.end():
url = ''.join(l[i] for i in range(*m.span())).rstrip('.')
# Remove trailing "] and similar
url = re.sub(r'''["'][)}\]]$''', '', url)
# Remove closing trailing character if it is matched by it's
# corresponding opening character before the url
if m.start() > 0:
before = l[m.start() - 1]
closing = {'(': ')', '[': ']', '{': '}', '<': '>', '"': '"', "'": "'", '`': '`', '|': '|', ':': ':'}.get(before)
if closing is not None and url.endswith(closing):
url = url[:-1]
if url:
open_url(url, self.opts.open_url_with)
def screen_line(self, y):
' Return the Line object corresponding to the yth line on the rendered screen '
if y >= 0 and y < self.screen.lines:
if self.scrolled_by:
if y < self.scrolled_by:
return self.screen.historybuf.line(self.scrolled_by - 1 - y)
return self.screen.line(y - self.scrolled_by)
else:
return self.screen.line(y)
def multi_click(self, count, x, y):
x, y = self.cell_for_pos(x, y)
if x is not None:
line = self.screen_line(y)
if line is not None and count in (2, 3):
s = self.current_selection
s.start_scrolled_by = s.end_scrolled_by = self.scrolled_by
s.start_y = s.end_y = y
s.in_progress = False
if count == 3:
for i in range(self.screen.columns):
if line[i] != ' ':
s.start_x = i
break
else:
s.start_x = 0
for i in range(self.screen.columns):
c = self.screen.columns - 1 - i
if line[c] != ' ':
s.end_x = c
break
else:
s.end_x = self.screen.columns - 1
elif count == 2:
i = x
while i >= 0 and self.word_pat.match(line[i]) is not None:
i -= 1
s.start_x = i if i == x else i + 1
i = x
while i < self.screen.columns and self.word_pat.match(line[i]) is not None:
i += 1
s.end_x = i if i == x else i - 1
ps = self.text_for_selection()
if ps:
set_primary_selection(ps)
def get_scrollback_as_ansi(self):
ans = []
self.screen.historybuf.as_ansi(ans.append)
self.screen.linebuf.as_ansi(ans.append)
return ''.join(ans).encode('utf-8')
def text_for_selection(self, sel=None):
s = sel or self.current_selection
return s.text(self.screen.linebuf, self.screen.historybuf)
def prepare_for_render(self, sprites):
with self.buffer_lock:
sg = self.render_data
if sg is None:
return
if self.buffer_id is None:
self.buffer_id = sprites.add_sprite_map()
buf = self.render_buf
start, end = sel = self.current_selection.limits(self.scrolled_by, self.screen.lines, self.screen.columns)
if start != end:
buf = self.selection_buf
if self.render_buf_is_dirty or sel != self.last_rendered_selection:
memmove(buf, self.render_buf, sizeof(type(buf)))
fg = self.screen.highlight_fg
fg = fg >> 8 if fg & 1 else self.highlight_fg
bg = self.screen.highlight_bg
bg = bg >> 8 if bg & 1 else self.highlight_bg
self.screen.apply_selection(addressof(buf), start[0], start[1], end[0], end[1], fg, bg)
if self.render_buf_is_dirty or self.last_rendered_selection != sel:
sprites.set_sprite_map(self.buffer_id, buf)
self.render_buf_is_dirty = False
self.last_rendered_selection = sel
return sg
def render_cells(self, sg, cell_program, sprites, invert_colors=False):
render_cells(self.buffer_id, sg, cell_program, sprites, invert_colors=invert_colors)
def render_cursor(self, sg, cursor_program, is_focused):
cursor = self.current_cursor
if not self.screen.cursor_visible or self.scrolled_by:
return
def width(w=2, vert=True):
dpi = self.dpix if vert else self.dpiy
w *= dpi / 72.0 # as pixels
factor = 2 / (viewport_size.width if vert else viewport_size.height)
return w * factor
ul = cursor_program.uniform_location
left = sg.xstart + cursor.x * sg.dx
top = sg.ystart - cursor.y * sg.dy
cc = self.screen.cursor_color
col = color_from_int(cc >> 8) if cc & 1 else self.opts.cursor
shape = cursor.shape or self.default_cursor.shape
alpha = self.opts.cursor_opacity
if alpha < 1.0 and shape == CURSOR_BLOCK:
glEnable(GL_BLEND)
mult = self.screen.current_char_width()
right = left + (width(1.5) if shape == CURSOR_BEAM else sg.dx * mult)
bottom = top - sg.dy
if shape == CURSOR_UNDERLINE:
top = bottom + width(vert=False)
glUniform4f(ul('color'), col[0] / 255.0, col[1] / 255.0, col[2] / 255.0, alpha)
glUniform2f(ul('xpos'), left, right)
glUniform2f(ul('ypos'), top, bottom)
glDrawArrays(GL_TRIANGLE_FAN if is_focused else GL_LINE_LOOP, 0, 4)
glDisable(GL_BLEND)

View File

@@ -237,3 +237,60 @@ decode_utf8(uint32_t* state, uint32_t* codep, uint8_t byte) {
*state = utf8_data[256 + *state*16 + type];
return *state;
}
unsigned int
encode_utf8(uint32_t ch, char* dest) {
if (ch < 0x80) {
dest[0] = (char)ch;
return 1;
}
if (ch < 0x800) {
dest[0] = (ch>>6) | 0xC0;
dest[1] = (ch & 0x3F) | 0x80;
return 2;
}
if (ch < 0x10000) {
dest[0] = (ch>>12) | 0xE0;
dest[1] = ((ch>>6) & 0x3F) | 0x80;
dest[2] = (ch & 0x3F) | 0x80;
return 3;
}
if (ch < 0x110000) {
dest[0] = (ch>>18) | 0xF0;
dest[1] = ((ch>>12) & 0x3F) | 0x80;
dest[2] = ((ch>>6) & 0x3F) | 0x80;
dest[3] = (ch & 0x3F) | 0x80;
return 4;
}
return 0;
}
// Base64
// standard decoding using + and / with = being the padding character
static uint8_t b64_decoding_table[256] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
const char*
base64_decode(const uint32_t *src, size_t src_sz, uint8_t *dest, size_t dest_capacity, size_t *dest_sz) {
if (!src_sz) { *dest_sz = 0; return NULL; }
if (src_sz % 4 != 0) return "base64 encoded data must have a length that is a multiple of four";
*dest_sz = (src_sz / 4) * 3;
if (src[src_sz - 1] == '=') (*dest_sz)--;
if (src[src_sz - 2] == '=') (*dest_sz)--;
if (*dest_sz > dest_capacity) return "output buffer too small";
for (size_t i = 0, j = 0; i < src_sz;) {
uint32_t sextet_a = src[i] == '=' ? 0 & i++ : b64_decoding_table[src[i++] & 0xff];
uint32_t sextet_b = src[i] == '=' ? 0 & i++ : b64_decoding_table[src[i++] & 0xff];
uint32_t sextet_c = src[i] == '=' ? 0 & i++ : b64_decoding_table[src[i++] & 0xff];
uint32_t sextet_d = src[i] == '=' ? 0 & i++ : b64_decoding_table[src[i++] & 0xff];
uint32_t triple = (sextet_a << 3 * 6) + (sextet_b << 2 * 6) + (sextet_c << 1 * 6) + (sextet_d << 0 * 6);
if (j < *dest_sz) dest[j++] = (triple >> 2 * 8) & 0xFF;
if (j < *dest_sz) dest[j++] = (triple >> 1 * 8) & 0xFF;
if (j < *dest_sz) dest[j++] = (triple >> 0 * 8) & 0xFF;
}
return NULL;
}

912
kitty/child-monitor.c Normal file
View File

@@ -0,0 +1,912 @@
/*
* child-monitor.c
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#ifdef __APPLE__
#include <pthread.h>
// I cant figure out how to get pthread.h to include this definition on macOS. MACOSX_DEPLOYMENT_TARGET does not work.
extern int pthread_setname_np(const char *name);
#else
// Need _GNU_SOURCE for pthread_setname_np on linux
#define _GNU_SOURCE
#include <pthread.h>
#undef _GNU_SOURCE
#endif
#include "state.h"
#include "screen.h"
#include <termios.h>
#include <unistd.h>
#include <float.h>
#include <fcntl.h>
#ifndef __APPLE__
#include <stropts.h>
#endif
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <signal.h>
#include <GLFW/glfw3.h>
#define EXTRA_FDS 2
#define wakeup_main_loop glfwPostEmptyEvent
static void (*parse_func)(Screen*, PyObject*);
typedef struct {
Screen *screen;
bool needs_removal;
int fd;
unsigned long id;
pid_t pid;
} Child;
static const Child EMPTY_CHILD = {0};
#define screen_mutex(op, which) \
pthread_mutex_##op(&screen->which##_buf_lock);
#define children_mutex(op) \
pthread_mutex_##op(&children_lock);
static Child children[MAX_CHILDREN] = {{0}};
static Child scratch[MAX_CHILDREN] = {{0}};
static Child add_queue[MAX_CHILDREN] = {{0}}, remove_queue[MAX_CHILDREN] = {{0}};
static unsigned long remove_notify[MAX_CHILDREN] = {0};
static size_t add_queue_count = 0, remove_queue_count = 0;
static struct pollfd fds[MAX_CHILDREN + EXTRA_FDS] = {{0}};
static pthread_mutex_t children_lock;
static bool signal_received = false;
static ChildMonitor *the_monitor = NULL;
static uint8_t drain_buf[1024];
static int signal_fds[2], wakeup_fds[2];
static void *glfw_window_id = NULL;
static inline void
set_thread_name(const char *name) {
int ret = 0;
#ifdef __APPLE__
ret = pthread_setname_np(name);
#else
ret = pthread_setname_np(pthread_self(), name);
#endif
if (ret != 0) perror("Failed to set thread name");
}
// Main thread functions {{{
#define FREE_CHILD(x) \
Py_CLEAR((x).screen); x = EMPTY_CHILD;
#define XREF_CHILD(x, OP) OP(x.screen);
#define INCREF_CHILD(x) XREF_CHILD(x, Py_INCREF)
#define DECREF_CHILD(x) XREF_CHILD(x, Py_DECREF)
// The max time (in secs) to wait for events from the window system
// before ticking over the main loop. Negative values mean wait forever.
static double maximum_wait = -1.0;
static inline void
set_maximum_wait(double val) {
if (val >= 0 && (val < maximum_wait || maximum_wait < 0)) maximum_wait = val;
}
static void
handle_signal(int sig_num) {
int save_err = errno;
unsigned char byte = (unsigned char)sig_num;
while(true) {
ssize_t ret = write(signal_fds[1], &byte, 1);
if (ret < 0 && errno == EINTR) continue;
break;
}
errno = save_err;
}
static inline bool
self_pipe(int fds[2]) {
int flags;
flags = pipe(fds);
if (flags != 0) return false;
flags = fcntl(fds[0], F_GETFD);
if (flags == -1) { return false; }
if (fcntl(fds[0], F_SETFD, flags | FD_CLOEXEC) == -1) { return false; }
flags = fcntl(fds[0], F_GETFL);
if (flags == -1) { return false; }
if (fcntl(fds[0], F_SETFL, flags | O_NONBLOCK) == -1) { return false; }
return true;
}
static PyObject *
new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
ChildMonitor *self;
PyObject *dump_callback, *death_notify, *wid;
int ret;
if (the_monitor) { PyErr_SetString(PyExc_RuntimeError, "Can have only a single ChildMonitor instance"); return NULL; }
if (!PyArg_ParseTuple(args, "OOO", &wid, &death_notify, &dump_callback)) return NULL;
glfw_window_id = PyLong_AsVoidPtr(wid);
if ((ret = pthread_mutex_init(&children_lock, NULL)) != 0) {
PyErr_Format(PyExc_RuntimeError, "Failed to create children_lock mutex: %s", strerror(ret));
return NULL;
}
if (!self_pipe(wakeup_fds)) return PyErr_SetFromErrno(PyExc_OSError);
if (!self_pipe(signal_fds)) return PyErr_SetFromErrno(PyExc_OSError);
if (signal(SIGINT, handle_signal) == SIG_ERR) return PyErr_SetFromErrno(PyExc_OSError);
if (signal(SIGTERM, handle_signal) == SIG_ERR) return PyErr_SetFromErrno(PyExc_OSError);
if (siginterrupt(SIGINT, false) != 0) return PyErr_SetFromErrno(PyExc_OSError);
if (siginterrupt(SIGTERM, false) != 0) return PyErr_SetFromErrno(PyExc_OSError);
self = (ChildMonitor *)type->tp_alloc(type, 0);
if (self == NULL) return PyErr_NoMemory();
self->death_notify = death_notify; Py_INCREF(death_notify);
if (dump_callback != Py_None) {
self->dump_callback = dump_callback; Py_INCREF(dump_callback);
parse_func = parse_worker_dump;
} else parse_func = parse_worker;
self->count = 0;
fds[0].fd = wakeup_fds[0]; fds[1].fd = signal_fds[0];
fds[0].events = POLLIN; fds[1].events = POLLIN;
the_monitor = self;
return (PyObject*) self;
}
static void
dealloc(ChildMonitor* self) {
pthread_mutex_destroy(&children_lock);
Py_CLEAR(self->dump_callback);
Py_CLEAR(self->death_notify);
Py_TYPE(self)->tp_free((PyObject*)self);
while (remove_queue_count) {
remove_queue_count--;
FREE_CHILD(remove_queue[remove_queue_count]);
}
while (add_queue_count) {
add_queue_count--;
FREE_CHILD(add_queue[add_queue_count]);
}
close(wakeup_fds[0]);
close(wakeup_fds[1]);
close(signal_fds[0]);
close(signal_fds[1]);
}
static void
wakeup_io_loop() {
while(true) {
ssize_t ret = write(wakeup_fds[1], "w", 1);
if (ret < 0) {
if (errno == EINTR) continue;
perror("Failed to write to wakeup fd with error");
}
break;
}
}
static void* io_loop(void *data);
static PyObject *
start(ChildMonitor *self) {
#define start_doc "start() -> Start the I/O thread"
int ret = pthread_create(&self->io_thread, NULL, io_loop, self);
if (ret != 0) return PyErr_SetFromErrno(PyExc_OSError);
Py_RETURN_NONE;
}
static PyObject *
join(ChildMonitor *self) {
#define join_doc "join() -> Wait for the I/O thread to finish"
int ret = pthread_join(self->io_thread, NULL);
if (ret != 0) return PyErr_SetFromErrno(PyExc_OSError);
Py_RETURN_NONE;
}
static PyObject *
wakeup(ChildMonitor UNUSED *self) {
#define wakeup_doc "wakeup() -> wakeup the ChildMonitor I/O thread, forcing it to exit from poll() if it is waiting there."
wakeup_io_loop();
Py_RETURN_NONE;
}
static PyObject *
add_child(ChildMonitor *self, PyObject *args) {
#define add_child_doc "add_child(id, pid, fd, screen) -> Add a child."
children_mutex(lock);
if (self->count + add_queue_count >= MAX_CHILDREN) { PyErr_SetString(PyExc_ValueError, "Too many children"); children_mutex(unlock); return NULL; }
add_queue[add_queue_count] = EMPTY_CHILD;
#define A(attr) &add_queue[add_queue_count].attr
if (!PyArg_ParseTuple(args, "kiiO", A(id), A(pid), A(fd), A(screen))) {
children_mutex(unlock);
return NULL;
}
#undef A
INCREF_CHILD(add_queue[add_queue_count]);
add_queue_count++;
children_mutex(unlock);
wakeup_io_loop();
Py_RETURN_NONE;
}
bool
schedule_write_to_child(unsigned long id, const char *data, size_t sz) {
ChildMonitor *self = the_monitor;
bool found = false;
children_mutex(lock);
for (size_t i = 0; i < self->count; i++) {
if (children[i].id == id) {
found = true;
Screen *screen = children[i].screen;
screen_mutex(lock, write);
size_t space_left = screen->write_buf_sz - screen->write_buf_used;
if (space_left < sz) {
if (screen->write_buf_used + sz > 100 * 1024 * 1024) {
fprintf(stderr, "Too much data being sent to child with id: %lu, ignoring it\n", id);
screen_mutex(unlock, write);
break;
}
screen->write_buf_sz = screen->write_buf_used + 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;
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);
if (screen->write_buf == NULL) { fatal("Out of memory."); }
}
if (screen->write_buf_used) wakeup_io_loop();
screen_mutex(unlock, write);
break;
}
}
children_mutex(unlock);
return found;
}
static PyObject *
needs_write(ChildMonitor UNUSED *self, PyObject *args) {
#define needs_write_doc "needs_write(id, data) -> Queue data to be written to child."
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; }
Py_RETURN_FALSE;
}
static PyObject *
shutdown(ChildMonitor *self) {
#define shutdown_doc "shutdown() -> Shutdown the monitor loop."
signal(SIGINT, SIG_DFL);
signal(SIGTERM, SIG_DFL);
self->shutting_down = true;
Py_RETURN_NONE;
}
static inline void
do_parse(ChildMonitor *self, Screen *screen, double now) {
screen_mutex(lock, read);
if (screen->read_buf_sz) {
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(); // Ensure the read fd has POLLIN set
screen->read_buf_sz = 0;
screen->new_input_at = 0;
} else set_maximum_wait(OPT(input_delay) - time_since_new_input);
}
screen_mutex(unlock, read);
}
static void
parse_input(ChildMonitor *self) {
// Parse all available input that was read in the I/O thread.
size_t count = 0, remove_count = 0;
double now = monotonic();
children_mutex(lock);
while (remove_queue_count) {
remove_queue_count--;
remove_notify[remove_count] = remove_queue[remove_queue_count].id;
remove_count++;
FREE_CHILD(remove_queue[remove_queue_count]);
}
if (UNLIKELY(signal_received)) {
glfwSetWindowShouldClose(glfw_window_id, true);
} else {
count = self->count;
for (size_t i = 0; i < count; i++) {
scratch[i] = children[i];
INCREF_CHILD(scratch[i]);
}
}
children_mutex(unlock);
while(remove_count) {
// must be done while no locks are held, since the locks are non-recursive and
// the python function could call into other functions in this module
remove_count--;
PyObject *t = PyObject_CallFunction(self->death_notify, "k", remove_notify[remove_count]);
if (t == NULL) PyErr_Print();
else Py_DECREF(t);
}
for (size_t i = 0; i < count; i++) {
if (!scratch[i].needs_removal) {
do_parse(self, scratch[i].screen, now);
}
DECREF_CHILD(scratch[i]);
}
}
static PyObject *
mark_for_close(ChildMonitor *self, PyObject *args) {
#define mark_for_close_doc "Mark a child to be removed from the child monitor"
unsigned long window_id;
if (!PyArg_ParseTuple(args, "k", &window_id)) return NULL;
children_mutex(lock);
for (size_t i = 0; i < self->count; i++) {
if (children[i].id == window_id) {
children[i].needs_removal = true;
break;
}
}
children_mutex(unlock);
wakeup_io_loop();
Py_RETURN_NONE;
}
static inline bool
pty_resize(int fd, struct winsize *dim) {
while(true) {
if (ioctl(fd, TIOCSWINSZ, dim) == -1) {
if (errno == EINTR) continue;
if (errno != EBADF && errno != ENOTTY) {
fprintf(stderr, "Failed to resize tty associated with fd: %d with error: %s", fd, strerror(errno));
return false;
}
}
break;
}
return true;
}
static PyObject *
resize_pty(ChildMonitor *self, PyObject *args) {
#define resize_pty_doc "Resize the pty associated with the specified child"
unsigned long window_id;
struct winsize dim;
int fd = -1;
if (!PyArg_ParseTuple(args, "kHHHH", &window_id, &dim.ws_row, &dim.ws_col, &dim.ws_xpixel, &dim.ws_ypixel)) return NULL;
children_mutex(lock);
#define FIND(queue, count) { \
for (size_t i = 0; i < count; i++) { \
if (queue[i].id == window_id) { \
fd = queue[i].fd; \
break; \
} \
}}
FIND(children, self->count);
if (fd == -1) FIND(add_queue, add_queue_count);
if (fd != -1) {
if (!pty_resize(fd, &dim)) PyErr_SetFromErrno(PyExc_OSError);
} else fprintf(stderr, "Failed to send resize signal to child with id: %lu (children count: %u) (add queue: %lu)\n", window_id, self->count, add_queue_count);
children_mutex(unlock);
if (PyErr_Occurred()) return NULL;
Py_RETURN_NONE;
}
bool
set_iutf8(int UNUSED fd, bool UNUSED on) {
#ifdef IUTF8
struct termios attrs;
if (tcgetattr(fd, &attrs) != 0) return false;
if (on) attrs.c_iflag |= IUTF8;
else attrs.c_iflag &= ~IUTF8;
if (tcsetattr(fd, TCSANOW, &attrs) != 0) return false;
#endif
return true;
}
static PyObject*
pyset_iutf8(ChildMonitor *self, PyObject *args) {
unsigned long window_id;
int on;
PyObject *found = Py_False;
if (!PyArg_ParseTuple(args, "kp", &window_id, &on)) return NULL;
children_mutex(lock);
for (size_t i = 0; i < self->count; i++) {
if (children[i].id == window_id) {
found = Py_True;
if (!set_iutf8(fds[EXTRA_FDS + i].fd, on & 1)) PyErr_SetFromErrno(PyExc_OSError);
break;
}
}
children_mutex(unlock);
if (PyErr_Occurred()) return NULL;
Py_INCREF(found);
return found;
}
#undef FREE_CHILD
#undef INCREF_CHILD
#undef DECREF_CHILD
static double last_render_at = -DBL_MAX;
draw_borders_func draw_borders = NULL;
draw_cells_func draw_cells = NULL;
draw_cursor_func draw_cursor = NULL;
static inline double
cursor_width(double w, bool vert) {
double dpi = vert ? global_state.logical_dpi_x : global_state.logical_dpi_y;
double ans = w * dpi / 72.0; // as pixels
double factor = 2.0 / (vert ? global_state.viewport_width : global_state.viewport_height);
return ans * factor;
}
extern void cocoa_update_title(PyObject*);
static inline void
collect_cursor_info(CursorRenderInfo *ans, Window *w, double now) {
ScreenRenderData *rd = &w->render_data;
if (rd->screen->scrolled_by || !screen_is_cursor_visible(rd->screen)) {
ans->is_visible = false;
return;
}
double time_since_start_blink = now - global_state.cursor_blink_zero_time;
bool cursor_blinking = OPT(cursor_blink_interval) > 0 && global_state.application_focused && time_since_start_blink <= OPT(cursor_stop_blinking_after) ? true : false;
bool do_draw_cursor = true;
if (cursor_blinking) {
int t = (int)(time_since_start_blink * 1000);
int d = (int)(OPT(cursor_blink_interval) * 1000);
int n = t / d;
do_draw_cursor = n % 2 == 0 ? true : false;
double bucket = (n + 1) * d;
double delay = (bucket / 1000.0) - time_since_start_blink;
set_maximum_wait(delay);
}
if (!do_draw_cursor) { ans->is_visible = false; return; }
ans->is_visible = true;
ColorProfile *cp = rd->screen->color_profile;
Cursor *cursor = rd->screen->cursor;
ans->shape = cursor->shape ? cursor->shape : OPT(cursor_shape);
ans->color = colorprofile_to_color(cp, cp->overridden.cursor_color, cp->configured.cursor_color);
if (ans->shape == CURSOR_BLOCK) 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) : rd->dx * mult);
double bottom = top - rd->dy;
if (ans->shape == CURSOR_UNDERLINE) top = bottom + cursor_width(2.0, false);
ans->left = left; ans->right = right; ans->top = top; ans->bottom = bottom;
}
static inline void
update_window_title(Window *w) {
if (w->title && w->title != global_state.application_title) {
global_state.application_title = w->title;
glfwSetWindowTitle(glfw_window_id, PyUnicode_AsUTF8(w->title));
#ifdef __APPLE__
cocoa_update_title(w->title);
#endif
}
}
static PyObject*
simple_render_screen(PyObject UNUSED *self, PyObject *args) {
#define simple_render_screen_doc "Render a Screen object, with no cursor"
Screen *screen;
ssize_t vao_idx, gvao_idx;
float xstart, ystart, dx, dy;
if (!PyArg_ParseTuple(args, "O!nnffff", &Screen_Type, &screen, &vao_idx, &gvao_idx, &xstart, &ystart, &dx, &dy)) return NULL;
static CursorRenderInfo cursor_info = {0};
draw_cells(vao_idx, gvao_idx, xstart, ystart, dx, dy, screen, &cursor_info);
Py_RETURN_NONE;
}
static inline void
render(double now) {
double time_since_last_render = now - last_render_at;
static CursorRenderInfo cursor_info;
if (time_since_last_render < OPT(repaint_delay)) {
set_maximum_wait(OPT(repaint_delay) - time_since_last_render);
return;
}
draw_borders();
cursor_info.is_visible = false;
#define TD global_state.tab_bar_render_data
if (TD.screen && global_state.num_tabs > 1) draw_cells(TD.vao_idx, 0, TD.xstart, TD.ystart, TD.dx, TD.dy, TD.screen, &cursor_info);
#undef TD
if (global_state.num_tabs) {
Tab *tab = global_state.tabs + global_state.active_tab;
for (unsigned int i = 0; i < tab->num_windows; i++) {
Window *w = tab->windows + i;
#define WD w->render_data
if (w->visible && WD.screen) {
if (w->last_drag_scroll_at > 0) {
if (now - w->last_drag_scroll_at >= 0.02) {
if (drag_scroll(w)) {
w->last_drag_scroll_at = now;
set_maximum_wait(0.02);
} else w->last_drag_scroll_at = 0;
} else set_maximum_wait(now - w->last_drag_scroll_at);
}
bool is_active_window = i == tab->active_window;
if (is_active_window) {
collect_cursor_info(&cursor_info, w, now);
update_window_title(w);
} else cursor_info.is_visible = false;
draw_cells(WD.vao_idx, WD.gvao_idx, WD.xstart, WD.ystart, WD.dx, WD.dy, WD.screen, &cursor_info);
if (is_active_window && cursor_info.is_visible && cursor_info.shape != CURSOR_BLOCK) draw_cursor(&cursor_info);
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);
}
}
}
#undef WD
}
glfwSwapBuffers(glfw_window_id);
last_render_at = now;
}
typedef struct { int fd; uint8_t *buf; size_t sz; } ThreadWriteData;
static inline ThreadWriteData*
alloc_twd(size_t sz) {
ThreadWriteData *data = malloc(sizeof(ThreadWriteData));
if (data != NULL) {
data->sz = sz;
data->buf = malloc(sz);
if (data->buf == NULL) { free(data); data = NULL; }
}
return data;
}
static inline void
free_twd(ThreadWriteData *x) {
if (x != NULL) free(x->buf);
free(x);
}
static void*
thread_write(void *x) {
ThreadWriteData *data = (ThreadWriteData*)x;
set_thread_name("KittyWriteStdin");
FILE *f = fdopen(data->fd, "w");
if (fwrite(data->buf, 1, data->sz, f) != data->sz) {
fprintf(stderr, "Failed to write all data\n");
}
fclose(f);
free_twd(data);
return 0;
}
PyObject*
cm_thread_write(PyObject UNUSED *self, PyObject *args) {
static pthread_t thread;
int fd;
Py_ssize_t sz;
const char *buf;
if (!PyArg_ParseTuple(args, "is#", &fd, &buf, &sz)) return NULL;
ThreadWriteData *data = alloc_twd(sz);
if (data == NULL) return PyErr_NoMemory();
data->fd = fd;
memcpy(data->buf, buf, data->sz);
int ret = pthread_create(&thread, NULL, thread_write, data);
if (ret != 0) { free_twd(data); return PyErr_SetFromErrno(PyExc_OSError); }
Py_RETURN_NONE;
}
static inline void
hide_mouse(double now) {
if (glfwGetInputMode(glfw_window_id, GLFW_CURSOR) == GLFW_CURSOR_NORMAL && OPT(mouse_hide_wait) > 0 && now - global_state.last_mouse_activity_at > OPT(mouse_hide_wait)) {
glfwSetInputMode(glfw_window_id, GLFW_CURSOR, GLFW_CURSOR_HIDDEN);
}
}
static inline void
wait_for_events() {
if (maximum_wait < 0) glfwWaitEvents();
else if (maximum_wait > 0) glfwWaitEventsTimeout(maximum_wait);
maximum_wait = -1;
}
static PyObject*
main_loop(ChildMonitor *self) {
#define main_loop_doc "The main thread loop"
while (!glfwWindowShouldClose(glfw_window_id)) {
double now = monotonic();
render(now);
hide_mouse(now);
wait_for_events();
parse_input(self);
}
if (PyErr_Occurred()) return NULL;
Py_RETURN_NONE;
}
// }}}
// I/O thread functions {{{
static inline void
add_children(ChildMonitor *self) {
for (; add_queue_count > 0 && self->count < MAX_CHILDREN;) {
add_queue_count--;
children[self->count] = add_queue[add_queue_count];
add_queue[add_queue_count] = EMPTY_CHILD;
fds[EXTRA_FDS + self->count].fd = children[self->count].fd;
fds[EXTRA_FDS + self->count].events = POLLIN;
self->count++;
}
}
static inline void
hangup(pid_t pid) {
errno = 0;
pid_t pgid = getpgid(pid);
if (errno == ESRCH) return;
if (errno != 0) { perror("Failed to get process group id for child"); return; }
if (killpg(pgid, SIGHUP) != 0) {
if (errno != ESRCH) perror("Failed to kill child");
}
}
static pid_t pid_buf[MAX_CHILDREN] = {0};
static size_t pid_buf_pos = 0;
static pthread_t reap_thread;
static void*
reap(void *pid_p) {
set_thread_name("KittyReapChild");
pid_t pid = *((pid_t*)pid_p);
while(true) {
pid_t ret = waitpid(pid, NULL, 0);
if (ret != pid) {
if (errno == EINTR) continue;
fprintf(stderr, "Failed to reap child process with pid: %d with error: %s\n", pid, strerror(errno));
}
break;
}
return 0;
}
static inline void
cleanup_child(ssize_t i) {
close(children[i].fd);
hangup(children[i].pid);
pid_buf[pid_buf_pos] = children[i].pid;
if (waitpid(pid_buf[pid_buf_pos], NULL, WNOHANG) != pid_buf[pid_buf_pos]) {
errno = 0;
int ret = pthread_create(&reap_thread, NULL, reap, pid_buf + pid_buf_pos);
if (ret != 0) perror("Failed to create thread to reap child");
}
pid_buf_pos = (pid_buf_pos + 1) % MAX_CHILDREN;
}
static inline void
remove_children(ChildMonitor *self) {
if (self->count > 0) {
size_t count = 0;
for (ssize_t i = self->count - 1; i >= 0; i--) {
if (children[i].needs_removal) {
count++;
cleanup_child(i);
remove_queue[remove_queue_count] = children[i];
remove_queue_count++;
children[i] = EMPTY_CHILD;
fds[EXTRA_FDS + i].fd = -1;
size_t num_to_right = self->count - 1 - i;
if (num_to_right > 0) {
memmove(children + i, children + i + 1, num_to_right * sizeof(Child));
memmove(fds + EXTRA_FDS + i, fds + EXTRA_FDS + i + 1, num_to_right * sizeof(struct pollfd));
}
}
}
self->count -= count;
}
}
static bool
read_bytes(int fd, Screen *screen) {
ssize_t len;
size_t available_buffer_space, orig_sz;
screen_mutex(lock, read);
orig_sz = screen->read_buf_sz;
if (orig_sz >= READ_BUF_SZ) { screen_mutex(unlock, read); return true; } // screen read buffer is full
available_buffer_space = READ_BUF_SZ - orig_sz;
screen_mutex(unlock, read);
while(true) {
len = read(fd, screen->read_buf + orig_sz, available_buffer_space);
if (len < 0) {
if (errno == EINTR) continue;
if (errno != EIO) perror("Call to read() from child fd failed");
return false;
}
break;
}
if (UNLIKELY(len == 0)) return false;
screen_mutex(lock, read);
if (screen->new_input_at == 0) screen->new_input_at = monotonic();
if (orig_sz != screen->read_buf_sz) {
// The other thread consumed some of the screen read buffer
memmove(screen->read_buf + screen->read_buf_sz, screen->read_buf + orig_sz, len);
}
screen->read_buf_sz += len;
screen_mutex(unlock, read);
return true;
}
static inline void
drain_fd(int fd) {
while(true) {
ssize_t len = read(fd, drain_buf, sizeof(drain_buf));
if (len < 0) {
if (errno == EINTR) continue;
if (errno != EIO) perror("Call to read() from drain fd failed");
break;
}
break;
}
}
static inline void
write_to_child(int fd, Screen *screen) {
size_t written = 0;
ssize_t ret = 0;
screen_mutex(lock, write);
while (written < screen->write_buf_used) {
ret = write(fd, screen->write_buf + written, screen->write_buf_used - written);
if (ret > 0) { written += ret; }
else if (ret == 0) {
// could mean anything, ignore
break;
} else {
if (errno == EINTR) continue;
if (errno == EWOULDBLOCK || errno == EAGAIN) break;
perror("Call to write() to child fd failed, discarding data.");
written = screen->write_buf_used;
}
}
screen->write_buf_used -= written;
screen_mutex(unlock, write);
}
static void*
io_loop(void *data) {
// The I/O thread loop
size_t i;
int ret;
bool has_more, data_received;
Screen *screen;
ChildMonitor *self = (ChildMonitor*)data;
set_thread_name("KittyChildMon");
while (LIKELY(!self->shutting_down)) {
children_mutex(lock);
remove_children(self);
add_children(self);
children_mutex(unlock);
data_received = false;
for (i = 0; i < self->count + EXTRA_FDS; i++) fds[i].revents = 0;
for (i = 0; i < self->count; i++) {
screen = children[i].screen;
/* printf("i:%lu id:%lu fd: %d read_buf_sz: %lu write_buf_used: %lu\n", i, children[i].id, children[i].fd, screen->read_buf_sz, screen->write_buf_used); */
screen_mutex(lock, read); screen_mutex(lock, write);
fds[EXTRA_FDS + i].events = (screen->read_buf_sz < READ_BUF_SZ ? POLLIN : 0) | (screen->write_buf_used ? POLLOUT : 0);
screen_mutex(unlock, read); screen_mutex(unlock, write);
}
ret = poll(fds, self->count + EXTRA_FDS, -1);
if (ret > 0) {
if (fds[0].revents && POLLIN) drain_fd(fds[0].fd); // wakeup
if (fds[1].revents && POLLIN) {
data_received = true;
drain_fd(fds[1].fd);
children_mutex(lock);
signal_received = true;
children_mutex(unlock);
}
for (i = 0; i < self->count; i++) {
if (fds[EXTRA_FDS + i].revents & (POLLIN | POLLHUP)) {
data_received = true;
has_more = read_bytes(fds[EXTRA_FDS + i].fd, children[i].screen);
if (!has_more) {
// child is dead
children_mutex(lock);
children[i].needs_removal = true;
children_mutex(unlock);
}
}
if (fds[EXTRA_FDS + i].revents & POLLOUT) {
write_to_child(children[i].fd, children[i].screen);
}
if (fds[EXTRA_FDS + i].revents & POLLNVAL) {
// fd was closed
children_mutex(lock);
children[i].needs_removal = true;
children_mutex(unlock);
fprintf(stderr, "The child %lu had its fd unexpectedly closed\n", children[i].id);
}
}
#ifdef DEBUG_POLL_EVENTS
for (i = 0; i < self->count + EXTRA_FDS; i++) {
#define P(w) if (fds[i].revents & w) printf("i:%lu %s\n", i, #w);
P(POLLIN); P(POLLPRI); P(POLLOUT); P(POLLERR); P(POLLHUP); P(POLLNVAL);
#undef P
}
#endif
} else if (ret < 0) {
if (errno != EAGAIN && errno != EINTR) {
perror("Call to poll() failed");
}
}
if (data_received) wakeup_main_loop();
}
children_mutex(lock);
for (i = 0; i < self->count; i++) children[i].needs_removal = true;
remove_children(self);
children_mutex(unlock);
return 0;
}
// }}}
// Boilerplate {{{
static PyMethodDef methods[] = {
METHOD(add_child, METH_VARARGS)
METHOD(needs_write, METH_VARARGS)
METHOD(start, METH_NOARGS)
METHOD(join, METH_NOARGS)
METHOD(wakeup, METH_NOARGS)
METHOD(shutdown, METH_NOARGS)
METHOD(main_loop, METH_NOARGS)
METHOD(mark_for_close, METH_VARARGS)
METHOD(resize_pty, METH_VARARGS)
{"set_iutf8", (PyCFunction)pyset_iutf8, METH_VARARGS, ""},
{NULL} /* Sentinel */
};
PyTypeObject ChildMonitor_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "fast_data_types.ChildMonitor",
.tp_basicsize = sizeof(ChildMonitor),
.tp_dealloc = (destructor)dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = "ChildMonitor",
.tp_methods = methods,
.tp_new = new,
};
static PyMethodDef module_methods[] = {
METHOD(simple_render_screen, METH_VARARGS)
{NULL} /* Sentinel */
};
bool
init_child_monitor(PyObject *module) {
if (PyType_Ready(&ChildMonitor_Type) < 0) return false;
if (PyModule_AddObject(module, "ChildMonitor", (PyObject *)&ChildMonitor_Type) != 0) return false;
Py_INCREF(&ChildMonitor_Type);
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
return true;
}
// }}}

View File

@@ -2,15 +2,13 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import os
import termios
import struct
import fcntl
import signal
from threading import Thread
import os
import sys
import kitty.fast_data_types as fast_data_types
from .constants import terminfo_dir
import kitty.fast_data_types as fast_data_types
def remove_cloexec(fd):
@@ -34,12 +32,11 @@ class Child:
self.forked = True
master, slave = os.openpty() # Note that master and slave are in blocking mode
remove_cloexec(slave)
self.set_iutf8(fd=master)
fast_data_types.set_iutf8(master, True)
stdin, self.stdin = self.stdin, None
if stdin is not None:
stdin_read_fd, stdin_write_fd = os.pipe()
remove_cloexec(stdin_read_fd)
stdin_file = os.fdopen(stdin_write_fd, 'wb')
pid = os.fork()
if pid == 0: # child
try:
@@ -64,50 +61,18 @@ class Child:
try:
os.execvp(self.argv[0], self.argv)
except Exception as err:
# Report he failure and exec a shell instead so that
# we are not left with a forked but not execed process
print('Could not launch:', self.argv[0])
print('\t', err)
input('\nPress Enter to exit:')
print('\nPress Enter to exit:', end=' ')
sys.stdout.flush()
os.execvp('/bin/sh', ['/bin/sh', '-c', 'read w'])
else: # master
os.close(slave)
self.pid = pid
self.child_fd = master
if stdin is not None:
t = Thread(name='WriteStdin', target=stdin_file.write, args=(stdin,))
t.daemon = True
t.start()
os.close(stdin_read_fd)
fast_data_types.thread_write(stdin_write_fd, stdin)
return pid
def resize_pty(self, w, h, ww, wh):
if self.child_fd is not None:
fcntl.ioctl(self.child_fd, termios.TIOCSWINSZ, struct.pack('4H', h, w, ww, wh))
def set_iutf8(self, on=True, fd=None):
fd = fd or self.child_fd
if fd is not None and hasattr(fast_data_types, 'IUTF8'):
attrs = termios.tcgetattr(fd)
if on:
attrs[0] |= fast_data_types.IUTF8
else:
attrs[0] &= ~fast_data_types.IUTF8
termios.tcsetattr(fd, termios.TCSANOW, attrs)
def hangup(self):
if self.pid is not None:
pid, self.pid = self.pid, None
try:
pgrp = os.getpgid(pid)
except ProcessLookupError:
return
os.killpg(pgrp, signal.SIGHUP)
os.close(self.child_fd)
self.child_fd = None
def __del__(self):
self.hangup()
def get_child_status(self):
if self.pid is not None:
try:
return os.waitid(os.P_PID, self.pid, os.WEXITED | os.WNOHANG)
except ChildProcessError:
self.pid = None

View File

@@ -12,6 +12,7 @@ import sys
CSI = '\033['
OSC = '\033]'
def write(x):
@@ -73,6 +74,10 @@ def screen_set_margins(t, b):
write(CSI + '%d;%dr' % (t, b))
def screen_indexn(n):
write(CSI + '%dS' % n)
def screen_erase_in_display(how, private):
write(CSI + ('?' if private else '') + str(how) + 'J')
@@ -85,6 +90,10 @@ def screen_cursor_up2(count):
write(CSI + '%dA' % count)
def screen_cursor_down(count):
write(CSI + '%dB' % count)
def screen_carriage_return():
write('\r')
@@ -101,11 +110,29 @@ def draw(*a):
write(' '.join(a))
def report_device_attributes(mode, char):
x = CSI
if char:
x += ord(char)
if mode:
x += str(mode)
write(CSI + x + 'c')
def write_osc(code, string=''):
if string:
string = ';' + string
write(OSC + str(code) + string + '\x07')
set_dynamic_color = set_color_table_color = write_osc
def replay(raw):
for line in raw.splitlines():
if line.strip():
cmd, rest = line.partition(' ')[::2]
if cmd in {'draw', 'set_title', 'set_icon'}:
if cmd in {'draw', 'set_title', 'set_icon', 'set_dynamic_color', 'set_color_table_color'}:
globals()[cmd](rest)
else:
rest = map(int, rest.split()) if rest else ()
@@ -115,4 +142,7 @@ def replay(raw):
def main(path):
raw = open(path).read()
replay(raw)
input()
try:
input()
except (EOFError, KeyboardInterrupt):
pass

201
kitty/cocoa_window.m Normal file
View File

@@ -0,0 +1,201 @@
/*
* cocoa_window.m
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "data-types.h"
#include <Cocoa/Cocoa.h>
#include <AvailabilityMacros.h>
// Needed for _NSGetProgname
#include <crt_externs.h>
#if (MAC_OS_X_VERSION_MAX_ALLOWED < 101200)
#define NSWindowStyleMaskResizable NSResizableWindowMask
#define NSEventModifierFlagOption NSAlternateKeyMask
#define NSEventModifierFlagCommand NSCommandKeyMask
#define NSEventModifierFlagControl NSControlKeyMask
#endif
@interface MenuDispatcher : NSObject
@end
@implementation MenuDispatcher
@end
static NSObject* menu_dispatcher = NULL;
static NSMenuItem* title_menu = NULL;
static NSString*
find_app_name(void) {
size_t i;
NSDictionary* infoDictionary = [[NSBundle mainBundle] infoDictionary];
// Keys to search for as potential application names
NSString* name_keys[] =
{
@"CFBundleDisplayName",
@"CFBundleName",
@"CFBundleExecutable",
};
for (i = 0; i < sizeof(name_keys) / sizeof(name_keys[0]); i++)
{
id name = [infoDictionary objectForKey:name_keys[i]];
if (name &&
[name isKindOfClass:[NSString class]] &&
![name isEqualToString:@""])
{
return name;
}
}
char** progname = _NSGetProgname();
if (progname && *progname)
return [NSString stringWithUTF8String:*progname];
// Really shouldn't get here
return @"kitty";
}
PyObject*
cocoa_init(PyObject UNUSED *_self) {
// Press and Hold prevents some keys from emitting repeated characters
// See https://github.com/glfw/glfw/issues/1010
NSDictionary* defaults = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:NO], @"ApplePressAndHoldEnabled", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:defaults];
Py_RETURN_NONE;
}
PyObject*
cocoa_create_global_menu(PyObject UNUSED *_self) {
if (menu_dispatcher != NULL) { Py_RETURN_NONE; }
NSString* app_name = find_app_name();
menu_dispatcher = [[MenuDispatcher alloc] init];
NSMenu* bar = [[NSMenu alloc] init];
[NSApp setMainMenu:bar];
NSMenuItem* appMenuItem =
[bar addItemWithTitle:@"" action:NULL keyEquivalent:@""];
NSMenu* appMenu = [[NSMenu alloc] init];
[appMenuItem setSubmenu:appMenu];
[appMenu addItemWithTitle:[NSString stringWithFormat:@"About %@", app_name]
action:@selector(orderFrontStandardAboutPanel:)
keyEquivalent:@""];
[appMenu addItem:[NSMenuItem separatorItem]];
[appMenu addItemWithTitle:[NSString stringWithFormat:@"Hide %@", app_name]
action:@selector(hide:)
keyEquivalent:@"h"];
[[appMenu addItemWithTitle:@"Hide Others"
action:@selector(hideOtherApplications:)
keyEquivalent:@"h"]
setKeyEquivalentModifierMask:NSEventModifierFlagOption | NSEventModifierFlagCommand];
[appMenu addItemWithTitle:@"Show All"
action:@selector(unhideAllApplications:)
keyEquivalent:@""];
[appMenu addItem:[NSMenuItem separatorItem]];
NSMenu* servicesMenu = [[NSMenu alloc] init];
[NSApp setServicesMenu:servicesMenu];
[[appMenu addItemWithTitle:@"Services"
action:NULL
keyEquivalent:@""] setSubmenu:servicesMenu];
[servicesMenu release];
[appMenu addItem:[NSMenuItem separatorItem]];
[appMenu addItemWithTitle:[NSString stringWithFormat:@"Quit %@", app_name]
action:@selector(terminate:)
keyEquivalent:@"q"];
[appMenu release];
NSMenuItem* windowMenuItem =
[bar addItemWithTitle:@"" action:NULL keyEquivalent:@""];
NSMenu* windowMenu = [[NSMenu alloc] initWithTitle:@"Window"];
[windowMenuItem setSubmenu:windowMenu];
[windowMenu addItemWithTitle:@"Minimize"
action:@selector(performMiniaturize:)
keyEquivalent:@"m"];
[windowMenu addItemWithTitle:@"Zoom"
action:@selector(performZoom:)
keyEquivalent:@""];
[windowMenu addItem:[NSMenuItem separatorItem]];
[windowMenu addItemWithTitle:@"Bring All to Front"
action:@selector(arrangeInFront:)
keyEquivalent:@""];
[windowMenu addItem:[NSMenuItem separatorItem]];
[[windowMenu addItemWithTitle:@"Enter Full Screen"
action:@selector(toggleFullScreen:)
keyEquivalent:@"f"]
setKeyEquivalentModifierMask:NSEventModifierFlagControl | NSEventModifierFlagCommand];
[NSApp setWindowsMenu:windowMenu];
[windowMenu release];
[bar release];
Py_RETURN_NONE;
}
void
cocoa_update_title(PyObject *pytitle) {
NSString *title = [[NSString alloc] initWithUTF8String:PyUnicode_AsUTF8(pytitle)];
NSMenu *bar = [NSApp mainMenu];
if (title_menu != NULL) {
[bar removeItem:title_menu];
}
title_menu = [bar addItemWithTitle:@"" action:NULL keyEquivalent:@""];
NSMenu *m = [[NSMenu alloc] initWithTitle:[NSString stringWithFormat:@" :: %@", title]];
[title_menu setSubmenu:m];
[m release];
[title release];
}
PyObject*
cocoa_make_window_resizable(PyObject UNUSED *self, PyObject *window_id) {
NSWindow *window = (NSWindow*)PyLong_AsVoidPtr(window_id);
@try {
[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]);
}
Py_RETURN_NONE;
}
PyObject*
cocoa_get_lang(PyObject UNUSED *self) {
NSString* locale = nil;
NSString* lang_code = [[NSLocale currentLocale] objectForKey:NSLocaleLanguageCode];
NSString* country_code = [[NSLocale currentLocale] objectForKey:NSLocaleCountryCode];
if (lang_code && country_code) {
locale = [NSString stringWithFormat:@"%@_%@", lang_code, country_code];
} else {
locale = [[NSLocale currentLocale] localeIdentifier];
}
if (!locale) { Py_RETURN_NONE; }
return Py_BuildValue("s", [locale UTF8String]);
}
static PyMethodDef module_methods[] = {
{"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""}, \
{"cocoa_make_window_resizable", (PyCFunction)cocoa_make_window_resizable, METH_O, ""}, \
{"cocoa_create_global_menu", (PyCFunction)cocoa_create_global_menu, METH_NOARGS, ""}, \
{"cocoa_init", (PyCFunction)cocoa_init, METH_NOARGS, ""}, \
{NULL, NULL, 0, NULL} /* Sentinel */
};
bool
init_cocoa(PyObject *module) {
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
return true;
}

View File

@@ -6,6 +6,7 @@
*/
#include "data-types.h"
#include <structmember.h>
static uint32_t FG_BG_256[256] = {
0x000000, // 0
@@ -59,6 +60,7 @@ new(PyTypeObject *type, PyObject UNUSED *args, PyObject UNUSED *kwds) {
if (FG_BG_256[255] == 0) create_256_color_table();
memcpy(self->color_table, FG_BG_256, sizeof(FG_BG_256));
memcpy(self->orig_color_table, FG_BG_256, sizeof(FG_BG_256));
self->dirty = true;
}
return (PyObject*) self;
}
@@ -68,6 +70,11 @@ dealloc(ColorProfile* self) {
Py_TYPE(self)->tp_free((PyObject*)self);
}
ColorProfile*
alloc_color_profile() {
return (ColorProfile*)new(&ColorProfile_Type, NULL, NULL);
}
static PyObject*
update_ansi_color_table(ColorProfile *self, PyObject *val) {
@@ -80,9 +87,25 @@ update_ansi_color_table(ColorProfile *self, PyObject *val) {
self->color_table[i] = PyLong_AsUnsignedLong(PyList_GET_ITEM(val, i));
self->orig_color_table[i] = self->color_table[i];
}
self->dirty = true;
Py_RETURN_NONE;
}
color_type
colorprofile_to_color(ColorProfile *self, color_type entry, color_type defval) {
color_type t = entry & 0xFF, r;
switch(t) {
case 1:
r = (entry >> 8) & 0xff;
return self->color_table[r];
case 2:
return entry >> 8;
default:
return defval;
}
}
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"
@@ -99,6 +122,7 @@ as_color(ColorProfile *self, PyObject *val) {
break;
case 2:
col = entry >> 8;
break;
default:
ans = Py_None; Py_INCREF(Py_None);
}
@@ -110,6 +134,7 @@ static PyObject*
reset_color_table(ColorProfile *self) {
#define reset_color_table_doc "Reset all customized colors back to defaults"
memcpy(self->color_table, self->orig_color_table, sizeof(FG_BG_256));
self->dirty = true;
Py_RETURN_NONE;
}
@@ -118,6 +143,7 @@ reset_color(ColorProfile *self, PyObject *val) {
#define reset_color_doc "Reset the specified color"
uint8_t i = PyLong_AsUnsignedLong(val) & 0xff;
self->color_table[i] = self->orig_color_table[i];
self->dirty = true;
Py_RETURN_NONE;
}
@@ -128,33 +154,69 @@ set_color(ColorProfile *self, PyObject *args) {
unsigned long val;
if (!PyArg_ParseTuple(args, "Bk", &i, &val)) return NULL;
self->color_table[i] = val;
self->dirty = true;
Py_RETURN_NONE;
}
uint32_t
to_color(ColorProfile *self, uint32_t entry, uint32_t defval) {
unsigned int t = entry & 0xFF, r;
switch(t) {
case 1:
r = (entry >> 8) & 0xff;
return self->color_table[r];
case 2:
return entry >> 8;
default:
return defval;
}
static PyObject*
set_configured_colors(ColorProfile *self, PyObject *args) {
#define set_configured_colors_doc "Set the configured colors"
if (!PyArg_ParseTuple(args, "II|III", &(self->configured.default_fg), &(self->configured.default_bg), &(self->configured.cursor_color), &(self->configured.highlight_fg), &(self->configured.highlight_bg))) return NULL;
self->dirty = true;
Py_RETURN_NONE;
}
void
copy_color_table_to_buffer(ColorProfile *self, color_type *buf, int offset, size_t stride) {
size_t i;
stride = MAX(1, stride);
for (i = 0, buf = buf + offset; i < sizeof(self->color_table)/sizeof(self->color_table[0]); i++, buf += stride) {
*buf = self->color_table[i];
}
self->dirty = false;
}
static PyObject*
color_table_address(ColorProfile *self) {
#define color_table_address_doc "Pointer address to start of color table"
return PyLong_FromVoidPtr((void*)self->color_table);
}
// Boilerplate {{{
#define CGETSET(name) \
static PyObject* name##_get(ColorProfile *self, void UNUSED *closure) { return PyLong_FromUnsignedLong(colorprofile_to_color(self, self->overridden.name, self->configured.name)); } \
static int name##_set(ColorProfile *self, PyObject *val, void UNUSED *closure) { if (val == NULL) { PyErr_SetString(PyExc_TypeError, "Cannot delete attribute"); return -1; } self->overridden.name = (color_type) PyLong_AsUnsignedLong(val); self->dirty = true; return 0; }
CGETSET(default_fg)
CGETSET(default_bg)
CGETSET(cursor_color)
CGETSET(highlight_fg)
CGETSET(highlight_bg)
static PyGetSetDef getsetters[] = {
GETSET(default_fg)
GETSET(default_bg)
GETSET(cursor_color)
GETSET(highlight_fg)
GETSET(highlight_bg)
{NULL} /* Sentinel */
};
static PyMemberDef members[] = {
{NULL}
};
static PyMethodDef methods[] = {
METHOD(update_ansi_color_table, METH_O)
METHOD(reset_color_table, METH_NOARGS)
METHOD(color_table_address, METH_NOARGS)
METHOD(as_color, METH_O)
METHOD(reset_color, METH_O)
METHOD(set_color, METH_VARARGS)
METHOD(set_configured_colors, METH_VARARGS)
{NULL} /* Sentinel */
};
@@ -166,7 +228,9 @@ PyTypeObject ColorProfile_Type = {
.tp_dealloc = (destructor)dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = "ColorProfile",
.tp_members = members,
.tp_methods = methods,
.tp_getset = getsetters,
.tp_new = new,
};

View File

@@ -2,6 +2,7 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import ast
import json
import os
import re
@@ -37,8 +38,9 @@ def to_cursor_shape(x):
return cshapes[x.lower()]
except KeyError:
raise ValueError(
'Invalid cursor shape: {} allowed values are {}'.
format(x, ', '.join(cshapes))
'Invalid cursor shape: {} allowed values are {}'.format(
x, ', '.join(cshapes)
)
)
@@ -46,10 +48,6 @@ def to_bool(x):
return x.lower() in 'y yes true'.split()
def to_opacity(x):
return max(0.3, min(float(x), 1))
def parse_mods(parts):
def map_mod(m):
@@ -61,8 +59,9 @@ def parse_mods(parts):
mods |= getattr(defines, 'GLFW_MOD_' + map_mod(m.upper()))
except AttributeError:
safe_print(
'Shortcut: {} has an unknown modifier, ignoring'.
format(parts.join('+')),
'Shortcut: {} has an unknown modifier, ignoring'.format(
parts.join('+')
),
file=sys.stderr
)
return
@@ -84,23 +83,57 @@ named_keys = {
}
def parse_key(val, keymap):
sc, action = val.partition(' ')[::2]
action = action.strip()
sc = sc.strip()
if not sc or not action:
return
def parse_shortcut(sc):
parts = sc.split('+')
mods = parse_mods(parts[:-1])
key = parts[-1].upper()
key = getattr(defines, 'GLFW_KEY_' + named_keys.get(key, key), None)
if key is not None:
return mods, key
return None, None
KeyAction = namedtuple('KeyAction', 'func args')
shlex_actions = {'pass_selection_to_program', 'new_window', 'new_tab'}
def parse_key_action(action):
parts = action.split(' ', 1)
func = parts[0]
if len(parts) == 1:
return KeyAction(func, ())
rest = parts[1]
if func == 'combine':
sep, rest = rest.split(' ', 1)
parts = re.split(r'\s*' + re.escape(sep) + r'\s*', rest)
args = tuple(map(parse_key_action, filter(None, parts)))
elif func in shlex_actions:
args = shlex.split(rest)
return KeyAction(func, args)
def parse_key(val, keymap):
sc, action = val.partition(' ')[::2]
sc, action = sc.strip(), action.strip()
if not sc or not action:
return
mods, key = parse_shortcut(sc)
if key is None:
safe_print(
'Shortcut: {} has an unknown key, ignoring'.format(val),
file=sys.stderr
)
return
keymap[(mods, key)] = action
try:
paction = parse_key_action(action)
except Exception:
safe_print(
'Invalid shortcut action: {}. Ignoring.'.format(action),
file=sys.stderr
)
else:
if paction is not None:
keymap[(mods, key)] = paction
def parse_symbol_map(val):
@@ -137,6 +170,42 @@ def parse_symbol_map(val):
return symbol_map
def parse_send_text(val):
parts = val.split(' ')
def abort(msg):
safe_print(
'Send text: {} is invalid ({}), ignoring'.format(val, msg),
file=sys.stderr
)
return {}
if len(parts) < 3:
return abort('Incomplete')
text = ' '.join(parts[2:])
mode, sc = parts[:2]
mods, key = parse_shortcut(sc.strip())
if key is None:
return abort('Invalid shortcut')
text = ast.literal_eval("'''" + text + "'''").encode('utf-8')
if not text:
return abort('Empty text')
if mode in ('all', '*'):
modes = parse_send_text.all_modes
else:
modes = frozenset(mode.split(',')).intersection(
parse_send_text.all_modes
)
if not modes:
return abort('Invalid keyboard modes')
return {mode: {(mods, key): text} for mode in modes}
parse_send_text.all_modes = frozenset({'normal', 'application', 'kitty'})
def to_open_url_modifiers(val):
return parse_mods(val.split('+'))
@@ -150,34 +219,52 @@ def to_layout_names(raw):
raise ValueError('The window layout {} is unknown'.format(p))
def positive_int(x):
return max(0, int(x))
def positive_float(x):
return max(0, float(x))
def adjust_line_height(x):
if x.endswith('%'):
return float(x[:-1].strip()) / 100.0
return int(x)
type_map = {
'scrollback_lines': int,
'adjust_line_height': adjust_line_height,
'scrollback_lines': positive_int,
'scrollback_pager': shlex.split,
'scrollback_in_new_tab': to_bool,
'font_size': to_font_size,
'font_size_delta': float,
'font_size_delta': positive_float,
'cursor_shape': to_cursor_shape,
'cursor_opacity': to_opacity,
'open_url_modifiers': to_open_url_modifiers,
'repaint_delay': int,
'window_border_width': float,
'repaint_delay': positive_int,
'input_delay': positive_int,
'window_border_width': positive_float,
'window_margin_width': positive_float,
'window_padding_width': positive_float,
'wheel_scroll_multiplier': float,
'visual_bell_duration': float,
'visual_bell_duration': positive_float,
'enable_audio_bell': to_bool,
'click_interval': float,
'mouse_hide_wait': float,
'cursor_blink_interval': float,
'cursor_stop_blinking_after': float,
'click_interval': positive_float,
'mouse_hide_wait': positive_float,
'cursor_blink_interval': positive_float,
'cursor_stop_blinking_after': positive_float,
'enabled_layouts': to_layout_names,
'remember_window_size': to_bool,
'initial_window_width': int,
'initial_window_height': int,
'initial_window_width': positive_int,
'initial_window_height': positive_int,
'use_system_wcwidth': to_bool,
'macos_hide_titlebar': to_bool,
}
for name in (
'foreground background cursor active_border_color inactive_border_color'
' selection_foreground selection_background'
' selection_foreground selection_background url_color'
).split():
type_map[name] = lambda x: to_color(x, validate=True)
for i in range(16):
@@ -187,8 +274,18 @@ for a in ('active', 'inactive'):
type_map['%s_tab_%s' % (a, b)] = lambda x: to_color(x, validate=True)
def parse_config(lines):
ans = {'keymap': {}, 'symbol_map': {}}
def parse_config(lines, check_keys=True):
ans = {
'keymap': {},
'symbol_map': {},
'send_text_map': {
'kitty': {},
'normal': {},
'application': {}
}
}
if check_keys:
all_keys = defaults._asdict()
for line in lines:
line = line.strip()
if not line or line.startswith('#'):
@@ -202,6 +299,18 @@ def parse_config(lines):
if key == 'symbol_map':
ans['symbol_map'].update(parse_symbol_map(val))
continue
if key == 'send_text':
stvals = parse_send_text(val)
for k, v in ans['send_text_map'].items():
v.update(stvals.get(k, {}))
continue
if check_keys:
if key not in all_keys:
safe_print(
'Ignoring unknown config key: {}'.format(key),
file=sys.stderr
)
continue
tm = type_map.get(key)
if tm is not None:
val = tm(val)
@@ -212,38 +321,45 @@ def parse_config(lines):
with open(
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'kitty.conf')
) as f:
defaults = parse_config(f.readlines())
defaults = parse_config(f.readlines(), check_keys=False)
Options = namedtuple('Defaults', ','.join(defaults.keys()))
defaults = Options(**defaults)
actions = frozenset(defaults.keymap.values())
actions = frozenset(a.func for a in defaults.keymap.values())
def update_dict(a, b):
a.update(b)
return a
def merge_keymaps(defaults, newvals):
ans = defaults.copy()
for k, v in newvals.items():
if v in {'noop', 'no-op', 'no_op'}:
ans.pop(k, None)
continue
if v in actions:
ans[k] = v
return ans
def merge_dicts(vals, defaults):
return {
k: update_dict(v, vals.get(k, {}))
if isinstance(v, dict) else vals.get(k, v)
for k, v in defaults.items()
}
def merge_dicts(defaults, newvals):
ans = defaults.copy()
ans.update(newvals)
return ans
def merge_configs(ans, vals):
vals['keymap'] = {
k: v
for k, v in vals.get('keymap', {}).items() if v in actions
}
remove_keys = {
k
for k, v in vals.get('keymap', {}).items()
if v in ('noop', 'no-op', 'no_op')
}
ans = merge_dicts(vals, ans)
for k in remove_keys:
ans['keymap'].pop(k, None)
def merge_configs(defaults, vals):
ans = {}
for k, v in defaults.items():
if isinstance(v, dict):
newvals = vals.get(k, {})
if k == 'keymap':
ans['keymap'] = merge_keymaps(v, newvals)
elif k == 'send_text_map':
ans[k] = {
m: merge_dicts(mm, newvals.get(m, {}))
for m, mm in v.items()
}
else:
ans[k] = merge_dicts(v, newvals)
else:
ans[k] = vals.get(k, v)
return ans
@@ -265,7 +381,7 @@ def load_config(*paths, overrides=None) -> Options:
return Options(**ans)
def build_ansi_color_table(opts: Options=defaults):
def build_ansi_color_table(opts: Options = defaults):
def as_int(x):
return (x[0] << 16) | (x[1] << 8) | x[2]

View File

@@ -3,19 +3,15 @@
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import os
import threading
import pwd
import ctypes
import sys
from collections import namedtuple, defaultdict
from collections import namedtuple
from .fast_data_types import (
GLFW_KEY_LEFT_SHIFT, GLFW_KEY_RIGHT_SHIFT, GLFW_KEY_LEFT_ALT,
GLFW_KEY_RIGHT_ALT, GLFW_KEY_LEFT_CONTROL, GLFW_KEY_RIGHT_CONTROL,
GLFW_KEY_LEFT_SUPER, GLFW_KEY_RIGHT_SUPER)
from .fast_data_types import set_boss as set_c_boss
appname = 'kitty'
version = (0, 2, 4)
version = (0, 4, 0)
str_version = '.'.join(map(str, version))
_plat = sys.platform.lower()
isosx = 'darwin' in _plat
@@ -32,15 +28,13 @@ def _get_config_dir():
candidate = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME') or ('~/Library/Preferences' if isosx else '~/.config')))
ans = os.path.join(candidate, appname)
try:
os.makedirs(ans)
except FileExistsError:
pass
os.makedirs(ans, exist_ok=True)
return ans
config_dir = _get_config_dir()
del _get_config_dir
defconf = os.path.join(config_dir, 'kitty.conf')
class ViewportSize:
@@ -61,25 +55,18 @@ def get_boss():
def set_boss(m):
get_boss.boss = m
set_c_boss(m)
def wakeup():
os.write(get_boss.boss.write_wakeup_fd, b'1')
get_boss.boss.child_monitor.wakeup()
def queue_action(func, *args):
get_boss.boss.queue_action(func, *args)
is_key_pressed = defaultdict(lambda: False)
mouse_button_pressed = defaultdict(lambda: False)
mouse_cursor_pos = [0, 0]
viewport_size = ViewportSize()
cell_size = ViewportSize()
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')
main_thread = threading.current_thread()
shell_path = pwd.getpwuid(os.geteuid()).pw_shell or '/bin/sh'
GLint = ctypes.c_int if ctypes.sizeof(ctypes.c_int) == 4 else ctypes.c_long
@@ -90,7 +77,51 @@ if ctypes.sizeof(GLfloat) != 4:
if ctypes.sizeof(GLint) != 4:
raise RuntimeError('int size is not 4')
MODIFIER_KEYS = (
GLFW_KEY_LEFT_SHIFT, GLFW_KEY_RIGHT_SHIFT, GLFW_KEY_LEFT_ALT,
GLFW_KEY_RIGHT_ALT, GLFW_KEY_LEFT_CONTROL, GLFW_KEY_RIGHT_CONTROL,
GLFW_KEY_LEFT_SUPER, GLFW_KEY_RIGHT_SUPER)
def get_glfw_lib_name():
try:
for line in open('/proc/self/maps'):
lib = line.split()[-1]
if '/libglfw.so' in lib:
return lib
except Exception as err:
try:
print(str(err), file=sys.stderr)
except Exception:
pass
return 'libglfw.so.3'
def glfw_lib():
ans = getattr(glfw_lib, 'ans', None)
if ans is None:
ans = glfw_lib.ans = ctypes.CDLL('libglfw.3.dylib' if isosx else get_glfw_lib_name())
return ans
def selection_clipboard_funcs():
ans = getattr(selection_clipboard_funcs, 'ans', None)
if ans is None:
lib = glfw_lib()
if hasattr(lib, 'glfwGetX11SelectionString'):
g = lib.glfwGetX11SelectionString
g.restype = ctypes.c_char_p
g.argtypes = []
s = lib.glfwSetX11SelectionString
s.restype = None
s.argtypes = [ctypes.c_char_p]
ans = g, s
else:
ans = None, None
selection_clipboard_funcs.ans = ans
return ans
def x11_window_id(window):
lib = glfw_lib()
lib.glfwGetX11Window.restype = ctypes.c_int32
lib.glfwGetX11Window.argtypes = [ctypes.c_void_p]
return lib.glfwGetX11Window(window.window_id())
iswayland = not isosx and hasattr(glfw_lib(), 'glfwGetWaylandDisplay')

View File

@@ -11,6 +11,8 @@
#include <math.h>
#import <CoreGraphics/CGBitmapContext.h>
#import <CoreText/CTFont.h>
#include <Foundation/Foundation.h>
#include <CoreText/CoreText.h>
#import <Foundation/NSString.h>
#import <Foundation/NSDictionary.h>
@@ -20,40 +22,113 @@ typedef struct {
unsigned int units_per_em;
float ascent, descent, leading, underline_position, underline_thickness, point_sz, scaled_point_sz;
CTFontRef font;
PyObject *family_name, *full_name, *postscript_name;
PyObject *family_name, *full_name, *postscript_name, *path;
} Face;
static PyObject*
convert_cfstring(CFStringRef src) {
static inline PyObject*
convert_cfstring(CFStringRef src, int free_src) {
#define SZ 2048
static char buf[SZ+2] = {0};
if(!CFStringGetCString(src, buf, SZ, kCFStringEncodingUTF8)) { PyErr_SetString(PyExc_ValueError, "Failed to convert CFString"); return NULL; }
return PyUnicode_FromString(buf);
PyObject *ans = NULL;
if(!CFStringGetCString(src, buf, SZ, kCFStringEncodingUTF8)) PyErr_SetString(PyExc_ValueError, "Failed to convert CFString");
else ans = PyUnicode_FromString(buf);
if (free_src) CFRelease(src);
return ans;
#undef SZ
}
static PyObject*
font_descriptor_to_python(CTFontDescriptorRef descriptor) {
NSURL *url = (NSURL *) CTFontDescriptorCopyAttribute(descriptor, kCTFontURLAttribute);
NSString *psName = (NSString *) CTFontDescriptorCopyAttribute(descriptor, kCTFontNameAttribute);
NSString *family = (NSString *) CTFontDescriptorCopyAttribute(descriptor, kCTFontFamilyNameAttribute);
NSString *style = (NSString *) CTFontDescriptorCopyAttribute(descriptor, kCTFontStyleNameAttribute);
NSDictionary *traits = (NSDictionary *) CTFontDescriptorCopyAttribute(descriptor, kCTFontTraitsAttribute);
unsigned int straits = [traits[(id)kCTFontSymbolicTrait] unsignedIntValue];
NSNumber *weightVal = traits[(id)kCTFontWeightTrait];
NSNumber *widthVal = traits[(id)kCTFontWidthTrait];
PyObject *ans = Py_BuildValue("{ssssssss sOsOsO sfsfsI}",
"path", [[url path] UTF8String],
"postscript_name", [psName UTF8String],
"family", [family UTF8String],
"style", [style UTF8String],
"bold", (straits & kCTFontBoldTrait) != 0 ? Py_True : Py_False,
"italic", (straits & kCTFontItalicTrait) != 0 ? Py_True : Py_False,
"monospace", (straits & kCTFontMonoSpaceTrait) != 0 ? Py_True : Py_False,
"weight", [weightVal floatValue],
"width", [widthVal floatValue],
"traits", straits
);
[url release];
[psName release];
[family release];
[style release];
[traits release];
return ans;
}
static CTFontDescriptorRef
font_descriptor_from_python(PyObject *src) {
CTFontSymbolicTraits symbolic_traits = 0;
NSMutableDictionary *attrs = [NSMutableDictionary dictionary];
PyObject *t = PyDict_GetItemString(src, "traits");
if (t == NULL) {
symbolic_traits = (
(PyDict_GetItemString(src, "bold") == Py_True) ? kCTFontBoldTrait : 0 |
(PyDict_GetItemString(src, "italic") == Py_True) ? kCTFontItalicTrait : 0 |
(PyDict_GetItemString(src, "monospace") == Py_True) ? kCTFontMonoSpaceTrait : 0);
} else {
symbolic_traits = PyLong_AsUnsignedLong(t);
}
NSDictionary *traits = @{(id)kCTFontSymbolicTrait:[NSNumber numberWithUnsignedInt:symbolic_traits]};
attrs[(id)kCTFontTraitsAttribute] = traits;
#define SET(x, attr) \
t = PyDict_GetItemString(src, #x); \
if (t) attrs[(id)attr] = [NSString stringWithUTF8String:PyUnicode_AsUTF8(t)];
SET(family, kCTFontFamilyNameAttribute);
SET(style, kCTFontStyleNameAttribute);
SET(postscript_name, kCTFontNameAttribute);
#undef SET
return CTFontDescriptorCreateWithAttributes((CFDictionaryRef) attrs);
}
PyObject*
coretext_all_fonts(PyObject UNUSED *_self) {
static CTFontCollectionRef collection = NULL;
if (collection == NULL) collection = CTFontCollectionCreateFromAvailableFonts(NULL);
NSArray *matches = (NSArray *) CTFontCollectionCreateMatchingFontDescriptors(collection);
PyObject *ans = PyTuple_New([matches count]), *temp;
if (ans == NULL) return PyErr_NoMemory();
for (unsigned int i = 0; i < [matches count]; i++) {
temp = font_descriptor_to_python((CTFontDescriptorRef) matches[i]);
if (temp == NULL) { Py_DECREF(ans); return NULL; }
PyTuple_SET_ITEM(ans, i, temp); temp = NULL;
}
return ans;
}
static PyObject*
new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
Face *self;
int bold, italic, monospace;
char *cfamily;
PyObject *descriptor;
float point_sz, dpi;
if(!PyArg_ParseTuple(args, "spppff", &cfamily, &bold, &italic, &monospace, &point_sz, &dpi)) return NULL;
NSString *family = [[NSString alloc] initWithCString:cfamily encoding:NSUTF8StringEncoding];
if (family == NULL) return PyErr_NoMemory();
if(!PyArg_ParseTuple(args, "Off", &descriptor, &point_sz, &dpi)) return NULL;
self = (Face *)type->tp_alloc(type, 0);
if (self) {
CTFontSymbolicTraits symbolic_traits = (bold ? kCTFontBoldTrait : 0) | (italic ? kCTFontItalicTrait : 0) | (monospace ? kCTFontMonoSpaceTrait : 0);
NSDictionary *font_traits = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:symbolic_traits] forKey:(NSString *)kCTFontSymbolicTrait];
NSDictionary *font_attributes = [NSDictionary dictionaryWithObjectsAndKeys:family, kCTFontFamilyNameAttribute, font_traits, kCTFontTraitsAttribute, nil];
CTFontDescriptorRef descriptor = CTFontDescriptorCreateWithAttributes((CFDictionaryRef)font_attributes);
if (descriptor) {
CTFontDescriptorRef desc = font_descriptor_from_python(descriptor);
if (desc) {
self->point_sz = point_sz;
self->scaled_point_sz = (dpi / 72.0) * point_sz;
self->font = CTFontCreateWithFontDescriptor(descriptor, self->scaled_point_sz, NULL);
CFRelease(descriptor);
self->font = CTFontCreateWithFontDescriptor(desc, self->scaled_point_sz, NULL);
CFRelease(desc);
if (!self->font) { Py_CLEAR(self); PyErr_SetString(PyExc_ValueError, "Failed to create CTFont object"); }
else {
self->units_per_em = CTFontGetUnitsPerEm(self->font);
@@ -63,17 +138,19 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
self->underline_position = CTFontGetUnderlinePosition(self->font);
self->underline_thickness = CTFontGetUnderlineThickness(self->font);
self->scaled_point_sz = CTFontGetSize(self->font);
self->family_name = convert_cfstring(CTFontCopyFamilyName(self->font));
self->full_name = convert_cfstring(CTFontCopyFullName(self->font));
self->postscript_name = convert_cfstring(CTFontCopyPostScriptName(self->font));
if (self->family_name == NULL || self->full_name == NULL || self->postscript_name == NULL) { Py_CLEAR(self); }
self->family_name = convert_cfstring(CTFontCopyFamilyName(self->font), 1);
self->full_name = convert_cfstring(CTFontCopyFullName(self->font), 1);
self->postscript_name = convert_cfstring(CTFontCopyPostScriptName(self->font), 1);
NSURL *url = (NSURL*)CTFontCopyAttribute(self->font, kCTFontURLAttribute);
self->path = PyUnicode_FromString([[url path] UTF8String]);
[url release];
if (self->family_name == NULL || self->full_name == NULL || self->postscript_name == NULL || self->path == NULL) { Py_CLEAR(self); }
}
} else {
Py_CLEAR(self);
PyErr_NoMemory();
}
}
[ family release ];
return (PyObject*)self;
}
@@ -81,7 +158,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
static void
dealloc(Face* self) {
if (self->font) CFRelease(self->font);
Py_CLEAR(self->family_name); Py_CLEAR(self->full_name); Py_CLEAR(self->postscript_name);
Py_CLEAR(self->family_name); Py_CLEAR(self->full_name); Py_CLEAR(self->postscript_name); Py_CLEAR(self->path);
Py_TYPE(self)->tp_free((PyObject*)self);
}
@@ -123,14 +200,13 @@ cell_size(Face *self) {
#define count (128 - 32)
unichar chars[count+1] = {0};
CGGlyph glyphs[count+1] = {0};
for (int i = 0; i < count; i++) chars[i] = 32 + i;
unsigned int width = 0, w, i;
for (i = 0; i < count; i++) chars[i] = 32 + i;
CTFontGetGlyphsForCharacters(self->font, chars, glyphs, count);
CGSize advances[1] = {0};
unsigned int width = 0, w;
for (int i = 0; i < count; i++) {
for (i = 0; i < count; i++) {
if (glyphs[i]) {
CTFontGetAdvancesForGlyphs(self->font, kCTFontOrientationHorizontal, glyphs+1, advances, 1);
w = (unsigned int)(ceilf(advances[0].width));
w = (unsigned int)(ceilf(
CTFontGetAdvancesForGlyphs(self->font, kCTFontOrientationHorizontal, glyphs+i, NULL, 1)));
if (w > width) width = w;
}
}
@@ -177,9 +253,8 @@ render_char(Face *self, PyObject *args) {
// TODO: Scale the glyph if its bbox is larger than the image by using a non-identity transform
/* CGRect rect = CTFontGetBoundingRectsForGlyphs(font, kCTFontOrientationHorizontal, glyphs, 0, 1); */
CGContextSetTextMatrix(ctx, transform);
CGFloat leading = self->leading > 0 ? self->leading : 1; // Ensure at least one pixel of leading so that antialiasing works at the left edge
CGFloat pos_x = leading, pos_y = height - self->ascent;
CGContextSetTextPosition(ctx, pos_x, pos_y);
CGFloat pos_y = height - self->ascent;
CGContextSetTextPosition(ctx, 0, pos_y);
CTFontDrawGlyphs(font, &glyph, &CGPointZero, 1, ctx);
}
@@ -198,8 +273,8 @@ repr(Face *self) {
snprintf(buf, sizeof(buf)/sizeof(buf[0]), "ascent=%.1f, descent=%.1f, leading=%.1f, point_sz=%.1f, scaled_point_sz=%.1f, underline_position=%.1f underline_thickness=%.1f",
(self->ascent), (self->descent), (self->leading), (self->point_sz), (self->scaled_point_sz), (self->underline_position), (self->underline_thickness));
return PyUnicode_FromFormat(
"Face(family=%U, full_name=%U, postscript_name=%U, units_per_em=%u, %s)",
self->family_name, self->full_name, self->postscript_name, self->units_per_em, buf
"Face(family=%U, full_name=%U, postscript_name=%U, path=%U, units_per_em=%u, %s)",
self->family_name, self->full_name, self->postscript_name, self->path, self->units_per_em, buf
);
}
@@ -217,6 +292,7 @@ static PyMemberDef members[] = {
MEM(underline_position, T_FLOAT),
MEM(underline_thickness, T_FLOAT),
MEM(family_name, T_OBJECT),
MEM(path, T_OBJECT),
MEM(full_name, T_OBJECT),
MEM(postscript_name, T_OBJECT),
{NULL} /* Sentinel */
@@ -244,11 +320,16 @@ PyTypeObject Face_Type = {
.tp_repr = (reprfunc)repr,
};
static PyMethodDef module_methods[] = {
{"coretext_all_fonts", (PyCFunction)coretext_all_fonts, METH_NOARGS, ""},
{NULL, NULL, 0, NULL} /* Sentinel */
};
int
init_CoreText(PyObject *module) {
if (PyType_Ready(&Face_Type) < 0) return 0;
if (PyModule_AddObject(module, "CTFace", (PyObject *)&Face_Type) != 0) return 0;
if (PyModule_AddFunctions(module, module_methods) != 0) return 0;
return 1;
}

View File

@@ -27,12 +27,15 @@ static int __eq__(Cursor *a, Cursor *b) {
return EQ(bold) && EQ(italic) && EQ(strikethrough) && EQ(reverse) && EQ(decoration) && EQ(fg) && EQ(bg) && EQ(decoration_fg) && EQ(x) && EQ(y) && EQ(shape) && EQ(blink);
}
static const char* cursor_names[NUM_OF_CURSOR_SHAPES] = { "NO_SHAPE", "BLOCK", "BEAM", "UNDERLINE" };
#define BOOL(x) ((x) ? Py_True : Py_False)
static PyObject *
repr(Cursor *self) {
return PyUnicode_FromFormat(
"Cursor(x=%u, y=%u, shape=%d, blink=%R, fg=#%08x, bg=#%08x, bold=%R, italic=%R, reverse=%R, strikethrough=%R, decoration=%d, decoration_fg=#%08x)",
self->x, self->y, self->shape, BOOL(self->blink), self->fg, self->bg, BOOL(self->bold), BOOL(self->italic), BOOL(self->reverse), BOOL(self->strikethrough), self->decoration, self->decoration_fg
"Cursor(x=%u, y=%u, shape=%s, blink=%R, fg=#%08x, bg=#%08x, bold=%R, italic=%R, reverse=%R, strikethrough=%R, decoration=%d, decoration_fg=#%08x)",
self->x, self->y, (self->shape < NUM_OF_CURSOR_SHAPES ? cursor_names[self->shape] : "INVALID"),
BOOL(self->blink), self->fg, self->bg, BOOL(self->bold), BOOL(self->italic), BOOL(self->reverse), BOOL(self->strikethrough), self->decoration, self->decoration_fg
);
}
@@ -51,7 +54,7 @@ reset_display_attrs(Cursor *self) {
void cursor_reset(Cursor *self) {
cursor_reset_display_attrs(self);
self->x = 0; self->y = 0;
self->shape = 0; self->blink = false;
self->shape = NO_CURSOR_SHAPE; self->blink = false;
}
void cursor_copy_to(Cursor *src, Cursor *dest) {
@@ -75,7 +78,7 @@ BOOL_GETSET(Cursor, blink)
static PyMemberDef members[] = {
{"x", T_UINT, offsetof(Cursor, x), 0, "x"},
{"y", T_UINT, offsetof(Cursor, y), 0, "y"},
{"shape", T_UBYTE, offsetof(Cursor, shape), 0, "shape"},
{"shape", T_INT, offsetof(Cursor, shape), 0, "shape"},
{"decoration", T_UBYTE, offsetof(Cursor, decoration), 0, "decoration"},
{"fg", T_ULONG, offsetof(Cursor, fg), 0, "fg"},
{"bg", T_ULONG, offsetof(Cursor, bg), 0, "bg"},

View File

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

18
kitty/cursor_vertex.glsl Normal file
View File

@@ -0,0 +1,18 @@
#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

@@ -6,19 +6,51 @@
*/
#include "data-types.h"
#include "glfw.h"
#include "gl.h"
#include "modes.h"
#include <stddef.h>
#include <termios.h>
#ifdef WITH_PROFILER
#include <gperftools/profiler.h>
#endif
static char drain_buf[1024] = {0};
/* To millisecond (10^-3) */
#define SEC_TO_MS 1000
static PyObject*
drain_read(PyObject UNUSED *self, PyObject *fd) {
ALLOW_UNUSED_RESULT
read(PyLong_AsLong(fd), drain_buf, sizeof(drain_buf));
END_ALLOW_UNUSED_RESULT
Py_RETURN_NONE;
/* To microseconds (10^-6) */
#define MS_TO_US 1000
#define SEC_TO_US (SEC_TO_MS * MS_TO_US)
/* To nanoseconds (10^-9) */
#define US_TO_NS 1000
#define MS_TO_NS (MS_TO_US * US_TO_NS)
#define SEC_TO_NS (SEC_TO_MS * MS_TO_NS)
/* Conversion from nanoseconds */
#define NS_TO_MS (1000 * 1000)
#define NS_TO_US (1000)
#ifdef __APPLE__
#include <mach/mach_time.h>
static mach_timebase_info_data_t timebase = {0};
static inline double monotonic_() {
return ((double)(mach_absolute_time() * timebase.numer) / timebase.denom)/SEC_TO_NS;
}
#else
#include <time.h>
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) / SEC_TO_NS) + (double)ts.tv_sec;
}
#endif
double monotonic() { return monotonic_(); }
static PyObject*
wcwidth_wrap(PyObject UNUSED *self, PyObject *chr) {
@@ -31,19 +63,52 @@ change_wcwidth_wrap(PyObject UNUSED *self, PyObject *use9) {
Py_RETURN_NONE;
}
static PyObject*
redirect_std_streams(PyObject UNUSED *self, PyObject *args) {
char *devnull = NULL;
if (!PyArg_ParseTuple(args, "s", &devnull)) return NULL;
if (freopen(devnull, "r", stdin) == NULL) return PyErr_SetFromErrno(PyExc_EnvironmentError);
if (freopen(devnull, "w", stdout) == NULL) return PyErr_SetFromErrno(PyExc_EnvironmentError);
if (freopen(devnull, "w", stderr) == NULL) return PyErr_SetFromErrno(PyExc_EnvironmentError);
Py_RETURN_NONE;
}
static PyObject*
pyset_iutf8(PyObject UNUSED *self, PyObject *args) {
int fd, on;
if (!PyArg_ParseTuple(args, "ip", &fd, &on)) return NULL;
if (!set_iutf8(fd, on & 1)) return PyErr_SetFromErrno(PyExc_OSError);
Py_RETURN_NONE;
}
#ifdef WITH_PROFILER
static PyObject*
start_profiler(PyObject UNUSED *self, PyObject *args) {
char *path;
if (!PyArg_ParseTuple(args, "s", &path)) return NULL;
ProfilerStart(path);
Py_RETURN_NONE;
}
static PyObject*
stop_profiler(PyObject UNUSED *self) {
ProfilerStop();
Py_RETURN_NONE;
}
#endif
static PyMethodDef module_methods[] = {
GL_METHODS
{"drain_read", (PyCFunction)drain_read, METH_O, ""},
{"set_iutf8", (PyCFunction)pyset_iutf8, METH_VARARGS, ""},
{"thread_write", (PyCFunction)cm_thread_write, METH_VARARGS, ""},
{"parse_bytes", (PyCFunction)parse_bytes, METH_VARARGS, ""},
{"parse_bytes_dump", (PyCFunction)parse_bytes_dump, METH_VARARGS, ""},
{"read_bytes", (PyCFunction)read_bytes, METH_VARARGS, ""},
{"read_bytes_dump", (PyCFunction)read_bytes_dump, METH_VARARGS, ""},
{"redirect_std_streams", (PyCFunction)redirect_std_streams, METH_VARARGS, ""},
{"wcwidth", (PyCFunction)wcwidth_wrap, METH_O, ""},
{"change_wcwidth", (PyCFunction)change_wcwidth_wrap, METH_O, ""},
#ifndef __APPLE__
{"get_fontconfig_font", (PyCFunction)get_fontconfig_font, METH_VARARGS, ""},
#ifdef WITH_PROFILER
{"start_profiler", (PyCFunction)start_profiler, METH_VARARGS, ""},
{"stop_profiler", (PyCFunction)stop_profiler, METH_NOARGS, ""},
#endif
GLFW_FUNC_WRAPPERS
{NULL, NULL, 0, NULL} /* Sentinel */
};
@@ -56,34 +121,71 @@ static struct PyModuleDef module = {
.m_methods = module_methods
};
#include <termios.h>
PyMODINIT_FUNC
extern int init_LineBuf(PyObject *);
extern int init_HistoryBuf(PyObject *);
extern int init_Cursor(PyObject *);
extern bool init_child_monitor(PyObject *);
extern int init_Line(PyObject *);
extern int init_ColorProfile(PyObject *);
extern int init_Screen(PyObject *);
extern int init_Face(PyObject *);
extern bool init_freetype_library(PyObject*);
extern bool init_fontconfig_library(PyObject*);
extern bool init_glfw(PyObject *m);
extern bool init_sprites(PyObject *module);
extern bool init_state(PyObject *module);
extern bool init_keys(PyObject *module);
extern bool init_graphics(PyObject *module);
extern bool init_shaders(PyObject *module);
extern bool init_shaders_debug(PyObject *module);
#ifdef __APPLE__
extern int init_CoreText(PyObject *);
extern bool init_cocoa(PyObject *module);
#endif
EXPORTED PyMODINIT_FUNC
PyInit_fast_data_types(void) {
PyObject *m;
m = PyModule_Create(&module);
if (m == NULL) return NULL;
#ifdef __APPLE__
mach_timebase_info(&timebase);
#endif
if (m != NULL) {
if (!init_LineBuf(m)) return NULL;
if (!init_HistoryBuf(m)) return NULL;
if (!init_Line(m)) return NULL;
if (!init_Cursor(m)) return NULL;
if (!init_child_monitor(m)) return NULL;
if (!init_ColorProfile(m)) return NULL;
if (!init_SpriteMap(m)) return NULL;
if (!init_ChangeTracker(m)) return NULL;
if (!init_Screen(m)) return NULL;
if (!add_module_gl_constants(m)) return NULL;
if (!init_glfw(m)) return NULL;
if (!init_Window(m)) return NULL;
if (!init_sprites(m)) return NULL;
if (!init_state(m)) return NULL;
if (!init_keys(m)) return NULL;
if (!init_graphics(m)) return NULL;
if (PySys_GetObject("debug_gl") == Py_True) {
if (!init_shaders_debug(m)) return NULL;
} else {
if (!init_shaders(m)) return NULL;
}
#ifdef __APPLE__
if (!init_CoreText(m)) return NULL;
if (!init_cocoa(m)) return NULL;
#else
if (!init_Face(m)) return NULL;
if (!init_freetype_library(m)) return NULL;
if (!init_fontconfig_library(m)) return NULL;
#endif
#define OOF(n) #n, offsetof(Cell, n)
if (PyModule_AddObject(m, "CELL", Py_BuildValue("{sI sI sI sI sI sI sI sI sI}",
OOF(ch), OOF(fg), OOF(bg), OOF(decoration_fg), OOF(cc), OOF(sprite_x), OOF(sprite_y), OOF(sprite_z), "size", sizeof(Cell))) != 0) return NULL;
#undef OOF
PyModule_AddIntConstant(m, "BOLD", BOLD_SHIFT);
PyModule_AddIntConstant(m, "ITALIC", ITALIC_SHIFT);
PyModule_AddIntConstant(m, "REVERSE", REVERSE_SHIFT);
@@ -99,17 +201,6 @@ PyInit_fast_data_types(void) {
PyModule_AddIntMacro(m, DECCOLM);
PyModule_AddIntMacro(m, DECOM);
PyModule_AddIntMacro(m, IRM);
PyModule_AddIntMacro(m, DATA_CELL_SIZE);
PyModule_AddIntMacro(m, ANY_MODE);
PyModule_AddIntMacro(m, MOTION_MODE);
PyModule_AddIntMacro(m, BUTTON_MODE);
PyModule_AddIntMacro(m, SGR_PROTOCOL);
PyModule_AddIntMacro(m, NORMAL_PROTOCOL);
PyModule_AddIntMacro(m, URXVT_PROTOCOL);
PyModule_AddIntMacro(m, UTF8_PROTOCOL);
#ifdef IUTF8
PyModule_AddIntMacro(m, IUTF8);
#endif
}
return m;

View File

@@ -10,36 +10,36 @@
#include <stdint.h>
#include <stdbool.h>
#include <poll.h>
#include <pthread.h>
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#define UNUSED __attribute__ ((unused))
#define EXPORTED __attribute__ ((visibility ("default")))
#define LIKELY(x) __builtin_expect (!!(x), 1)
#define UNLIKELY(x) __builtin_expect (!!(x), 0)
#define MAX(x, y) (((x) > (y)) ? (x) : (y))
#define MIN(x, y) (((x) > (y)) ? (y) : (x))
#define xstr(s) str(s)
#define str(s) #s
#define fatal(...) { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); exit(EXIT_FAILURE); }
typedef Py_UCS4 char_type;
typedef uint32_t char_type;
typedef uint32_t color_type;
typedef uint32_t combining_type;
typedef unsigned int index_type;
typedef uint16_t sprite_index;
typedef uint16_t attrs_type;
typedef enum CursorShapes { NO_CURSOR_SHAPE, CURSOR_BLOCK, CURSOR_BEAM, CURSOR_UNDERLINE, NUM_OF_CURSOR_SHAPES } CursorShape;
#define ERROR_PREFIX "[PARSE ERROR]"
#define ANY_MODE 3
#define MOTION_MODE 2
#define BUTTON_MODE 1
#define NORMAL_PROTOCOL 0
#define UTF8_PROTOCOL 1
#define SGR_PROTOCOL 2
#define URXVT_PROTOCOL 3
typedef enum MouseTrackingModes { NO_TRACKING, BUTTON_MODE, MOTION_MODE, ANY_MODE } MouseTrackingMode;
typedef enum MouseTrackingProtocols { NORMAL_PROTOCOL, UTF8_PROTOCOL, SGR_PROTOCOL, URXVT_PROTOCOL} MouseTrackingProtocol;
typedef enum MouseShapes { BEAM, HAND, ARROW } MouseShape;
#define CELL_FIELD_COUNT 5
#define CELL_SIZE (CELL_FIELD_COUNT * 4)
// The data cell size must be a multiple of 3
#define DATA_CELL_SIZE 2 * 3
#define CHAR_MASK 0xFFFFFF
#define ATTRS_SHIFT 24
#define ATTRS_MASK_WITHOUT_WIDTH 0xFC000000
#define MAX_CHILDREN 256
#define BLANK_CHAR 0
#define ATTRS_MASK_WITHOUT_WIDTH 0xFFC
#define WIDTH_MASK 3
#define DECORATION_SHIFT 2
#define DECORATION_MASK 3
@@ -54,50 +54,24 @@ typedef unsigned int index_type;
#define UTF8_REJECT 1
#define UNDERCURL_CODE 6
#define DECORATION_FG_CODE 58
#define CHAR_IS_BLANK(ch) ((ch) == 32 || (ch) == 0)
#define CURSOR_BLOCK 1
#define CURSOR_BEAM 2
#define CURSOR_UNDERLINE 3
#define FG 1
#define BG 2
#define CURSOR_TO_ATTRS(c, w) \
((w) | (((c->decoration & 3) << DECORATION_SHIFT) | ((c->bold & 1) << BOLD_SHIFT) | \
((c->italic & 1) << ITALIC_SHIFT) | ((c->reverse & 1) << REVERSE_SHIFT) | ((c->strikethrough & 1) << STRIKE_SHIFT))) << ATTRS_SHIFT
((c->italic & 1) << ITALIC_SHIFT) | ((c->reverse & 1) << REVERSE_SHIFT) | ((c->strikethrough & 1) << STRIKE_SHIFT)))
#define ATTRS_TO_CURSOR(a, c) \
c->decoration = (a >> DECORATION_SHIFT) & 3; c->bold = (a >> BOLD_SHIFT) & 1; c->italic = (a >> ITALIC_SHIFT) & 1; \
c->reverse = (a >> REVERSE_SHIFT) & 1; c->strikethrough = (a >> STRIKE_SHIFT) & 1;
#define SET_ATTRIBUTE(chars, shift, val) \
mask = shift == DECORATION_SHIFT ? 3 : 1; \
val = (val & mask) << (ATTRS_SHIFT + shift); \
mask = ~(mask << (ATTRS_SHIFT + shift)); \
for (index_type i = 0; i < self->xnum; i++) (chars)[i] = ((chars)[i] & mask) | val;
#define COPY_CELL(src, s, dest, d) \
(dest)->chars[d] = (src)->chars[s]; \
(dest)->fg_colors[d] = (src)->fg_colors[s]; \
(dest)->bg_colors[d] = (src)->bg_colors[s]; \
(dest)->decoration_fg[d] = (src)->decoration_fg[s]; \
(dest)->combining_chars[d] = (src)->combining_chars[s];
(dest)->cells[d] = (src)->cells[s];
#define COPY_SELF_CELL(s, d) COPY_CELL(self, s, self, d)
#define COPY_LINE(src, dest) \
memcpy((dest)->chars, (src)->chars, sizeof(char_type) * MIN((src)->xnum, (dest)->xnum)); \
memcpy((dest)->fg_colors, (src)->fg_colors, sizeof(color_type) * MIN((src)->xnum, (dest)->xnum)); \
memcpy((dest)->bg_colors, (src)->bg_colors, sizeof(color_type) * MIN((src)->xnum, (dest)->xnum)); \
memcpy((dest)->decoration_fg, (src)->decoration_fg, sizeof(color_type) * MIN((src)->xnum, (dest)->xnum)); \
memcpy((dest)->combining_chars, (src)->combining_chars, sizeof(combining_type) * MIN((src)->xnum, (dest)->xnum));
#define CLEAR_LINE(l, num) \
for (index_type i = 0; i < (num); i++) (l)->chars[i] = (1 << ATTRS_SHIFT) | 32; \
memset((l)->fg_colors, 0, (num) * sizeof(color_type)); \
memset((l)->bg_colors, 0, (num) * sizeof(color_type)); \
memset((l)->decoration_fg, 0, (num) * sizeof(color_type)); \
memset((l)->combining_chars, 0, (num) * sizeof(combining_type));
#define METHOD(name, arg_type) {#name, (PyCFunction)name, arg_type, name##_doc},
#define BOOL_GETSET(type, x) \
@@ -107,11 +81,15 @@ typedef unsigned int index_type;
#define GETSET(x) \
{#x, (getter) x##_get, (setter) x##_set, #x, NULL},
#ifndef EXTRA_INIT
#define EXTRA_INIT
#endif
#define INIT_TYPE(type) \
int init_##type(PyObject *module) {\
if (PyType_Ready(&type##_Type) < 0) return 0; \
if (PyModule_AddObject(module, #type, (PyObject *)&type##_Type) != 0) return 0; \
Py_INCREF(&type##_Type); \
EXTRA_INIT; \
return 1; \
}
@@ -141,14 +119,18 @@ typedef unsigned int index_type;
#define END_ALLOW_UNUSED_RESULT _Pragma("GCC diagnostic pop")
#endif
typedef struct {
char_type ch;
color_type fg, bg, decoration_fg;
combining_type cc;
sprite_index sprite_x, sprite_y, sprite_z;
attrs_type attrs;
} Cell;
typedef struct {
PyObject_HEAD
char_type *chars;
color_type *fg_colors;
color_type *bg_colors;
color_type *decoration_fg;
combining_type *combining_chars;
Cell *cells;
index_type xnum, ynum;
bool continued;
bool needs_free;
@@ -159,28 +141,22 @@ PyTypeObject Line_Type;
typedef struct {
PyObject_HEAD
uint8_t *buf;
Cell *buf;
index_type xnum, ynum, *line_map, *scratch;
index_type block_size;
bool *continued_map;
Line *line;
// Pointers into buf
char_type *chars;
color_type *fg_colors;
color_type *bg_colors;
color_type *decoration_fg;
combining_type *combining_chars;
} LineBuf;
PyTypeObject LineBuf_Type;
typedef struct {
PyObject_HEAD
uint32_t *buf;
Cell *buf;
index_type xnum, ynum;
Line *line;
index_type start_of_data, count;
bool *continued_map;
} HistoryBuf;
PyTypeObject HistoryBuf_Type;
@@ -189,7 +165,8 @@ typedef struct {
bool bold, italic, reverse, strikethrough, blink;
unsigned int x, y;
uint8_t decoration, shape;
uint8_t decoration;
CursorShape shape;
unsigned long fg, bg, decoration_fg;
} Cursor;
@@ -198,60 +175,20 @@ PyTypeObject Cursor_Type;
PyTypeObject Face_Type;
PyTypeObject Window_Type;
typedef struct {
color_type default_fg, default_bg, cursor_color, highlight_fg, highlight_bg;
} DynamicColor;
typedef struct {
PyObject_HEAD
bool dirty;
uint32_t color_table[256];
uint32_t orig_color_table[256];
DynamicColor configured, overridden;
} ColorProfile;
PyTypeObject ColorProfile_Type;
typedef struct SpritePosition SpritePosition;
struct SpritePosition {
SpritePosition *next;
unsigned int x, y, z;
char_type ch;
combining_type cc;
bool is_second;
bool filled;
bool rendered;
};
PyTypeObject SpritePosition_Type;
typedef struct {
PyObject_HEAD
size_t max_array_len, max_texture_size, max_y;
unsigned int x, y, z, xnum, ynum;
SpritePosition cache[1024];
bool dirty;
} SpriteMap;
PyTypeObject SpriteMap_Type;
typedef struct {
PyObject_HEAD
index_type xnum, ynum;
bool screen_changed;
bool cursor_changed;
bool dirty;
bool *changed_lines;
bool *lines_with_changed_cells;
bool *changed_cells;
unsigned int history_line_added_count;
} ChangeTracker;
PyTypeObject ChangeTracker_Type;
typedef struct {
bool mLNM, mIRM, mDECTCEM, mDECSCNM, mDECOM, mDECAWM, mDECCOLM, mDECARM, mDECCKM,
mBRACKETED_PASTE, mFOCUS_TRACKING, mEXTENDED_KEYBOARD;
unsigned long mouse_tracking_mode, mouse_tracking_protocol;
} ScreenModes;
PyTypeObject ScreenModes_Type;
#define SAVEPOINTS_SZ 256
typedef struct {
@@ -272,152 +209,66 @@ typedef struct {
#define PARSER_BUF_SZ (8 * 1024)
#define READ_BUF_SZ (1024*1024)
typedef struct {
double at;
PyObject *callback;
PyObject *args;
} TimerEvent;
typedef struct {
PyObject_HEAD
unsigned int columns, lines, margin_top, margin_bottom, charset;
uint32_t utf8_state, utf8_codepoint, *g0_charset, *g1_charset, *g_charset;
bool use_latin1;
Cursor *cursor;
SavepointBuffer main_savepoints, alt_savepoints;
PyObject *callbacks;
LineBuf *linebuf, *main_linebuf, *alt_linebuf;
HistoryBuf *historybuf;
bool *tabstops, *main_tabstops, *alt_tabstops;
ChangeTracker *change_tracker;
ScreenModes modes;
PyObject *dump_callback, *update_screen, *death_notify;
unsigned int count;
bool shutting_down;
pthread_t io_thread;
} ChildMonitor;
PyTypeObject ChildMonitor_Type;
uint32_t parser_buf[PARSER_BUF_SZ];
unsigned int parser_state, parser_text_start, parser_buf_pos;
bool parser_has_pending_text;
uint8_t read_buf[READ_BUF_SZ];
uint32_t default_fg, default_bg, highlight_fg, highlight_bg, cursor_color;
} Screen;
PyTypeObject Screen_Type;
#define clear_sprite_position(cell) (cell).sprite_x = 0; (cell).sprite_y = 0; (cell).sprite_z = 0;
#define left_shift_line(line, at, num) \
for(index_type __i__ = (at); __i__ < (line)->xnum - (num); __i__++) { \
COPY_CELL(line, __i__ + (num), line, __i__) \
} \
if ((((line)->chars[(at)] >> ATTRS_SHIFT) & WIDTH_MASK) != 1) (line)->chars[(at)] = (1 << ATTRS_SHIFT) | 32;
if ((((line)->cells[(at)].attrs) & WIDTH_MASK) != 1) { \
(line)->cells[(at)].ch = BLANK_CHAR; \
(line)->cells[(at)].attrs = BLANK_CHAR ? 1 : 0; \
clear_sprite_position((line)->cells[(at)]); \
}
// Global functions
const char* base64_decode(const uint32_t *src, size_t src_sz, uint8_t *dest, size_t dest_capacity, size_t *dest_sz);
Line* alloc_line();
Cursor* alloc_cursor();
LineBuf* alloc_linebuf(unsigned int, unsigned int);
HistoryBuf* alloc_historybuf(unsigned int, unsigned int);
ChangeTracker* alloc_change_tracker(unsigned int, unsigned int);
int init_LineBuf(PyObject *);
int init_HistoryBuf(PyObject *);
int init_Cursor(PyObject *);
int init_Line(PyObject *);
int init_ColorProfile(PyObject *);
int init_SpriteMap(PyObject *);
int init_ChangeTracker(PyObject *);
int init_Screen(PyObject *);
int init_Face(PyObject *);
int init_Window(PyObject *);
int init_CoreText(PyObject *);
ColorProfile* alloc_color_profile();
PyObject* create_256_color_table();
PyObject* read_bytes_dump(PyObject UNUSED *, PyObject *);
PyObject* read_bytes(PyObject UNUSED *, PyObject *);
PyObject* parse_bytes_dump(PyObject UNUSED *, PyObject *);
PyObject* parse_bytes(PyObject UNUSED *, PyObject *);
uint32_t decode_utf8(uint32_t*, uint32_t*, uint8_t byte);
unsigned int encode_utf8(uint32_t ch, char* dest);
void cursor_reset(Cursor*);
Cursor* cursor_copy(Cursor*);
void cursor_copy_to(Cursor *src, Cursor *dest);
void cursor_reset_display_attrs(Cursor*);
bool update_cell_range_data(ScreenModes *modes, SpriteMap *, Line *, unsigned int, unsigned int, ColorProfile *, const uint32_t, const uint32_t, unsigned int *);
uint32_t to_color(ColorProfile *, uint32_t, uint32_t);
void set_sprite_position(Cell *cell, Cell *previous_cell);
PyObject* line_text_at(char_type, combining_type);
void line_clear_text(Line *self, unsigned int at, unsigned int num, int ch);
void line_apply_cursor(Line *self, Cursor *cursor, unsigned int at, unsigned int num, bool clear_char);
void line_set_char(Line *, unsigned int , uint32_t , unsigned int , Cursor *);
void line_right_shift(Line *, unsigned int , unsigned int );
void line_add_combining_char(Line *, uint32_t , unsigned int );
index_type line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen);
unsigned int line_length(Line *self);
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 set_iutf8(int, bool);
void linebuf_init_line(LineBuf *, index_type);
void linebuf_clear(LineBuf *, char_type ch);
void linebuf_init_line(LineBuf *, index_type);
void linebuf_index(LineBuf* self, index_type top, index_type bottom);
void linebuf_reverse_index(LineBuf *self, index_type top, index_type bottom);
void linebuf_clear_line(LineBuf *self, index_type y);
void linebuf_insert_lines(LineBuf *self, unsigned int num, unsigned int y, unsigned int bottom);
void linebuf_delete_lines(LineBuf *self, index_type num, index_type y, index_type bottom);
void linebuf_set_attribute(LineBuf *, unsigned int , unsigned int );
void linebuf_rewrap(LineBuf *self, LineBuf *other, int *cursor_y_out, HistoryBuf *);
unsigned int linebuf_char_width_at(LineBuf *self, index_type x, index_type y);
bool historybuf_resize(HistoryBuf *self, index_type lines);
void historybuf_add_line(HistoryBuf *self, const Line *line);
void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other);
void historybuf_init_line(HistoryBuf *self, index_type num, Line *l);
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);
unsigned int safe_wcwidth(uint32_t ch);
void change_wcwidth(bool use9);
void screen_align(Screen*);
void screen_restore_cursor(Screen *);
void screen_save_cursor(Screen *);
void screen_cursor_position(Screen*, unsigned int, unsigned int);
void screen_cursor_back(Screen *self, unsigned int count/*=1*/, int move_direction/*=-1*/);
void screen_erase_in_line(Screen *, unsigned int, bool);
void screen_erase_in_display(Screen *, unsigned int, bool);
void screen_draw(Screen *screen, uint32_t codepoint);
void screen_ensure_bounds(Screen *self, bool use_margins);
void screen_toggle_screen_buffer(Screen *self);
void screen_normal_keypad_mode(Screen *self);
void screen_alternate_keypad_mode(Screen *self);
void screen_change_default_color(Screen *self, unsigned int which, uint32_t col);
void screen_alignment_display(Screen *self);
void screen_reverse_index(Screen *self);
void screen_index(Screen *self);
void screen_reset(Screen *self);
void screen_set_tab_stop(Screen *self);
void screen_tab(Screen *self);
void screen_backtab(Screen *self, unsigned int);
void screen_clear_tab_stop(Screen *self, unsigned int how);
void screen_set_mode(Screen *self, unsigned int mode);
void screen_reset_mode(Screen *self, unsigned int mode);
void screen_insert_characters(Screen *self, unsigned int count);
void screen_cursor_up(Screen *self, unsigned int count/*=1*/, bool do_carriage_return/*=false*/, int move_direction/*=-1*/);
void screen_set_cursor(Screen *self, unsigned int mode, uint8_t secondary);
void screen_cursor_to_column(Screen *self, unsigned int column);
void screen_cursor_down(Screen *self, unsigned int count/*=1*/);
void screen_cursor_forward(Screen *self, unsigned int count/*=1*/);
void screen_cursor_down1(Screen *self, unsigned int count/*=1*/);
void screen_cursor_up1(Screen *self, unsigned int count/*=1*/);
void screen_cursor_to_line(Screen *screen, unsigned int line);
void screen_insert_lines(Screen *self, unsigned int count/*=1*/);
void screen_delete_lines(Screen *self, unsigned int count/*=1*/);
void screen_delete_characters(Screen *self, unsigned int count);
void screen_erase_characters(Screen *self, unsigned int count);
void screen_set_margins(Screen *self, unsigned int top, unsigned int bottom);
void screen_change_charset(Screen *, uint32_t to);
void screen_designate_charset(Screen *, uint32_t which, uint32_t as);
void screen_use_latin1(Screen *, bool);
void set_title(Screen *self, PyObject*);
void set_icon(Screen *self, PyObject*);
void set_dynamic_color(Screen *self, unsigned int code, PyObject*);
void set_color_table_color(Screen *self, unsigned int code, PyObject*);
uint32_t* translation_table(uint32_t which);
uint32_t *latin1_charset;
void screen_request_capabilities(Screen *, PyObject *);
void report_device_attributes(Screen *self, unsigned int UNUSED mode, char start_modifier);
void select_graphic_rendition(Screen *self, unsigned int *params, unsigned int count);
void report_device_status(Screen *self, unsigned int which, bool UNUSED);
void report_mode_status(Screen *self, unsigned int which, bool);
#define DECLARE_CH_SCREEN_HANDLER(name) void screen_##name(Screen *screen);
DECLARE_CH_SCREEN_HANDLER(bell)
DECLARE_CH_SCREEN_HANDLER(backspace)
DECLARE_CH_SCREEN_HANDLER(tab)
DECLARE_CH_SCREEN_HANDLER(linefeed)
DECLARE_CH_SCREEN_HANDLER(carriage_return)
bool init_freetype_library(PyObject*);
bool init_fontconfig_library(PyObject*);
PyObject *get_fontconfig_font(PyObject *self, PyObject *args);
void set_mouse_cursor(MouseShape);
void mouse_event(int, int);
void scroll_event(double, double);
void set_special_key_combo(int glfw_key, int mods);
void on_text_input(unsigned int codepoint, int mods);
void on_key_input(int key, int scancode, int action, int mods);
void request_window_attention();

View File

@@ -8,7 +8,7 @@
#include "data-types.h"
#include <fontconfig/fontconfig.h>
PyObject*
static PyObject*
get_fontconfig_font(PyObject UNUSED *self, PyObject *args) {
char *family;
int bold, italic, allow_bitmapped_fonts, index = 0, hint_style=0, weight=0, slant=0;
@@ -74,11 +74,21 @@ end:
return ans;
}
static PyMethodDef module_methods[] = {
{"get_fontconfig_font", (PyCFunction)get_fontconfig_font, METH_VARARGS, ""},
{NULL, NULL, 0, NULL} /* Sentinel */
};
bool
init_fontconfig_library(PyObject UNUSED *m) {
init_fontconfig_library(PyObject *module) {
if (!FcInit()) {
PyErr_SetString(PyExc_RuntimeError, "Failed to initialize the fontconfig library");
return false;
}
if (Py_AtExit(FcFini) != 0) {
PyErr_SetString(PyExc_RuntimeError, "Failed to register the fontconfig library at exit handler");
return false;
}
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
return true;
}

View File

@@ -3,18 +3,73 @@
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import ctypes
from kitty.fast_data_types import CTFace as Face
from kitty.utils import get_logical_dpi, wcwidth, ceil_int
import re
import sys
from kitty.fast_data_types import CTFace as Face, coretext_all_fonts
from kitty.utils import ceil_int, get_logical_dpi, safe_print, wcwidth, adjust_line_height
main_font = {}
symbol_map = {}
cell_width = cell_height = baseline = CellTexture = WideCellTexture = underline_thickness = underline_position = None
attr_map = {(False, False): 'font_family', (True, False): 'bold_font', (False, True): 'italic_font', (True, True): 'bold_italic_font'}
def install_symbol_map(val, font_size, dpi):
def create_font_map(all_fonts):
ans = {'family_map': {}, 'ps_map': {}, 'full_map': {}}
for x in all_fonts:
f = (x['family'] or '').lower()
s = (x['style'] or '').lower()
ps = (x['postscript_name'] or '').lower()
ans['family_map'].setdefault(f, []).append(x)
ans['ps_map'].setdefault(ps, []).append(x)
ans['full_map'].setdefault(f + ' ' + s, []).append(x)
return ans
def find_best_match(font_map, family, bold, italic):
q = re.sub(r'\s+', ' ', family.lower())
def score(candidate):
style_match = 1 if candidate['bold'] == bold and candidate['italic'] == italic else 0
monospace_match = 1 if candidate['monospace'] else 0
return style_match, monospace_match
# First look for an exact match
for selector in ('ps_map', 'full_map'):
candidates = font_map[selector].get(q)
if candidates:
candidates.sort(key=score)
return candidates[-1]
# Let CoreText choose the font if the family exists, otherwise
# fallback to Menlo
if q not in font_map['family_map']:
safe_print('The font {} was not found, falling back to Menlo'.format(family), file=sys.stderr)
family = 'Menlo'
return {
'monospace': True,
'bold': bold,
'italic': italic,
'family': family
}
def get_face(font_map, family, main_family, font_size, dpi, bold=False, italic=False):
def resolve_family(f):
if (bold or italic) and f == 'auto':
f = main_family
if f.lower() == 'monospace':
f = 'Menlo'
return f
descriptor = find_best_match(font_map, resolve_family(family), bold, italic)
return Face(descriptor, font_size, dpi)
def install_symbol_map(all_fonts, val, font_size, dpi):
global symbol_map
symbol_map = {}
family_map = {f: Face(f, False, False, False, font_size, dpi) for f in set(val.values())}
family_map = {f: get_face(all_fonts, f, 'Menlo', font_size, dpi) for f in set(val.values())}
for ch, family in val.items():
symbol_map[ch] = family_map[family]
@@ -28,23 +83,16 @@ def set_font_family(opts, override_font_size=None, ignore_dpi_failure=False):
raise
dpi = (72, 72) # Happens when running via develop() in an ssh session
dpi = sum(dpi) / 2.0
attr_map = {(False, False): 'font_family', (True, False): 'bold_font', (False, True): 'italic_font', (True, True): 'bold_italic_font'}
def get_family(bold, italic):
ans = getattr(opts, attr_map[(bold, italic)])
if ans.lower() == 'monospace':
ans = 'Menlo'
if ans == 'auto' and (bold or italic):
ans = get_family(False, False)
return ans
font_size = override_font_size or opts.font_size
all_fonts = create_font_map(coretext_all_fonts())
for bold in (False, True):
for italic in (False, True):
main_font[(bold, italic)] = Face(get_family(bold, italic), bold, italic, True, font_size, dpi)
install_symbol_map(opts.symbol_map, font_size, dpi)
for (bold, italic), attr in attr_map.items():
main_font[(bold, italic)] = get_face(all_fonts, getattr(opts, attr), opts.font_family, font_size, dpi, bold, italic)
install_symbol_map(all_fonts, opts.symbol_map, font_size, dpi)
mf = main_font[(False, False)]
cell_width, cell_height = mf.cell_size()
cell_height = adjust_line_height(cell_height, opts.adjust_line_height)
CellTexture = ctypes.c_ubyte * (cell_width * cell_height)
WideCellTexture = ctypes.c_ubyte * (2 * cell_width * cell_height)
baseline = int(round(mf.ascent))
@@ -53,6 +101,20 @@ def set_font_family(opts, override_font_size=None, ignore_dpi_failure=False):
return cell_width, cell_height
def test_font_matching(name='Menlo', bold=False, italic=False, dpi=72.0, font_size=11.0):
all_fonts = create_font_map(coretext_all_fonts())
face = get_face(all_fonts, name, 'Menlo', font_size, dpi, bold, italic)
return face
def test_family_matching(name='Menlo', dpi=72.0, font_size=11.0):
all_fonts = create_font_map(coretext_all_fonts())
for bold in (False, True):
for italic in (False, True):
face = get_face(all_fonts, name, 'Menlo', font_size, dpi, bold, italic)
print(bold, italic, face)
def current_cell():
return CellTexture, cell_width, cell_height, baseline, underline_thickness, underline_position

View File

@@ -4,15 +4,13 @@
import ctypes
import sys
import unicodedata
from collections import namedtuple
from functools import lru_cache
from itertools import chain
from threading import Lock
from kitty.fast_data_types import FT_PIXEL_MODE_GRAY, Face
from kitty.fast_data_types import FT_PIXEL_MODE_GRAY, Face, FreeTypeError
from kitty.fonts.box_drawing import render_missing_glyph
from kitty.utils import ceil_int, get_logical_dpi, safe_print, wcwidth
from kitty.utils import ceil_int, get_logical_dpi, safe_print, wcwidth, adjust_line_height
from .fontconfig import (
FontNotFound, find_font_for_character, font_for_family, get_font_files
@@ -93,6 +91,7 @@ def set_font_family(opts, override_font_size=None):
cell_height = font_units_to_pixels(
face.height, face.units_per_EM, size_in_pts, dpi[1]
)
cell_height = adjust_line_height(cell_height, opts.adjust_line_height)
baseline = font_units_to_pixels(
face.ascender, face.units_per_EM, size_in_pts, dpi[1]
)
@@ -113,7 +112,6 @@ def set_font_family(opts, override_font_size=None):
CharBitmap = namedtuple(
'CharBitmap', 'data bearingX bearingY advance rows columns'
)
freetype_lock = Lock()
def render_to_bitmap(font, face, text):
@@ -161,26 +159,25 @@ def render_char(text, bold=False, italic=False, width=1):
elif italic:
key = 'italic'
ch = text[0]
with freetype_lock:
font = symbol_map.get(ch)
if font is None or not font.face.get_char_index(ch):
font = current_font_family.get(key) or current_font_family['regular']
face = font.face
if not face.get_char_index(ch):
try:
font = font_for_char(ch, bold, italic)
except FontNotFound:
font = font_for_char(
ch, bold, italic, allow_bitmaped_fonts=True
)
face = alt_face_cache.get(font)
if face is None:
face = alt_face_cache[font] = Face(font.face, font.index)
if face.is_scalable:
set_char_size(face, **cff_size)
else:
face = font.face
return render_using_face(font, face, text, width, italic, bold)
font = symbol_map.get(ch)
if font is None or not font.face.get_char_index(ch):
font = current_font_family.get(key) or current_font_family['regular']
face = font.face
if not face.get_char_index(ch):
try:
font = font_for_char(ch, bold, italic)
except FontNotFound:
font = font_for_char(
ch, bold, italic, allow_bitmaped_fonts=True
)
face = alt_face_cache.get(font)
if face is None:
face = alt_face_cache[font] = Face(font.face, font.index)
if face.is_scalable:
set_char_size(face, **cff_size)
else:
face = font.face
return render_using_face(font, face, text, width, italic, bold)
def place_char_in_cell(bitmap_char):
@@ -254,13 +251,16 @@ def missing_glyph(width):
def render_cell(text=' ', bold=False, italic=False):
# TODO: Handle non-normalizable combining chars. Probably need to use
# harfbuzz for that
text = unicodedata.normalize('NFC', text)[0]
text = text[0]
width = wcwidth(text)
try:
bitmap_char = render_char(text, bold, italic, width)
except FontNotFound as err:
safe_print('ERROR:', err, file=sys.stderr)
return missing_glyph(width)
except FreeTypeError as err:
safe_print('Failed to render text:', repr(text), 'with error:', err, file=sys.stderr)
return missing_glyph(width)
second = None
if width == 2:
if bitmap_char.columns > cell_width:

View File

@@ -3,6 +3,7 @@
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import ctypes
from math import sin, pi, ceil, floor, sqrt
from kitty.constants import isosx
from .box_drawing import render_box_char, is_renderable_box_char
@@ -12,7 +13,7 @@ else:
from .freetype import set_font_family, render_cell as rc, current_cell # noqa
def add_line(buf, cell_width, position, thickness):
def add_line(buf, cell_width, position, thickness, cell_height):
y = position - thickness // 2
while thickness:
thickness -= 1
@@ -22,15 +23,28 @@ def add_line(buf, cell_width, position, thickness):
y += 1
def add_curl(buf, cell_width, position, thickness):
for y in range(position - thickness, position):
for x in range(0, cell_width // 2):
offset = cell_width * y
buf[offset + x] = 255
for y in range(position, position + thickness):
for x in range(cell_width // 2, cell_width):
offset = cell_width * y
buf[offset + x] = 255
def add_curl(buf, cell_width, position, thickness, cell_height):
xfactor = 2.0 * pi / cell_width
yfactor = thickness
def clamp_y(y):
return max(0, min(int(y), cell_height - 1))
def clamp_x(x):
return max(0, min(int(x), cell_width - 1))
def add_intensity(x, y, distance):
buf[cell_width * y + x] = min(255, buf[cell_width * y + x] + int(255 * (1 - distance)))
for x_exact in range(cell_width):
y_exact = yfactor * sin(x_exact * xfactor) + position
y_below = clamp_y(floor(y_exact))
y_above = clamp_y(ceil(y_exact))
x_before, x_after = map(clamp_x, (x_exact - 1, x_exact + 1))
for x in {x_before, x_exact, x_after}:
for y in {y_below, y_above}:
dist = sqrt((x - x_exact)**2 + (y - y_exact)**2) / 2
add_intensity(x, y, dist)
def render_cell(text=' ', bold=False, italic=False, underline=0, strikethrough=False):
@@ -49,14 +63,30 @@ def render_cell(text=' ', bold=False, italic=False, underline=0, strikethrough=F
t = underline_thickness
if underline == 2:
t = max(1, min(cell_height - underline_position - 1, t))
dl(add_curl if underline == 2 else add_line, underline_position, t)
dl(add_curl if underline == 2 else add_line, underline_position, t, cell_height)
if strikethrough:
pos = int(0.65 * baseline)
dl(add_line, pos, underline_thickness)
dl(add_line, pos, underline_thickness, cell_height)
return first, second
class Buf:
def __init__(self, buf):
self.buf = buf
def __call__(self):
return ctypes.addressof(self.buf)
def render_cell_wrapper(text, bold, italic, underline, strikethrough, is_second):
first, second = render_cell(text, bold, italic, underline, strikethrough)
ans = second if is_second else first
ans = ans or render_cell()[0]
return Buf(ans)
def join_cells(cell_width, cell_height, *cells):
dstride = len(cells) * cell_width
ans = (ctypes.c_ubyte * (cell_height * dstride))()

View File

@@ -16,8 +16,10 @@ typedef struct {
unsigned int units_per_EM;
int ascender, descender, height, max_advance_width, max_advance_height, underline_position, underline_thickness;
bool is_scalable;
PyObject *path;
} Face;
static PyObject* FreeType_Exception = NULL;
void
set_freetype_error(const char* prefix, int err_code) {
@@ -41,12 +43,12 @@ set_freetype_error(const char* prefix, int err_code) {
while(ft_errors[i].err_msg != NULL) {
if (ft_errors[i].err_code == err_code) {
PyErr_Format(PyExc_ValueError, "%s %s", prefix, ft_errors[i].err_msg);
PyErr_Format(FreeType_Exception, "%s %s", prefix, ft_errors[i].err_msg);
return;
}
i++;
}
PyErr_Format(PyExc_ValueError, "%s (error code: %d)", prefix, err_code);
PyErr_Format(FreeType_Exception, "%s (error code: %d)", prefix, err_code);
}
static FT_Library library;
@@ -63,9 +65,9 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
self = (Face *)type->tp_alloc(type, 0);
if (self != NULL) {
Py_BEGIN_ALLOW_THREADS;
self->path = PyTuple_GET_ITEM(args, 0);
Py_INCREF(self->path);
error = FT_New_Face(library, path, index, &(self->face));
Py_END_ALLOW_THREADS;
if(error) { set_freetype_error("Failed to load face, with error:", error); Py_CLEAR(self); return NULL; }
#define CPY(n) self->n = self->face->n;
CPY(units_per_EM); CPY(ascender); CPY(descender); CPY(height); CPY(max_advance_width); CPY(max_advance_height); CPY(underline_position); CPY(underline_thickness);
@@ -78,9 +80,20 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
static void
dealloc(Face* self) {
FT_Done_Face(self->face);
Py_CLEAR(self->path);
Py_TYPE(self)->tp_free((PyObject*)self);
}
static PyObject *
repr(Face *self) {
return PyUnicode_FromFormat(
"Face(path=%S, is_scalable=%S, units_per_EM=%u, ascender=%i, descender=%i, height=%i, max_advance_width=%i max_advance_height=%i, underline_position=%i, underline_thickness=%i)",
self->path, self->is_scalable ? Py_True : Py_False,
self->ascender, self->descender, self->height, self->max_advance_width, self->max_advance_height, self->underline_position, self->underline_thickness
);
}
static PyObject*
set_char_size(Face *self, PyObject *args) {
#define set_char_size_doc "set_char_size(width, height, xdpi, ydpi) -> set the character size. width, height is in 1/64th of a pt. dpi is in pixels per inch"
@@ -88,9 +101,7 @@ set_char_size(Face *self, PyObject *args) {
unsigned int xdpi, ydpi;
int error;
if (!PyArg_ParseTuple(args, "llII", &char_width, &char_height, &xdpi, &ydpi)) return NULL;
Py_BEGIN_ALLOW_THREADS;
error = FT_Set_Char_Size(self->face, char_width, char_height, xdpi, ydpi);
Py_END_ALLOW_THREADS;
if (error) { set_freetype_error("Failed to set char size, with error:", error); Py_CLEAR(self); return NULL; }
Py_RETURN_NONE;
}
@@ -107,9 +118,7 @@ load_char(Face *self, PyObject *args) {
if (hintstyle >= 3) flags |= FT_LOAD_TARGET_NORMAL;
else if (0 < hintstyle && hintstyle < 3) flags |= FT_LOAD_TARGET_LIGHT;
} else flags |= FT_LOAD_NO_HINTING;
Py_BEGIN_ALLOW_THREADS;
error = FT_Load_Glyph(self->face, glyph_index, flags);
Py_END_ALLOW_THREADS;
if (error) { set_freetype_error("Failed to load glyph, with error:", error); Py_CLEAR(self); return NULL; }
Py_RETURN_NONE;
}
@@ -120,9 +129,7 @@ get_char_index(Face *self, PyObject *args) {
int code;
unsigned int ans;
if (!PyArg_ParseTuple(args, "C", &code)) return NULL;
Py_BEGIN_ALLOW_THREADS;
ans = FT_Get_Char_Index(self->face, code);
Py_END_ALLOW_THREADS;
return Py_BuildValue("I", ans);
}
@@ -198,7 +205,7 @@ trim_to_width(Face UNUSED *self, PyObject *args) {
rows = PyLong_AsUnsignedLong(PyStructSequence_GET_ITEM(bitmap, 0));
width = PyLong_AsUnsignedLong(PyStructSequence_GET_ITEM(bitmap, 1));
extra = width - cell_width;
if (extra >= cell_width) { PyErr_SetString(PyExc_ValueError, "Too large for trimming"); return NULL; }
if (extra >= cell_width) { PyErr_SetString(FreeType_Exception, "Too large for trimming"); return NULL; }
PyObject *ans = PyStructSequence_New(&BitmapType);
if (ans == NULL) return PyErr_NoMemory();
src = (unsigned char*)PyByteArray_AS_STRING(PyStructSequence_GET_ITEM(bitmap, 3));
@@ -240,6 +247,7 @@ static PyMemberDef members[] = {
MEM(underline_position, T_INT),
MEM(underline_thickness, T_INT),
MEM(is_scalable, T_BOOL),
MEM(path, T_OBJECT_EX),
{NULL} /* Sentinel */
};
@@ -264,18 +272,30 @@ PyTypeObject Face_Type = {
.tp_methods = methods,
.tp_members = members,
.tp_new = new,
.tp_repr = (reprfunc)repr,
};
INIT_TYPE(Face)
static void
free_freetype() {
FT_Done_FreeType(library);
}
bool
init_freetype_library(PyObject *m) {
FreeType_Exception = PyErr_NewException("fast_data_types.FreeTypeError", NULL, NULL);
if (FreeType_Exception == NULL) return false;
if (PyModule_AddObject(m, "FreeTypeError", FreeType_Exception) != 0) return false;
int error = FT_Init_FreeType(&library);
if (error) {
set_freetype_error("Failed to initialize FreeType library, with error:", error);
return false;
}
if (Py_AtExit(free_freetype) != 0) {
PyErr_SetString(FreeType_Exception, "Failed to register the freetype library at exit handler");
return false;
}
if (PyStructSequence_InitType2(&GlpyhMetricsType, &gm_desc) != 0) return false;
if (PyStructSequence_InitType2(&BitmapType, &bm_desc) != 0) return false;
PyModule_AddObject(m, "GlyphMetrics", (PyObject*)&GlpyhMetricsType);

File diff suppressed because it is too large Load Diff

View File

@@ -4,133 +4,178 @@
* Distributed under terms of the GPL3 license.
*/
#include "data-types.h"
#include "state.h"
#include <structmember.h>
#include <GLFW/glfw3.h>
#if defined(__APPLE__)
#define GLFW_EXPOSE_NATIVE_COCOA
#include <GLFW/glfw3native.h>
#endif
#if GLFW_VERSION_MAJOR < 3 || (GLFW_VERSION_MAJOR == 3 && GLFW_VERSION_MINOR < 2)
#error "glfw >= 3.2 required"
#endif
#define MAX_WINDOWS 256
#if GLFW_VERSION_MAJOR > 3 || (GLFW_VERSION_MAJOR == 3 && GLFW_VERSION_MINOR > 2)
#define has_request_attention
#define has_init_hint_string
#endif
#if GLFW_KEY_LAST >= MAX_KEY_COUNT
#error "glfw has too many keys, you should increase MAX_KEY_COUNT"
#endif
#define CALLBACK(name, fmt, ...) \
if ((name) != NULL) { \
PyGILState_STATE _pystate = PyGILState_Ensure(); \
PyObject *_pyret = PyObject_CallFunction((name), fmt, __VA_ARGS__); \
if (_pyret == NULL && PyErr_Occurred() != NULL) PyErr_Print(); \
Py_CLEAR(_pyret); \
PyGILState_Release(_pystate); \
if (_pyret == NULL) PyErr_Print(); \
else Py_DECREF(_pyret); \
}
#define WINDOW_CALLBACK(name, fmt, ...) \
Window *self = find_window(w); \
if (self) { CALLBACK(self->name, "O" fmt, self, __VA_ARGS__); }
CALLBACK(the_window->name, "O" fmt, the_window, __VA_ARGS__);
typedef struct {
PyObject_HEAD
GLFWwindow *window;
PyObject *framebuffer_size_callback, *char_mods_callback, *key_callback, *mouse_button_callback, *scroll_callback, *cursor_pos_callback, *window_focus_callback;
GLFWcursor *standard_cursor, *click_cursor;
} Window;
PyObject *framebuffer_size_callback, *window_focus_callback;
GLFWcursor *standard_cursor, *click_cursor, *arrow_cursor;
} WindowWrapper;
static void update_viewport(GLFWwindow *window) {
int w, h;
glfwGetFramebufferSize(window, &global_state.viewport_width, &global_state.viewport_height);
glfwGetWindowSize(window, &w, &h);
global_state.viewport_x_ratio = (double)global_state.viewport_width / (double)w;
global_state.viewport_y_ratio = (double)global_state.viewport_height / (double)h;
}
// callbacks {{{
static Window* window_weakrefs[MAX_WINDOWS] = {0};
static inline Window*
find_window(GLFWwindow *w) {
for(int i = 0; i < MAX_WINDOWS; i++) {
if (window_weakrefs[i] == NULL) break;
if (window_weakrefs[i]->window == w) return window_weakrefs[i];
}
return NULL;
}
static WindowWrapper* the_window = NULL;
update_viewport_size_func update_viewport_size = NULL;
static void
framebuffer_size_callback(GLFWwindow *w, int width, int height) {
WINDOW_CALLBACK(framebuffer_size_callback, "ii", width, height);
if (width > 100 && height > 100) {
update_viewport_size(width, height);
update_viewport(w);
WINDOW_CALLBACK(framebuffer_size_callback, "ii", width, height);
glfwPostEmptyEvent();
} else fprintf(stderr, "Ignoring resize request for tiny size: %dx%d\n", width, height);
}
static void
char_mods_callback(GLFWwindow *w, unsigned int codepoint, int mods) {
WINDOW_CALLBACK(char_mods_callback, "Ii", codepoint, mods);
char_mods_callback(GLFWwindow UNUSED *w, unsigned int codepoint, int mods) {
global_state.cursor_blink_zero_time = monotonic();
on_text_input(codepoint, mods);
}
static void
key_callback(GLFWwindow *w, int key, int scancode, int action, int mods) {
WINDOW_CALLBACK(key_callback, "iiii", key, scancode, action, mods);
key_callback(GLFWwindow UNUSED *w, int key, int scancode, int action, int mods) {
global_state.cursor_blink_zero_time = monotonic();
if (key >= 0 && key <= GLFW_KEY_LAST) {
global_state.is_key_pressed[key] = action == GLFW_RELEASE ? false : true;
on_key_input(key, scancode, action, mods);
}
}
static void
mouse_button_callback(GLFWwindow *w, int button, int action, int mods) {
WINDOW_CALLBACK(mouse_button_callback, "iii", button, action, mods);
}
static void
scroll_callback(GLFWwindow *w, double xoffset, double yoffset) {
WINDOW_CALLBACK(scroll_callback, "dd", xoffset, yoffset);
if (glfwGetInputMode(w, GLFW_CURSOR) != GLFW_CURSOR_NORMAL) { glfwSetInputMode(w, GLFW_CURSOR, GLFW_CURSOR_NORMAL); }
double now = monotonic();
global_state.last_mouse_activity_at = now;
if (button >= 0 && (unsigned int)button < sizeof(global_state.mouse_button_pressed)/sizeof(global_state.mouse_button_pressed[0])) {
global_state.mouse_button_pressed[button] = action == GLFW_PRESS ? true : false;
mouse_event(button, mods);
}
}
static void
cursor_pos_callback(GLFWwindow *w, double x, double y) {
WINDOW_CALLBACK(cursor_pos_callback, "dd", x, y);
if (glfwGetInputMode(w, GLFW_CURSOR) != GLFW_CURSOR_NORMAL) { glfwSetInputMode(w, GLFW_CURSOR, GLFW_CURSOR_NORMAL); }
double now = monotonic();
global_state.last_mouse_activity_at = now;
global_state.cursor_blink_zero_time = now;
global_state.mouse_x = x * global_state.viewport_x_ratio;
global_state.mouse_y = y * global_state.viewport_y_ratio;
mouse_event(-1, 0);
}
static void
window_focus_callback(GLFWwindow *w, int focused) {
scroll_callback(GLFWwindow *w, double xoffset, double yoffset) {
if (glfwGetInputMode(w, GLFW_CURSOR) != GLFW_CURSOR_NORMAL) { glfwSetInputMode(w, GLFW_CURSOR, GLFW_CURSOR_NORMAL); }
double now = monotonic();
global_state.last_mouse_activity_at = now;
scroll_event(xoffset, yoffset);
}
static void
window_focus_callback(GLFWwindow UNUSED *w, int focused) {
global_state.application_focused = focused ? true : false;
double now = monotonic();
global_state.last_mouse_activity_at = now;
global_state.cursor_blink_zero_time = now;
WINDOW_CALLBACK(window_focus_callback, "O", focused ? Py_True : Py_False);
}
// }}}
void
set_mouse_cursor(MouseShape type) {
switch(type) {
case HAND:
glfwSetCursor(the_window->window, the_window->click_cursor);
break;
case ARROW:
glfwSetCursor(the_window->window, the_window->arrow_cursor);
break;
default:
glfwSetCursor(the_window->window, the_window->standard_cursor);
break;
}
}
static PyObject*
new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
Window *self;
WindowWrapper *self;
char *title;
int width, height, i;
int width, height;
if (!PyArg_ParseTuple(args, "iis", &width, &height, &title)) return NULL;
if (the_window != NULL) { PyErr_SetString(PyExc_ValueError, "Only one glfw window is supported"); return NULL; }
self = (Window *)type->tp_alloc(type, 0);
self = (WindowWrapper *)type->tp_alloc(type, 0);
if (self != NULL) {
the_window = self;
self->window = glfwCreateWindow(width, height, title, NULL, NULL);
if (self->window == NULL) { Py_CLEAR(self); PyErr_SetString(PyExc_ValueError, "Failed to create GLFWWindow"); return NULL; }
for(i = 0; i < MAX_WINDOWS; i++) {
if (window_weakrefs[i] == NULL) { window_weakrefs[i] = self; break; }
}
if (i >= MAX_WINDOWS) { Py_CLEAR(self); PyErr_SetString(PyExc_ValueError, "Too many windows created"); return NULL; }
if (self->window == NULL) { Py_CLEAR(self); the_window = NULL; PyErr_SetString(PyExc_ValueError, "Failed to create GLFWwindow"); return NULL; }
update_viewport(self->window);
self->standard_cursor = glfwCreateStandardCursor(GLFW_IBEAM_CURSOR);
self->click_cursor = glfwCreateStandardCursor(GLFW_HAND_CURSOR);
if (self->standard_cursor == NULL || self->click_cursor == NULL) { Py_CLEAR(self); PyErr_SetString(PyExc_ValueError, "Failed to create standard mouse cursors"); return NULL; }
self->arrow_cursor = glfwCreateStandardCursor(GLFW_ARROW_CURSOR);
if (self->standard_cursor == NULL || self->click_cursor == NULL || self->arrow_cursor == NULL) {
Py_CLEAR(self); PyErr_SetString(PyExc_ValueError, "Failed to create standard mouse cursors"); return NULL; }
set_mouse_cursor(0);
glfwSetFramebufferSizeCallback(self->window, framebuffer_size_callback);
glfwSetCharModsCallback(self->window, char_mods_callback);
glfwSetKeyCallback(self->window, key_callback);
glfwSetMouseButtonCallback(self->window, mouse_button_callback);
glfwSetScrollCallback(self->window, scroll_callback);
glfwSetCursorPosCallback(self->window, cursor_pos_callback);
glfwSetKeyCallback(self->window, key_callback);
glfwSetWindowFocusCallback(self->window, window_focus_callback);
}
return (PyObject*)self;
}
// Global functions {{{
static PyObject *error_callback = NULL;
static void
cb_error_callback(int error, const char* description) {
CALLBACK(error_callback, "is", error, description) else fprintf(stderr, "[glfw error]: %s\n", description);
}
PyObject*
glfw_set_error_callback(PyObject UNUSED *self, PyObject *callback) {
Py_CLEAR(error_callback);
error_callback = callback;
Py_INCREF(callback);
Py_RETURN_NONE;
error_callback(int error, const char* description) {
fprintf(stderr, "[glfw error %d]: %s\n", error, description);
}
PyObject*
glfw_init(PyObject UNUSED *self) {
glfwSetErrorCallback(error_callback);
PyObject *ans = glfwInit() ? Py_True: Py_False;
Py_INCREF(ans);
return ans;
@@ -165,10 +210,8 @@ glfw_wait_events(PyObject UNUSED *self, PyObject *args) {
time = PyFloat_AsDouble(PyTuple_GET_ITEM(args, 0));
if (PyErr_Occurred()) PyErr_Clear();
}
Py_BEGIN_ALLOW_THREADS;
if (time < 0) glfwWaitEvents();
else glfwWaitEventsTimeout(time);
Py_END_ALLOW_THREADS;
Py_RETURN_NONE;
}
@@ -178,10 +221,8 @@ glfw_post_empty_event(PyObject UNUSED *self) {
Py_RETURN_NONE;
}
PyObject*
glfw_get_physical_dpi(PyObject UNUSED *self) {
GLFWmonitor *m = glfwGetPrimaryMonitor();
if (m == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to get primary monitor"); return NULL; }
static PyObject*
get_physical_dpi(GLFWmonitor *m) {
int width = 0, height = 0;
glfwGetMonitorPhysicalSize(m, &width, &height);
if (width == 0 || height == 0) { PyErr_SetString(PyExc_ValueError, "Failed to get primary monitor size"); return NULL; }
@@ -192,6 +233,13 @@ glfw_get_physical_dpi(PyObject UNUSED *self) {
return Py_BuildValue("ff", dpix, dpiy);
}
PyObject*
glfw_get_physical_dpi(PyObject UNUSED *self) {
GLFWmonitor *m = glfwGetPrimaryMonitor();
if (m == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to get primary monitor"); return NULL; }
return get_physical_dpi(m);
}
PyObject*
glfw_get_key_name(PyObject UNUSED *self, PyObject *args) {
int key, scancode;
@@ -199,53 +247,78 @@ glfw_get_key_name(PyObject UNUSED *self, PyObject *args) {
return Py_BuildValue("s", glfwGetKeyName(key, scancode));
}
PyObject*
glfw_init_hint_string(PyObject UNUSED *self, PyObject *args) {
int hint_id;
char *hint;
if (!PyArg_ParseTuple(args, "is", &hint_id, &hint)) return NULL;
#ifdef has_init_hint_string
glfwInitHintString(hint_id, hint);
#endif
Py_RETURN_NONE;
}
// }}}
static void
dealloc(Window* self) {
for(unsigned int i = 0; i < MAX_WINDOWS; i++) {
if (window_weakrefs[i] == self) window_weakrefs[i] = NULL;
}
Py_CLEAR(self->framebuffer_size_callback); Py_CLEAR(self->char_mods_callback); Py_CLEAR(self->key_callback); Py_CLEAR(self->mouse_button_callback); Py_CLEAR(self->scroll_callback); Py_CLEAR(self->cursor_pos_callback); Py_CLEAR(self->window_focus_callback);
dealloc(WindowWrapper* self) {
the_window = NULL;
Py_CLEAR(self->framebuffer_size_callback); Py_CLEAR(self->window_focus_callback);
if (self->window != NULL) glfwDestroyWindow(self->window);
Py_TYPE(self)->tp_free((PyObject*)self);
}
static PyObject*
swap_buffers(Window *self) {
Py_BEGIN_ALLOW_THREADS;
swap_buffers(WindowWrapper *self) {
glfwSwapBuffers(self->window);
Py_END_ALLOW_THREADS;
Py_RETURN_NONE;
}
static PyObject*
make_context_current(Window *self) {
make_context_current(WindowWrapper *self) {
glfwMakeContextCurrent(self->window);
Py_RETURN_NONE;
}
static PyObject*
should_close(Window *self) {
window_id(WindowWrapper *self) {
return PyLong_FromVoidPtr(self->window);
}
static PyObject*
show(WindowWrapper *self) {
glfwShowWindow(self->window);
Py_RETURN_NONE;
}
static PyObject*
hide(WindowWrapper *self) {
glfwHideWindow(self->window);
Py_RETURN_NONE;
}
static PyObject*
should_close(WindowWrapper *self) {
PyObject *ans = glfwWindowShouldClose(self->window) ? Py_True : Py_False;
Py_INCREF(ans);
return ans;
}
static PyObject*
get_clipboard_string(Window *self) {
get_clipboard_string(WindowWrapper *self) {
return Py_BuildValue("s", glfwGetClipboardString(self->window));
}
static PyObject*
get_cursor_pos(Window *self) {
get_cursor_pos(WindowWrapper *self) {
double x=0, y=0;
glfwGetCursorPos(self->window, &x, &y);
return Py_BuildValue("dd", x, y);
}
static PyObject*
set_should_close(Window *self, PyObject *args) {
set_should_close(WindowWrapper *self, PyObject *args) {
int c;
if (!PyArg_ParseTuple(args, "p", &c)) return NULL;
glfwSetWindowShouldClose(self->window, c);
@@ -253,15 +326,15 @@ set_should_close(Window *self, PyObject *args) {
}
static PyObject*
set_input_mode(Window *self, PyObject *args) {
int which, value;
if (!PyArg_ParseTuple(args, "ii", &which, &value)) return NULL;
glfwSetInputMode(self->window, which, value);
set_pos(WindowWrapper *self, PyObject *args) {
int x, y;
if (!PyArg_ParseTuple(args, "ii", &x, &y)) return NULL;
glfwSetWindowPos(self->window, x, y);
Py_RETURN_NONE;
}
static PyObject*
is_key_pressed(Window *self, PyObject *args) {
is_key_pressed(WindowWrapper *self, PyObject *args) {
int c;
if (!PyArg_ParseTuple(args, "i", &c)) return NULL;
PyObject *ans = glfwGetKey(self->window, c) == GLFW_PRESS ? Py_True : Py_False;
@@ -270,15 +343,7 @@ is_key_pressed(Window *self, PyObject *args) {
}
static PyObject*
set_click_cursor(Window *self, PyObject *args) {
int c;
if (!PyArg_ParseTuple(args, "p", &c)) return NULL;
glfwSetCursor(self->window, c ? self->click_cursor : self->standard_cursor);
Py_RETURN_NONE;
}
static PyObject*
set_clipboard_string(Window *self, PyObject *args) {
set_clipboard_string(WindowWrapper *self, PyObject *args) {
char *title;
if(!PyArg_ParseTuple(args, "s", &title)) return NULL;
glfwSetClipboardString(self->window, title);
@@ -286,7 +351,7 @@ set_clipboard_string(Window *self, PyObject *args) {
}
static PyObject*
set_window_icon(Window *self, PyObject *args) {
set_window_icon(WindowWrapper *self, PyObject *args) {
GLFWimage logo;
Py_ssize_t sz;
if(!PyArg_ParseTuple(args, "s#ii", &(logo.pixels), &sz, &(logo.width), &(logo.height))) return NULL;
@@ -296,36 +361,84 @@ set_window_icon(Window *self, PyObject *args) {
static PyObject*
_set_title(Window *self, PyObject *args) {
char *title;
if(!PyArg_ParseTuple(args, "s", &title)) return NULL;
glfwSetWindowTitle(self->window, title);
Py_RETURN_NONE;
}
static PyObject*
get_framebuffer_size(Window *self) {
get_framebuffer_size(WindowWrapper *self) {
int w, h;
glfwGetFramebufferSize(self->window, &w, &h);
return Py_BuildValue("ii", w, h);
}
static PyObject*
get_window_size(Window *self) {
get_window_size(WindowWrapper *self) {
int w, h;
glfwGetWindowSize(self->window, &w, &h);
return Py_BuildValue("ii", w, h);
}
#ifdef glfwRequestWindowAttention
static GLFWmonitor*
current_monitor(GLFWwindow *window) {
// Find the monitor that has the maximum overlap with this window
int nmonitors, i;
int wx, wy, ww, wh;
int mx, my, mw, mh;
int overlap = 0, bestoverlap = 0;
GLFWmonitor *bestmonitor = NULL;
GLFWmonitor **monitors = NULL;
const GLFWvidmode *mode;
glfwGetWindowPos(window, &wx, &wy);
glfwGetWindowSize(window, &ww, &wh);
monitors = glfwGetMonitors(&nmonitors);
if (monitors == NULL || nmonitors < 1) { PyErr_SetString(PyExc_ValueError, "No monitors connected"); return NULL; }
for (i = 0; i < nmonitors; i++) {
mode = glfwGetVideoMode(monitors[i]);
glfwGetMonitorPos(monitors[i], &mx, &my);
mw = mode->width;
mh = mode->height;
overlap =
MAX(0, MIN(wx + ww, mx + mw) - MAX(wx, mx)) *
MAX(0, MIN(wy + wh, my + mh) - MAX(wy, my));
if (bestoverlap < overlap || bestmonitor == NULL) {
bestoverlap = overlap;
bestmonitor = monitors[i];
}
}
return bestmonitor;
}
static PyObject*
request_window_attention(Window *self) {
glfwRequestWindowAttention(self->window);
Py_RETURN_NONE;
current_monitor_dpi(WindowWrapper *self) {
GLFWmonitor *m = current_monitor(self->window);
if (m == NULL) return NULL;
return get_physical_dpi(m);
}
void
request_window_attention() {
#ifdef has_request_attention
glfwRequestWindowAttention(the_window->window);
#endif
glfwPostEmptyEvent();
}
#ifdef __APPLE__
static PyObject*
cocoa_window_id(WindowWrapper *self) {
void *wid = glfwGetCocoaWindow(self->window);
if (wid == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to get native window handle"); return NULL; }
return PyLong_FromVoidPtr(wid);
}
#endif
static PyObject*
primary_monitor_size(PyObject UNUSED *self) {
GLFWmonitor* monitor = glfwGetPrimaryMonitor();
const GLFWvidmode* mode = glfwGetVideoMode(monitor);
return Py_BuildValue("ii", mode->width, mode->height);
}
// Boilerplate {{{
#define MND(name, args) {#name, (PyCFunction)name, args, ""}
@@ -337,37 +450,34 @@ static PyMethodDef methods[] = {
MND(should_close, METH_NOARGS),
MND(get_framebuffer_size, METH_NOARGS),
MND(get_window_size, METH_NOARGS),
#ifdef glfwRequestWindowAttention
MND(request_window_attention, METH_NOARGS),
MND(current_monitor_dpi, METH_NOARGS),
#ifdef __APPLE__
MND(cocoa_window_id, METH_NOARGS),
#endif
MND(set_should_close, METH_VARARGS),
MND(set_input_mode, METH_VARARGS),
MND(is_key_pressed, METH_VARARGS),
MND(set_click_cursor, METH_VARARGS),
MND(set_pos, METH_VARARGS),
MND(set_clipboard_string, METH_VARARGS),
MND(make_context_current, METH_NOARGS),
{"set_title", (PyCFunction)_set_title, METH_VARARGS, ""},
MND(window_id, METH_NOARGS),
MND(show, METH_NOARGS),
MND(hide, METH_NOARGS),
{"set_icon", (PyCFunction)set_window_icon, METH_VARARGS, ""},
{NULL} /* Sentinel */
};
static PyMemberDef members[] = {
#define CBE(name) {#name, T_OBJECT_EX, offsetof(Window, name), 0, #name}
#define CBE(name) {#name, T_OBJECT_EX, offsetof(WindowWrapper, name), 0, #name}
CBE(framebuffer_size_callback),
CBE(char_mods_callback),
CBE(key_callback),
CBE(mouse_button_callback),
CBE(scroll_callback),
CBE(cursor_pos_callback),
CBE(window_focus_callback),
{NULL}
#undef CBE
};
PyTypeObject Window_Type = {
PyTypeObject WindowWrapper_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "fast_data_types.Window",
.tp_basicsize = sizeof(Window),
.tp_name = "fast_data_types.GLFWWindow",
.tp_basicsize = sizeof(WindowWrapper),
.tp_dealloc = (destructor)dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = "A GLFW window",
@@ -376,15 +486,32 @@ PyTypeObject Window_Type = {
.tp_new = new,
};
INIT_TYPE(Window)
static PyMethodDef module_methods[] = {
{"glfw_init", (PyCFunction)glfw_init, METH_NOARGS, ""}, \
{"glfw_terminate", (PyCFunction)glfw_terminate, METH_NOARGS, ""}, \
{"glfw_window_hint", (PyCFunction)glfw_window_hint, METH_VARARGS, ""}, \
{"glfw_swap_interval", (PyCFunction)glfw_swap_interval, METH_VARARGS, ""}, \
{"glfw_wait_events", (PyCFunction)glfw_wait_events, METH_VARARGS, ""}, \
{"glfw_post_empty_event", (PyCFunction)glfw_post_empty_event, METH_NOARGS, ""}, \
{"glfw_get_physical_dpi", (PyCFunction)glfw_get_physical_dpi, METH_NOARGS, ""}, \
{"glfw_get_key_name", (PyCFunction)glfw_get_key_name, METH_VARARGS, ""}, \
{"glfw_init_hint_string", (PyCFunction)glfw_init_hint_string, METH_VARARGS, ""}, \
{"glfw_primary_monitor_size", (PyCFunction)primary_monitor_size, METH_NOARGS, ""}, \
{NULL, NULL, 0, NULL} /* Sentinel */
};
// constants {{{
bool
init_glfw(PyObject *m) {
PyEval_InitThreads();
glfwSetErrorCallback(cb_error_callback);
if (PyModule_AddFunctions(m, module_methods) != 0) return false;
if (PyType_Ready(&WindowWrapper_Type) < 0) return false;
if (PyModule_AddObject(m, "GLFWWindow", (PyObject *)&WindowWrapper_Type) != 0) return 0;
Py_INCREF(&WindowWrapper_Type);
#define ADDC(n) if(PyModule_AddIntConstant(m, #n, n) != 0) return false;
#ifdef GLFW_X11_WM_CLASS_NAME
ADDC(GLFW_X11_WM_CLASS_NAME)
ADDC(GLFW_X11_WM_CLASS_CLASS)
#endif
ADDC(GLFW_RELEASE);
ADDC(GLFW_PRESS);
ADDC(GLFW_REPEAT);

View File

@@ -1,31 +0,0 @@
/*
* Copyright (C) 2016 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#pragma once
bool init_glfw(PyObject *m);
PyObject* glfw_set_error_callback(PyObject UNUSED *self, PyObject *callback);
PyObject* glfw_init(PyObject UNUSED *self);
PyObject* glfw_terminate(PyObject UNUSED *self);
PyObject* glfw_window_hint(PyObject UNUSED *self, PyObject *args);
PyObject* glfw_swap_interval(PyObject UNUSED *self, PyObject *args);
PyObject* glfw_wait_events(PyObject UNUSED *self, PyObject*);
PyObject* glfw_post_empty_event(PyObject UNUSED *self);
PyObject* glfw_get_physical_dpi(PyObject UNUSED *self);
PyObject* glfw_get_key_name(PyObject UNUSED *self, PyObject *args);
#define GLFW_FUNC_WRAPPERS \
{"glfw_set_error_callback", (PyCFunction)glfw_set_error_callback, METH_O, ""}, \
{"glfw_init", (PyCFunction)glfw_init, METH_NOARGS, ""}, \
{"glfw_terminate", (PyCFunction)glfw_terminate, METH_NOARGS, ""}, \
{"glfw_window_hint", (PyCFunction)glfw_window_hint, METH_VARARGS, ""}, \
{"glfw_swap_interval", (PyCFunction)glfw_swap_interval, METH_VARARGS, ""}, \
{"glfw_wait_events", (PyCFunction)glfw_wait_events, METH_VARARGS, ""}, \
{"glfw_post_empty_event", (PyCFunction)glfw_post_empty_event, METH_NOARGS, ""}, \
{"glfw_get_physical_dpi", (PyCFunction)glfw_get_physical_dpi, METH_NOARGS, ""}, \
{"glfw_get_key_name", (PyCFunction)glfw_get_key_name, METH_VARARGS, ""}, \

997
kitty/graphics.c Normal file
View File

@@ -0,0 +1,997 @@
/*
* graphics.c
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "graphics.h"
#include "state.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <zlib.h>
#include <png.h>
#include <structmember.h>
#define STORAGE_LIMIT (320 * (1024 * 1024))
#define REPORT_ERROR(...) { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); }
static bool send_to_gpu = true;
GraphicsManager*
grman_alloc() {
GraphicsManager *self = (GraphicsManager *)GraphicsManager_Type.tp_alloc(&GraphicsManager_Type, 0);
self->images_capacity = 64;
self->images = calloc(self->images_capacity, sizeof(Image));
self->capacity = 64;
self->render_data = calloc(self->capacity, sizeof(ImageRenderData));
if (self->images == NULL || self->render_data == NULL) {
PyErr_NoMemory();
Py_CLEAR(self); return NULL;
}
return self;
}
static inline void
free_refs_data(Image *img) {
free(img->refs); img->refs = NULL;
img->refcnt = 0; img->refcap = 0;
}
static inline void
free_load_data(LoadData *ld) {
free(ld->buf); ld->buf_used = 0; ld->buf_capacity = 0;
ld->buf = NULL;
if (ld->mapped_file) munmap(ld->mapped_file, ld->mapped_file_sz);
ld->mapped_file = NULL; ld->mapped_file_sz = 0;
}
free_texture_func free_texture = NULL;
send_image_to_gpu_func send_image_to_gpu = NULL;
static inline void
free_image(GraphicsManager *self, Image *img) {
if (img->texture_id) free_texture(&img->texture_id);
free_refs_data(img);
free_load_data(&(img->load_data));
self->used_storage -= img->used_storage;
}
static void
dealloc(GraphicsManager* self) {
size_t i;
if (self->images) {
for (i = 0; i < self->image_count; i++) free_image(self, self->images + i);
free(self->images);
}
free(self->render_data);
Py_TYPE(self)->tp_free((PyObject*)self);
}
static size_t internal_id_counter = 1;
static inline void*
ensure_space(void *array, size_t *capacity, size_t count, size_t item_size, bool initialize) {
if (count < *capacity) return array;
void *ans = realloc(array, (*capacity) * item_size * 2);
if (ans == NULL) fatal("Out of memory re-allocating array.");
if (initialize) {
memset(((uint8_t*)array) + ((*capacity) * item_size), 0, ((*capacity) * item_size));
}
*capacity *= 2;
return ans;
}
static inline void
remove_from_array(void *array, size_t item_size, size_t idx, size_t array_count) {
size_t num_to_right = array_count - 1 - idx;
uint8_t *p = (uint8_t*)array;
if (num_to_right > 0) memmove(p + (idx * item_size), p + ((idx + 1) * item_size), num_to_right * item_size);
memset(p + (item_size * (array_count - 1)), 0, item_size);
}
static inline Image*
img_by_internal_id(GraphicsManager *self, size_t id) {
for (size_t i = 0; i < self->image_count; i++) {
if (self->images[i].internal_id == id) return self->images + i;
}
return NULL;
}
static inline Image*
img_by_client_id(GraphicsManager *self, uint32_t id) {
for (size_t i = 0; i < self->image_count; i++) {
if (self->images[i].client_id == id) return self->images + i;
}
return NULL;
}
static inline void
remove_image(GraphicsManager *self, size_t idx) {
free_image(self, self->images + idx);
remove_from_array(self->images, sizeof(Image), idx, self->image_count--);
self->layers_dirty = true;
}
static inline void
remove_images(GraphicsManager *self, bool(*predicate)(Image*), Image* skip_image) {
for (size_t i = self->image_count; i-- > 0;) {
Image *img = self->images + i;
if (img != skip_image && predicate(img)) {
remove_image(self, i);
}
}
}
// Loading image data {{{
static bool
trim_predicate(Image *img) {
return !img->data_loaded || !img->refcnt;
}
static int
oldest_last(const void* a, const void *b) {
double ans = ((Image*)(b))->atime - ((Image*)(a))->atime;
return ans < 0 ? -1 : (ans == 0 ? 0 : 1);
}
static inline void
apply_storage_quota(GraphicsManager *self, size_t storage_limit, Image *currently_added_image) {
// First remove unreferenced images, even if they have an id
remove_images(self, trim_predicate, currently_added_image);
if (self->used_storage < storage_limit) return;
qsort(self->images, self->image_count, sizeof(Image), oldest_last);
while (self->used_storage > storage_limit && self->image_count > 0) {
remove_image(self, self->image_count - 1);
}
if (!self->image_count) self->used_storage = 0; // sanity check
}
static char add_response[512] = {0};
static bool has_add_respose = false;
static inline void
set_add_response(const char *code, const char *fmt, ...) {
va_list args;
va_start(args, fmt);
size_t sz = sizeof(add_response)/sizeof(add_response[0]);
int num = snprintf(add_response, sz, "%s:", code);
vsnprintf(add_response + num, sz - num, fmt, args);
va_end(args);
has_add_respose = true;
}
// Decode formats {{{
#define ABRT(code, ...) { set_add_response(#code, __VA_ARGS__); goto err; }
static inline bool
mmap_img_file(GraphicsManager UNUSED *self, Image *img, int fd, size_t sz, off_t offset) {
if (!sz) {
struct stat s;
if (fstat(fd, &s) != 0) ABRT(EBADF, "Failed to fstat() the fd: %d file with error: [%d] %s", fd, errno, strerror(errno));
sz = s.st_size;
}
void *addr = mmap(0, sz, PROT_READ, MAP_SHARED, fd, offset);
if (addr == MAP_FAILED) ABRT(EBADF, "Failed to map image file fd: %d at offset: %zd with size: %zu with error: [%d] %s", fd, offset, sz, errno, strerror(errno));
img->load_data.mapped_file = addr;
img->load_data.mapped_file_sz = sz;
return true;
err:
return false;
}
static inline const char*
zlib_strerror(int ret) {
#define Z(x) case x: return #x;
static char buf[128];
switch(ret) {
case Z_ERRNO:
return strerror(errno);
default:
snprintf(buf, sizeof(buf)/sizeof(buf[0]), "Unknown error: %d", ret);
return buf;
Z(Z_STREAM_ERROR);
Z(Z_DATA_ERROR);
Z(Z_MEM_ERROR);
Z(Z_BUF_ERROR);
Z(Z_VERSION_ERROR);
}
#undef Z
}
static inline bool
inflate_zlib(GraphicsManager UNUSED *self, Image *img, uint8_t *buf, size_t bufsz) {
bool ok = false;
z_stream z;
uint8_t *decompressed = malloc(img->load_data.data_sz);
if (decompressed == NULL) fatal("Out of memory allocating decompression buffer");
z.zalloc = Z_NULL;
z.zfree = Z_NULL;
z.opaque = Z_NULL;
z.avail_in = bufsz;
z.next_in = (Bytef*)buf;
z.avail_out = img->load_data.data_sz;
z.next_out = decompressed;
int ret;
if ((ret = inflateInit(&z)) != Z_OK) ABRT(ENOMEM, "Failed to initialize inflate with error: %s", zlib_strerror(ret));
if ((ret = inflate(&z, Z_FINISH)) != Z_STREAM_END) ABRT(EINVAL, "Failed to inflate image data with error: %s", zlib_strerror(ret));
if (z.avail_out) ABRT(EINVAL, "Image data size post inflation does not match expected size");
free_load_data(&img->load_data);
img->load_data.buf_capacity = img->load_data.data_sz;
img->load_data.buf = decompressed;
img->load_data.buf_used = img->load_data.data_sz;
ok = true;
err:
inflateEnd(&z);
if (!ok) free(decompressed);
return ok;
}
struct fake_file { uint8_t *buf; size_t sz, cur; };
static void
read_png_from_buffer(png_structp png, png_bytep out, png_size_t length) {
struct fake_file *f = png_get_io_ptr(png);
if (f) {
size_t amt = MIN(length, f->sz - f->cur);
memcpy(out, f->buf + f->cur, amt);
f->cur += amt;
}
}
static void
read_png_error_handler(png_structp png_ptr, png_const_charp msg) {
jmp_buf *jb;
set_add_response("EBADPNG", msg);
jb = png_get_error_ptr(png_ptr);
if (jb == NULL) fatal("read_png_error_handler: could not retrieve jmp_buf");
longjmp(*jb, 1);
}
static void
read_png_warn_handler(png_structp UNUSED png_ptr, png_const_charp UNUSED msg) {
// ignore warnings
}
struct png_jmp_data { uint8_t *decompressed; bool ok; png_bytep *row_pointers; int width, height; size_t sz; };
static void
inflate_png_inner(struct png_jmp_data *d, uint8_t *buf, size_t bufsz) {
struct fake_file f = {.buf = buf, .sz = bufsz};
png_structp png = NULL;
png_infop info = NULL;
jmp_buf jb;
png = png_create_read_struct(PNG_LIBPNG_VER_STRING, &jb, read_png_error_handler, read_png_warn_handler);
if (!png) ABRT(ENOMEM, "Failed to create PNG read structure");
info = png_create_info_struct(png);
if (!info) ABRT(ENOMEM, "Failed to create PNG info structure");
if (setjmp(jb)) goto err;
png_set_read_fn(png, &f, read_png_from_buffer);
png_read_info(png, info);
png_byte color_type, bit_depth;
d->width = png_get_image_width(png, info);
d->height = png_get_image_height(png, info);
color_type = png_get_color_type(png, info);
bit_depth = png_get_bit_depth(png, info);
// Ensure we get RGBA data out of libpng
if (bit_depth == 16) png_set_strip_16(png);
if (color_type == PNG_COLOR_TYPE_PALETTE) png_set_palette_to_rgb(png);
// PNG_COLOR_TYPE_GRAY_ALPHA is always 8 or 16bit depth.
if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) png_set_expand_gray_1_2_4_to_8(png);
if (png_get_valid(png, info, PNG_INFO_tRNS)) png_set_tRNS_to_alpha(png);
// These color_type don't have an alpha channel then fill it with 0xff.
if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE) png_set_filler(png, 0xFF, PNG_FILLER_AFTER);
if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) png_set_gray_to_rgb(png);
png_read_update_info(png, info);
int rowbytes = png_get_rowbytes(png, info);
d->sz = rowbytes * d->height * sizeof(png_byte);
d->decompressed = malloc(d->sz + 16);
if (d->decompressed == NULL) ABRT(ENOMEM, "Out of memory allocating decompression buffer for PNG");
d->row_pointers = malloc(d->height * sizeof(png_bytep));
if (d->row_pointers == NULL) ABRT(ENOMEM, "Out of memory allocating row_pointers buffer for PNG");
for (int i = 0; i < d->height; i++) d->row_pointers[i] = d->decompressed + i * rowbytes;
png_read_image(png, d->row_pointers);
d->ok = true;
err:
if (png) png_destroy_read_struct(&png, info ? &info : NULL, NULL);
return;
}
static inline bool
inflate_png(GraphicsManager UNUSED *self, Image *img, uint8_t *buf, size_t bufsz) {
struct png_jmp_data d = {0};
inflate_png_inner(&d, buf, bufsz);
if (d.ok) {
free_load_data(&img->load_data);
img->load_data.buf = d.decompressed;
img->load_data.buf_capacity = d.sz;
img->load_data.buf_used = d.sz;
img->load_data.data_sz = d.sz;
img->width = d.width; img->height = d.height;
}
else free(d.decompressed);
free(d.row_pointers);
return d.ok;
}
#undef ABRT
// }}}
static bool
add_trim_predicate(Image *img) {
return !img->data_loaded || (!img->client_id && !img->refcnt);
}
static inline Image*
find_or_create_image(GraphicsManager *self, uint32_t id, bool *existing) {
if (id) {
for (size_t i = 0; i < self->image_count; i++) {
if (self->images[i].client_id == id) {
*existing = true;
return self->images + i;
}
}
}
*existing = false;
self->images = ensure_space(self->images, &self->images_capacity, self->image_count, sizeof(Image), true);
return self->images + self->image_count++;
}
static Image*
handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_t *payload, bool *is_dirty, uint32_t iid) {
#define ABRT(code, ...) { set_add_response(#code, __VA_ARGS__); self->loading_image = 0; if (img) img->data_loaded = false; return NULL; }
#define MAX_DATA_SZ (4 * 100000000)
has_add_respose = false;
bool existing, init_img = true;
Image *img = NULL;
unsigned char tt = g->transmission_type ? g->transmission_type : 'd';
enum FORMATS { RGB=24, RGBA=32, PNG=100 };
uint32_t fmt = g->format ? g->format : RGBA;
if (tt == 'd' && self->loading_image) init_img = false;
if (init_img) {
self->last_init_graphics_command = *g;
self->last_init_graphics_command.id = iid;
self->loading_image = 0;
if (g->data_width > 10000 || g->data_height > 10000) ABRT(EINVAL, "Image too large");
remove_images(self, add_trim_predicate, NULL);
img = find_or_create_image(self, iid, &existing);
if (existing) {
free_load_data(&img->load_data);
img->data_loaded = false;
free_refs_data(img);
*is_dirty = true;
self->layers_dirty = true;
} else {
img->internal_id = internal_id_counter++;
img->client_id = iid;
}
img->atime = monotonic(); img->used_storage = 0;
img->width = g->data_width; img->height = g->data_height;
switch(fmt) {
case PNG:
if (g->data_sz > MAX_DATA_SZ) ABRT(EINVAL, "PNG data size too large");
img->load_data.is_4byte_aligned = true;
img->load_data.is_opaque = false;
img->load_data.data_sz = g->data_sz ? g->data_sz : 1024 * 100;
break;
case RGB:
case RGBA:
img->load_data.data_sz = g->data_width * g->data_height * (fmt / 8);
if (!img->load_data.data_sz) ABRT(EINVAL, "Zero width/height not allowed");
img->load_data.is_4byte_aligned = fmt == RGBA || (img->width % 4 == 0);
img->load_data.is_opaque = fmt == RGB;
break;
default:
ABRT(EINVAL, "Unknown image format: %u", fmt);
}
if (tt == 'd') {
if (g->more) self->loading_image = img->internal_id;
img->load_data.buf_capacity = img->load_data.data_sz + (g->compressed ? 1024 : 10); // compression header
img->load_data.buf = malloc(img->load_data.buf_capacity);
img->load_data.buf_used = 0;
if (img->load_data.buf == NULL) {
ABRT(ENOMEM, "Out of memory");
img->load_data.buf_capacity = 0; img->load_data.buf_used = 0;
}
}
} else {
self->last_init_graphics_command.more = g->more;
self->last_init_graphics_command.payload_sz = g->payload_sz;
g = &self->last_init_graphics_command;
tt = g->transmission_type ? g->transmission_type : 'd';
fmt = g->format ? g->format : RGBA;
img = img_by_internal_id(self, self->loading_image);
if (img == NULL) {
self->loading_image = 0;
ABRT(EILSEQ, "More payload loading refers to non-existent image");
}
}
int fd;
static char fname[2056] = {0};
switch(tt) {
case 'd': // direct
if (img->load_data.buf_capacity - img->load_data.buf_used < g->payload_sz) {
if (img->load_data.buf_used + g->payload_sz > MAX_DATA_SZ || fmt != PNG) ABRT(EFBIG, "Too much data");
img->load_data.buf_capacity = MIN(2 * img->load_data.buf_capacity, MAX_DATA_SZ);
img->load_data.buf = realloc(img->load_data.buf, img->load_data.buf_capacity);
if (img->load_data.buf == NULL) {
ABRT(ENOMEM, "Out of memory");
img->load_data.buf_capacity = 0; img->load_data.buf_used = 0;
}
}
memcpy(img->load_data.buf + img->load_data.buf_used, payload, g->payload_sz);
img->load_data.buf_used += g->payload_sz;
if (!g->more) { img->data_loaded = true; self->loading_image = 0; }
break;
case 'f': // file
case 't': // temporary file
case 's': // POSIX shared memory
if (g->payload_sz > 2048) ABRT(EINVAL, "Filename too long");
snprintf(fname, sizeof(fname)/sizeof(fname[0]), "%.*s", (int)g->payload_sz, payload);
if (tt == 's') fd = shm_open(fname, O_RDONLY, 0);
else fd = open(fname, O_CLOEXEC | O_RDONLY);
if (fd == -1) ABRT(EBADF, "Failed to open file %s for graphics transmission with error: [%d] %s", fname, errno, strerror(errno));
img->data_loaded = mmap_img_file(self, img, fd, g->data_sz, g->data_offset);
close(fd);
if (tt == 't') unlink(fname);
else if (tt == 's') shm_unlink(fname);
break;
default:
ABRT(EINVAL, "Unknown transmission type: %c", g->transmission_type);
}
if (!img->data_loaded) return NULL;
self->loading_image = 0;
bool needs_processing = g->compressed || fmt == PNG;
if (needs_processing) {
uint8_t *buf; size_t bufsz;
#define IB { if (img->load_data.buf) { buf = img->load_data.buf; bufsz = img->load_data.buf_used; } else { buf = img->load_data.mapped_file; bufsz = img->load_data.mapped_file_sz; } }
switch(g->compressed) {
case 'z':
IB;
if (!inflate_zlib(self, img, buf, bufsz)) {
img->data_loaded = false; return NULL;
}
break;
case 0:
break;
default:
ABRT(EINVAL, "Unknown image compression: %c", g->compressed);
}
switch(fmt) {
case PNG:
IB;
if (!inflate_png(self, img, buf, bufsz)) {
img->data_loaded = false; return NULL;
}
break;
default: break;
}
#undef IB
img->load_data.data = img->load_data.buf;
if (img->load_data.buf_used < img->load_data.data_sz) {
ABRT(ENODATA, "Insufficient image data: %zu < %zu", img->load_data.buf_used, img->load_data.data_sz);
}
if (img->load_data.mapped_file) {
munmap(img->load_data.mapped_file, img->load_data.mapped_file_sz);
img->load_data.mapped_file = NULL; img->load_data.mapped_file_sz = 0;
}
} else {
if (tt == 'd') {
if (img->load_data.buf_used < img->load_data.data_sz) {
ABRT(ENODATA, "Insufficient image data: %zu < %zu", img->load_data.buf_used, img->load_data.data_sz);
} else img->load_data.data = img->load_data.buf;
} else {
if (img->load_data.mapped_file_sz < img->load_data.data_sz) {
ABRT(ENODATA, "Insufficient image data: %zu < %zu", img->load_data.mapped_file_sz, img->load_data.data_sz);
} else img->load_data.data = img->load_data.mapped_file;
}
}
size_t required_sz = (img->load_data.is_opaque ? 3 : 4) * img->width * img->height;
if (img->load_data.data_sz != required_sz) ABRT(EINVAL, "Image dimensions: %ux%u do not match data size: %zu, expected size: %zu", img->width, img->height, img->load_data.data_sz, required_sz);
if (LIKELY(img->data_loaded && send_to_gpu)) {
send_image_to_gpu(&img->texture_id, img->load_data.data, img->width, img->height, img->load_data.is_opaque, img->load_data.is_4byte_aligned);
free_load_data(&img->load_data);
self->used_storage += required_sz;
img->used_storage = required_sz;
}
return img;
#undef MAX_DATA_SZ
#undef ABRT
}
static inline const char*
create_add_response(GraphicsManager UNUSED *self, bool data_loaded, uint32_t iid) {
static char rbuf[sizeof(add_response)/sizeof(add_response[0])];
if (iid) {
if (!has_add_respose) {
if (!data_loaded) return NULL;
snprintf(add_response, 10, "OK");
}
snprintf(rbuf, sizeof(rbuf)/sizeof(rbuf[0]) - 1, "\033_Gi=%u;%s\033\\", iid, add_response);
return rbuf;
}
return NULL;
}
// }}}
// Displaying images {{{
#define ensure_space_for(base, array, type, num, capacity, initial_cap) \
if (base->capacity < num) { \
base->capacity = MAX(initial_cap, MAX(2 * base->capacity, num)); \
base->array = realloc(base->array, sizeof(type) * base->capacity); \
if (base->array == NULL) fatal("Out of memory while ensuring space in array"); \
}
static inline void
update_src_rect(ImageRef *ref, Image *img) {
// The src rect in OpenGL co-ords [0, 1] with origin at top-left corner of image
ref->src_rect.left = (float)ref->src_x / (float)img->width;
ref->src_rect.right = (float)(ref->src_x + ref->src_width) / (float)img->width;
ref->src_rect.top = (float)ref->src_y / (float)img->height;
ref->src_rect.bottom = (float)(ref->src_y + ref->src_height) / (float)img->height;
}
static inline void
update_dest_rect(ImageRef *ref, uint32_t num_cols, uint32_t num_rows) {
uint32_t t;
if (num_cols == 0) {
t = ref->src_width + ref->cell_x_offset;
num_cols = t / global_state.cell_width;
if (t > num_cols * global_state.cell_width) num_cols += 1;
}
if (num_rows == 0) {
t = ref->src_height + ref->cell_y_offset;
num_rows = t / global_state.cell_height;
if (t > num_rows * global_state.cell_height) num_rows += 1;
}
ref->effective_num_rows = num_rows;
ref->effective_num_cols = num_cols;
}
static void
handle_put_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, bool *is_dirty, Image *img) {
has_add_respose = false;
if (img == NULL) img = img_by_client_id(self, g->id);
if (img == NULL) { set_add_response("ENOENT", "Put command refers to non-existent image with id: %u", g->id); return; }
if (!img->data_loaded) { set_add_response("ENOENT", "Put command refers to image with id: %u that could not load its data", g->id); return; }
ensure_space_for(img, refs, ImageRef, img->refcnt + 1, refcap, 10);
*is_dirty = true;
self->layers_dirty = true;
ImageRef *ref = NULL;
for (size_t i=0; i < img->refcnt; i++) {
if ((unsigned)img->refs[i].start_row == c->x && (unsigned)img->refs[i].start_column == c->y) {
ref = img->refs + i;
break;
}
}
if (ref == NULL) ref = img->refs + img->refcnt++;
img->atime = monotonic();
ref->src_x = g->x_offset; ref->src_y = g->y_offset; ref->src_width = g->width ? g->width : img->width; ref->src_height = g->height ? g->height : img->height;
ref->src_width = MIN(ref->src_width, img->width - (img->width > ref->src_x ? ref->src_x : img->width));
ref->src_height = MIN(ref->src_height, img->height - (img->height > ref->src_y ? ref->src_y : img->height));
ref->z_index = g->z_index;
ref->start_row = c->y; ref->start_column = c->x;
ref->cell_x_offset = MIN(g->cell_x_offset, global_state.cell_width - 1);
ref->cell_y_offset = MIN(g->cell_y_offset, global_state.cell_height - 1);
ref->num_cols = g->num_cells; ref->num_rows = g->num_lines;
update_src_rect(ref, img);
update_dest_rect(ref, g->num_cells, g->num_lines);
// Move the cursor, the screen will take care of ensuring it is in bounds
c->x += ref->effective_num_cols; c->y += ref->effective_num_rows - 1;
}
static int
cmp_by_zindex_and_image(const void *a_, const void *b_) {
const ImageRenderData *a = (const ImageRenderData*)a_, *b = (const ImageRenderData*)b_;
int ans = a->z_index - b->z_index;
if (ans == 0) ans = a->image_id - b->image_id;
return ans;
}
bool
grman_update_layers(GraphicsManager *self, unsigned int scrolled_by, float screen_left, float screen_top, float dx, float dy, unsigned int num_cols, unsigned int num_rows) {
if (self->last_scrolled_by != scrolled_by) self->layers_dirty = true;
self->last_scrolled_by = scrolled_by;
if (!self->layers_dirty) return false;
self->layers_dirty = false;
size_t i, j;
self->num_of_negative_refs = 0; self->num_of_positive_refs = 0;
Image *img; ImageRef *ref;
ImageRect r;
float screen_width = dx * num_cols, screen_height = dy * num_rows;
float screen_bottom = screen_top - screen_height;
float screen_width_px = num_cols * global_state.cell_width;
float screen_height_px = num_rows * global_state.cell_height;
float y0 = screen_top - dy * scrolled_by;
// Iterate over all visible refs and create render data
self->count = 0;
for (i = 0; i < self->image_count; i++) { img = self->images + i; for (j = 0; j < img->refcnt; j++) { ref = img->refs + j;
r.top = y0 - ref->start_row * dy - dy * (float)ref->cell_y_offset / (float)global_state.cell_height;
if (ref->num_rows > 0) r.bottom = y0 - (ref->start_row + (int32_t)ref->num_rows) * dy;
else r.bottom = r.top - screen_height * (float)ref->src_height / screen_height_px;
if (r.top <= screen_bottom || r.bottom >= screen_top) continue; // not visible
r.left = screen_left + ref->start_column * dx + dx * (float)ref->cell_x_offset / (float) global_state.cell_width;
if (ref->num_cols > 0) r.right = screen_left + (ref->start_column + (int32_t)ref->num_cols) * dx;
else r.right = r.left + screen_width * (float)ref->src_width / screen_width_px;
if (ref->z_index < 0) self->num_of_negative_refs++; else self->num_of_positive_refs++;
ensure_space_for(self, render_data, ImageRenderData, self->count + 1, capacity, 100);
ImageRenderData *rd = self->render_data + self->count;
#define R(n, a, b) rd->vertices[n*4] = ref->src_rect.a; rd->vertices[n*4 + 1] = ref->src_rect.b; rd->vertices[n*4 + 2] = r.a; rd->vertices[n*4 + 3] = r.b;
R(0, right, top); R(1, right, bottom); R(2, left, bottom); R(3, left, top);
#undef R
self->count++;
rd->z_index = ref->z_index; rd->image_id = img->internal_id;
rd->texture_id = img->texture_id;
}}
if (!self->count) return false;
// Sort visible refs in draw order (z-index, img)
qsort(self->render_data, self->count, sizeof(ImageRenderData), cmp_by_zindex_and_image);
// Calculate the group counts
i = 0;
while (i < self->count) {
size_t image_id = self->render_data[i].image_id, start = i;
if (start == self->count - 1) i = self->count;
else {
while (i < self->count - 1 && self->render_data[++i].image_id == image_id) {}
}
self->render_data[start].group_count = i - start;
}
return true;
}
// }}}
// Image lifetime/scrolling {{{
static inline void
filter_refs(GraphicsManager *self, const void* data, bool free_images, bool (*filter_func)(ImageRef*, Image*, const void*)) {
Image *img; ImageRef *ref;
size_t i, j;
if (self->image_count) {
for (i = self->image_count; i-- > 0;) {
img = self->images + i;
for (j = img->refcnt; j-- > 0;) {
ref = img->refs + j;
if (filter_func(ref, img, data)) {
remove_from_array(img->refs, sizeof(ImageRef), j, img->refcnt--);
}
}
if (img->refcnt == 0 && (free_images || img->client_id == 0)) remove_image(self, i);
}
self->layers_dirty = true;
}
}
static inline bool
scroll_filter_func(ImageRef *ref, Image UNUSED *img, const void *data) {
ScrollData *d = (ScrollData*)data;
ref->start_row += d->amt;
return ref->start_row + (int32_t)ref->effective_num_rows <= d->limit;
}
static inline bool
ref_within_region(ImageRef *ref, index_type margin_top, index_type margin_bottom) {
return ref->start_row >= (int32_t)margin_top && ref->start_row + ref->effective_num_rows <= margin_bottom;
}
static inline bool
ref_outside_region(ImageRef *ref, index_type margin_top, index_type margin_bottom) {
return ref->start_row + ref->effective_num_rows <= margin_top || ref->start_row > (int32_t)margin_bottom;
}
static inline bool
scroll_filter_margins_func(ImageRef* ref, Image* img, const void* data) {
ScrollData *d = (ScrollData*)data;
if (ref_within_region(ref, d->margin_top, d->margin_bottom)) {
ref->start_row += d->amt;
if (ref_outside_region(ref, d->margin_top, d->margin_bottom)) return true;
// Clip the image if scrolling has resulted in part of it being outside the page area
uint32_t clip_amt, clipped_rows;
if (ref->start_row < (int32_t)d->margin_top) {
// image moved up
clipped_rows = d->margin_top - ref->start_row;
clip_amt = global_state.cell_height * clipped_rows;
if (ref->src_height <= clip_amt) return true;
ref->src_y += clip_amt; ref->src_height -= clip_amt;
ref->effective_num_rows -= clipped_rows;
update_src_rect(ref, img);
ref->start_row += clipped_rows;
} else if (ref->start_row + ref->effective_num_rows > d->margin_bottom) {
// image moved down
clipped_rows = ref->start_row + ref->effective_num_rows - d->margin_bottom;
clip_amt = global_state.cell_height * clipped_rows;
if (ref->src_height <= clip_amt) return true;
ref->src_height -= clip_amt;
ref->effective_num_rows -= clipped_rows;
update_src_rect(ref, img);
}
return ref_outside_region(ref, d->margin_top, d->margin_bottom);
}
return false;
}
void
grman_scroll_images(GraphicsManager *self, const ScrollData *data) {
filter_refs(self, data, true, data->has_margins ? scroll_filter_margins_func : scroll_filter_func);
}
static inline bool
clear_filter_func(ImageRef *ref, Image UNUSED *img, const void UNUSED *data) {
return ref->start_row + (int32_t)ref->effective_num_rows > 0;
}
void
grman_clear(GraphicsManager *self) {
filter_refs(self, NULL, true, clear_filter_func);
}
static inline bool
id_filter_func(ImageRef UNUSED *ref, Image *img, const void *data) {
uint32_t iid = *(uint32_t*)data;
return img->client_id == iid;
}
static inline bool
x_filter_func(ImageRef *ref, Image UNUSED *img, const void *data) {
const GraphicsCommand *g = data;
return ref->start_column <= (int32_t)g->x_offset - 1 && ((int32_t)g->x_offset - 1) < ((int32_t)(ref->start_column + ref->effective_num_cols));
}
static inline bool
y_filter_func(ImageRef *ref, Image UNUSED *img, const void *data) {
const GraphicsCommand *g = data;
return ref->start_row <= (int32_t)g->y_offset - 1 && ((int32_t)(g->y_offset - 1 < ref->start_row + ref->effective_num_rows));
}
static inline bool
z_filter_func(ImageRef *ref, Image UNUSED *img, const void *data) {
const GraphicsCommand *g = data;
return ref->z_index == g->z_index;
}
static inline bool
point_filter_func(ImageRef *ref, Image *img, const void *data) {
return x_filter_func(ref, img, data) && y_filter_func(ref, img, data);
}
static inline bool
point3d_filter_func(ImageRef *ref, Image *img, const void *data) {
return z_filter_func(ref, img, data) && point_filter_func(ref, img, data);
}
static void
handle_delete_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, bool *is_dirty) {
static GraphicsCommand d;
switch (g->delete_action) {
#define I(u, data, func) filter_refs(self, data, g->delete_action == u, func); *is_dirty = true; break
#define D(l, u, data, func) case l: case u: I(u, data, func)
#define G(l, u, func) D(l, u, g, func)
case 0:
D('a', 'A', NULL, clear_filter_func);
D('i', 'I', &g->id, id_filter_func);
G('p', 'P', point_filter_func);
G('q', 'Q', point3d_filter_func);
G('x', 'X', x_filter_func);
G('y', 'Y', y_filter_func);
G('z', 'Z', z_filter_func);
case 'c':
case 'C':
d.x_offset = c->x + 1; d.y_offset = c->y + 1;
I('C', &d, point_filter_func);
default:
REPORT_ERROR("Unknown graphics command delete action: %c", g->delete_action);
break;
#undef G
#undef D
#undef I
}
}
// }}}
void
grman_resize(GraphicsManager *self, index_type UNUSED old_lines, index_type UNUSED lines, index_type UNUSED old_columns, index_type UNUSED columns) {
self->layers_dirty = true;
}
void
grman_rescale(GraphicsManager *self, unsigned int UNUSED old_cell_width, unsigned int UNUSED old_cell_height) {
ImageRef *ref; Image *img;
self->layers_dirty = true;
for (size_t i = self->image_count; i-- > 0;) {
img = self->images + i;
for (size_t j = img->refcnt; j-- > 0;) {
ref = img->refs + j;
ref->cell_x_offset = MIN(ref->cell_x_offset, global_state.cell_width - 1);
ref->cell_y_offset = MIN(ref->cell_y_offset, global_state.cell_height - 1);
update_dest_rect(ref, ref->num_cols, ref->num_rows);
}
}
}
const char*
grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_t *payload, Cursor *c, bool *is_dirty) {
Image *image;
const char *ret = NULL;
uint32_t iid, q_iid;
switch(g->action) {
case 0:
case 't':
case 'T':
case 'q':
iid = g->id;
if (g->action == 'q') { q_iid = iid; iid = 0; if (!q_iid) { REPORT_ERROR("Query graphics command without image id"); break; } }
image = handle_add_command(self, g, payload, is_dirty, iid);
ret = create_add_response(self, image != NULL, g->action == 'q' ? q_iid: self->last_init_graphics_command.id);
if (self->last_init_graphics_command.action == 'T' && image && image->data_loaded) handle_put_command(self, &self->last_init_graphics_command, c, is_dirty, image);
if (g->action == 'q') remove_images(self, add_trim_predicate, NULL);
if (self->used_storage > STORAGE_LIMIT) apply_storage_quota(self, STORAGE_LIMIT, image);
break;
case 'p':
if (!g->id) {
REPORT_ERROR("Put graphics command without image id");
break;
}
handle_put_command(self, g, c, is_dirty, NULL);
ret = create_add_response(self, true, g->id);
break;
case 'd':
handle_delete_command(self, g, c, is_dirty);
break;
default:
REPORT_ERROR("Unknown graphics command action: %c", g->action);
break;
}
return ret;
}
// Boilerplate {{{
static PyObject *
new(PyTypeObject UNUSED *type, PyObject UNUSED *args, PyObject UNUSED *kwds) {
PyObject *ans = (PyObject*)grman_alloc();
if (ans == NULL) PyErr_NoMemory();
return ans;
}
static inline PyObject*
image_as_dict(Image *img) {
#define U(x) #x, img->x
return Py_BuildValue("{sI sI sI sI sI sI sH sH sN}",
U(texture_id), U(client_id), U(width), U(height), U(internal_id), U(refcnt), U(data_loaded),
"is_4byte_aligned", img->load_data.is_4byte_aligned,
"data", Py_BuildValue("y#", img->load_data.data, img->load_data.data_sz)
);
#undef U
}
#define W(x) static PyObject* py##x(GraphicsManager UNUSED *self, PyObject *args)
#define PA(fmt, ...) if(!PyArg_ParseTuple(args, fmt, __VA_ARGS__)) return NULL;
W(image_for_client_id) {
unsigned long id = PyLong_AsUnsignedLong(args);
bool existing = false;
Image *img = find_or_create_image(self, id, &existing);
if (!existing) { Py_RETURN_NONE; }
return image_as_dict(img);
}
W(shm_write) {
const char *name, *data;
Py_ssize_t sz;
PA("ss#", &name, &data, &sz);
int fd = shm_open(name, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
if (fd == -1) { PyErr_SetFromErrnoWithFilename(PyExc_OSError, name); return NULL; }
int ret = ftruncate(fd, sz);
if (ret != 0) { close(fd); PyErr_SetFromErrnoWithFilename(PyExc_OSError, name); return NULL; }
void *addr = mmap(0, sz, PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) { close(fd); PyErr_SetFromErrnoWithFilename(PyExc_OSError, name); return NULL; }
memcpy(addr, data, sz);
if (munmap(addr, sz) != 0) { close(fd); PyErr_SetFromErrnoWithFilename(PyExc_OSError, name); return NULL; }
close(fd);
Py_RETURN_NONE;
}
W(shm_unlink) {
char *name;
PA("s", &name);
int ret = shm_unlink(name);
if (ret == -1) { PyErr_SetFromErrnoWithFilename(PyExc_OSError, name); return NULL; }
Py_RETURN_NONE;
}
W(set_send_to_gpu) {
send_to_gpu = PyObject_IsTrue(args) ? true : false;
Py_RETURN_NONE;
}
W(update_layers) {
unsigned int scrolled_by, sx, sy; float xstart, ystart, dx, dy;
PA("IffffII", &scrolled_by, &xstart, &ystart, &dx, &dy, &sx, &sy);
grman_update_layers(self, scrolled_by, xstart, ystart, dx, dy, sx, sy);
PyObject *ans = PyTuple_New(self->count);
for (size_t i = 0; i < self->count; i++) {
ImageRenderData *r = self->render_data + i;
#define R(offset) Py_BuildValue("{sf sf sf sf}", "left", r->vertices[offset + 8], "top", r->vertices[offset + 1], "right", r->vertices[offset], "bottom", r->vertices[offset + 5])
PyTuple_SET_ITEM(ans, i,
Py_BuildValue("{sN sN sI si sI}", "src_rect", R(0), "dest_rect", R(2), "group_count", r->group_count, "z_index", r->z_index, "image_id", r->image_id)
);
#undef R
}
return ans;
}
#define M(x, va) {#x, (PyCFunction)py##x, va, ""}
static PyMethodDef methods[] = {
M(image_for_client_id, METH_O),
M(update_layers, METH_VARARGS),
{NULL} /* Sentinel */
};
static PyMemberDef members[] = {
{"image_count", T_UINT, offsetof(GraphicsManager, image_count), 0, "image_count"},
{NULL},
};
PyTypeObject GraphicsManager_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "fast_data_types.GraphicsManager",
.tp_basicsize = sizeof(GraphicsManager),
.tp_dealloc = (destructor)dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = "GraphicsManager",
.tp_new = new,
.tp_methods = methods,
.tp_members = members,
};
static PyMethodDef module_methods[] = {
M(shm_write, METH_VARARGS),
M(shm_unlink, METH_VARARGS),
M(set_send_to_gpu, METH_O),
{NULL, NULL, 0, NULL} /* Sentinel */
};
bool
init_graphics(PyObject *module) {
if (PyType_Ready(&GraphicsManager_Type) < 0) return false;
if (PyModule_AddObject(module, "GraphicsManager", (PyObject *)&GraphicsManager_Type) != 0) return false;
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
Py_INCREF(&GraphicsManager_Type);
return true;
}
// }}}

92
kitty/graphics.h Normal file
View File

@@ -0,0 +1,92 @@
/*
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#pragma once
#include "data-types.h"
typedef struct {
unsigned char action, transmission_type, compressed, delete_action;
uint32_t format, more, id, data_sz, data_offset;
uint32_t width, height, x_offset, y_offset, data_height, data_width, num_cells, num_lines, cell_x_offset, cell_y_offset;
int32_t z_index;
size_t payload_sz;
} GraphicsCommand;
typedef struct {
uint8_t *buf;
size_t buf_capacity, buf_used;
uint8_t *mapped_file;
size_t mapped_file_sz;
size_t data_sz;
uint8_t *data;
bool is_4byte_aligned;
bool is_opaque;
} LoadData;
typedef struct {
float left, top, right, bottom;
} ImageRect;
typedef struct {
uint32_t src_width, src_height, src_x, src_y;
uint32_t cell_x_offset, cell_y_offset, num_cols, num_rows, effective_num_rows, effective_num_cols;
int32_t z_index;
int32_t start_row, start_column;
ImageRect src_rect;
} ImageRef;
typedef struct {
uint32_t texture_id, client_id, width, height;
size_t internal_id;
bool data_loaded;
LoadData load_data;
ImageRef *refs;
size_t refcnt, refcap;
double atime;
size_t used_storage;
} Image;
typedef struct {
float vertices[16];
uint32_t texture_id, group_count;
int z_index;
size_t image_id;
} ImageRenderData;
typedef struct {
PyObject_HEAD
size_t image_count, images_capacity, loading_image;
GraphicsCommand last_init_graphics_command;
Image *images;
size_t count, capacity;
ImageRenderData *render_data;
bool layers_dirty;
size_t num_of_negative_refs, num_of_positive_refs;
unsigned int last_scrolled_by;
size_t used_storage;
} GraphicsManager;
PyTypeObject GraphicsManager_Type;
typedef struct {
int32_t amt, limit;
index_type margin_top, margin_bottom;
bool has_margins;
} ScrollData;
GraphicsManager* grman_alloc();
void grman_clear(GraphicsManager*);
const char* grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_t *payload, Cursor *c, bool *is_dirty);
bool grman_update_layers(GraphicsManager *self, unsigned int scrolled_by, float screen_left, float screen_top, float dx, float dy, unsigned int num_cols, unsigned int num_rows);
void grman_scroll_images(GraphicsManager *self, const ScrollData*);
void grman_resize(GraphicsManager*, index_type, index_type, index_type, index_type);
void grman_rescale(GraphicsManager *self, unsigned int old_cell_width, unsigned int old_cell_height);

View File

@@ -0,0 +1,10 @@
#version GLSL_VERSION
uniform sampler2D image;
in vec2 texcoord;
out vec4 color;
void main() {
color = texture(image, texcoord);
}

View File

@@ -0,0 +1,9 @@
#version GLSL_VERSION
in vec4 src;
out vec2 texcoord;
void main() {
texcoord = vec2(src[0], src[1]);
gl_Position = vec4(src[2], src[3], 0, 1);
}

View File

@@ -6,15 +6,12 @@
*/
#include "data-types.h"
#include "lineops.h"
#include <structmember.h>
#define CELL_FIELD_COUNT_H (CELL_FIELD_COUNT + 1)
#define CELL_SIZE_H (CELL_FIELD_COUNT_H * 4)
static inline uint32_t*
start_of(HistoryBuf *self, index_type num) {
// Pointer to the start of the line with index (buffer position) num
return self->buf + CELL_FIELD_COUNT_H * num * self->xnum;
static inline Cell*
lineptr(HistoryBuf *linebuf, index_type y) {
return linebuf->buf + y * linebuf->xnum;
}
static PyObject *
@@ -33,17 +30,17 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
if (self != NULL) {
self->xnum = xnum;
self->ynum = ynum;
self->buf = (uint32_t*)PyMem_Calloc(xnum * ynum, CELL_SIZE_H);
self->buf = PyMem_Calloc(xnum * ynum, sizeof(Cell));
self->continued_map = PyMem_Calloc(ynum, sizeof(bool));
self->line = alloc_line();
if (self->buf == NULL || self->line == NULL) {
if (self->buf == NULL || self->line == NULL || self->continued_map == NULL) {
PyErr_NoMemory();
PyMem_Free(self->buf); Py_CLEAR(self->line);
PyMem_Free(self->buf); Py_CLEAR(self->line); PyMem_Free(self->continued_map);
Py_CLEAR(self);
} else {
self->line->xnum = xnum;
for(index_type y = 0; y < self->ynum; y++) {
self->line->chars = start_of(self, y) + 1;
for (index_type i = 0; i < self->xnum; i++) self->line->chars[i] = (1 << ATTRS_SHIFT) | 32;
clear_chars_in_line(lineptr(self, y), self->xnum, BLANK_CHAR);
}
}
}
@@ -55,9 +52,17 @@ static void
dealloc(HistoryBuf* self) {
Py_CLEAR(self->line);
PyMem_Free(self->buf);
PyMem_Free(self->continued_map);
Py_TYPE(self)->tp_free((PyObject*)self);
}
void
historybuf_refresh_sprite_positions(HistoryBuf *self) {
for (index_type i = 0; i < self->ynum; i++) {
update_sprites_in_line(lineptr(self, i), self->xnum);
}
}
static inline index_type
index_of(HistoryBuf *self, index_type lnum) {
// The index (buffer position) of the line with line number lnum
@@ -70,13 +75,8 @@ index_of(HistoryBuf *self, index_type lnum) {
static inline void
init_line(HistoryBuf *self, index_type num, Line *l) {
// Initialize the line l, setting its pointer to the offsets for the line at index (buffer position) num
uint32_t *start_ptr = start_of(self, num);
l->continued = (bool)(*start_ptr);
l->chars = (char_type*)(start_ptr + 1);
l->fg_colors = (color_type*)(l->chars + self->xnum);
l->bg_colors = (color_type*)(l->fg_colors + self->xnum);
l->decoration_fg = (color_type*)(l->bg_colors + self->xnum);
l->combining_chars = (combining_type*)(l->decoration_fg + self->xnum);
l->cells = lineptr(self, num);
l->continued = self->continued_map[num];
}
void
@@ -84,8 +84,8 @@ historybuf_init_line(HistoryBuf *self, index_type lnum, Line *l) {
init_line(self, index_of(self, lnum), l);
}
static inline
index_type historybuf_push(HistoryBuf *self) {
static inline index_type
historybuf_push(HistoryBuf *self) {
index_type idx = (self->start_of_data + self->count) % self->ynum;
init_line(self, idx, self->line);
if (self->count == self->ynum) self->start_of_data = (self->start_of_data + 1) % self->ynum;
@@ -99,20 +99,21 @@ historybuf_resize(HistoryBuf *self, index_type lines) {
t.xnum=self->xnum;
t.ynum=lines;
if (t.ynum > 0 && t.ynum != self->ynum) {
t.buf = (uint32_t*)PyMem_Calloc(t.xnum * t.ynum, CELL_SIZE_H);
t.buf = PyMem_Calloc(t.xnum * t.ynum, sizeof(Cell));
if (t.buf == NULL) { PyErr_NoMemory(); return false; }
t.continued_map = PyMem_Calloc(t.ynum, sizeof(bool));
if (t.continued_map == NULL) { PyMem_Free(t.buf); PyErr_NoMemory(); return false; }
t.count = MIN(self->count, t.ynum);
if (t.count > 0) {
for (index_type s=0; s < t.count; s++) {
uint32_t *src = start_of(self, index_of(self, s)), *dest = start_of(&t, index_of(&t, s));
memcpy(dest, src, CELL_SIZE_H * t.xnum);
}
for (index_type s=0; s < t.count; s++) {
index_type si = index_of(self, s), ti = index_of(&t, s);
copy_cells(lineptr(self, si), lineptr(&t, ti), t.xnum);
t.continued_map[ti] = self->continued_map[si];
}
self->count = t.count;
self->start_of_data = t.start_of_data;
self->ynum = t.ynum;
PyMem_Free(self->buf);
self->buf = t.buf;
PyMem_Free(self->buf); PyMem_Free(self->continued_map);
self->buf = t.buf; self->continued_map = t.continued_map;
}
return true;
}
@@ -120,8 +121,8 @@ historybuf_resize(HistoryBuf *self, index_type lines) {
void
historybuf_add_line(HistoryBuf *self, const Line *line) {
index_type idx = historybuf_push(self);
COPY_LINE(line, self->line);
*(start_of(self, idx)) = line->continued;
copy_line(line, self->line);
self->continued_map[idx] = line->continued;
}
static PyObject*
@@ -142,6 +143,22 @@ line(HistoryBuf *self, PyObject *val) {
return (PyObject*)self->line;
}
static PyObject*
__str__(HistoryBuf *self) {
PyObject *lines = PyTuple_New(self->ynum);
if (lines == NULL) return PyErr_NoMemory();
for (index_type i = 0; i < self->count; i++) {
init_line(self, index_of(self, i), self->line);
PyObject *t = PyObject_Str((PyObject*)self->line);
if (t == NULL) { Py_CLEAR(lines); return NULL; }
PyTuple_SET_ITEM(lines, i, t);
}
PyObject *sep = PyUnicode_FromString("\n");
PyObject *ans = PyUnicode_Join(sep, lines);
Py_CLEAR(lines); Py_CLEAR(sep);
return ans;
}
static PyObject*
push(HistoryBuf *self, PyObject *args) {
#define push_doc "Push a line into this buffer, removing the oldest line, if necessary"
@@ -159,7 +176,7 @@ as_ansi(HistoryBuf *self, PyObject *callback) {
for(unsigned int i = 0; i < self->count; i++) {
init_line(self, i, &l);
if (i < self->count - 1) {
l.continued = *start_of(self, index_of(self, i + 1));
l.continued = self->continued_map[index_of(self, i + 1)];
} else l.continued = false;
index_type num = line_as_ansi(&l, t, 5120);
if (!(l.continued) && num < 5119) t[num++] = 10; // 10 = \n
@@ -202,6 +219,7 @@ PyTypeObject HistoryBuf_Type = {
.tp_doc = "History buffers",
.tp_methods = methods,
.tp_members = members,
.tp_str = (reprfunc)__str__,
.tp_new = new
};
@@ -218,9 +236,9 @@ HistoryBuf *alloc_historybuf(unsigned int lines, unsigned int columns) {
#define init_src_line(src_y) init_line(src, map_src_index(src_y), src->line);
#define is_src_line_continued(src_y) (map_src_index(src_y) < src->ynum - 1 ? (bool)(*(start_of(src, map_src_index(src_y + 1)))) : false)
#define is_src_line_continued(src_y) (map_src_index(src_y) < src->ynum - 1 ? src->continued_map[map_src_index(src_y + 1)] : false)
#define next_dest_line(cont) *start_of(dest, historybuf_push(dest)) = cont; dest->line->continued = cont;
#define next_dest_line(cont) dest->continued_map[historybuf_push(dest)] = cont; dest->line->continued = cont;
#define first_dest_line next_dest_line(false);
@@ -229,10 +247,9 @@ HistoryBuf *alloc_historybuf(unsigned int lines, unsigned int columns) {
void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other) {
// Fast path
if (other->xnum == self->xnum && other->ynum == self->ynum) {
Py_BEGIN_ALLOW_THREADS;
memcpy(other->buf, self->buf, CELL_SIZE_H * self->xnum * self->ynum);
memcpy(other->buf, self->buf, sizeof(Cell) * self->xnum * self->ynum);
memcpy(other->continued_map, self->continued_map, sizeof(bool) * self->ynum);
other->count = self->count; other->start_of_data = self->start_of_data;
Py_END_ALLOW_THREADS;
return;
}
other->count = 0; other->start_of_data = 0;

277
kitty/icat.py Executable file
View File

@@ -0,0 +1,277 @@
#!/usr/bin/env python3
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import argparse
import codecs
import fcntl
import mimetypes
import os
import re
import selectors
import signal
import struct
import subprocess
import sys
import termios
import tty
import zlib
from base64 import standard_b64encode
from collections import namedtuple
from gettext import gettext as _
from math import ceil, floor
from tempfile import NamedTemporaryFile
from time import monotonic
try:
from kitty.constants import appname
except ImportError:
appname = ''
try:
fsenc = sys.getfilesystemencoding() or 'utf-8'
codecs.lookup(fsenc)
except Exception:
fsenc = 'utf-8'
class OpenFailed(ValueError):
def __init__(self, path, message):
ValueError.__init__(
self, 'Failed to open: {} with error: {}'.format(path, message)
)
self.path = path
def option_parser():
parser = argparse.ArgumentParser(
prog=appname + '-icat' if appname else 'icat',
description=_('Display images in the terminal')
)
a = parser.add_argument
a(
'items',
nargs='+',
help=_(
'Image files or directories. Directories are scanned recursively.'
)
)
return parser
Size = namedtuple('Size', 'rows cols width height')
def screen_size(refresh=False):
if refresh or getattr(screen_size, 'ans', None) is None:
s = struct.pack('HHHH', 0, 0, 0, 0)
x = fcntl.ioctl(1, termios.TIOCGWINSZ, s)
screen_size.ans = Size(*struct.unpack('HHHH', x))
return screen_size.ans
def write_gr_cmd(cmd, payload):
cmd = ','.join('{}={}'.format(k, v) for k, v in cmd.items())
w = sys.stdout.buffer.write
w(b'\033_G'), w(cmd.encode('ascii')), w(b';'), w(payload), w(b'\033\\')
sys.stdout.flush()
def fit_image(width, height, pwidth, pheight):
if height > pheight:
corrf = pheight / float(height)
width, height = floor(corrf * width), pheight
if width > pwidth:
corrf = pwidth / float(width)
width, height = pwidth, floor(corrf * height)
if height > pheight:
corrf = pheight / float(height)
width, height = floor(corrf * width), pheight
return int(width), int(height)
def set_cursor(cmd, width, height):
ss = screen_size()
cw = int(ss.width / ss.cols)
num_of_cells_needed = int(ceil(width / cw))
if num_of_cells_needed > ss.cols:
w, h = fit_image(width, height, ss.width, height)
ch = int(ss.height / ss.rows)
num_of_rows_needed = int(ceil(height / ch))
y_off = height % ch
cmd['c'], cmd['r'] = ss.cols, num_of_rows_needed
cmd['Y'] = y_off
else:
x_off = width % cw
cmd['X'] = x_off
extra_cells = (ss.cols - num_of_cells_needed) // 2
if extra_cells:
sys.stdout.buffer.write(b' ' * extra_cells)
def write_chunked(cmd, data):
if cmd['f'] != 100:
data = zlib.compress(data)
cmd['o'] = 'z'
data = standard_b64encode(data)
while data:
chunk, data = data[:4096], data[4096:]
m = 1 if data else 0
cmd['m'] = m
write_gr_cmd(cmd, chunk)
cmd.clear()
def show(outfile, width, height, fmt, transmit_mode):
cmd = {'a': 'T', 'f': fmt, 's': width, 'v': height}
set_cursor(cmd, width, height)
if detect_support.has_files:
cmd['t'] = transmit_mode
write_gr_cmd(cmd, standard_b64encode(os.path.abspath(outfile).encode(fsenc)))
else:
with open(outfile, 'rb') as f:
data = f.read()
if fmt == 100:
cmd['S'] = len(data)
write_chunked(cmd, data)
ImageData = namedtuple('ImageData', 'fmt width height mode')
def run_imagemagick(path, cmd, keep_stdout=True):
try:
p = subprocess.run(cmd, stdout=subprocess.PIPE if keep_stdout else subprocess.DEVNULL, stderr=subprocess.PIPE)
except FileNotFoundError:
raise SystemExit('ImageMagick is required to cat images')
if p.returncode != 0:
raise OpenFailed(path, p.stderr.decode('utf-8'))
return p
def identify(path):
p = run_imagemagick(path, ['identify', '-format', '%m %w %h %A', path])
parts = tuple(filter(None, p.stdout.decode('utf-8').split()))
mode = 'rgb' if parts[3].lower() == 'false' else 'rgba'
return ImageData(parts[0].lower(), int(parts[1]), int(parts[2]), mode)
def convert(path, m, ss):
width, height = m.width, m.height
cmd = ['convert', '-background', 'none', path]
if width > ss.width:
width, height = fit_image(width, height, ss.width, height)
cmd += ['-resize', '{}x{}'.format(width, height)]
with NamedTemporaryFile(prefix='icat-', suffix='.' + m.mode, delete=False) as outfile:
run_imagemagick(path, cmd + [outfile.name])
return outfile.name, width, height
def process(path):
m = identify(path)
ss = screen_size()
needs_scaling = m.width > ss.width
if m.fmt == 'png' and not needs_scaling:
outfile = path
transmit_mode = 'f'
fmt = 100
width, height = m.width, m.height
else:
fmt = 24 if m.mode == 'rgb' else 32
transmit_mode = 't'
outfile, width, height = convert(path, m, ss)
show(outfile, width, height, fmt, transmit_mode)
print() # ensure cursor is on a new line
def scan(d):
for dirpath, dirnames, filenames in os.walk(d):
for f in filenames:
mt = mimetypes.guess_type(f)[0]
if mt and mt.startswith('image/'):
yield os.path.join(dirpath, f), mt
def detect_support(wait_for=10):
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
oldfl = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, oldfl | os.O_NONBLOCK)
print('Checking for graphics ({}s max. wait)...'.format(wait_for), end='\r')
sys.stdout.flush()
tty.setraw(fd)
try:
received = b''
start_time = monotonic()
responses = {}
def parse_responses():
for m in re.finditer(b'\033_Gi=([1|2]);(.+?)\033\\\\', received):
iid = m.group(1)
if iid in (b'1', b'2'):
iid = int(iid.decode('ascii'))
if iid not in responses:
responses[iid] = m.group(2) == b'OK'
def read():
nonlocal received
d = sys.stdin.buffer.read()
if not d: # EOF
responses[1] = responses[2] = False
return
received += d
parse_responses()
with NamedTemporaryFile() as f:
f.write(b'abcd'), f.flush()
write_gr_cmd(dict(a='q', s=1, v=1, i=1), standard_b64encode(b'abcd'))
write_gr_cmd(dict(a='q', s=1, v=1, i=2, t='f'), standard_b64encode(f.name.encode(fsenc)))
sel = selectors.DefaultSelector()
sel.register(sys.stdin, selectors.EVENT_READ, read)
while monotonic() - start_time < wait_for and 1 not in responses and 2 not in responses:
for key, mask in sel.select(0.1):
read()
finally:
sys.stdout.buffer.write(b'\033[J'), sys.stdout.flush()
termios.tcsetattr(fd, termios.TCSADRAIN, old)
fcntl.fcntl(fd, fcntl.F_SETFL, oldfl)
detect_support.has_files = bool(responses.get(2))
return responses.get(1, False)
def main(args=sys.argv):
signal.signal(signal.SIGWINCH, lambda: screen_size(refresh=True))
if not sys.stdout.isatty() or not sys.stdin.isatty():
raise SystemExit(
'Must be run in a terminal, stdout is currently not a terminal'
)
if screen_size().width == 0:
raise SystemExit(
'Terminal does not support reporting screen sizes via the TIOCGWINSZ ioctl'
)
args = option_parser().parse_args(args[1:])
if not args.items:
raise SystemExit('You must specify at least one file to cat')
if not detect_support():
raise SystemExit('This terminal emulator does not support the graphics protocol, use a terminal emulator such as kitty that does support it')
errors = []
for item in args.items:
try:
if os.path.isdir(item):
for x in scan(item):
process(item)
else:
process(item)
except OpenFailed as e:
errors.append(e)
if not errors:
return
for err in errors:
print(err, file=sys.stderr)
raise SystemExit(1)
if __name__ == '__main__':
main()

202
kitty/keys.c Normal file
View File

@@ -0,0 +1,202 @@
/*
* keys.c
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "keys.h"
#include "state.h"
#include "screen.h"
#include <GLFW/glfw3.h>
const uint8_t*
key_to_bytes(int glfw_key, bool smkx, bool extended, int mods, int action) {
if ((action & 3) == 3) return NULL;
if ((unsigned)glfw_key >= sizeof(key_map)/sizeof(key_map[0]) || glfw_key < 0) return NULL;
uint16_t key = key_map[glfw_key];
if (key == UINT8_MAX) return NULL;
mods &= 0xF;
key |= (mods & 0xF) << 7;
key |= (action & 3) << 11;
key |= (smkx & 1) << 13;
key |= (extended & 1) << 14;
if (key >= SIZE_OF_KEY_BYTES_MAP) return NULL;
return key_bytes[key];
}
#define SPECIAL_INDEX(key) ((key & 0x7f) | ( (mods & 0xF) << 7))
void
set_special_key_combo(int glfw_key, int mods) {
uint16_t key = key_map[glfw_key];
if (key != UINT8_MAX) {
key = SPECIAL_INDEX(key);
needs_special_handling[key] = true;
}
}
static inline Window*
active_window() {
Tab *t = global_state.tabs + global_state.active_tab;
Window *w = t->windows + t->active_window;
if (w->render_data.screen) return w;
return NULL;
}
void
on_text_input(unsigned int codepoint, int mods) {
Window *w = active_window();
static char buf[10];
unsigned int sz = 0;
if (w != NULL) {
Screen *screen = w->render_data.screen;
bool in_alt_mods = !screen->modes.mEXTENDED_KEYBOARD && (mods == GLFW_MOD_ALT || mods == (GLFW_MOD_ALT | GLFW_MOD_SHIFT));
bool is_text = mods <= GLFW_MOD_SHIFT;
if (in_alt_mods) {
sz = encode_utf8(codepoint, buf + 1);
if (sz) {
buf[0] = 033;
sz++;
}
} else if (is_text) sz = encode_utf8(codepoint, buf);
if (sz) schedule_write_to_child(w->id, buf, sz);
}
}
static inline bool
is_modifier_key(int key) {
switch(key) {
case GLFW_KEY_LEFT_SHIFT:
case GLFW_KEY_RIGHT_SHIFT:
case GLFW_KEY_LEFT_ALT:
case GLFW_KEY_RIGHT_ALT:
case GLFW_KEY_LEFT_CONTROL:
case GLFW_KEY_RIGHT_CONTROL:
case GLFW_KEY_LEFT_SUPER:
case GLFW_KEY_RIGHT_SUPER:
return true;
default:
return false;
}
}
static inline int
get_localized_key(int key, int scancode) {
const char *name = glfwGetKeyName(key, scancode);
if (name == NULL || name[1] != 0) return key;
switch(name[0]) {
#define K(ch, name) case ch: return GLFW_KEY_##name
// key names {{{
K('A', A);
K('B', B);
K('C', C);
K('D', D);
K('E', E);
K('F', F);
K('G', G);
K('H', H);
K('I', I);
K('J', J);
K('K', K);
K('L', L);
K('M', M);
K('N', N);
K('O', O);
K('P', P);
K('Q', Q);
K('S', S);
K('T', T);
K('U', U);
K('V', V);
K('W', W);
K('X', X);
K('Y', Y);
K('Z', Z);
K('0', 0);
K('1', 1);
K('2', 2);
K('3', 3);
K('5', 5);
K('6', 6);
K('7', 7);
K('8', 8);
K('9', 9);
K('\'', APOSTROPHE);
K(',', COMMA);
K('.', PERIOD);
K('/', SLASH);
K('-', MINUS);
K(';', SEMICOLON);
K('=', EQUAL);
K('[', LEFT_BRACKET);
K(']', RIGHT_BRACKET);
K('`', GRAVE_ACCENT);
K('\\', BACKSLASH);
// }}}
#undef K
default:
return key;
}
}
void
on_key_input(int key, int scancode, int action, int mods) {
Window *w = active_window();
if (!w) return;
Screen *screen = w->render_data.screen;
int lkey = get_localized_key(key, scancode);
if (action == GLFW_PRESS || action == GLFW_REPEAT) {
uint16_t qkey = key_map[lkey];
bool special = false;
if (qkey != UINT8_MAX) {
qkey = SPECIAL_INDEX(qkey);
special = needs_special_handling[qkey];
}
/* printf("key: %s mods: %d special: %d\n", key_name(lkey), mods, special); */
if (special) {
PyObject *ret = PyObject_CallMethod(global_state.boss, "dispatch_special_key", "iiii", lkey, scancode, action, mods);
if (ret == NULL) { PyErr_Print(); }
else {
bool consumed = ret == Py_True;
Py_DECREF(ret);
if (consumed) return;
}
}
}
if (screen->scrolled_by && action == GLFW_PRESS && !is_modifier_key(key)) {
screen_history_scroll(screen, SCROLL_FULL, false); // scroll back to bottom
}
if (
action == GLFW_PRESS ||
(action == GLFW_REPEAT && screen->modes.mDECARM) ||
screen->modes.mEXTENDED_KEYBOARD
) {
const uint8_t *data = key_to_bytes(lkey, screen->modes.mDECCKM, screen->modes.mEXTENDED_KEYBOARD, mods, action);
if (data) schedule_write_to_child(w->id, (char*)(data + 1), *data);
}
}
#define PYWRAP1(name) static PyObject* py##name(PyObject UNUSED *self, PyObject *args)
#define PA(fmt, ...) if(!PyArg_ParseTuple(args, fmt, __VA_ARGS__)) return NULL;
#define M(name, arg_type) {#name, (PyCFunction)py##name, arg_type, NULL}
PYWRAP1(key_to_bytes) {
int glfw_key, smkx, extended, mods, action;
PA("ippii", &glfw_key, &smkx, &extended, &mods, &action);
const uint8_t *ans = key_to_bytes(glfw_key, smkx & 1, extended & 1, mods, action);
if (ans == NULL) return Py_BuildValue("y#", "", 0);
return Py_BuildValue("y#", ans + 1, *ans);
}
static PyMethodDef module_methods[] = {
M(key_to_bytes, METH_VARARGS),
{0}
};
bool
init_keys(PyObject *module) {
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
return true;
}

33255
kitty/keys.h Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,28 @@ from .terminfo import key_as_bytes
from .utils import base64_encode
from .key_encoding import KEY_MAP
smkx_key_map = {
def modify_key_bytes(keybytes, amt):
ans = bytearray(keybytes)
amt = str(amt).encode('ascii')
if ans[-1] == ord('~'):
return bytes(ans[:-1] + bytearray(b';' + amt + b'~'))
if ans[1] == ord('O'):
return bytes(ans[:1] + bytearray(b'[1;' + amt) + ans[-1:])
raise ValueError('Unknown key type')
def modify_complex_key(name, amt):
return modify_key_bytes(key_as_bytes(name), amt)
control_codes = {}
smkx_key_map = {}
alt_codes = {defines.GLFW_KEY_TAB: b'\033\t'}
shift_alt_codes = {defines.GLFW_KEY_TAB: key_as_bytes('kcbt')}
alt_mods = (defines.GLFW_MOD_ALT, defines.GLFW_MOD_SHIFT | defines.GLFW_MOD_ALT)
for kf, kn in {
defines.GLFW_KEY_UP: 'kcuu1',
defines.GLFW_KEY_DOWN: 'kcud1',
defines.GLFW_KEY_LEFT: 'kcub1',
@@ -18,12 +39,30 @@ smkx_key_map = {
defines.GLFW_KEY_DELETE: 'kdch1',
defines.GLFW_KEY_PAGE_UP: 'kpp',
defines.GLFW_KEY_PAGE_DOWN: 'knp',
}
smkx_key_map = {k: key_as_bytes(v) for k, v in smkx_key_map.items()}
}.items():
smkx_key_map[kf] = key_as_bytes(kn)
alt_codes[kf] = modify_complex_key(kn, 3)
shift_alt_codes[kf] = modify_complex_key(kn, 4)
control_codes[kf] = modify_complex_key(kn, 5)
for f in range(1, 13):
kf = getattr(defines, 'GLFW_KEY_F{}'.format(f))
smkx_key_map[kf] = key_as_bytes('kf{}'.format(f))
del f, kf
kn = 'kf{}'.format(f)
smkx_key_map[kf] = key_as_bytes(kn)
alt_codes[kf] = modify_complex_key(kn, 3)
shift_alt_codes[kf] = modify_complex_key(kn, 4)
control_codes[kf] = modify_complex_key(kn, 5)
f = {k: k for k in '0123456789'}
f.update({
'COMMA': ',',
'PERIOD': '.',
'SEMICOLON': ';',
'APOSTROPHE': "'",
'MINUS': '-',
'EQUAL': '=',
})
for kf, kn in f.items():
control_codes[getattr(defines, 'GLFW_KEY_' + kf)] = (ord(kn),)
del f, kf, kn
smkx_key_map[defines.GLFW_KEY_ESCAPE] = b'\033'
smkx_key_map[defines.GLFW_KEY_ENTER] = b'\r'
@@ -39,27 +78,16 @@ SHIFTED_KEYS = {
defines.GLFW_KEY_RIGHT: key_as_bytes('kRIT'),
}
control_codes = {
control_codes.update({
k: (1 + i, )
for i, k in
enumerate(range(defines.GLFW_KEY_A, defines.GLFW_KEY_RIGHT_BRACKET + 1))
}
})
control_codes[defines.GLFW_KEY_6] = (30,)
control_codes[defines.GLFW_KEY_SLASH] = (31,)
control_codes[defines.GLFW_KEY_SPACE] = (0,)
def rkey(name, a, b):
return bytearray(key_as_bytes(name).replace(a, b))
control_codes[defines.GLFW_KEY_PAGE_UP] = rkey('kpp', b'~', b';5~')
control_codes[defines.GLFW_KEY_PAGE_DOWN] = rkey('knp', b'~', b';5~')
control_codes[defines.GLFW_KEY_DELETE] = rkey('kdch1', b'~', b';5~')
alt_codes = {
k: (0x1b, k)
for i, k in enumerate(
range(defines.GLFW_KEY_SPACE, defines.GLFW_KEY_RIGHT_BRACKET + 1)
)
}
rmkx_key_map = smkx_key_map.copy()
rmkx_key_map.update({
defines.GLFW_KEY_UP: b'\033[A',
@@ -69,9 +97,6 @@ rmkx_key_map.update({
defines.GLFW_KEY_HOME: b'\033[H',
defines.GLFW_KEY_END: b'\033[F',
})
for sk in 'UP DOWN LEFT RIGHT HOME END'.split():
sk = getattr(defines, 'GLFW_KEY_' + sk)
control_codes[sk] = rmkx_key_map[sk].replace(b'[', b'[1;5')
cursor_key_mode_map = {True: smkx_key_map, False: rmkx_key_map}
@@ -80,42 +105,22 @@ def get_key_map(screen):
return cursor_key_mode_map[screen.cursor_key_mode]
valid_localized_key_names = {
k: getattr(defines, 'GLFW_KEY_' + k)
for k in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
}
for name, ch in {
'APOSTROPHE': "'",
'COMMA': ',',
'PERIOD': '.',
'SLASH': '/',
'MINUS': '-',
'SEMICOLON': ';',
'EQUAL': '=',
'LEFT_BRACKET': '[',
'RIGHT_BRACKET': ']',
'GRAVE_ACCENT': '`',
'BACKSLASH': '\\'
}.items():
valid_localized_key_names[ch] = getattr(defines, 'GLFW_KEY_' + name)
def get_localized_key(key, scancode):
name = defines.glfw_get_key_name(key, scancode)
return valid_localized_key_names.get((name or '').upper(), key)
def keyboard_mode_name(screen):
if screen.extended_keyboard:
return 'kitty'
return 'application' if screen.cursor_key_mode else 'normal'
action_map = {
defines.GLFW_PRESS: b'p',
defines.GLFW_RELEASE: b'r',
defines.GLFW_REPEAT: b't'
defines.GLFW_PRESS: 'p',
defines.GLFW_RELEASE: 'r',
defines.GLFW_REPEAT: 't'
}
def extended_key_event(key, scancode, mods, action):
def extended_key_event(key, mods, action):
if key >= defines.GLFW_KEY_LAST or key == defines.GLFW_KEY_UNKNOWN or (
# Shifted printable key should be handled by interpret_text_event()
# Shifted printable key should be handled by on_text_input()
mods == defines.GLFW_MOD_SHIFT and 32 <= key <= 126
):
return b''
@@ -131,38 +136,128 @@ def extended_key_event(key, scancode, mods, action):
).encode('ascii')
def interpret_key_event(key, scancode, mods, window, action):
screen = window.screen
key = get_localized_key(key, scancode)
if screen.extended_keyboard:
return extended_key_event(key, scancode, mods, action)
def key_to_bytes(key, smkx, extended, mods, action):
if extended:
return extended_key_event(key, mods, action)
data = bytearray()
if action == defines.GLFW_PRESS or (
action == defines.GLFW_REPEAT and screen.auto_repeat_enabled
):
if mods == defines.GLFW_MOD_CONTROL and key in control_codes:
# Map Ctrl-key to ascii control code
data.extend(control_codes[key])
elif mods == defines.GLFW_MOD_ALT and key in alt_codes:
# Map Alt+key to Esc-key
data.extend(alt_codes[key])
else:
key_map = get_key_map(screen)
x = key_map.get(key)
if x is not None:
if mods == defines.GLFW_MOD_SHIFT:
x = SHIFTED_KEYS.get(key, x)
data.extend(x)
if mods == defines.GLFW_MOD_CONTROL and key in control_codes:
# Map Ctrl-key to ascii control code
data.extend(control_codes[key])
elif mods in alt_mods and key in alt_codes:
# Printable keys handled by on_text_input()
data.extend((alt_codes if mods == defines.GLFW_MOD_ALT else shift_alt_codes)[key])
else:
key_map = cursor_key_mode_map[smkx]
x = key_map.get(key)
if x is not None:
if mods == defines.GLFW_MOD_SHIFT:
x = SHIFTED_KEYS.get(key, x)
data.extend(x)
return bytes(data)
def interpret_text_event(codepoint, mods):
if mods > defines.GLFW_MOD_SHIFT:
return b'' # Handled by interpret_key_event above
data = chr(codepoint).encode('utf-8')
return data
def interpret_key_event(key, scancode, mods, window, action):
screen = window.screen
if (
action == defines.GLFW_PRESS or
(action == defines.GLFW_REPEAT and screen.auto_repeat_enabled) or
screen.extended_keyboard
):
return defines.key_to_bytes(key, screen.cursor_key_mode, screen.extended_keyboard, mods, action)
return b''
def get_shortcut(keymap, mods, key, scancode):
key = get_localized_key(key, scancode)
return keymap.get((mods & 0b1111, key))
def get_sent_data(send_text_map, key, scancode, mods, window, action):
if action in (defines.GLFW_PRESS, defines.GLFW_REPEAT):
m = keyboard_mode_name(window.screen)
keymap = send_text_map[m]
return keymap.get((mods & 0b1111, key))
def generate_key_table():
# To run this, use: python3 . -c "from kitty.keys import *; generate_key_table()"
import os
from functools import partial
f = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'keys.h'), 'w')
w = partial(print, file=f)
w('// auto-generated from keys.py, do not edit!')
w('#pragma once')
w('#include <stddef.h>')
w('#include <stdint.h>')
w('#include <stdbool.h>')
w('#include <limits.h>')
w('static bool needs_special_handling[%d] = {0};' % (128 * 16))
number_of_keys = defines.GLFW_KEY_LAST + 1
w('// map glfw key numbers to 7-bit numbers for compact data storage')
w('static const uint8_t key_map[%d] = {' % number_of_keys)
key_count = 0
def key_name(k):
return k[len('GLFW_KEY_'):]
keys = {v: k for k, v in vars(defines).items() if k.startswith('GLFW_KEY_') and k not in {'GLFW_KEY_LAST', 'GLFW_KEY_UNKNOWN'}}
key_rmap = []
for i in range(number_of_keys):
k = keys.get(i)
if k is None:
w('UINT8_MAX,')
else:
w('%d, /* %s */' % (key_count, key_name(k)))
key_rmap.append(i)
key_count += 1
if key_count > 128:
raise OverflowError('Too many keys')
w('};\n')
w('static inline const char* key_name(int key) { switch(key) {')
for i in range(number_of_keys):
k = keys.get(i)
if k is not None:
w('case %d: return "%s";' % (i, key_name(k)))
w('default: return NULL; }}\n')
number_entries = 128 * 256
inits = []
longest = 0
for i in range(number_entries):
key = i & 0x7f # lowest seven bits
if key < key_count:
glfw_key = key_rmap[key]
k = keys.get(glfw_key)
else:
k = None
if k is None:
inits.append(None)
else:
mods = (i >> 7) & 0b1111
rest = i >> 11
action = rest & 0b11
if action == 0b11: # no such action
inits.append(None)
else:
smkx = bool(rest & 0b100)
extended = bool(rest & 0b1000)
data = key_to_bytes(glfw_key, smkx, extended, mods, action)
if data:
longest = max(len(data), longest)
inits.append((data, k, mods, smkx, extended))
else:
inits.append(None)
longest += 1
w('#define SIZE_OF_KEY_BYTES_MAP %d\n' % number_entries)
w('static const uint8_t key_bytes[%d][%d] = {' % (number_entries, longest))
# empty = '{' + ('0, ' * longest) + '},'
empty = '{0},'
all_mods = {k.rpartition('_')[2]: v for k, v in vars(defines).items() if k.startswith('GLFW_MOD_')}
all_mods = {k: v for k, v in sorted(all_mods.items(), key=lambda x: x[0])}
for b in inits:
if b is None:
w(empty)
else:
b, k, mods, smkx, extended = b
b = bytearray(b)
name = '+'.join([k for k, v in all_mods.items() if v & mods] + [key_name(k)])
w('{%d, ' % len(b) + ', '.join(map(hex, b)) + '}, //', name, 'smkx:', smkx, 'extended:', extended)
w('};')

View File

@@ -7,6 +7,12 @@
# font_family Operator Mono Book
# bold_font Operator Mono Thick
# bold_italic_font Operator Mono Medium
# or
# font_family SF Mono Medium
# bold_font SF Mono Semibold
# bold_italic_font SF Mono Semibold
# Note that you should use the full family name but do not add Bold or Italic qualifiers
# to the name.
font_family monospace
italic_font auto
bold_font auto
@@ -19,6 +25,14 @@ font_size 11.0
# the font size in a running terminal.
font_size_delta 2
# Adjust the line height.
# You can use either numbers, which are interpreted as pixels or percentages
# (number followed by %), which are interpreted as percentages of the
# unmodified line height. You can use negative pixels or percentages less than
# 100% to reduce line height (but this might cause rendering artifacts).
adjust_line_height 0
# The foreground color
foreground #dddddd
@@ -31,21 +45,22 @@ selection_foreground #000000
# The background for selections
selection_background #FFFACD
# The cursor color
cursor #ffffff
# The color for highlighting URLs on mouse-over
url_color #0087BD
# The cursor opacity
cursor_opacity 0.7
# The cursor color
cursor #cccccc
# The cursor shape can be one of (block, beam, underline)
cursor_shape block
# The interval (in seconds) at which to blink the cursor. Set to zero to
# disable blinking.
# disable blinking. Note that numbers smaller than repaint_delay will be
# limited to repaint_delay.
cursor_blink_interval 0.5
# Stop blinking cursor after the specified number of seconds of keyboard inactivity. Set to
# zero or a negative number to never stop blinking.
# zero to never stop blinking.
cursor_stop_blinking_after 15.0
# Number of lines of history to keep in memory for scrolling back
@@ -59,7 +74,8 @@ scrollback_pager less +G -R
# When viewing scrollback in a new window, put it in a new tab as well
scrollback_in_new_tab no
# Wheel scroll multiplier (modify the amount scrolled by the mouse wheel)
# Wheel scroll multiplier (modify the amount scrolled by the mouse wheel). Use negative
# numbers to change scroll direction.
wheel_scroll_multiplier 5.0
# The interval between successive clicks to detect double/triple clicks (in seconds)
@@ -71,7 +87,7 @@ click_interval 0.5
select_by_word_characters :@-./_~?&=%+#
# Hide mouse cursor after the specified number of seconds of the mouse not being used. Set to
# zero or a negative number to disable mouse cursor hiding.
# zero to disable mouse cursor hiding.
mouse_hide_wait 3.0
# The enabled window layouts. A comma separated list of layout names. The special value * means
@@ -86,11 +102,18 @@ remember_window_size yes
initial_window_width 640
initial_window_height 400
# Delay (in milliseconds) between screen updates. Decreasing it, increases fps
# at the cost of more CPU usage. The default value yields ~100fps which is more
# that sufficient for most uses.
# Delay (in milliseconds) between screen updates. Decreasing it, increases
# frames-per-second (FPS) at the cost of more CPU usage. The default value
# yields ~100 FPS which is more than sufficient for most uses.
repaint_delay 10
# Delay (in milliseconds) before input from the program running in the terminal
# is processed. Note that decreasing it will increase responsiveness, but also
# increase CPU usage and might cause flicker in full screen programs that
# redraw the entire screen on each loop, because kitty is so fast that partial
# screen updates will be drawn.
input_delay 3
# Visual bell duration. Flash the screen when a bell occurs for the specified number of
# seconds. Set to zero to disable.
visual_bell_duration 0.0
@@ -118,8 +141,15 @@ use_system_wcwidth yes
term xterm-kitty
# 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.
window_border_width 1
# The window margin (in pts) (blank area outside the border)
window_margin_width 0
# The window padding (in pts) (blank area between the text and the window border)
window_padding_width 0
# The color for the border of the active window
active_border_color #00ff00
@@ -168,17 +198,31 @@ color14 #14ffff
color7 #dddddd
color15 #ffffff
# Key mapping
# For a list of key names, see: http://www.glfw.org/docs/latest/group__keys.html
# For a list of modifier names, see: http://www.glfw.org/docs/latest/group__mods.html
#
# You can use the special action no_op to unmap a keyboard shortcut that is
# assigned in the default configuration.
#
# You can combine multiple actions to be triggered by a single shortcut, using the
# syntax below:
# map key combine <separator> action1 <separator> action2 <separator> action3 ...
# For example:
# map ctrl+shift+e combine : new_window : next_layout
# this will create a new window and switch to the next available layout
# Clipboard
map ctrl+shift+v paste_from_clipboard
map ctrl+shift+s paste_from_selection
map ctrl+shift+c copy_to_clipboard
map shift+insert paste_from_selection
# You can also pass the contents of the current selection to any program using
# pass_selection_to_program. By default, the system's open program is used, but
# you can specify your own, for example:
# map ctrl+shift+o pass_selection_to_program firefox
map ctrl+shift+o pass_selection_to_program
# Scrolling
map ctrl+shift+up scroll_line_up
@@ -192,7 +236,7 @@ map ctrl+shift+end scroll_end
map ctrl+shift+h show_scrollback
# Window management
map ctrl+shift+enter new_window
map ctrl+shift+enter new_window
map ctrl+shift+w close_window
map ctrl+shift+] next_window
map ctrl+shift+[ previous_window
@@ -209,6 +253,15 @@ map ctrl+shift+7 seventh_window
map ctrl+shift+8 eighth_window
map ctrl+shift+9 ninth_window
map ctrl+shift+0 tenth_window
# You can also open a new window running an arbitrary program, for example:
# map ctrl+shift+y new_window mutt
# You can also pass the current selection to the new program by using the @selection placeholder
# map ctrl+shift+y new_window less @selection
# Finally, you can even send the contents of the current screen + history buffer as stdin using
# the placeholders @text (which is the plain text) and @ansi (which includes text styling escape codes)
# For example, the following command opens the scrollback buffer in less in a new window.
# map ctrl+shift+y new_window @ansi less +G -R
# Tab management
map ctrl+shift+right next_tab
@@ -218,12 +271,32 @@ map ctrl+shift+q close_tab
map ctrl+shift+l next_layout
map ctrl+shift+. move_tab_forward
map ctrl+shift+, move_tab_backward
# Just as with new_window above, you can also pass the name of arbitrary
# commands to run when using new_tab.
# Miscellaneous
map ctrl+shift+equal increase_font_size
map ctrl+shift+minus decrease_font_size
map ctrl+shift+backspace restore_font_size
# Sending arbitrary text on shortcut key presses
# You can tell kitty to send arbitrary (UTF-8) encoded text to
# the client program when pressing specified shortcut keys. For example:
# send_text all ctrl+alt+a Special text
# This will send "Special text" when you press the Ctrl+Alt+a key combination.
# The text to be sent is a python string literal so you can use escapes like
# \x1b to send control codes or \u21fb to send unicode characters (or you can
# just input the unicode characters directly as UTF-8 text). The first argument
# to send_text is the keyboard modes in which to activate the shortcut. The possible
# values are normal or application or kitty or a comma separated combination of them.
# The special keyword all means all modes. The modes normal and application refer to
# the DECCKM cursor key mode for terminals, and kitty refers to the special kitty
# extended keyboard protocol. Another example, that outputs a word and then moves the cursor
# to the start of the line (same as pressing the Home key):
# send_text normal ctrl+alt+a Word\x1b[H
# send_text application ctrl+alt+a Word\x1bOH
# Symbol mapping (special font for specified unicode code points). Map the
# specified unicode codepoints to a particular font. Useful if you need special
# rendering for some symbols, such as for Powerline. Avoids the need for
@@ -237,3 +310,9 @@ map ctrl+shift+backspace restore_font_size
# For example:
#
# symbol_map U+E0A0-U+E0A2,U+E0B0-U+E0B3 PowerlineSymbols
# OS specific tweaks
# Hide the kitty window's title bar on macOS.
macos_hide_titlebar no

View File

@@ -6,25 +6,29 @@ from collections import namedtuple
from itertools import islice
from .constants import WindowGeometry, viewport_size, cell_size, get_boss
from .utils import pt_to_px
def available_height():
return viewport_size.height - get_boss().current_tab_bar_height
def layout_dimension(length, cell_length, number_of_windows=1, border_length=0, left_align=False):
def layout_dimension(length, cell_length, number_of_windows=1, border_length=0, margin_length=0, padding_length=0, left_align=False):
number_of_cells = length // cell_length
border_length += padding_length
space_needed_for_border = number_of_windows * 2 * border_length
space_needed_for_padding = number_of_windows * 2 * margin_length
space_needed = space_needed_for_padding + space_needed_for_border
extra = length - number_of_cells * cell_length
while extra < space_needed_for_border:
while extra < space_needed:
number_of_cells -= 1
extra = length - number_of_cells * cell_length
cells_per_window = number_of_cells // number_of_windows
extra -= space_needed_for_border
extra -= space_needed
pos = 0 if left_align else (extra // 2)
pos += border_length
pos += border_length + margin_length
inner_length = cells_per_window * cell_length
window_length = 2 * border_length + inner_length
window_length = 2 * (border_length + margin_length) + inner_length
extra = number_of_cells - (cells_per_window * number_of_windows)
while number_of_windows > 0:
number_of_windows -= 1
@@ -43,6 +47,8 @@ class Layout:
def __init__(self, opts, border_width, windows):
self.opts = opts
self.border_width = border_width
self.margin_width = pt_to_px(opts.window_margin_width)
self.padding_width = pt_to_px(opts.window_padding_width)
# A set of rectangles corresponding to the blank spaces at the edges of
# this layout, i.e. spaces that are not covered by any window
self.blank_rects = ()
@@ -76,9 +82,9 @@ def window_geometry(xstart, xnum, ystart, ynum):
return WindowGeometry(left=xstart, top=ystart, xnum=xnum, ynum=ynum, right=xstart + cell_size.width * xnum, bottom=ystart + cell_size.height * ynum)
def layout_single_window():
xstart, xnum = next(layout_dimension(viewport_size.width, cell_size.width))
ystart, ynum = next(layout_dimension(available_height(), cell_size.height))
def layout_single_window(margin_length, padding_length):
xstart, xnum = next(layout_dimension(viewport_size.width, cell_size.width, margin_length=margin_length, padding_length=padding_length))
ystart, ynum = next(layout_dimension(available_height(), cell_size.height, margin_length=margin_length, padding_length=padding_length))
return window_geometry(xstart, xnum, ystart, ynum)
@@ -116,14 +122,14 @@ class Stack(Layout):
def set_active_window(self, windows, active_window_idx):
for i, w in enumerate(windows):
w.is_visible_in_layout = i == active_window_idx
w.set_visible_in_layout(i, i == active_window_idx)
def __call__(self, windows, active_window_idx):
self.blank_rects = []
wg = layout_single_window()
wg = layout_single_window(self.margin_width, self.padding_width)
for i, w in enumerate(windows):
w.is_visible_in_layout = i == active_window_idx
w.set_geometry(wg)
w.set_visible_in_layout(i, i == active_window_idx)
w.set_geometry(i, wg)
if w.is_visible_in_layout:
self.blank_rects = blank_rects_for_window(w)
@@ -135,19 +141,25 @@ class Tall(Layout):
def __call__(self, windows, active_window_idx):
self.blank_rects = br = []
if len(windows) == 1:
wg = layout_single_window()
windows[0].set_geometry(wg)
wg = layout_single_window(self.margin_width, self.padding_width)
windows[0].set_geometry(0, wg)
self.blank_rects = blank_rects_for_window(windows[0])
return
xlayout = layout_dimension(viewport_size.width, cell_size.width, 2, self.border_width)
xlayout = layout_dimension(
viewport_size.width, cell_size.width, 2, self.border_width,
margin_length=self.margin_width, padding_length=self.padding_width)
xstart, xnum = next(xlayout)
ystart, ynum = next(layout_dimension(available_height(), cell_size.height, 1, self.border_width, left_align=True))
windows[0].set_geometry(window_geometry(xstart, xnum, ystart, ynum))
ystart, ynum = next(layout_dimension(
available_height(), cell_size.height, 1, self.border_width, left_align=True,
margin_length=self.margin_width, padding_length=self.padding_width))
windows[0].set_geometry(0, window_geometry(xstart, xnum, ystart, ynum))
vh = available_height()
xstart, xnum = next(xlayout)
ylayout = layout_dimension(available_height(), cell_size.height, len(windows) - 1, self.border_width, left_align=True)
for w, (ystart, ynum) in zip(islice(windows, 1, None), ylayout):
w.set_geometry(window_geometry(xstart, xnum, ystart, ynum))
ylayout = layout_dimension(
available_height(), cell_size.height, len(windows) - 1, self.border_width, left_align=True,
margin_length=self.margin_width, padding_length=self.padding_width)
for i, (w, (ystart, ynum)) in enumerate(zip(islice(windows, 1, None), ylayout)):
w.set_geometry(i + 1, window_geometry(xstart, xnum, ystart, ynum))
left_blank_rect(windows[0], br, vh), top_blank_rect(windows[0], br, vh), right_blank_rect(windows[-1], br, vh)
br.append(Rect(windows[0].geometry.right, 0, windows[1].geometry.left, vh))
br.append(Rect(0, windows[0].geometry.bottom, windows[0].geometry.right, vh))

View File

@@ -6,27 +6,33 @@
*/
#include "data-types.h"
#include "lineops.h"
#include <structmember.h>
static inline Cell*
lineptr(LineBuf *linebuf, index_type y) {
return linebuf->buf + y * linebuf->xnum;
}
static inline void
clear_chars_to(LineBuf* linebuf, index_type y, char_type ch) {
char_type *chars = linebuf->chars + linebuf->xnum * y;
for (index_type i = 0; i < linebuf->xnum; i++) chars[i] = (1 << ATTRS_SHIFT) | ch;
clear_chars_in_line(lineptr(linebuf, y), linebuf->xnum, ch);
}
void linebuf_clear(LineBuf *self, char_type ch) {
memset(self->buf, 0, self->block_size * CELL_SIZE);
void
linebuf_clear(LineBuf *self, char_type ch) {
memset(self->buf, 0, self->xnum * self->ynum * sizeof(Cell));
memset(self->continued_map, 0, self->ynum * sizeof(bool));
for (index_type i = 0; i < self->ynum; i++) {
clear_chars_to(self, i, ch);
self->line_map[i] = i;
for (index_type i = 0; i < self->ynum; i++) self->line_map[i] = i;
if (ch != 0) {
for (index_type i = 0; i < self->ynum; i++) clear_chars_to(self, i, ch);
}
}
static PyObject*
clear(LineBuf *self) {
#define clear_doc "Clear all lines in this LineBuf"
linebuf_clear(self, ' ');
linebuf_clear(self, BLANK_CHAR);
Py_RETURN_NONE;
}
@@ -51,8 +57,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
if (self != NULL) {
self->xnum = xnum;
self->ynum = ynum;
self->block_size = xnum * ynum;
self->buf = PyMem_Calloc(xnum * ynum, CELL_SIZE);
self->buf = PyMem_Calloc(xnum * ynum, sizeof(Cell));
self->line_map = PyMem_Calloc(ynum, sizeof(index_type));
self->scratch = PyMem_Calloc(ynum, sizeof(index_type));
self->continued_map = PyMem_Calloc(ynum, sizeof(bool));
@@ -62,15 +67,10 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
PyMem_Free(self->buf); PyMem_Free(self->line_map); PyMem_Free(self->continued_map); Py_CLEAR(self->line);
Py_CLEAR(self);
} else {
self->chars = (char_type*)self->buf;
self->fg_colors = (color_type*)(self->chars + self->block_size);
self->bg_colors = (color_type*)(self->fg_colors + self->block_size);
self->decoration_fg = (color_type*)(self->bg_colors + self->block_size);
self->combining_chars = (combining_type*)(self->decoration_fg + self->block_size);
self->line->xnum = xnum;
for(index_type i = 0; i < ynum; i++) {
self->line_map[i] = i;
clear_chars_to(self, i, ' ');
if (BLANK_CHAR != 0) clear_chars_to(self, i, BLANK_CHAR);
}
}
}
@@ -88,18 +88,17 @@ dealloc(LineBuf* self) {
Py_TYPE(self)->tp_free((PyObject*)self);
}
#define INIT_LINE(lb, l, ynum) \
(l)->chars = (lb)->chars + (ynum) * (lb)->xnum; \
(l)->fg_colors = (lb)->fg_colors + (ynum) * (lb)->xnum; \
(l)->bg_colors = (lb)->bg_colors + (ynum) * (lb)->xnum; \
(l)->decoration_fg = (lb)->decoration_fg + (ynum) * (lb)->xnum; \
(l)->combining_chars = (lb)->combining_chars + (ynum) * (lb)->xnum;
static inline void
init_line(LineBuf *lb, Line *l, index_type ynum) {
l->cells = lineptr(lb, ynum);
}
void linebuf_init_line(LineBuf *self, index_type idx) {
void
linebuf_init_line(LineBuf *self, index_type idx) {
self->line->ynum = idx;
self->line->xnum = self->xnum;
self->line->continued = self->continued_map[idx];
INIT_LINE(self, self->line, self->line_map[idx]);
init_line(self, self->line, self->line_map[idx]);
}
static PyObject*
@@ -115,15 +114,15 @@ line(LineBuf *self, PyObject *y) {
return (PyObject*)self->line;
}
unsigned int linebuf_char_width_at(LineBuf *self, index_type x, index_type y) {
char_type *chars = self->chars + self->line_map[y] * self->xnum;
return (chars[x] >> ATTRS_SHIFT) & WIDTH_MASK;
unsigned int
linebuf_char_width_at(LineBuf *self, index_type x, index_type y) {
return (lineptr(self, self->line_map[y])[x].attrs) & WIDTH_MASK;
}
void linebuf_set_attribute(LineBuf *self, unsigned int shift, unsigned int val) {
char_type mask;
void
linebuf_set_attribute(LineBuf *self, unsigned int shift, unsigned int val) {
for (index_type y = 0; y < self->ynum; y++) {
SET_ATTRIBUTE(self->chars + y * self->xnum, shift, val);
set_attribute_on_line(lineptr(self, y), shift, val, self->xnum);
}
}
@@ -148,36 +147,22 @@ set_continued(LineBuf *self, PyObject *args) {
Py_RETURN_NONE;
}
static inline int
static inline bool
allocate_line_storage(Line *line, bool initialize) {
if (initialize) {
line->chars = PyMem_Calloc(line->xnum, sizeof(char_type));
line->fg_colors = PyMem_Calloc(line->xnum, sizeof(color_type));
line->bg_colors = PyMem_Calloc(line->xnum, sizeof(color_type));
line->decoration_fg = PyMem_Calloc(line->xnum, sizeof(color_type));
line->combining_chars = PyMem_Calloc(line->xnum, sizeof(combining_type));
for (index_type i = 0; i < line->xnum; i++) line->chars[i] = (1 << ATTRS_SHIFT) | 32;
line->cells = PyMem_Calloc(line->xnum, sizeof(Cell));
if (line->cells == NULL) { PyErr_NoMemory(); return false; }
if (BLANK_CHAR != 0) clear_chars_in_line(line->cells, line->xnum, BLANK_CHAR);
} else {
line->chars = PyMem_Malloc(line->xnum * sizeof(char_type));
line->fg_colors = PyMem_Malloc(line->xnum * sizeof(color_type));
line->bg_colors = PyMem_Malloc(line->xnum * sizeof(color_type));
line->decoration_fg = PyMem_Malloc(line->xnum * sizeof(color_type));
line->combining_chars = PyMem_Malloc(line->xnum * sizeof(combining_type));
}
if (line->chars == NULL || line->fg_colors == NULL || line->bg_colors == NULL || line->decoration_fg == NULL || line->combining_chars == NULL) {
PyMem_Free(line->chars); line->chars = NULL;
PyMem_Free(line->fg_colors); line->fg_colors = NULL;
PyMem_Free(line->bg_colors); line->bg_colors = NULL;
PyMem_Free(line->decoration_fg); line->decoration_fg = NULL;
PyMem_Free(line->combining_chars); line->combining_chars = NULL;
PyErr_NoMemory();
return 0;
line->cells = PyMem_Malloc(line->xnum * sizeof(Cell));
if (line->cells == NULL) { PyErr_NoMemory(); return false; }
}
line->needs_free = 1;
return 1;
return true;
}
static inline PyObject* create_line_copy_inner(LineBuf* self, index_type y) {
static inline PyObject*
create_line_copy_inner(LineBuf* self, index_type y) {
Line src, *line;
line = alloc_line();
if (line == NULL) return PyErr_NoMemory();
@@ -185,8 +170,8 @@ static inline PyObject* create_line_copy_inner(LineBuf* self, index_type y) {
if (!allocate_line_storage(line, 0)) { Py_CLEAR(line); return PyErr_NoMemory(); }
line->ynum = y;
line->continued = self->continued_map[y];
INIT_LINE(self, &src, self->line_map[y]);
COPY_LINE(&src, line);
init_line(self, &src, self->line_map[y]);
copy_line(&src, line);
return (PyObject*)line;
}
@@ -207,15 +192,21 @@ copy_line_to(LineBuf *self, PyObject *args) {
src.xnum = self->xnum; dest->xnum = self->xnum;
dest->ynum = y;
dest->continued = self->continued_map[y];
INIT_LINE(self, &src, self->line_map[y]);
COPY_LINE(&src, dest);
init_line(self, &src, self->line_map[y]);
copy_line(&src, dest);
Py_RETURN_NONE;
}
static inline void
clear_line_(Line *l, index_type xnum) {
memset(l->cells, 0, xnum * sizeof(Cell));
if (BLANK_CHAR != 0) clear_chars_in_line(l->cells, xnum, BLANK_CHAR);
}
void linebuf_clear_line(LineBuf *self, index_type y) {
Line l;
INIT_LINE(self, &l, self->line_map[y]);
CLEAR_LINE(&l, self->xnum);
init_line(self, &l, self->line_map[y]);
clear_line_(&l, self->xnum);
self->continued_map[y] = 0;
}
@@ -299,8 +290,8 @@ void linebuf_insert_lines(LineBuf *self, unsigned int num, unsigned int y, unsig
}
Line l;
for (i = y; i < y + num; i++) {
INIT_LINE(self, &l, self->line_map[i]);
CLEAR_LINE(&l, self->xnum);
init_line(self, &l, self->line_map[i]);
clear_line_(&l, self->xnum);
self->continued_map[i] = 0;
}
}
@@ -334,8 +325,8 @@ linebuf_delete_lines(LineBuf *self, index_type num, index_type y, index_type bot
}
Line l;
for (i = ylimit - num; i < ylimit; i++) {
INIT_LINE(self, &l, self->line_map[i]);
CLEAR_LINE(&l, self->xnum);
init_line(self, &l, self->line_map[i]);
clear_line_(&l, self->xnum);
self->continued_map[i] = 0;
}
}
@@ -356,7 +347,7 @@ as_ansi(LineBuf *self, PyObject *callback) {
Line l = {.xnum=self->xnum};
for(index_type i = 0; i < self->ynum; i++) {
l.continued = (i < self->ynum - 1) ? self->continued_map[i+1] : self->continued_map[i];
INIT_LINE(self, (&l), self->line_map[i]);
init_line(self, (&l), self->line_map[i]);
index_type num = line_as_ansi(&l, t, 5120);
if (!(l.continued) && num < 5119) t[num++] = 10; // 10 = \n
PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, t, num);
@@ -369,6 +360,29 @@ as_ansi(LineBuf *self, PyObject *callback) {
Py_RETURN_NONE;
}
void
linebuf_refresh_sprite_positions(LineBuf *self) {
for (index_type i = 0; i < self->ynum; i++) {
update_sprites_in_line(lineptr(self, i), self->xnum);
}
}
static PyObject*
__str__(LineBuf *self) {
PyObject *lines = PyTuple_New(self->ynum);
if (lines == NULL) return PyErr_NoMemory();
for (index_type i = 0; i < self->ynum; i++) {
init_line(self, self->line, self->line_map[i]);
PyObject *t = PyObject_Str((PyObject*)self->line);
if (t == NULL) { Py_CLEAR(lines); return NULL; }
PyTuple_SET_ITEM(lines, i, t);
}
PyObject *sep = PyUnicode_FromString("\n");
PyObject *ans = PyUnicode_Join(sep, lines);
Py_CLEAR(lines); Py_CLEAR(sep);
return ans;
}
// Boilerplate {{{
static PyObject*
copy_old(LineBuf *self, PyObject *y);
@@ -412,6 +426,7 @@ PyTypeObject LineBuf_Type = {
.tp_doc = "Line buffers",
.tp_methods = methods,
.tp_members = members,
.tp_str = (reprfunc)__str__,
.tp_new = new
};
@@ -430,8 +445,8 @@ copy_old(LineBuf *self, PyObject *y) {
index_type s = self->ynum - 1 - i, o = other->ynum - 1 - i;
self->continued_map[s] = other->continued_map[o];
s = self->line_map[s]; o = other->line_map[o];
INIT_LINE(self, &sl, s); INIT_LINE(other, &ol, o);
COPY_LINE(&ol, &sl);
init_line(self, &sl, s); init_line(other, &ol, o);
copy_line(&ol, &sl);
}
Py_RETURN_NONE;
}
@@ -439,47 +454,50 @@ copy_old(LineBuf *self, PyObject *y) {
#include "rewrap.h"
void
linebuf_rewrap(LineBuf *self, LineBuf *other, int *cursor_y_out, HistoryBuf *historybuf) {
linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *num_content_lines_before, index_type *num_content_lines_after, HistoryBuf *historybuf) {
index_type first, i;
bool is_empty = true;
// Fast path
if (other->xnum == self->xnum && other->ynum == self->ynum) {
Py_BEGIN_ALLOW_THREADS;
memcpy(other->line_map, self->line_map, sizeof(index_type) * self->ynum);
memcpy(other->continued_map, self->continued_map, sizeof(bool) * self->ynum);
memcpy(other->buf, self->buf, self->xnum * self->ynum * CELL_SIZE);
Py_END_ALLOW_THREADS;
memcpy(other->buf, self->buf, self->xnum * self->ynum * sizeof(Cell));
*num_content_lines_before = self->ynum; *num_content_lines_after = self->ynum;
return;
}
// Find the first line that contains some content
Py_BEGIN_ALLOW_THREADS;
for (first = self->ynum - 1; true; first--) {
char_type *chars = self->chars + self->xnum * first;
first = self->ynum;
do {
first--;
Cell *cells = lineptr(self, self->line_map[first]);
for(i = 0; i < self->xnum; i++) {
if ((chars[i] & CHAR_MASK) != 32) { is_empty = false; break; }
if ((cells[i].ch) != BLANK_CHAR) { is_empty = false; break; }
}
if (!is_empty || !first) break;
}
Py_END_ALLOW_THREADS;
} while(is_empty && first > 0);
if (first == 0) { *cursor_y_out = 0; return; } // All lines are empty
if (is_empty) { // All lines are empty
*num_content_lines_after = 0;
*num_content_lines_before = 0;
return;
}
rewrap_inner(self, other, first + 1, historybuf);
*cursor_y_out = other->line->ynum;
*num_content_lines_after = other->line->ynum + 1;
*num_content_lines_before = first + 1;
}
static PyObject*
rewrap(LineBuf *self, PyObject *args) {
LineBuf* other;
HistoryBuf *historybuf;
int cursor_y = -1;
unsigned int nclb, ncla;
if (!PyArg_ParseTuple(args, "O!O!", &LineBuf_Type, &other, &HistoryBuf_Type, &historybuf)) return NULL;
linebuf_rewrap(self, other, &cursor_y, historybuf);
linebuf_rewrap(self, other, &nclb, &ncla, historybuf);
return Py_BuildValue("i", cursor_y);
return Py_BuildValue("II", nclb, ncla);
}
LineBuf *alloc_linebuf(unsigned int lines, unsigned int columns) {

View File

@@ -6,6 +6,8 @@
*/
#include "data-types.h"
#include "unicode-data.h"
#include "lineops.h"
static PyObject *
new(PyTypeObject UNUSED *type, PyObject UNUSED *args, PyObject UNUSED *kwds) {
@@ -16,19 +18,16 @@ new(PyTypeObject UNUSED *type, PyObject UNUSED *args, PyObject UNUSED *kwds) {
static void
dealloc(Line* self) {
if (self->needs_free) {
PyMem_Free(self->chars);
PyMem_Free(self->fg_colors);
PyMem_Free(self->bg_colors);
PyMem_Free(self->decoration_fg);
PyMem_Free(self->combining_chars);
PyMem_Free(self->cells);
}
Py_TYPE(self)->tp_free((PyObject*)self);
}
unsigned int
line_length(Line *self) {
for (int i = self->xnum - 1; i >= 0; i++) {
if ((self->chars[i] & CHAR_MASK) != 32) return i + 1;
index_type last = self->xnum - 1;
for (index_type i = 0; i < self->xnum; i++) {
if ((self->cells[last - i].ch) != BLANK_CHAR) return self->xnum - i;
}
return 0;
}
@@ -42,6 +41,8 @@ line_text_at(char_type ch, combining_type cc) {
PyUnicode_WriteChar(ans, 0, ch);
} else {
Py_UCS4 cc1 = cc & CC_MASK, cc2 = cc >> 16;
Py_UCS4 normalized = normalize(ch, cc1, cc2);
if (normalized) { return line_text_at(normalized, 0); }
Py_UCS4 maxc = (ch > cc1) ? MAX(ch, cc2) : MAX(cc1, cc2);
ans = PyUnicode_New(cc2 ? 3 : 2, maxc);
if (ans == NULL) return PyErr_NoMemory();
@@ -53,54 +54,152 @@ line_text_at(char_type ch, combining_type cc) {
return ans;
}
// URL detection {{{
static const char* url_prefixes[4] = {"https", "http", "file", "ftp"};
static size_t url_prefix_lengths[sizeof(url_prefixes)/sizeof(url_prefixes[0])] = {0};
typedef enum URL_PARSER_STATES {ANY, FIRST_SLASH, SECOND_SLASH} URL_PARSER_STATE;
static inline index_type
find_colon_slash(Line *self, index_type x, index_type limit) {
// Find :// at or before x
index_type pos = x;
URL_PARSER_STATE state = ANY;
limit = MAX(2, limit);
if (pos < limit) return 0;
do {
char_type ch = self->cells[pos].ch;
if (!is_url_char(ch)) return false;
switch(state) {
case ANY:
if (ch == '/') state = FIRST_SLASH;
break;
case FIRST_SLASH:
state = ch == '/' ? SECOND_SLASH : ANY;
break;
case SECOND_SLASH:
if (ch == ':') return pos;
state = ANY;
break;
}
pos--;
} while(pos >= limit);
return 0;
}
static inline bool
prefix_matches(Line *self, index_type at, const char* prefix, index_type prefix_len) {
if (prefix_len > at) return false;
index_type p, i;
for (p = at - prefix_len, i = 0; i < prefix_len && p < self->xnum; i++, p++) {
if ((self->cells[p].ch) != (unsigned char)prefix[i]) return false;
}
return i == prefix_len;
}
static inline bool
has_url_prefix_at(Line *self, index_type at, index_type min_prefix_len, index_type *ans) {
if (UNLIKELY(!url_prefix_lengths[0])) {
for (index_type i = 0; i < sizeof(url_prefixes)/sizeof(url_prefixes[0]); i++) url_prefix_lengths[i] = strlen(url_prefixes[i]);
}
for (index_type i = 0; i < sizeof(url_prefixes)/sizeof(url_prefixes[0]); i++) {
index_type prefix_len = url_prefix_lengths[i];
if (at < prefix_len || prefix_len < min_prefix_len) continue;
if (prefix_matches(self, at, url_prefixes[i], prefix_len)) { *ans = at - prefix_len; return true; }
}
return false;
}
#define MAX_URL_SCHEME_LEN 5
#define MIN_URL_LEN 5
static inline bool
has_url_beyond(Line *self, index_type x) {
if (self->xnum <= x + MIN_URL_LEN + 3) return false;
for (index_type i = x; i < MIN(x + MIN_URL_LEN + 3, self->xnum); i++) {
if (!is_url_char(self->cells[i].ch)) return false;
}
return true;
}
index_type
line_url_start_at(Line *self, index_type x) {
// Find the starting cell for a URL that contains the position x. A URL is defined as
// known-prefix://url-chars. If no URL is found self->xnum is returned.
if (x >= self->xnum || self->xnum <= MIN_URL_LEN + 3) return self->xnum;
index_type ds_pos = 0, t;
// First look for :// ahead of x
if (self->xnum - x > MAX_URL_SCHEME_LEN + 3) ds_pos = find_colon_slash(self, x + MAX_URL_SCHEME_LEN + 3, x < 2 ? 0 : x - 2);
if (ds_pos != 0 && has_url_beyond(self, ds_pos)) {
if (has_url_prefix_at(self, ds_pos, ds_pos > x ? ds_pos - x: 0, &t)) return t;
}
ds_pos = find_colon_slash(self, x, 0);
if (ds_pos == 0 || self->xnum < ds_pos + MIN_URL_LEN + 3 || !has_url_beyond(self, ds_pos)) return self->xnum;
if (has_url_prefix_at(self, ds_pos, 0, &t)) return t;
return self->xnum;
}
index_type
line_url_end_at(Line *self, index_type x) {
index_type ans = x;
if (x >= self->xnum || self->xnum <= MIN_URL_LEN + 3) return 0;
while (ans < self->xnum && is_url_char(self->cells[ans].ch)) ans++;
ans--;
while (ans > x && can_strip_from_end_of_url(self->cells[ans].ch)) ans--;
return ans;
}
static PyObject*
url_start_at(Line *self, PyObject *x) {
#define url_start_at_doc "url_start_at(x) -> Return the start cell number for a URL containing x or self->xnum if not found"
return PyLong_FromUnsignedLong((unsigned long)line_url_start_at(self, PyLong_AsUnsignedLong(x)));
}
static PyObject*
url_end_at(Line *self, PyObject *x) {
#define url_end_at_doc "url_end_at(x) -> Return the end cell number for a URL containing x or 0 if not found"
return PyLong_FromUnsignedLong((unsigned long)line_url_end_at(self, PyLong_AsUnsignedLong(x)));
}
// }}}
static PyObject*
text_at(Line* self, Py_ssize_t xval) {
#define text_at_doc "[x] -> Return the text in the specified cell"
char_type ch;
combining_type cc;
if (xval >= self->xnum) { PyErr_SetString(PyExc_IndexError, "Column number out of bounds"); return NULL; }
ch = self->chars[xval] & CHAR_MASK;
cc = self->combining_chars[xval];
return line_text_at(ch, cc);
return line_text_at(self->cells[xval].ch, self->cells[xval].cc);
}
PyObject*
unicode_in_range(Line *self, index_type start, index_type limit, bool include_cc, char leading_char) {
size_t n = 0;
static Py_UCS4 buf[4096];
if (leading_char) buf[n++] = leading_char;
char_type previous_width = 0;
for(index_type i = start; i < limit && n < sizeof(buf)/sizeof(buf[0]) - 4; i++) {
char_type ch = self->cells[i].ch;
if (ch == 0) {
if (previous_width == 2) { previous_width = 0; continue; };
ch = ' ';
}
buf[n++] = ch;
if (include_cc) {
char_type cc = self->cells[i].cc;
Py_UCS4 cc1 = cc & CC_MASK, cc2;
if (cc1) {
buf[n++] = cc1;
cc2 = cc >> 16;
if (cc2) buf[n++] = cc2;
}
}
previous_width = self->cells[i].attrs & WIDTH_MASK;
}
return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, buf, n);
}
static PyObject *
as_unicode(Line* self) {
Py_ssize_t n = 0;
Py_UCS4 *buf = PyMem_Malloc(3 * self->xnum * sizeof(Py_UCS4));
if (buf == NULL) {
PyErr_NoMemory();
return NULL;
}
for(index_type i = 0; i < self->xnum; i++) {
char_type attrs = self->chars[i] >> ATTRS_SHIFT;
if ((attrs & WIDTH_MASK) < 1) continue;
buf[n++] = self->chars[i] & CHAR_MASK;
char_type cc = self->combining_chars[i];
Py_UCS4 cc1 = cc & CC_MASK, cc2;
if (cc1) {
buf[n++] = cc1;
cc2 = cc >> 16;
if (cc2) buf[n++] = cc2;
}
}
PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, buf, n);
PyMem_Free(buf);
return ans;
}
static PyObject *
as_base_text(Line* self) {
#define as_base_text_doc "Return the line as a string, discarding all combining characters"
PyObject *ans = PyUnicode_New(self->xnum, 1114111);
if (ans == NULL) return PyErr_NoMemory();
for(index_type i = 0; i < self->xnum; i++) {
char_type attrs = self->chars[i] >> ATTRS_SHIFT;
if ((attrs & WIDTH_MASK) < 1) continue;
PyUnicode_WRITE(PyUnicode_4BYTE_KIND, PyUnicode_DATA(ans), i, self->chars[i] & CHAR_MASK);
}
return ans;
return unicode_in_range(self, 0, xlimit_for_line(self), true, 0);
}
static inline bool
@@ -141,21 +240,18 @@ line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen) {
#define CHECK_COLOR(name, val, off_code) if (name != (val)) { name = (val); WRITE_COLOR(name, off_code); }
#define WRITE_CH(val) if (i > buflen - 1) return i; buf[i++] = val;
index_type limit = self->xnum, i=0;
int r;
if (!self->continued) { // Trim trailing spaces
for(r = self->xnum - 1; r >= 0; r--) {
if ((self->chars[r] & CHAR_MASK) != 32) break;
}
limit = r + 1;
}
index_type limit = xlimit_for_line(self), i=0;
bool bold = false, italic = false, reverse = false, strike = false;
uint32_t fg = 0, bg = 0, decoration_fg = 0, decoration = 0;
char_type previous_width = 0;
WRITE_SGR(0);
for (index_type pos=0; pos < limit; pos++) {
char_type attrs = self->chars[pos] >> ATTRS_SHIFT, ch = self->chars[pos] & CHAR_MASK;
if (ch == 0 || (attrs & WIDTH_MASK) < 1) continue;
char_type attrs = self->cells[pos].attrs, ch = self->cells[pos].ch;
if (ch == 0) {
if (previous_width == 2) { previous_width = 0; continue; }
ch = ' ';
}
CHECK_BOOL(bold, BOLD_SHIFT, 1, 22);
CHECK_BOOL(italic, ITALIC_SHIFT, 3, 23);
CHECK_BOOL(reverse, REVERSE_SHIFT, 7, 27);
@@ -171,17 +267,18 @@ line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen) {
WRITE_SGR(0); break;
}
}
CHECK_COLOR(fg, self->fg_colors[pos], 38);
CHECK_COLOR(bg, self->bg_colors[pos], 48);
CHECK_COLOR(decoration_fg, self->decoration_fg[pos], DECORATION_FG_CODE);
CHECK_COLOR(fg, self->cells[pos].fg, 38);
CHECK_COLOR(bg, self->cells[pos].bg, 48);
CHECK_COLOR(decoration_fg, self->cells[pos].decoration_fg, DECORATION_FG_CODE);
WRITE_CH(ch);
char_type cc = self->combining_chars[pos];
char_type cc = self->cells[pos].cc;
Py_UCS4 cc1 = cc & CC_MASK;
if (cc1) {
WRITE_CH(cc1);
cc1 = cc >> 16;
if (cc1) { WRITE_CH(cc1); }
}
previous_width = attrs & WIDTH_MASK;
}
return i;
#undef CHECK_BOOL
@@ -222,14 +319,18 @@ width(Line *self, PyObject *val) {
#define width_doc "width(x) -> the width of the character at x"
unsigned long x = PyLong_AsUnsignedLong(val);
if (x >= self->xnum) { PyErr_SetString(PyExc_ValueError, "Out of bounds"); return NULL; }
char_type attrs = self->chars[x] >> ATTRS_SHIFT;
char_type attrs = self->cells[x].attrs;
return PyLong_FromUnsignedLong((unsigned long) (attrs & WIDTH_MASK));
}
void line_add_combining_char(Line *self, uint32_t ch, unsigned int x) {
combining_type c = self->combining_chars[x];
if (c & CC_MASK) self->combining_chars[x] = (c & CC_MASK) | ( (ch & CC_MASK) << CC_SHIFT );
else self->combining_chars[x] = ch & CC_MASK;
#define set_sprite_position_at(x) set_sprite_position(self->cells + x, x == 0 ? NULL : self->cells + x - 1);
void
line_add_combining_char(Line *self, uint32_t ch, unsigned int x) {
combining_type c = self->cells[x].cc;
if (c & CC_MASK) self->cells[x].cc = (c & CC_MASK) | ( (ch & CC_MASK) << CC_SHIFT );
else self->cells[x].cc = ch & CC_MASK;
set_sprite_position_at(x);
}
static PyObject*
@@ -274,11 +375,13 @@ set_text(Line* self, PyObject *args) {
color_type dfg = cursor->decoration_fg & COL_MASK;
for (index_type i = cursor->x; offset < limit && i < self->xnum; i++, offset++) {
self->chars[i] = (PyUnicode_READ(kind, buf, offset) & CHAR_MASK) | attrs;
self->fg_colors[i] = fg;
self->bg_colors[i] = bg;
self->decoration_fg[i] = dfg;
self->combining_chars[i] = 0;
self->cells[i].ch = (PyUnicode_READ(kind, buf, offset));
self->cells[i].attrs = attrs;
self->cells[i].fg = fg;
self->cells[i].bg = bg;
self->cells[i].decoration_fg = dfg;
self->cells[i].cc = 0;
set_sprite_position_at(i);
}
Py_RETURN_NONE;
@@ -297,28 +400,35 @@ cursor_from(Line* self, PyObject *args) {
ans = alloc_cursor();
if (ans == NULL) { PyErr_NoMemory(); return NULL; }
ans->x = x; ans->y = y;
char_type attrs = self->chars[x] >> ATTRS_SHIFT;
char_type attrs = self->cells[x].attrs;
ATTRS_TO_CURSOR(attrs, ans);
ans->fg = self->fg_colors[x]; ans->bg = self->bg_colors[x];
ans->decoration_fg = self->decoration_fg[x] & COL_MASK;
ans->fg = self->cells[x].fg; ans->bg = self->cells[x].bg;
ans->decoration_fg = self->cells[x].decoration_fg & COL_MASK;
return (PyObject*)ans;
}
void
line_clear_text(Line *self, unsigned int at, unsigned int num, int ch) {
const char_type repl = ((char_type)ch & CHAR_MASK) | (1 << ATTRS_SHIFT);
for (index_type i = at; i < MIN(self->xnum, at + num); i++) {
self->chars[i] = (self->chars[i] & ATTRS_MASK_WITHOUT_WIDTH) | repl;
line_clear_text(Line *self, unsigned int at, unsigned int num, char_type ch) {
attrs_type width = ch ? 1 : 0;
#define PREFIX \
for (index_type i = at; i < MIN(self->xnum, at + num); i++) { \
self->cells[i].ch = ch; self->cells[i].cc = 0; \
self->cells[i].attrs = (self->cells[i].attrs & ATTRS_MASK_WITHOUT_WIDTH) | width;
if (CHAR_IS_BLANK(ch)) {
PREFIX
clear_sprite_position(self->cells[i]); }
} else {
PREFIX
set_sprite_position_at(i)}
}
memset(self->combining_chars + at, 0, MIN(num, self->xnum - at) * sizeof(combining_type));
}
static PyObject*
clear_text(Line* self, PyObject *args) {
#define clear_text_doc "clear_text(at, num, ch=' ') -> Clear characters in the specified range, preserving formatting."
#define clear_text_doc "clear_text(at, num, ch=BLANK_CHAR) -> Clear characters in the specified range, preserving formatting."
unsigned int at, num;
int ch = 32;
int ch = BLANK_CHAR;
if (!PyArg_ParseTuple(args, "II|C", &at, &num, &ch)) return NULL;
line_clear_text(self, at, num, ch);
Py_RETURN_NONE;
@@ -329,18 +439,21 @@ line_apply_cursor(Line *self, Cursor *cursor, unsigned int at, unsigned int num,
char_type attrs = CURSOR_TO_ATTRS(cursor, 1);
color_type fg = (cursor->fg & COL_MASK), bg = (cursor->bg & COL_MASK);
color_type dfg = cursor->decoration_fg & COL_MASK;
if (!clear_char) attrs = ((attrs >> ATTRS_SHIFT) & ~WIDTH_MASK) << ATTRS_SHIFT;
if (!clear_char) attrs = attrs & ATTRS_MASK_WITHOUT_WIDTH;
for (index_type i = at; i < self->xnum && i < at + num; i++) {
if (clear_char) {
self->chars[i] = 32 | attrs;
self->combining_chars[i] = 0;
self->cells[i].ch = BLANK_CHAR;
self->cells[i].cc = 0;
self->cells[i].attrs = attrs;
clear_sprite_position(self->cells[i]);
} else {
char_type w = ((self->chars[i] >> ATTRS_SHIFT) & WIDTH_MASK) << ATTRS_SHIFT;
self->chars[i] = (self->chars[i] & CHAR_MASK) | attrs | w;
attrs_type w = self->cells[i].attrs & WIDTH_MASK;
self->cells[i].attrs = attrs | w;
set_sprite_position_at(i);
}
self->fg_colors[i] = fg; self->bg_colors[i] = bg;
self->decoration_fg[i] = dfg;
self->cells[i].fg = fg; self->cells[i].bg = bg;
self->cells[i].decoration_fg = dfg;
}
}
@@ -360,8 +473,12 @@ void line_right_shift(Line *self, unsigned int at, unsigned int num) {
COPY_SELF_CELL(i - num, i)
}
// Check if a wide character was split at the right edge
char_type w = (self->chars[self->xnum - 1] >> ATTRS_SHIFT) & 3;
if (w != 1) self->chars[self->xnum - 1] = (1 << ATTRS_SHIFT) | 32;
char_type w = (self->cells[self->xnum - 1].attrs) & WIDTH_MASK;
if (w != 1) {
self->cells[self->xnum - 1].ch = BLANK_CHAR;
self->cells[self->xnum - 1].attrs = BLANK_CHAR ? 1 : 0;
clear_sprite_position(self->cells[self->xnum - 1]);
}
}
static PyObject*
@@ -393,18 +510,19 @@ left_shift(Line *self, PyObject *args) {
}
void
line_set_char(Line *self, unsigned int at, uint32_t ch, unsigned int width, Cursor *cursor) {
char_type attrs;
line_set_char(Line *self, unsigned int at, uint32_t ch, unsigned int width, Cursor *cursor, bool is_second) {
if (cursor == NULL) {
attrs = (((self->chars[at] >> ATTRS_SHIFT) & ~3) | (width & 3)) << ATTRS_SHIFT;
self->cells[at].attrs = (self->cells[at].attrs & ATTRS_MASK_WITHOUT_WIDTH) | width;
} else {
attrs = CURSOR_TO_ATTRS(cursor, width & 3);
self->fg_colors[at] = (cursor->fg & COL_MASK);
self->bg_colors[at] = (cursor->bg & COL_MASK);
self->decoration_fg[at] = cursor->decoration_fg & COL_MASK;
self->cells[at].attrs = CURSOR_TO_ATTRS(cursor, width & WIDTH_MASK);
self->cells[at].fg = (cursor->fg & COL_MASK);
self->cells[at].bg = (cursor->bg & COL_MASK);
self->cells[at].decoration_fg = cursor->decoration_fg & COL_MASK;
}
self->chars[at] = (ch & CHAR_MASK) | attrs;
self->combining_chars[at] = 0;
self->cells[at].ch = ch;
self->cells[at].cc = 0;
if (!is_second && CHAR_IS_BLANK(ch)) { clear_sprite_position(self->cells[at]); }
else set_sprite_position_at(at);
}
static PyObject*
@@ -419,7 +537,7 @@ set_char(Line *self, PyObject *args) {
PyErr_SetString(PyExc_ValueError, "Out of bounds");
return NULL;
}
line_set_char(self, at, ch, width, cursor);
line_set_char(self, at, ch, width, cursor, false);
Py_RETURN_NONE;
}
@@ -427,25 +545,19 @@ static PyObject*
set_attribute(Line *self, PyObject *args) {
#define set_attribute_doc "set_attribute(which, val) -> Set the attribute on all cells in the line."
unsigned int shift, val;
char_type mask;
if (!PyArg_ParseTuple(args, "II", &shift, &val)) return NULL;
if (shift < DECORATION_SHIFT || shift > STRIKE_SHIFT) { PyErr_SetString(PyExc_ValueError, "Unknown attribute"); return NULL; }
SET_ATTRIBUTE(self->chars, shift, val);
set_attribute_on_line(self->cells, shift, val, self->xnum);
Py_RETURN_NONE;
}
static Py_ssize_t
__len__(PyObject *self) {
return (Py_ssize_t)(((Line*)self)->ynum);
return (Py_ssize_t)(((Line*)self)->xnum);
}
static int __eq__(Line *a, Line *b) {
return a->xnum == b->xnum && \
memcmp(a->chars, b->chars, sizeof(char_type) * a->xnum) == 0 && \
memcmp(a->fg_colors, b->fg_colors, sizeof(color_type) * a->xnum) == 0 && \
memcmp(a->bg_colors, b->bg_colors, sizeof(color_type) * a->xnum) == 0 && \
memcmp(a->decoration_fg, b->decoration_fg, sizeof(color_type) * a->xnum) == 0 && \
memcmp(a->combining_chars, b->combining_chars, sizeof(combining_type) * a->xnum) == 0;
return a->xnum == b->xnum && memcmp(a->cells, b->cells, sizeof(Cell) * a->xnum) == 0;
}
// Boilerplate {{{
@@ -473,10 +585,11 @@ static PyMethodDef methods[] = {
METHOD(left_shift, METH_VARARGS)
METHOD(set_char, METH_VARARGS)
METHOD(set_attribute, METH_VARARGS)
METHOD(as_base_text, METH_NOARGS)
METHOD(as_ansi, METH_NOARGS)
METHOD(is_continued, METH_NOARGS)
METHOD(width, METH_O)
METHOD(url_start_at, METH_O)
METHOD(url_end_at, METH_O)
{NULL} /* Sentinel */
};
@@ -503,7 +616,8 @@ Line *alloc_line() {
}
RICHCMP(Line)
// }}
INIT_TYPE(Line)
// }}}
static PyObject*
copy_char(Line* self, PyObject *args) {
@@ -514,12 +628,7 @@ copy_char(Line* self, PyObject *args) {
PyErr_SetString(PyExc_ValueError, "Out of bounds");
return NULL;
}
to->chars[dest] = self->chars[src];
to->fg_colors[dest] = self->fg_colors[src];
to->bg_colors[dest] = self->bg_colors[src];
to->decoration_fg[dest] = self->decoration_fg[src];
to->combining_chars[dest] = self->combining_chars[src];
COPY_CELL(self, src, to, dest);
Py_RETURN_NONE;
}
INIT_TYPE(Line)

88
kitty/lineops.h Normal file
View File

@@ -0,0 +1,88 @@
/*
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#pragma once
#include "data-types.h"
static inline void
update_sprites_in_line(Cell *cells, index_type xnum) {
if (LIKELY(xnum > 0)) {
if (CHAR_IS_BLANK(cells->ch)) { clear_sprite_position(cells[0]); }
else set_sprite_position(cells, NULL);
for (index_type i = 1; i < xnum; i++) {
if (CHAR_IS_BLANK(cells[i].ch)) { clear_sprite_position(cells[i]); }
else set_sprite_position(cells + i, cells + i - 1);
}
}
}
static inline void
set_attribute_on_line(Cell *cells, uint32_t shift, uint32_t val, index_type xnum) {
// Set a single attribute on all cells in the line
attrs_type mask = shift == DECORATION_SHIFT ? 3 : 1;
attrs_type aval = (val & mask) << shift;
mask = ~(mask << shift);
for (index_type i = 0; i < xnum; i++) cells[i].attrs = (cells[i].attrs & mask) | aval;
if (shift == BOLD_SHIFT || shift == ITALIC_SHIFT) update_sprites_in_line(cells, xnum);
}
static inline void
copy_cells(const Cell *src, Cell *dest, index_type xnum) {
memcpy(dest, src, sizeof(Cell) * xnum);
}
static inline void
copy_line(const Line *src, Line *dest) {
copy_cells(src->cells, dest->cells, MIN(src->xnum, dest->xnum));
}
static inline void
clear_chars_in_line(Cell *cells, index_type xnum, char_type ch) {
// Clear only the char part of each cell, the rest must have been cleared by a memset or similar
if (ch) {
for (index_type i = 0; i < xnum; i++) { cells[i].ch = ch; cells[i].attrs = 1; }
}
}
static inline index_type
xlimit_for_line(Line *line) {
index_type xlimit = line->xnum;
if (BLANK_CHAR == 0) {
while (xlimit > 0 && (line->cells[xlimit - 1].ch) == BLANK_CHAR) xlimit--;
}
return xlimit;
}
PyObject* line_text_at(char_type, combining_type);
void line_clear_text(Line *self, unsigned int at, unsigned int num, char_type ch);
void line_apply_cursor(Line *self, Cursor *cursor, unsigned int at, unsigned int num, bool clear_char);
void line_set_char(Line *, unsigned int , uint32_t , unsigned int , Cursor *, bool);
void line_right_shift(Line *, unsigned int , unsigned int );
void line_add_combining_char(Line *, uint32_t , unsigned int );
index_type line_url_start_at(Line *self, index_type x);
index_type line_url_end_at(Line *self, index_type x);
index_type line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen);
unsigned int line_length(Line *self);
PyObject* unicode_in_range(Line *self, index_type start, index_type limit, bool include_cc, char leading_char);
void linebuf_init_line(LineBuf *, index_type);
void linebuf_clear(LineBuf *, char_type ch);
void linebuf_init_line(LineBuf *, index_type);
void linebuf_index(LineBuf* self, index_type top, index_type bottom);
void linebuf_reverse_index(LineBuf *self, index_type top, index_type bottom);
void linebuf_clear_line(LineBuf *self, index_type y);
void linebuf_insert_lines(LineBuf *self, unsigned int num, unsigned int y, unsigned int bottom);
void linebuf_delete_lines(LineBuf *self, index_type num, index_type y, index_type bottom);
void linebuf_set_attribute(LineBuf *, unsigned int , unsigned int );
void linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *, index_type *, HistoryBuf *);
unsigned int linebuf_char_width_at(LineBuf *self, index_type x, index_type y);
void linebuf_refresh_sprite_positions(LineBuf *self);
bool historybuf_resize(HistoryBuf *self, index_type lines);
void historybuf_add_line(HistoryBuf *self, const Line *line);
void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other);
void historybuf_init_line(HistoryBuf *self, index_type num, Line *l);
void historybuf_refresh_sprite_positions(HistoryBuf *self);

View File

@@ -6,33 +6,33 @@ import argparse
import locale
import os
import sys
import tempfile
from contextlib import contextmanager
from gettext import gettext as _
from queue import Empty
from .boss import Boss
from .config import (
cached_values, load_cached_values, load_config, save_cached_values
)
from .constants import (
appname, config_dir, isosx, logo_data_file, str_version, viewport_size
appname, defconf, isosx, iswayland, logo_data_file, str_version,
viewport_size
)
from .fast_data_types import (
GL_COLOR_BUFFER_BIT, GLFW_CONTEXT_VERSION_MAJOR,
GLFW_CONTEXT_VERSION_MINOR, GLFW_OPENGL_CORE_PROFILE,
GL_VERSION_REQUIRED, GLFW_CONTEXT_VERSION_MAJOR,
GLFW_CONTEXT_VERSION_MINOR, GLFW_DECORATED, GLFW_OPENGL_CORE_PROFILE,
GLFW_OPENGL_FORWARD_COMPAT, GLFW_OPENGL_PROFILE, GLFW_SAMPLES,
GLFW_STENCIL_BITS, Window, change_wcwidth,
enable_automatic_opengl_error_checking, glClear, glClearColor, glewInit,
glfw_init, glfw_set_error_callback, glfw_swap_interval, glfw_terminate,
glfw_wait_events, glfw_window_hint
GLFW_STENCIL_BITS, GLFWWindow, change_wcwidth, check_for_extensions,
clear_buffers, glewInit, glfw_init, glfw_init_hint_string,
glfw_swap_interval, glfw_terminate, glfw_window_hint, set_logical_dpi,
set_options
)
from .layout import all_layouts
from .shaders import GL_VERSION
from .utils import safe_print
from .utils import color_as_int, detach, get_logical_dpi, safe_print
defconf = os.path.join(config_dir, 'kitty.conf')
try:
from .fast_data_types import GLFW_X11_WM_CLASS_NAME, GLFW_X11_WM_CLASS_CLASS
except ImportError:
GLFW_X11_WM_CLASS_NAME = GLFW_X11_WM_CLASS_CLASS = None
def option_parser():
@@ -83,18 +83,19 @@ def option_parser():
action='version',
version='{} {} by Kovid Goyal'.format(appname, str_version)
)
a(
'--profile',
action='store_true',
default=False,
help=_('Show profiling data after exit')
)
a(
'--dump-commands',
action='store_true',
default=False,
help=_('Output commands received from child process to stdout')
)
if not isosx:
a(
'--detach',
action='store_true',
default=False,
help=_('Detach from the controlling terminal, if any')
)
a(
'--replay-commands',
default=None,
@@ -106,7 +107,7 @@ def option_parser():
' child process. Useful for debugging.')
)
a(
'--debug-gl',
'--debug-kitty-gl',
action='store_true',
default=False,
help=_('Debug OpenGL commands. This will cause all OpenGL calls'
@@ -137,9 +138,11 @@ def option_parser():
return parser
def setup_opengl():
glfw_window_hint(GLFW_CONTEXT_VERSION_MAJOR, GL_VERSION[0])
glfw_window_hint(GLFW_CONTEXT_VERSION_MINOR, GL_VERSION[1])
def setup_opengl(opts):
if opts.macos_hide_titlebar:
glfw_window_hint(GLFW_DECORATED, False)
glfw_window_hint(GLFW_CONTEXT_VERSION_MAJOR, GL_VERSION_REQUIRED[0])
glfw_window_hint(GLFW_CONTEXT_VERSION_MINOR, GL_VERSION_REQUIRED[1])
glfw_window_hint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE)
glfw_window_hint(GLFW_OPENGL_FORWARD_COMPAT, True)
glfw_window_hint(GLFW_SAMPLES, 0)
@@ -148,34 +151,23 @@ def setup_opengl():
glfw_window_hint(GLFW_STENCIL_BITS, 8)
def clear_buffers(window, opts):
bg = opts.background
glClearColor(bg.red / 255, bg.green / 255, bg.blue / 255, 1)
def initialize_window(window, opts):
set_logical_dpi(*get_logical_dpi())
viewport_size.width, viewport_size.height = window.get_framebuffer_size()
w, h = window.get_window_size()
viewport_size.x_ratio = viewport_size.width / float(w)
viewport_size.y_ratio = viewport_size.height / float(h)
glewInit()
glfw_swap_interval(0)
glClear(GL_COLOR_BUFFER_BIT)
window.swap_buffers()
glClear(GL_COLOR_BUFFER_BIT)
clear_buffers(window.swap_buffers, color_as_int(opts.background))
# We dont turn this on as it causes rendering performance to be much worse,
# for example, dragging the mouse to select is laggy
# glfw_swap_interval(1)
def dispatch_pending_calls(boss):
while True:
try:
func, args = boss.pending_ui_thread_calls.get_nowait()
except Empty:
break
try:
func(*args)
except Exception:
import traceback
safe_print(traceback.format_exc())
boss.ui_timers()
def run_app(opts, args):
setup_opengl()
set_options(opts)
setup_opengl(opts)
load_cached_values()
if 'window-size' in cached_values and opts.remember_window_size:
ws = cached_values['window-size']
@@ -188,26 +180,29 @@ def run_app(opts, args):
else:
viewport_size.width = opts.initial_window_width
viewport_size.height = opts.initial_window_height
window = Window(viewport_size.width, viewport_size.height, args.cls)
window.set_title(appname)
try:
window = GLFWWindow(viewport_size.width, viewport_size.height, args.cls)
except ValueError:
safe_print('Failed to create GLFW window with initial size:', viewport_size)
viewport_size.width = 640
viewport_size.height = 400
window = GLFWWindow(viewport_size.width, viewport_size.height, args.cls)
window.make_context_current()
if not isosx:
if isosx:
from .fast_data_types import cocoa_make_window_resizable, cocoa_create_global_menu, cocoa_init
check_for_extensions()
cocoa_init()
cocoa_create_global_menu()
if opts.macos_hide_titlebar:
cocoa_make_window_resizable(window.cocoa_window_id())
elif not iswayland: # no window icons on wayland
with open(logo_data_file, 'rb') as f:
window.set_icon(f.read(), 256, 256)
viewport_size.width, viewport_size.height = window.get_framebuffer_size()
w, h = window.get_window_size()
viewport_size.x_ratio = viewport_size.width / float(w)
viewport_size.y_ratio = viewport_size.height / float(h)
glewInit()
initialize_window(window, opts)
boss = Boss(window, opts, args)
boss.start()
clear_buffers(window, opts)
try:
while not window.should_close():
boss.render()
window.swap_buffers()
glfw_wait_events(boss.ui_timers.timeout())
dispatch_pending_calls(boss)
boss.child_monitor.main_loop()
finally:
boss.destroy()
del window
@@ -215,21 +210,65 @@ def run_app(opts, args):
save_cached_values()
def on_glfw_error(code, msg):
if isinstance(msg, bytes):
def ensure_osx_locale():
# Ensure the LANG env var is set. See
# https://github.com/kovidgoyal/kitty/issues/90
from .fast_data_types import cocoa_get_lang
if 'LANG' not in os.environ:
lang = cocoa_get_lang()
if lang is not None:
os.environ['LANG'] = lang + '.UTF-8'
@contextmanager
def setup_profiling(args):
try:
from .fast_data_types import start_profiler, stop_profiler
except ImportError:
start_profiler = stop_profiler = None
if start_profiler is not None:
start_profiler('/tmp/kitty-profile.log')
yield
if stop_profiler is not None:
import subprocess
stop_profiler()
exe = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'kitty-profile')
cg = '/tmp/kitty-profile.callgrind'
print('Post processing profile data for', exe, '...')
subprocess.check_call(['pprof', '--callgrind', exe, '/tmp/kitty-profile.log'], stdout=open(cg, 'wb'))
try:
msg = msg.decode('utf-8')
except Exception:
msg = repr(msg)
safe_print('[glfw error] ', msg, file=sys.stderr)
subprocess.Popen(['kcachegrind', cg])
except FileNotFoundError:
subprocess.check_call(['pprof', '--text', exe, '/tmp/kitty-profile.log'])
print('To view the graphical call data, use: kcachegrind', cg)
def main():
locale.setlocale(locale.LC_ALL, '')
try:
sys.setswitchinterval(1000.0) # we have only a single python thread
except AttributeError:
pass # python compiled without threading
if isosx:
ensure_osx_locale()
try:
locale.setlocale(locale.LC_ALL, '')
except Exception:
if not isosx:
raise
print('Failed to set locale with LANG:', os.environ.get('LANG'), file=sys.stderr)
os.environ.pop('LANG')
try:
locale.setlocale(locale.LC_ALL, '')
except Exception:
print('Failed to set locale with no LANG, ignoring', file=sys.stderr)
if os.environ.pop('KITTY_LAUNCHED_BY_LAUNCH_SERVICES',
None) == '1' and getattr(sys, 'frozen', True):
os.chdir(os.path.expanduser('~'))
if not os.path.isdir(os.getcwd()):
os.chdir(os.path.expanduser('~'))
args = option_parser().parse_args()
if getattr(args, 'detach', False):
detach()
if args.cmd:
exec(args.cmd)
return
@@ -241,28 +280,12 @@ def main():
overrides = (a.replace('=', ' ', 1) for a in args.override or ())
opts = load_config(*config, overrides=overrides)
change_wcwidth(not opts.use_system_wcwidth)
glfw_set_error_callback(on_glfw_error)
enable_automatic_opengl_error_checking(args.debug_gl)
if GLFW_X11_WM_CLASS_CLASS is not None:
glfw_init_hint_string(GLFW_X11_WM_CLASS_CLASS, args.cls)
if not glfw_init():
raise SystemExit('GLFW initialization failed')
try:
if args.profile:
tf = tempfile.NamedTemporaryFile(prefix='kitty-profiling-stats-')
args.profile = tf.name
import cProfile
import pstats
pr = cProfile.Profile()
pr.enable()
run_app(opts, args)
pr.disable()
pr.create_stats()
s = pstats.Stats(pr)
s.add(args.profile)
tf.close()
s.strip_dirs()
s.sort_stats('time', 'name')
s.print_stats(30)
else:
with setup_profiling(args):
run_app(opts, args)
finally:
glfw_terminate()

358
kitty/mouse.c Normal file
View File

@@ -0,0 +1,358 @@
/*
* mouse.c
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "state.h"
#include "screen.h"
#include "lineops.h"
#include <limits.h>
#include <math.h>
#include <GLFW/glfw3.h>
static MouseShape mouse_cursor_shape = BEAM;
typedef enum MouseActions { PRESS, RELEASE, DRAG, MOVE } MouseAction;
#define SHIFT_INDICATOR (1 << 2)
#define ALT_INDICATOR (1 << 3)
#define CONTROL_INDICATOR (1 << 4)
#define MOTION_INDICATOR (1 << 5)
#define EXTRA_BUTTON_INDICATOR (1 << 6)
static inline unsigned int
button_map(int button) {
switch(button) {
case GLFW_MOUSE_BUTTON_1:
return 0;
case GLFW_MOUSE_BUTTON_2:
return 1;
case GLFW_MOUSE_BUTTON_3:
return 2;
case GLFW_MOUSE_BUTTON_4:
return EXTRA_BUTTON_INDICATOR;
case GLFW_MOUSE_BUTTON_5:
return EXTRA_BUTTON_INDICATOR | 1;
default:
return UINT_MAX;
}
}
static char mouse_event_buf[64];
size_t
encode_mouse_event(Window *w, int button, MouseAction action, int mods) {
unsigned int x = w->mouse_cell_x + 1, y = w->mouse_cell_y + 1; // 1 based indexing
unsigned int cb = 0;
Screen *screen = w->render_data.screen;
if (action == MOVE) {
if (screen->modes.mouse_tracking_protocol != SGR_PROTOCOL) cb = 3;
} else {
cb = button_map(button);
if (cb == UINT_MAX) return 0;
}
if (action == DRAG || action == MOVE) cb |= MOTION_INDICATOR;
else if (action == RELEASE && screen->modes.mouse_tracking_protocol != SGR_PROTOCOL) cb = 3;
if (mods & GLFW_MOD_SHIFT) cb |= SHIFT_INDICATOR;
if (mods & GLFW_MOD_ALT) cb |= ALT_INDICATOR;
if (mods & GLFW_MOD_CONTROL) cb |= CONTROL_INDICATOR;
switch(screen->modes.mouse_tracking_protocol) {
case SGR_PROTOCOL:
return snprintf(mouse_event_buf, sizeof(mouse_event_buf), "\033[<%d;%d;%d%s", cb, x, y, action == RELEASE ? "m" : "M");
break;
case URXVT_PROTOCOL:
return snprintf(mouse_event_buf, sizeof(mouse_event_buf), "\033[%d;%d;%dM", cb, x, y);
break;
case UTF8_PROTOCOL:
mouse_event_buf[0] = 033; mouse_event_buf[1] = '['; mouse_event_buf[2] = cb + 32;
unsigned int sz = 3;
sz += encode_utf8(x + 32, mouse_event_buf + sz);
sz += encode_utf8(y + 32, mouse_event_buf + sz);
return sz;
break;
default:
if (x > 223 || y > 223) return 0;
else {
mouse_event_buf[0] = 033; mouse_event_buf[1] = '['; mouse_event_buf[2] = 'M'; mouse_event_buf[3] = cb + 32; mouse_event_buf[4] = x + 32; mouse_event_buf[5] = y + 32;
return 6;
}
break;
}
return 0;
}
static inline bool
contains_mouse(Window *w) {
WindowGeometry *g = &w->geometry;
double x = global_state.mouse_x, y = global_state.mouse_y;
return (w->visible && g->left <= x && x <= g->right && g->top <= y && y <= g->bottom);
}
static inline bool
cell_for_pos(Window *w, unsigned int *x, unsigned int *y) {
WindowGeometry *g = &w->geometry;
unsigned int qx = (unsigned int)((double)(global_state.mouse_x - g->left) / global_state.cell_width);
unsigned int qy = (unsigned int)((double)(global_state.mouse_y - g->top) / global_state.cell_height);
bool ret = false;
Screen *screen = w->render_data.screen;
if (screen && qx <= screen->columns && qy <= screen->lines) {
*x = qx; *y = qy; ret = true;
}
return ret;
}
#define HANDLER(name) static inline void name(Window UNUSED *w, int UNUSED button, int UNUSED modifiers, unsigned int UNUSED window_idx)
static inline void
update_drag(bool from_button, Window *w, bool is_release) {
Screen *screen = w->render_data.screen;
if (from_button) {
if (is_release) screen_update_selection(screen, w->mouse_cell_x, w->mouse_cell_y, true);
else screen_start_selection(screen, w->mouse_cell_x, w->mouse_cell_y);
} else if (screen->selection.in_progress) {
screen_update_selection(screen, w->mouse_cell_x, w->mouse_cell_y, false);
call_boss(set_primary_selection, NULL);
}
}
bool
drag_scroll(Window *w) {
unsigned int margin = global_state.cell_height / 2;
double x = global_state.mouse_x, y = global_state.mouse_y;
if (y < w->geometry.top || y > w->geometry.bottom) return false;
if (x < w->geometry.left || x > w->geometry.right) return false;
bool upwards = y <= (w->geometry.top + margin);
if (upwards || y >= w->geometry.bottom - margin) {
Screen *screen = w->render_data.screen;
if (screen->linebuf == screen->main_linebuf) {
screen_history_scroll(screen, SCROLL_LINE, upwards);
update_drag(false, w, false);
global_state.last_mouse_activity_at = monotonic();
if (mouse_cursor_shape != ARROW) {
mouse_cursor_shape = ARROW;
set_mouse_cursor(mouse_cursor_shape);
}
return true;
}
}
return false;
}
static inline void
detect_url(Window *w, Screen *screen, unsigned int x, unsigned int y) {
bool has_url = false;
index_type url_start, url_end = 0;
Line *line = screen_visual_line(screen, y);
if (line) {
url_start = line_url_start_at(line, x);
if (url_start < line->xnum) url_end = line_url_end_at(line, x);
has_url = url_end > url_start;
}
if (has_url) {
mouse_cursor_shape = HAND;
screen_mark_url(screen, url_start, w->mouse_cell_y, url_end, w->mouse_cell_y);
} else {
mouse_cursor_shape = BEAM;
screen_mark_url(screen, 0, 0, 0, 0);
}
}
HANDLER(handle_move_event) {
unsigned int x, y;
if (!cell_for_pos(w, &x, &y)) return;
Screen *screen = w->render_data.screen;
detect_url(w, screen, x, y);
bool mouse_cell_changed = x != w->mouse_cell_x || y != w->mouse_cell_y;
w->mouse_cell_x = x; w->mouse_cell_y = y;
bool handle_in_kitty = (
(screen->modes.mouse_tracking_mode == ANY_MODE ||
(screen->modes.mouse_tracking_mode == MOTION_MODE && button >= 0)) &&
!(global_state.is_key_pressed[GLFW_KEY_LEFT_SHIFT] || global_state.is_key_pressed[GLFW_KEY_RIGHT_SHIFT])
) ? false : true;
if (handle_in_kitty) {
if (screen->selection.in_progress && button == GLFW_MOUSE_BUTTON_LEFT) {
double now = monotonic();
if ((now - w->last_drag_scroll_at) >= 0.02 || mouse_cell_changed) {
update_drag(false, w, false);
w->last_drag_scroll_at = monotonic();
}
}
} else {
if (!mouse_cell_changed) return;
size_t sz = encode_mouse_event(w, MAX(0, button), button >=0 ? DRAG : MOVE, 0);
if (sz) schedule_write_to_child(w->id, mouse_event_buf, sz);
}
}
static inline void
multi_click(Window *w, unsigned int count) {
Screen *screen = w->render_data.screen;
index_type start, end;
bool found_selection = false;
switch(count) {
case 2:
found_selection = screen_selection_range_for_word(screen, w->mouse_cell_x, w->mouse_cell_y, &start, &end);
break;
case 3:
found_selection = screen_selection_range_for_line(screen, w->mouse_cell_y, &start, &end);
break;
default:
break;
}
if (found_selection) {
screen_start_selection(screen, start, w->mouse_cell_y);
screen_update_selection(screen, end, w->mouse_cell_y, true);
call_boss(set_primary_selection, NULL);
}
}
HANDLER(add_click) {
ClickQueue *q = &w->click_queue;
if (q->length == CLICK_QUEUE_SZ) { memmove(q->clicks, q->clicks + 1, sizeof(Click) * (CLICK_QUEUE_SZ - 1)); q->length--; }
double now = monotonic();
#define N(n) (q->clicks[q->length - n])
N(0).at = now; N(0).button = button; N(0).modifiers = modifiers;
q->length++;
// Now dispatch the multi-click if any
if (q->length > 2 && N(1).at - N(3).at <= 2 * OPT(click_interval)) {
multi_click(w, 3);
q->length = 0;
} else if (q->length > 1 && N(1).at - N(2).at <= OPT(click_interval)) {
multi_click(w, 2);
}
#undef N
}
static inline void
open_url(Window *w) {
Line *line = screen_visual_line(w->render_data.screen, w->mouse_cell_y);
if (line) {
index_type start = line_url_start_at(line, w->mouse_cell_x);
if (start < line->xnum) {
index_type end = line_url_end_at(line, w->mouse_cell_x);
if (end > start) {
call_boss(open_url, "N", unicode_in_range(line, start, end + 1, true, 0));
}
}
}
}
HANDLER(handle_button_event) {
Tab *t = global_state.tabs + global_state.active_tab;
bool is_release = !global_state.mouse_button_pressed[button];
if (window_idx != t->active_window) {
call_boss(switch_focus_to, "I", window_idx);
}
Screen *screen = w->render_data.screen;
if (!screen) return;
bool handle_in_kitty = (
modifiers == GLFW_MOD_SHIFT ||
screen->modes.mouse_tracking_mode == 0 ||
button == GLFW_MOUSE_BUTTON_MIDDLE ||
(modifiers == (int)OPT(open_url_modifiers) && button == GLFW_MOUSE_BUTTON_LEFT)
);
if (handle_in_kitty) {
switch(button) {
case GLFW_MOUSE_BUTTON_LEFT:
update_drag(true, w, is_release);
if (is_release) {
if (modifiers == (int)OPT(open_url_modifiers)) {
open_url(w);
} else {
if (is_release) add_click(w, button, modifiers, window_idx);
}
}
break;
case GLFW_MOUSE_BUTTON_MIDDLE:
if (is_release && !modifiers) { call_boss(paste_from_selection, NULL); return; }
break;
}
} else {
size_t sz = encode_mouse_event(w, button, is_release ? RELEASE : PRESS, modifiers);
if (sz) schedule_write_to_child(w->id, mouse_event_buf, sz);
}
}
HANDLER(handle_event) {
switch(button) {
case -1:
for (int i = 0; i < GLFW_MOUSE_BUTTON_5; i++) { if (global_state.mouse_button_pressed[i]) { button = i; break; } }
handle_move_event(w, button, modifiers, window_idx);
break;
case GLFW_MOUSE_BUTTON_LEFT:
case GLFW_MOUSE_BUTTON_RIGHT:
case GLFW_MOUSE_BUTTON_MIDDLE:
case GLFW_MOUSE_BUTTON_4:
case GLFW_MOUSE_BUTTON_5:
handle_button_event(w, button, modifiers, window_idx);
break;
default:
break;
}
}
static inline void
handle_tab_bar_mouse(int button, int UNUSED modifiers) {
if (button != GLFW_MOUSE_BUTTON_LEFT || !global_state.mouse_button_pressed[button]) return;
call_boss(activate_tab_at, "d", global_state.mouse_x);
}
static inline Window*
window_for_event(unsigned int *window_idx, bool *in_tab_bar) {
*in_tab_bar = global_state.num_tabs > 1 && global_state.mouse_y >= global_state.viewport_height - global_state.cell_height;
if (!*in_tab_bar) {
Tab *t = global_state.tabs + global_state.active_tab;
for (unsigned int i = 0; i < t->num_windows; i++) {
if (contains_mouse(t->windows + i) && t->windows[i].render_data.screen) {
*window_idx = i; return t->windows + i;
}
}
}
return NULL;
}
void
mouse_event(int button, int modifiers) {
MouseShape old_cursor = mouse_cursor_shape;
bool in_tab_bar;
unsigned int window_idx;
Window *w = window_for_event(&window_idx, &in_tab_bar);
if (in_tab_bar) {
mouse_cursor_shape = HAND;
handle_tab_bar_mouse(button, modifiers);
} else if(w) {
handle_event(w, button, modifiers, window_idx);
}
if (mouse_cursor_shape != old_cursor) {
set_mouse_cursor(mouse_cursor_shape);
}
}
void
scroll_event(double UNUSED xoffset, double yoffset) {
int s = (int) round(yoffset * OPT(wheel_scroll_multiplier));
if (s == 0) return;
bool upwards = s > 0;
bool in_tab_bar;
unsigned int window_idx;
Window *w = window_for_event(&window_idx, &in_tab_bar);
if (w) {
Screen *screen = w->render_data.screen;
if (screen->linebuf == screen->main_linebuf) {
screen_history_scroll(screen, abs(s), upwards);
} else {
if (screen->modes.mouse_tracking_mode) {
size_t sz = encode_mouse_event(w, upwards ? GLFW_MOUSE_BUTTON_4 : GLFW_MOUSE_BUTTON_5, PRESS, 0);
if (sz) schedule_write_to_child(w->id, mouse_event_buf, sz);
} else {
call_boss(send_fake_scroll, "IiO", window_idx, abs(s), upwards ? Py_True : Py_False);
}
}
}
}

View File

@@ -1,62 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from .fast_data_types import (
GLFW_MOUSE_BUTTON_2, GLFW_MOUSE_BUTTON_3, GLFW_MOD_ALT, GLFW_MOD_CONTROL,
GLFW_MOD_SHIFT, GLFW_MOUSE_BUTTON_4, GLFW_MOUSE_BUTTON_5, SGR_PROTOCOL,
GLFW_MOUSE_BUTTON_1, URXVT_PROTOCOL, UTF8_PROTOCOL
)
PRESS, RELEASE, DRAG, MOVE = range(4)
SHIFT_INDICATOR = 1 << 2
ALT_INDICATOR = 1 << 3
CONTROL_INDICATOR = 1 << 4
MOTION_INDICATOR = 1 << 5
EXTRA_BUTTON_INDICATOR = 1 << 6
cb_map = {
GLFW_MOUSE_BUTTON_1: 0,
GLFW_MOUSE_BUTTON_2: 0b1,
GLFW_MOUSE_BUTTON_3: 0b10,
GLFW_MOUSE_BUTTON_4: EXTRA_BUTTON_INDICATOR,
GLFW_MOUSE_BUTTON_5: EXTRA_BUTTON_INDICATOR | 0b1
}
def encode_mouse_event(tracking_mode, tracking_protocol, button, action, mods, x, y):
x, y = x + 1, y + 1 # One based indexing
cb = 0
if action is MOVE:
if tracking_protocol != SGR_PROTOCOL:
cb = 0b11
else:
cb = cb_map.get(button)
if cb is None:
return
if action in (DRAG, MOVE):
cb |= MOTION_INDICATOR
elif action is RELEASE:
if tracking_protocol != SGR_PROTOCOL:
cb = 0b11
if mods & GLFW_MOD_SHIFT:
cb |= SHIFT_INDICATOR
if mods & GLFW_MOD_ALT:
cb |= ALT_INDICATOR
if mods & GLFW_MOD_CONTROL:
cb |= CONTROL_INDICATOR
ans = None
if tracking_protocol == SGR_PROTOCOL:
ans = '\033[<%d;%d;%d%s' % (cb, x, y, 'm' if action is RELEASE else 'M')
ans = ans.encode('ascii')
elif tracking_protocol == URXVT_PROTOCOL:
ans = '\033[%d;%d;%dM' % (cb + 32, x, y)
ans = ans.encode('ascii')
elif tracking_protocol == UTF8_PROTOCOL:
ans = bytearray([0o33, ord('['), cb + 32])
ans.extend(chr(x + 32).encode('utf-8') + chr(y + 32).encode('utf-8'))
ans = bytes(ans)
else:
if x <= 223 and y <= 223:
ans = bytearray([0o33, ord('['), cb + 32, x + 32, y + 32])
return ans

View File

@@ -6,22 +6,25 @@
#include "data-types.h"
#include "control-codes.h"
#include "screen.h"
#include "graphics.h"
#include <time.h>
// utils {{{
static unsigned int pow10[10] = {
1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000
static unsigned long pow10[] = {
1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 10000000000
};
static inline unsigned int
static inline unsigned long
utoi(uint32_t *buf, unsigned int sz) {
unsigned int ans = 0;
unsigned long ans = 0;
uint32_t *p = buf;
// Ignore leading zeros
while(sz > 0) {
if (*p == '0') { p++; sz--; }
else break;
}
if (sz < sizeof(pow10)/sizeof(pow10[10])) {
if (sz < sizeof(pow10)/sizeof(pow10[0])) {
for (int i = sz-1, j=0; i >=0; i--, j++) {
ans += (p[i] - '0') * pow10[j];
}
@@ -84,6 +87,7 @@ _report_params(PyObject *dump_callback, const char *name, unsigned int *params,
#define GET_MACRO(_1,_2,_3,NAME,...) NAME
#define REPORT_COMMAND(...) GET_MACRO(__VA_ARGS__, REPORT_COMMAND3, REPORT_COMMAND2, REPORT_COMMAND1, SENTINEL)(__VA_ARGS__)
#define REPORT_VA_COMMAND(...) Py_XDECREF(PyObject_CallFunction(dump_callback, __VA_ARGS__)); PyErr_Clear();
#define REPORT_DRAW(ch) \
Py_XDECREF(PyObject_CallFunction(dump_callback, "sC", "draw", ch)); PyErr_Clear();
@@ -96,6 +100,9 @@ _report_params(PyObject *dump_callback, const char *name, unsigned int *params,
#define REPORT_OSC(name, string) \
Py_XDECREF(PyObject_CallFunction(dump_callback, "sO", #name, string)); PyErr_Clear();
#define REPORT_OSC2(name, code, string) \
Py_XDECREF(PyObject_CallFunction(dump_callback, "sIO", #name, code, string)); PyErr_Clear();
#else
#define DUMP_UNUSED UNUSED
@@ -103,10 +110,12 @@ _report_params(PyObject *dump_callback, const char *name, unsigned int *params,
#define REPORT_ERROR(...) fprintf(stderr, "%s ", ERROR_PREFIX); fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n");
#define REPORT_COMMAND(...)
#define REPORT_VA_COMMAND(...)
#define REPORT_DRAW(ch)
#define REPORT_PARAMS(...)
#define FLUSH_DRAW
#define REPORT_OSC(name, string)
#define REPORT_OSC2(name, code, string)
#endif
@@ -266,7 +275,7 @@ handle_esc_mode_char(Screen *screen, uint32_t ch, PyObject DUMP_UNUSED *dump_cal
static inline void
dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
#define DISPATCH_OSC(name) REPORT_OSC(name, string); name(screen, string);
#define SET_COLOR(name) REPORT_OSC(name, string); name(screen, code, string);
#define SET_COLOR(name) REPORT_OSC2(name, code, string); name(screen, code, string);
const unsigned int limit = screen->parser_buf_pos;
unsigned int code=0, i;
for (i = 0; i < MIN(limit, 5); i++) {
@@ -331,11 +340,7 @@ screen_cursor_up2(Screen *s, unsigned int count) { screen_cursor_up(s, count, fa
static inline void
screen_cursor_back1(Screen *s, unsigned int count) { screen_cursor_back(s, count, -1); }
static inline void
screen_indexn(Screen *s, unsigned int count) { for (index_type i=0; i < MAX(1, count); i++) screen_index(s); }
static inline void
screen_tabn(Screen *s, unsigned int count) { for (index_type i=0; i < MAX(1, count); i++) screen_tab(s); }
static inline void
screen_reverse_indexn(Screen *s, unsigned int count) { for (index_type i=0; i < count; i++) screen_reverse_index(s); }
static inline void
save_cursor(Screen *s, unsigned int UNUSED param, bool private) {
if (private) fprintf(stderr, "%s %s", ERROR_PREFIX, "CSI s in private mode not supported");
@@ -484,9 +489,9 @@ dispatch_csi(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
case DECSCUSR:
CALL_CSI_HANDLER1M(screen_set_cursor, 1);
case SU:
CALL_CSI_HANDLER1(screen_indexn, 1);
CALL_CSI_HANDLER1(screen_scroll, 1);
case SD:
CALL_CSI_HANDLER1(screen_reverse_indexn, 1);
CALL_CSI_HANDLER1(screen_reverse_scroll, 1);
case DECSTR:
if (end_modifier == '$') {
// DECRQM
@@ -526,6 +531,193 @@ dispatch_dcs(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
}
// }}}
// APC mode {{{
static inline void
parse_graphics_code(Screen *screen, PyObject UNUSED *dump_callback) {
unsigned int pos = 1;
enum GR_STATES { KEY, EQUAL, UINT, INT, FLAG, AFTER_VALUE, PAYLOAD };
enum GR_STATES state = KEY, value_state = FLAG;
enum KEYS {
action='a',
delete_action='d',
transmission_type='t',
compressed='o',
format = 'f',
more = 'm',
id = 'i',
width = 'w',
height = 'h',
x_offset = 'x',
y_offset = 'y',
data_height = 'v',
data_width = 's',
data_sz = 'S',
data_offset = 'O',
num_cells = 'c',
num_lines = 'r',
cell_x_offset = 'X',
cell_y_offset = 'Y',
z_index = 'z'
};
enum KEYS key = 'a';
static GraphicsCommand g;
unsigned int i, code;
unsigned long lcode;
bool is_negative;
memset(&g, 0, sizeof(g));
static uint8_t payload[4096];
size_t sz;
const char *err;
while (pos < screen->parser_buf_pos) {
switch(state) {
case KEY:
key = screen->parser_buf[pos++];
switch(key) {
#define KS(n, vs) case n: state = EQUAL; value_state = vs; break
#define U(x) KS(x, UINT)
KS(action, FLAG); KS(delete_action, FLAG); KS(transmission_type, FLAG); KS(compressed, FLAG); KS(z_index, INT);
U(format); U(more); U(id); U(data_sz); U(data_offset); U(width); U(height); U(x_offset); U(y_offset); U(data_height); U(data_width); U(num_cells); U(num_lines); U(cell_x_offset); U(cell_y_offset);
#undef U
#undef KS
default:
REPORT_ERROR("Malformed graphics control block, invalid key character: 0x%x", key);
return;
}
break;
case EQUAL:
if (screen->parser_buf[pos++] != '=') {
REPORT_ERROR("Malformed graphics control block, no = after key, found: 0x%x instead", screen->parser_buf[pos-1]);
return;
}
state = value_state;
break;
case FLAG:
switch(key) {
#define F(a) case a: g.a = screen->parser_buf[pos++] & 0xff; break
F(action); F(delete_action); F(transmission_type); F(compressed);
default:
break;
}
state = AFTER_VALUE;
break;
#undef F
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 graphics 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("id is too large"); return; } \
code = lcode;
is_negative = false;
if(screen->parser_buf[pos] == '-') { is_negative = true; pos++; }
#define U(x) case x: g.x = is_negative ? 0 - (int32_t)code : (int32_t)code; break
READ_UINT;
switch(key) {
U(z_index);
default: break;
}
state = AFTER_VALUE;
break;
#undef U
case UINT:
READ_UINT;
#define U(x) case x: g.x = code; break
switch(key) {
U(format); U(more); U(id); U(data_sz); U(data_offset); U(width); U(height); U(x_offset); U(y_offset); U(data_height); U(data_width); U(num_cells); U(num_lines); U(cell_x_offset); U(cell_y_offset);
default: break;
}
state = AFTER_VALUE;
break;
#undef U
#undef SET_ATTR
#undef READ_UINT
case AFTER_VALUE:
switch (screen->parser_buf[pos++]) {
case ',':
state = KEY;
break;
case ';':
state = PAYLOAD;
break;
default:
REPORT_ERROR("Malformed graphics control block, expecting a comma or semi-colon after a value, found: 0x%x", screen->parser_buf[pos - 1]);
return;
}
break;
case PAYLOAD:
sz = screen->parser_buf_pos - pos;
err = base64_decode(screen->parser_buf + pos, sz, payload, sizeof(payload), &g.payload_sz);
if (err != NULL) { REPORT_ERROR("Failed to parse graphics command payload with error: %s", err); return; }
pos = screen->parser_buf_pos;
break;
}
}
switch(state) {
case EQUAL:
REPORT_ERROR("Malformed graphics control block, no = after key"); return;
case INT:
case UINT:
REPORT_ERROR("Malformed graphics control block, expecting an integer value"); return;
case FLAG:
REPORT_ERROR("Malformed graphics control block, expecting a flag value"); return;
default:
break;
}
#define A(x) #x, g.x
#define U(x) #x, (unsigned int)(g.x)
#define I(x) #x, (int)(g.x)
REPORT_VA_COMMAND("s {sc sc sc sc sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI si} y#", "graphics_command",
A(action), A(delete_action), A(transmission_type), A(compressed),
U(format), U(more), U(id), U(data_sz), U(data_offset),
U(width), U(height), U(x_offset), U(y_offset), U(data_height), U(data_width), U(num_cells), U(num_lines), U(cell_x_offset), U(cell_y_offset),
U(payload_sz), I(z_index),
payload, g.payload_sz
);
#undef U
#undef A
#undef I
screen_handle_graphics_command(screen, &g, payload);
}
static inline void
dispatch_apc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
if (screen->parser_buf_pos < 2) return;
switch(screen->parser_buf[0]) {
case 'G':
parse_graphics_code(screen, dump_callback);
break;
default:
REPORT_ERROR("Unrecognized APC code: 0x%x", screen->parser_buf[0]);
break;
}
}
// }}}
// PM mode {{{
static inline void
dispatch_pm(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
if (screen->parser_buf_pos < 2) return;
switch(screen->parser_buf[0]) {
default:
REPORT_ERROR("Unrecognized PM code: 0x%x", screen->parser_buf[0]);
break;
}
}
// }}}
// Parse loop {{{
static inline bool
@@ -543,6 +735,7 @@ accumulate_osc(Screen *screen, uint32_t ch, PyObject DUMP_UNUSED *dump_callback)
screen->parser_buf_pos--;
return true;
}
/* fallthrough */
default:
if (screen->parser_buf_pos >= PARSER_BUF_SZ - 1) {
REPORT_ERROR("OSC sequence too long, truncating.");
@@ -594,6 +787,7 @@ accumulate_oth(Screen *screen, uint32_t ch, PyObject DUMP_UNUSED *dump_callback)
screen->parser_buf_pos--;
return true;
}
/* fallthrough */
default:
if (screen->parser_buf_pos >= PARSER_BUF_SZ - 1) {
REPORT_ERROR("OTH sequence too long, truncating.");
@@ -685,10 +879,10 @@ dispatch_unicode_char(Screen *screen, uint32_t codepoint, PyObject DUMP_UNUSED *
if (accumulate_osc(screen, codepoint, dump_callback)) { dispatch_osc(screen, dump_callback); SET_STATE(0); }
break;
case APC:
if (accumulate_oth(screen, codepoint, dump_callback)) { SET_STATE(0); }
if (accumulate_oth(screen, codepoint, dump_callback)) { dispatch_apc(screen, dump_callback); SET_STATE(0); }
break;
case PM:
if (accumulate_oth(screen, codepoint, dump_callback)) { SET_STATE(0); }
if (accumulate_oth(screen, codepoint, dump_callback)) { dispatch_pm(screen, dump_callback); SET_STATE(0); }
break;
case DCS:
if (accumulate_dcs(screen, codepoint, dump_callback)) { dispatch_dcs(screen, dump_callback); SET_STATE(0); }
@@ -722,7 +916,7 @@ FLUSH_DRAW;
}
// }}}
// Boilerplate {{{
// Entry points {{{
#ifdef DUMP_COMMANDS
#define FNAME(x) x##_dump
#else
@@ -743,38 +937,13 @@ FNAME(parse_bytes)(PyObject UNUSED *self, PyObject *args) {
Py_RETURN_NONE;
}
PyObject*
FNAME(read_bytes)(PyObject UNUSED *self, PyObject *args) {
PyObject *dump_callback = NULL;
Py_ssize_t len;
Screen *screen;
int fd;
#ifdef DUMP_COMMANDS
if (!PyArg_ParseTuple(args, "OOi", &dump_callback, &screen, &fd)) return NULL;
#else
if (!PyArg_ParseTuple(args, "Oi", &screen, &fd)) return NULL;
#endif
while(true) {
Py_BEGIN_ALLOW_THREADS;
len = read(fd, screen->read_buf, READ_BUF_SZ);
Py_END_ALLOW_THREADS;
if (len == -1) {
if (errno == EINTR) continue;
if (errno == EIO) { Py_RETURN_FALSE; }
return PyErr_SetFromErrno(PyExc_OSError);
}
/* PyObject_Print(Py_BuildValue("y#", screen->read_buf, len), stderr, 0); */
break;
}
void
FNAME(parse_worker)(Screen *screen, PyObject *dump_callback) {
#ifdef DUMP_COMMANDS
if (len > 0) {
Py_XDECREF(PyObject_CallFunction(dump_callback, "sy#", "bytes", screen->read_buf, len)); PyErr_Clear();
}
Py_XDECREF(PyObject_CallFunction(dump_callback, "sy#", "bytes", screen->read_buf, screen->read_buf_sz)); PyErr_Clear();
#endif
_parse_bytes(screen, screen->read_buf, len, dump_callback);
if(len > 0) { Py_RETURN_TRUE; }
Py_RETURN_FALSE;
}
_parse_bytes(screen, screen->read_buf, screen->read_buf_sz, dump_callback);
#undef FNAME
}
// }}}

View File

@@ -12,11 +12,11 @@
#endif
#ifndef init_src_line
#define init_src_line(src_y) INIT_LINE(src, src->line, src->line_map[src_y]);
#define init_src_line(src_y) init_line(src, src->line, src->line_map[src_y]);
#endif
#ifndef init_dest_line
#define init_dest_line(dest_y) INIT_LINE(dest, dest->line, dest->line_map[dest_y]);
#define init_dest_line(dest_y) init_line(dest, dest->line, dest->line_map[dest_y]);
#endif
#ifndef first_dest_line
@@ -28,7 +28,7 @@
if (dest_y >= dest->ynum - 1) { \
linebuf_index(dest, 0, dest->ynum - 1); \
if (historybuf != NULL) { \
linebuf_init_line(dest, dest->ynum - 1); \
init_dest_line(dest->ynum - 1); \
historybuf_add_line(historybuf, dest->line); \
}\
linebuf_clear_line(dest, dest->ynum - 1); \
@@ -41,19 +41,16 @@
#define is_src_line_continued(src_y) (src_y < src->ynum - 1 ? src->continued_map[src_y + 1] : false)
#endif
static inline void copy_range(Line *src, index_type src_at, Line* dest, index_type dest_at, index_type num) {
memcpy(dest->chars + dest_at, src->chars + src_at, num * sizeof(char_type));
memcpy(dest->fg_colors + dest_at, src->fg_colors + src_at, num * sizeof(color_type));
memcpy(dest->bg_colors + dest_at, src->bg_colors + src_at, num * sizeof(color_type));
memcpy(dest->decoration_fg + dest_at, src->decoration_fg + src_at, num * sizeof(color_type));
memcpy(dest->combining_chars + dest_at, src->combining_chars + src_at, num * sizeof(combining_type));
static inline void
copy_range(Line *src, index_type src_at, Line* dest, index_type dest_at, index_type num) {
memcpy(dest->cells + dest_at, src->cells + src_at, num * sizeof(Cell));
}
static void rewrap_inner(BufType *src, BufType *dest, const index_type src_limit, HistoryBuf UNUSED *historybuf) {
static void
rewrap_inner(BufType *src, BufType *dest, const index_type src_limit, HistoryBuf UNUSED *historybuf) {
bool src_line_is_continued = false;
index_type src_y = 0, src_x = 0, dest_x = 0, dest_y = 0, num = 0, src_x_limit = 0;
Py_BEGIN_ALLOW_THREADS;
first_dest_line;
do {
@@ -61,8 +58,8 @@ static void rewrap_inner(BufType *src, BufType *dest, const index_type src_limit
src_line_is_continued = is_src_line_continued(src_y);
src_x_limit = src->xnum;
if (!src_line_is_continued) {
// Trim trailing white-space since there is a hard line break at the end of this line
while(src_x_limit && (src->line->chars[src_x_limit - 1] & CHAR_MASK) == 32) src_x_limit--;
// Trim trailing blanks since there is a hard line break at the end of this line
while(src_x_limit && (src->line->cells[src_x_limit - 1].ch) == BLANK_CHAR) src_x_limit--;
}
while (src_x < src_x_limit) {
@@ -75,5 +72,4 @@ static void rewrap_inner(BufType *src, BufType *dest, const index_type src_limit
if (!src_line_is_continued && src_y < src_limit) { next_dest_line(false); dest_x = 0; }
} while (src_y < src_limit);
dest->line->ynum = dest_y;
Py_END_ALLOW_THREADS;
}

File diff suppressed because it is too large Load Diff

137
kitty/screen.h Normal file
View File

@@ -0,0 +1,137 @@
/*
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#pragma once
#include "graphics.h"
typedef enum ScrollTypes { SCROLL_LINE = -999999, SCROLL_PAGE, SCROLL_FULL } ScrollType;
typedef struct {
bool mLNM, mIRM, mDECTCEM, mDECSCNM, mDECOM, mDECAWM, mDECCOLM, mDECARM, mDECCKM,
mBRACKETED_PASTE, mFOCUS_TRACKING, mEXTENDED_KEYBOARD;
MouseTrackingMode mouse_tracking_mode;
MouseTrackingProtocol mouse_tracking_protocol;
} ScreenModes;
typedef struct {
unsigned int x, y;
} SelectionBoundary;
typedef struct {
unsigned int start_x, start_y, start_scrolled_by, end_x, end_y, end_scrolled_by;
bool in_progress;
} Selection;
typedef struct {
PyObject_HEAD
unsigned int columns, lines, margin_top, margin_bottom, charset, scrolled_by, last_selection_scrolled_by, window_id;
uint32_t utf8_state, utf8_codepoint, *g0_charset, *g1_charset, *g_charset;
Selection selection;
SelectionBoundary last_rendered_selection_start, last_rendered_selection_end;
Selection url_range;
bool use_latin1, selection_updated_once, is_dirty, scroll_changed;
Cursor *cursor;
SavepointBuffer main_savepoints, alt_savepoints;
PyObject *callbacks, *test_child;
LineBuf *linebuf, *main_linebuf, *alt_linebuf;
GraphicsManager *grman, *main_grman, *alt_grman;
HistoryBuf *historybuf;
unsigned int history_line_added_count;
bool *tabstops, *main_tabstops, *alt_tabstops;
ScreenModes modes;
ColorProfile *color_profile;
double start_visual_bell_at;
uint32_t parser_buf[PARSER_BUF_SZ];
unsigned int parser_state, parser_text_start, parser_buf_pos;
bool parser_has_pending_text;
uint8_t read_buf[READ_BUF_SZ], *write_buf;
double new_input_at;
size_t read_buf_sz, write_buf_sz, write_buf_used;
pthread_mutex_t read_buf_lock, write_buf_lock;
} Screen;
PyTypeObject Screen_Type;
void parse_worker(Screen *screen, PyObject *dump_callback);
void parse_worker_dump(Screen *screen, PyObject *dump_callback);
void screen_align(Screen*);
void screen_restore_cursor(Screen *);
void screen_save_cursor(Screen *);
void screen_cursor_position(Screen*, unsigned int, unsigned int);
void screen_cursor_back(Screen *self, unsigned int count/*=1*/, int move_direction/*=-1*/);
void screen_erase_in_line(Screen *, unsigned int, bool);
void screen_erase_in_display(Screen *, unsigned int, bool);
void screen_draw(Screen *screen, uint32_t codepoint);
void screen_ensure_bounds(Screen *self, bool use_margins);
void screen_toggle_screen_buffer(Screen *self);
void screen_normal_keypad_mode(Screen *self);
void screen_alternate_keypad_mode(Screen *self);
void screen_change_default_color(Screen *self, unsigned int which, uint32_t col);
void screen_alignment_display(Screen *self);
void screen_reverse_index(Screen *self);
void screen_index(Screen *self);
void screen_scroll(Screen *self, unsigned int count);
void screen_reverse_scroll(Screen *self, unsigned int count);
void screen_reset(Screen *self);
void screen_set_tab_stop(Screen *self);
void screen_tab(Screen *self);
void screen_backtab(Screen *self, unsigned int);
void screen_clear_tab_stop(Screen *self, unsigned int how);
void screen_set_mode(Screen *self, unsigned int mode);
void screen_reset_mode(Screen *self, unsigned int mode);
void screen_insert_characters(Screen *self, unsigned int count);
void screen_cursor_up(Screen *self, unsigned int count/*=1*/, bool do_carriage_return/*=false*/, int move_direction/*=-1*/);
void screen_set_cursor(Screen *self, unsigned int mode, uint8_t secondary);
void screen_cursor_to_column(Screen *self, unsigned int column);
void screen_cursor_down(Screen *self, unsigned int count/*=1*/);
void screen_cursor_forward(Screen *self, unsigned int count/*=1*/);
void screen_cursor_down1(Screen *self, unsigned int count/*=1*/);
void screen_cursor_up1(Screen *self, unsigned int count/*=1*/);
void screen_cursor_to_line(Screen *screen, unsigned int line);
void screen_insert_lines(Screen *self, unsigned int count/*=1*/);
void screen_delete_lines(Screen *self, unsigned int count/*=1*/);
void screen_delete_characters(Screen *self, unsigned int count);
void screen_erase_characters(Screen *self, unsigned int count);
void screen_set_margins(Screen *self, unsigned int top, unsigned int bottom);
void screen_change_charset(Screen *, uint32_t to);
void screen_designate_charset(Screen *, uint32_t which, uint32_t as);
void screen_use_latin1(Screen *, bool);
void set_title(Screen *self, PyObject*);
void set_icon(Screen *self, PyObject*);
void set_dynamic_color(Screen *self, unsigned int code, PyObject*);
void set_color_table_color(Screen *self, unsigned int code, PyObject*);
uint32_t* translation_table(uint32_t which);
uint32_t *latin1_charset;
void screen_request_capabilities(Screen *, PyObject *);
void report_device_attributes(Screen *self, unsigned int UNUSED mode, char start_modifier);
void select_graphic_rendition(Screen *self, unsigned int *params, unsigned int count);
void report_device_status(Screen *self, unsigned int which, bool UNUSED);
void report_mode_status(Screen *self, unsigned int which, bool);
void screen_apply_selection(Screen *self, void *address, size_t size);
bool screen_is_selection_dirty(Screen *self);
bool screen_invert_colors(Screen *self);
void screen_update_cell_data(Screen *self, void *address, size_t sz);
bool screen_is_cursor_visible(Screen *self);
bool screen_selection_range_for_line(Screen *self, index_type y, index_type *start, index_type *end);
bool screen_selection_range_for_word(Screen *self, index_type x, index_type y, index_type *start, index_type *end);
void screen_start_selection(Screen *self, index_type x, index_type y);
void screen_update_selection(Screen *self, index_type x, index_type y, bool ended);
bool screen_history_scroll(Screen *self, int amt, bool upwards);
Line* screen_visual_line(Screen *self, index_type y);
unsigned long screen_current_char_width(Screen *self);
void screen_url_range(Screen *self, uint32_t *);
void screen_mark_url(Screen *self, index_type start_x, index_type start_y, index_type end_x, index_type end_y);
void screen_handle_graphics_command(Screen *self, const GraphicsCommand *cmd, const uint8_t *payload);
#define DECLARE_CH_SCREEN_HANDLER(name) void screen_##name(Screen *screen);
DECLARE_CH_SCREEN_HANDLER(bell)
DECLARE_CH_SCREEN_HANDLER(backspace)
DECLARE_CH_SCREEN_HANDLER(tab)
DECLARE_CH_SCREEN_HANDLER(linefeed)
DECLARE_CH_SCREEN_HANDLER(carriage_return)

669
kitty/shaders.c Normal file
View File

@@ -0,0 +1,669 @@
/*
* shaders.c
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "gl.h"
#include <sys/sysctl.h>
enum { CELL_PROGRAM, CELL_BACKGROUND_PROGRAM, CELL_SPECIAL_PROGRAM, CELL_FOREGROUND_PROGRAM, CURSOR_PROGRAM, BORDERS_PROGRAM, GRAPHICS_PROGRAM, NUM_PROGRAMS };
enum { SPRITE_MAP_UNIT, GRAPHICS_UNIT };
// Sprites {{{
typedef struct {
int xnum, ynum, x, y, z, last_num_of_layers, last_ynum;
unsigned int cell_width, cell_height;
GLuint texture_id;
GLenum texture_unit;
GLint max_texture_size, max_array_texture_layers;
PyObject *render_cell;
} SpriteMap;
static SpriteMap sprite_map = { .xnum = 1, .ynum = 1, .last_num_of_layers = 1, .last_ynum = -1, .texture_unit = GL_TEXTURE0 };
#ifdef __APPLE__
#define glCopyImageSubData(...)
#define GLEW_ARB_copy_image false
#endif
static bool copy_image_warned = false;
static void
copy_image_sub_data(GLuint src_texture_id, GLuint dest_texture_id, unsigned int width, unsigned int height, unsigned int num_levels) {
if (!GLEW_ARB_copy_image) {
// ARB_copy_image not available, do a slow roundtrip copy
if (!copy_image_warned) {
copy_image_warned = true;
fprintf(stderr, "WARNING: Your system's OpenGL implementation does not have glCopyImageSubData, falling back to a slower implementation.\n");
}
uint8_t *src = malloc(5 * width * height * num_levels);
if (src == NULL) { fatal("Out of memory."); }
uint8_t *dest = src + (4 * width * height * num_levels);
glBindTexture(GL_TEXTURE_2D_ARRAY, src_texture_id); check_gl();
glGetTexImage(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, GL_UNSIGNED_BYTE, src); check_gl();
glBindTexture(GL_TEXTURE_2D_ARRAY, dest_texture_id); check_gl();
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); check_gl();
for(size_t i = 0; i < width * height * num_levels; i++) dest[i] = src[4*i];
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, width, height, num_levels, GL_RED, GL_UNSIGNED_BYTE, dest); check_gl();
free(src);
} else {
glCopyImageSubData(src_texture_id, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, dest_texture_id, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, width, height, num_levels); check_gl();
}
}
static void
realloc_sprite_texture() {
GLuint tex;
glGenTextures(1, &tex); check_gl();
glBindTexture(GL_TEXTURE_2D_ARRAY, tex); check_gl();
// We use GL_NEAREST otherwise glyphs that touch the edge of the cell
// often show a border between cells
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); check_gl();
unsigned int xnum, ynum, z, znum, width, height, src_ynum;
sprite_map_current_layout(&xnum, &ynum, &z);
znum = z + 1;
width = xnum * sprite_map.cell_width; height = ynum * sprite_map.cell_height;
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_R8, width, height, znum); check_gl();
if (sprite_map.texture_id) {
// need to re-alloc
src_ynum = MAX(1, sprite_map.last_ynum);
copy_image_sub_data(sprite_map.texture_id, tex, width, src_ynum * sprite_map.cell_height, sprite_map.last_num_of_layers);
glDeleteTextures(1, &sprite_map.texture_id); check_gl();
}
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
sprite_map.last_num_of_layers = znum;
sprite_map.last_ynum = ynum;
sprite_map.texture_id = tex;
}
static inline PyObject*
render_cell(PyObject *text, bool bold, bool italic, unsigned int underline, bool strikethrough, bool is_second) {
#define B(x) (x ? Py_True : Py_False)
PyObject *ret = PyObject_CallFunction(sprite_map.render_cell, "OOOIOO", text, B(bold), B(italic), underline, B(strikethrough), B(is_second));
if (ret == NULL) { PyErr_Print(); fatal("Rendering of a cell failed, aborting"); }
return ret;
#undef B
}
static inline void
ensure_sprite_map() {
static GLuint bound_texture_id = 0;
if (!sprite_map.texture_id) realloc_sprite_texture();
if (bound_texture_id != sprite_map.texture_id) {
glActiveTexture(GL_TEXTURE0 + SPRITE_MAP_UNIT); check_gl();
glBindTexture(GL_TEXTURE_2D_ARRAY, sprite_map.texture_id); check_gl();
bound_texture_id = sprite_map.texture_id;
}
}
static void
sprite_send_to_gpu(unsigned int x, unsigned int y, unsigned int z, PyObject *buf) {
unsigned int xnum, ynum, znum;
sprite_map_current_layout(&xnum, &ynum, &znum);
if ((int)znum >= sprite_map.last_num_of_layers || (znum == 0 && (int)ynum > sprite_map.last_ynum)) realloc_sprite_texture();
glBindTexture(GL_TEXTURE_2D_ARRAY, sprite_map.texture_id); check_gl();
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); check_gl();
x *= sprite_map.cell_width; y *= sprite_map.cell_height;
PyObject *ret = PyObject_CallObject(buf, NULL);
if (ret == NULL) { PyErr_Print(); fatal("Failed to get address of rendered cell buffer"); }
void *address = PyLong_AsVoidPtr(ret);
Py_DECREF(ret);
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, x, y, z, sprite_map.cell_width, sprite_map.cell_height, 1, GL_RED, GL_UNSIGNED_BYTE, address); check_gl();
Py_DECREF(buf);
}
static void
render_and_send_dirty_sprites(PyObject *text, bool bold, bool italic, bool is_second, sprite_index x, sprite_index y, sprite_index z) {
if (text == NULL) { fatal("The text for a sprite was NULL, probably out of memory."); }
PyObject *buf = render_cell(text, bold, italic, false, false, is_second);
sprite_send_to_gpu(x, y, z, buf);
}
static inline sprite_index
send_prerendered(unsigned int underline, bool strikethrough) {
sprite_index x, y, z;
PyObject *blank = PyUnicode_FromString(" ");
if (blank == NULL) { fatal("Out of memory"); }
PyObject *buf = render_cell(blank, false, false, underline, strikethrough, false);
Py_CLEAR(blank);
if (sprite_map_increment(&x, &y, &z) != 0) { fatal("Failed to increment sprite map for prerendering"); }
sprite_send_to_gpu(x, y, z, buf);
return x;
}
static void
layout_sprite_map(unsigned int cell_width, unsigned int cell_height, PyObject *render_cell) {
sprite_map.cell_width = MAX(1, cell_width);
sprite_map.cell_height = MAX(1, cell_height);
global_state.cell_width = sprite_map.cell_width;
global_state.cell_height = sprite_map.cell_height;
if (sprite_map.max_texture_size == 0) {
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &(sprite_map.max_texture_size)); check_gl();
glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &(sprite_map.max_array_texture_layers)); check_gl();
sprite_map_set_limits(sprite_map.max_texture_size, sprite_map.max_array_texture_layers);
}
sprite_map_set_layout(sprite_map.cell_width, sprite_map.cell_height);
Py_CLEAR(sprite_map.render_cell);
sprite_map.render_cell = render_cell; Py_INCREF(sprite_map.render_cell);
if (sprite_map.texture_id) { glDeleteTextures(1, &(sprite_map.texture_id)); sprite_map.texture_id = 0; }
realloc_sprite_texture();
// Pre-render the basic cells to ensure they have known sprite numbers
send_prerendered(0, false);
send_prerendered(1, false);
send_prerendered(2, false);
if (send_prerendered(0, true) != 3) { fatal("Available OpenGL texture size is too small"); }
}
static void
destroy_sprite_map() {
sprite_map_free();
Py_CLEAR(sprite_map.render_cell);
if (sprite_map.texture_id) {
glDeleteTextures(1, &(sprite_map.texture_id));
check_gl();
sprite_map.texture_id = 0;
}
}
// }}}
// Cell {{{
typedef struct {
UniformBlock render_data;
ArrayInformation color_table;
} CellProgramLayout;
static CellProgramLayout cell_program_layouts[NUM_PROGRAMS];
static void
init_cell_program() {
for (int i = CELL_PROGRAM; i <= CELL_FOREGROUND_PROGRAM; i++) {
cell_program_layouts[i].render_data.index = block_index(i, "CellRenderData");
cell_program_layouts[i].render_data.size = block_size(i, cell_program_layouts[i].render_data.index);
cell_program_layouts[i].color_table.size = get_uniform_information(i, "color_table[0]", GL_UNIFORM_SIZE);
cell_program_layouts[i].color_table.offset = get_uniform_information(i, "color_table[0]", GL_UNIFORM_OFFSET);
cell_program_layouts[i].color_table.stride = get_uniform_information(i, "color_table[0]", GL_UNIFORM_ARRAY_STRIDE);
}
// Sanity check to ensure the attribute location binding worked
#define C(p, name, expected) { int aloc = attrib_location(p, #name); if (aloc != expected && aloc != -1) fatal("The attribute location for %s is %d != %d in program: %d", #name, aloc, expected, p); }
for (int p = CELL_PROGRAM; p <= CELL_FOREGROUND_PROGRAM; p++) {
C(p, colors, 0); C(p, sprite_coords, 1); C(p, is_selected, 2);
}
#undef C
}
#define CELL_BUFFERS enum { cell_data_buffer, selection_buffer, uniform_buffer };
static ssize_t
create_cell_vao() {
ssize_t vao_idx = create_vao();
#define A(name, size, dtype, offset, stride) \
add_attribute_to_vao(CELL_PROGRAM, vao_idx, #name, \
/*size=*/size, /*dtype=*/dtype, /*stride=*/stride, /*offset=*/offset, /*divisor=*/1);
#define A1(name, size, dtype, offset) A(name, size, dtype, (void*)(offsetof(Cell, offset)), sizeof(Cell))
add_buffer_to_vao(vao_idx, GL_ARRAY_BUFFER);
A1(sprite_coords, 4, GL_UNSIGNED_SHORT, sprite_x);
A1(colors, 3, GL_UNSIGNED_INT, fg);
add_buffer_to_vao(vao_idx, GL_ARRAY_BUFFER);
A(is_selected, 1, GL_FLOAT, NULL, 0);
size_t bufnum = add_buffer_to_vao(vao_idx, GL_UNIFORM_BUFFER);
alloc_vao_buffer(vao_idx, cell_program_layouts[CELL_PROGRAM].render_data.size, bufnum, GL_STREAM_DRAW);
return vao_idx;
#undef A
#undef A1
}
static ssize_t
create_graphics_vao() {
ssize_t vao_idx = create_vao();
add_buffer_to_vao(vao_idx, GL_ARRAY_BUFFER);
add_attribute_to_vao(GRAPHICS_PROGRAM, vao_idx, "src", 4, GL_FLOAT, 0, NULL, 0);
return vao_idx;
}
static inline void
cell_update_uniform_block(ssize_t vao_idx, Screen *screen, int uniform_buffer, GLfloat xstart, GLfloat ystart, GLfloat dx, GLfloat dy, CursorRenderInfo *cursor) {
struct CellRenderData {
GLfloat xstart, ystart, dx, dy, sprite_dx, sprite_dy;
GLuint default_fg, default_bg, highlight_fg, highlight_bg, cursor_color, url_color;
GLint color1, color2;
GLuint xnum, ynum, cursor_x, cursor_y, cursor_w, url_xl, url_yl, url_xr, url_yr;
};
static struct CellRenderData *rd;
bool inverted = screen_invert_colors(screen);
// Send the uniform data
rd = (struct CellRenderData*)map_vao_buffer(vao_idx, uniform_buffer, GL_WRITE_ONLY);
if (UNLIKELY(screen->color_profile->dirty)) {
copy_color_table_to_buffer(screen->color_profile, (GLuint*)rd, cell_program_layouts[CELL_PROGRAM].color_table.offset / sizeof(GLuint), cell_program_layouts[CELL_PROGRAM].color_table.stride / sizeof(GLuint));
}
// Cursor position
if (cursor->is_visible && cursor->shape == CURSOR_BLOCK) {
rd->cursor_x = screen->cursor->x, rd->cursor_y = screen->cursor->y;
} else {
rd->cursor_x = screen->columns, rd->cursor_y = screen->lines;
}
rd->cursor_w = rd->cursor_x + MAX(1, screen_current_char_width(screen)) - 1;
rd->xnum = screen->columns; rd->ynum = screen->lines;
screen_url_range(screen, &rd->url_xl);
rd->xstart = xstart; rd->ystart = ystart; rd->dx = dx; rd->dy = dy;
unsigned int x, y, z;
sprite_map_current_layout(&x, &y, &z);
rd->sprite_dx = 1.0f / (float)x; rd->sprite_dy = 1.0f / (float)y;
rd->color1 = inverted & 1; rd->color2 = 1 - (inverted & 1);
#define COLOR(name) colorprofile_to_color(screen->color_profile, screen->color_profile->overridden.name, screen->color_profile->configured.name)
rd->default_fg = COLOR(default_fg); rd->default_bg = COLOR(default_bg); rd->highlight_fg = COLOR(highlight_fg); rd->highlight_bg = COLOR(highlight_bg);
#undef COLOR
rd->cursor_color = cursor->color; rd->url_color = OPT(url_color);
unmap_vao_buffer(vao_idx, uniform_buffer); rd = NULL;
}
static inline void
cell_prepare_to_render(ssize_t vao_idx, ssize_t gvao_idx, Screen *screen, GLfloat xstart, GLfloat ystart, GLfloat dx, GLfloat dy, CursorRenderInfo *cursor) {
size_t sz;
CELL_BUFFERS;
void *address;
if (screen->scroll_changed || screen->is_dirty) {
sz = sizeof(Cell) * screen->lines * screen->columns;
address = alloc_and_map_vao_buffer(vao_idx, sz, cell_data_buffer, GL_STREAM_DRAW, GL_WRITE_ONLY);
screen_update_cell_data(screen, address, sz);
unmap_vao_buffer(vao_idx, cell_data_buffer); address = NULL;
}
if (screen_is_selection_dirty(screen)) {
sz = sizeof(GLfloat) * screen->lines * screen->columns;
address = alloc_and_map_vao_buffer(vao_idx, sz, selection_buffer, GL_STREAM_DRAW, GL_WRITE_ONLY);
screen_apply_selection(screen, address, sz);
unmap_vao_buffer(vao_idx, selection_buffer); address = NULL;
}
if (gvao_idx && grman_update_layers(screen->grman, screen->scrolled_by, xstart, ystart, dx, dy, screen->columns, screen->lines)) {
sz = sizeof(GLfloat) * 16 * screen->grman->count;
GLfloat *a = alloc_and_map_vao_buffer(gvao_idx, sz, 0, GL_STREAM_DRAW, GL_WRITE_ONLY);
for (size_t i = 0; i < screen->grman->count; i++, a += 16) memcpy(a, screen->grman->render_data[i].vertices, sizeof(screen->grman->render_data[0].vertices));
unmap_vao_buffer(gvao_idx, 0); a = NULL;
}
cell_update_uniform_block(vao_idx, screen, uniform_buffer, xstart, ystart, dx, dy, cursor);
ensure_sprite_map();
render_dirty_sprites(render_and_send_dirty_sprites);
bind_vao_uniform_buffer(vao_idx, uniform_buffer, cell_program_layouts[CELL_PROGRAM].render_data.index);
bind_vertex_array(vao_idx);
}
static void
draw_graphics(ssize_t vao_idx, ssize_t gvao_idx, ImageRenderData *data, GLuint start, GLuint count) {
bind_vertex_array(gvao_idx);
bind_program(GRAPHICS_PROGRAM);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
static bool graphics_constants_set = false;
if (!graphics_constants_set) {
glUniform1i(glGetUniformLocation(program_id(GRAPHICS_PROGRAM), "image"), GRAPHICS_UNIT); check_gl();
graphics_constants_set = true;
}
glActiveTexture(GL_TEXTURE0 + GRAPHICS_UNIT); check_gl();
GLuint base = 4 * start;
glEnable(GL_SCISSOR_TEST);
for (GLuint i=0; i < count;) {
ImageRenderData *rd = data + start + i;
glBindTexture(GL_TEXTURE_2D, rd->texture_id); check_gl();
// You could reduce the number of draw calls by using
// glDrawArraysInstancedBaseInstance but Apple chose to abandon OpenGL
// before implementing it.
for (GLuint k=0; k < rd->group_count; k++, base += 4, i++) glDrawArrays(GL_TRIANGLE_FAN, base, 4);
}
glDisable(GL_SCISSOR_TEST);
bind_vertex_array(vao_idx);
}
static void
draw_all_cells(ssize_t vao_idx, ssize_t gvao_idx, Screen *screen) {
bind_program(CELL_PROGRAM);
static bool cell_constants_set = false;
if (!cell_constants_set) {
glUniform1i(glGetUniformLocation(program_id(CELL_PROGRAM), "sprites"), SPRITE_MAP_UNIT); check_gl();
cell_constants_set = true;
}
glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, screen->lines * screen->columns); check_gl();
if (screen->grman->count) draw_graphics(vao_idx, gvao_idx, screen->grman->render_data, 0, screen->grman->count);
}
static void
draw_cells_interleaved(ssize_t vao_idx, ssize_t gvao_idx, Screen *screen) {
bind_program(CELL_BACKGROUND_PROGRAM);
glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, screen->lines * screen->columns); check_gl();
if (screen->grman->num_of_negative_refs) draw_graphics(vao_idx, gvao_idx, screen->grman->render_data, 0, screen->grman->num_of_negative_refs);
bind_program(CELL_SPECIAL_PROGRAM);
glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, screen->lines * screen->columns); check_gl();
bind_program(CELL_FOREGROUND_PROGRAM);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, screen->lines * screen->columns); check_gl();
if (screen->grman->num_of_positive_refs) draw_graphics(vao_idx, gvao_idx, screen->grman->render_data, screen->grman->num_of_negative_refs, screen->grman->num_of_positive_refs);
}
static void
draw_cells_impl(ssize_t vao_idx, ssize_t gvao_idx, GLfloat xstart, GLfloat ystart, GLfloat dx, GLfloat dy, Screen *screen, CursorRenderInfo *cursor) {
GLfloat h = (GLfloat)screen->lines * dy;
#define SCALE(w, x) ((GLfloat)(global_state.viewport_##w) * (GLfloat)(x))
glScissor(
(GLint)(SCALE(width, (xstart + 1.0f) / 2.0f)),
(GLint)(SCALE(height, ((ystart - h) + 1.0f) / 2.0f)),
(GLsizei)(ceilf(SCALE(width, (float)screen->columns * dx / 2.0f))),
(GLsizei)(ceilf(SCALE(height, h / 2.0f)))
);
#undef SCALE
cell_prepare_to_render(vao_idx, gvao_idx, screen, xstart, ystart, dx, dy, cursor);
if (screen->grman->num_of_negative_refs) draw_cells_interleaved(vao_idx, gvao_idx, screen);
else draw_all_cells(vao_idx, gvao_idx, screen);
}
// }}}
// Cursor {{{
enum CursorUniforms { CURSOR_color, CURSOR_pos, NUM_CURSOR_UNIFORMS };
static GLint cursor_uniform_locations[NUM_CURSOR_UNIFORMS] = {0};
static ssize_t cursor_vertex_array;
static void
init_cursor_program() {
Program *p = programs + CURSOR_PROGRAM;
int left = NUM_CURSOR_UNIFORMS;
cursor_vertex_array = create_vao();
for (int i = 0; i < p->num_of_uniforms; i++, left--) {
#define SET_LOC(which) if (strcmp(p->uniforms[i].name, #which) == 0) cursor_uniform_locations[CURSOR_##which] = p->uniforms[i].location
SET_LOC(color);
else SET_LOC(pos);
else { fatal("Unknown uniform in cursor program"); }
}
if (left) { fatal("Left over uniforms in cursor program"); }
#undef SET_LOC
}
static void
draw_cursor_impl(CursorRenderInfo *cursor) {
bind_program(CURSOR_PROGRAM); bind_vertex_array(cursor_vertex_array); check_gl();
glUniform3f(cursor_uniform_locations[CURSOR_color], ((cursor->color >> 16) & 0xff) / 255.0, ((cursor->color >> 8) & 0xff) / 255.0, (cursor->color & 0xff) / 255.0); check_gl();
glUniform4f(cursor_uniform_locations[CURSOR_pos], cursor->left, cursor->top, cursor->right, cursor->bottom); check_gl();
glDrawArrays(global_state.application_focused ? GL_TRIANGLE_FAN : GL_LINE_LOOP, 0, 4); check_gl();
unbind_vertex_array(); unbind_program();
}
// }}}
// Borders {{{
enum BorderUniforms { BORDER_viewport, NUM_BORDER_UNIFORMS };
static GLint border_uniform_locations[NUM_BORDER_UNIFORMS] = {0};
static ssize_t border_vertex_array;
static GLsizei num_border_rects = 0;
static GLuint rect_buf[5 * 1024];
static GLuint *rect_pos = NULL;
static void
init_borders_program() {
Program *p = programs + BORDERS_PROGRAM;
int left = NUM_BORDER_UNIFORMS;
border_vertex_array = create_vao();
for (int i = 0; i < p->num_of_uniforms; i++, left--) {
#define SET_LOC(which) if (strcmp(p->uniforms[i].name, #which) == 0) border_uniform_locations[BORDER_##which] = p->uniforms[i].location
SET_LOC(viewport);
else { fatal("Unknown uniform in borders program"); return; }
}
if (left) { fatal("Left over uniforms in borders program"); return; }
#undef SET_LOC
add_buffer_to_vao(border_vertex_array, GL_ARRAY_BUFFER);
add_attribute_to_vao(BORDERS_PROGRAM, border_vertex_array, "rect",
/*size=*/4, /*dtype=*/GL_UNSIGNED_INT, /*stride=*/sizeof(GLuint)*5, /*offset=*/0, /*divisor=*/1);
add_attribute_to_vao(BORDERS_PROGRAM, border_vertex_array, "rect_color",
/*size=*/1, /*dtype=*/GL_UNSIGNED_INT, /*stride=*/sizeof(GLuint)*5, /*offset=*/(void*)(sizeof(GLuint)*4), /*divisor=*/1);
}
static void
draw_borders_impl() {
if (num_border_rects) {
bind_program(BORDERS_PROGRAM);
bind_vertex_array(border_vertex_array);
glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, num_border_rects);
check_gl();
unbind_vertex_array();
unbind_program();
}
}
static void
add_borders_rect(GLuint left, GLuint top, GLuint right, GLuint bottom, GLuint color) {
if (!left && !top && !right && !bottom) { num_border_rects = 0; rect_pos = rect_buf; return; }
num_border_rects++;
*(rect_pos++) = left;
*(rect_pos++) = top;
*(rect_pos++) = right;
*(rect_pos++) = bottom;
*(rect_pos++) = color;
}
static void
send_borders_rects(GLuint vw, GLuint vh) {
if (num_border_rects) {
size_t sz = sizeof(GLuint) * 5 * num_border_rects;
void *borders_buf_address = alloc_and_map_vao_buffer(border_vertex_array, sz, 0, GL_STATIC_DRAW, GL_WRITE_ONLY);
if (borders_buf_address) memcpy(borders_buf_address, rect_buf, sz);
unmap_vao_buffer(border_vertex_array, 0);
}
bind_program(BORDERS_PROGRAM);
glUniform2ui(border_uniform_locations[BORDER_viewport], vw, vh);
check_gl();
unbind_program();
}
// }}}
// Python API {{{
static PyObject*
compile_program(PyObject UNUSED *self, PyObject *args) {
const char *vertex_shader, *fragment_shader;
int which;
GLuint vertex_shader_id = 0, fragment_shader_id = 0;
if (!PyArg_ParseTuple(args, "iss", &which, &vertex_shader, &fragment_shader)) return NULL;
if (which < 0 || which >= NUM_PROGRAMS) { PyErr_Format(PyExc_ValueError, "Unknown program: %d", which); return NULL; }
if (programs[which].id != 0) { PyErr_SetString(PyExc_ValueError, "program already compiled"); return NULL; }
programs[which].id = glCreateProgram();
check_gl();
vertex_shader_id = compile_shader(GL_VERTEX_SHADER, vertex_shader); check_gl();
fragment_shader_id = compile_shader(GL_FRAGMENT_SHADER, fragment_shader); check_gl();
glAttachShader(programs[which].id, vertex_shader_id); check_gl();
glAttachShader(programs[which].id, fragment_shader_id); check_gl();
glLinkProgram(programs[which].id); check_gl();
GLint ret = GL_FALSE;
glGetProgramiv(programs[which].id, GL_LINK_STATUS, &ret);
if (ret != GL_TRUE) {
GLsizei len;
glGetProgramInfoLog(programs[which].id, sizeof(glbuf), &len, glbuf);
fprintf(stderr, "Failed to compile GLSL shader!\n%s", glbuf);
PyErr_SetString(PyExc_ValueError, "Failed to compile shader");
goto end;
}
init_uniforms(which);
end:
if (vertex_shader_id != 0) glDeleteShader(vertex_shader_id);
if (fragment_shader_id != 0) glDeleteShader(fragment_shader_id);
check_gl();
if (PyErr_Occurred()) { glDeleteProgram(programs[which].id); programs[which].id = 0; return NULL;}
return Py_BuildValue("I", programs[which].id);
Py_RETURN_NONE;
}
#define PYWRAP0(name) static PyObject* py##name(PyObject UNUSED *self)
#define PYWRAP1(name) static PyObject* py##name(PyObject UNUSED *self, PyObject *args)
#define PYWRAP2(name) static PyObject* py##name(PyObject UNUSED *self, PyObject *args, PyObject *kw)
#define PA(fmt, ...) if(!PyArg_ParseTuple(args, fmt, __VA_ARGS__)) return NULL;
#define ONE_INT(name) PYWRAP1(name) { name(PyLong_AsSsize_t(args)); Py_RETURN_NONE; }
#define TWO_INT(name) PYWRAP1(name) { int a, b; PA("ii", &a, &b); name(a, b); Py_RETURN_NONE; }
#define NO_ARG(name) PYWRAP0(name) { name(); Py_RETURN_NONE; }
#define NO_ARG_INT(name) PYWRAP0(name) { return PyLong_FromSsize_t(name()); }
ONE_INT(bind_program)
NO_ARG(unbind_program)
PYWRAP0(create_vao) {
int ans = create_vao();
if (ans < 0) return NULL;
return Py_BuildValue("i", ans);
}
ONE_INT(remove_vao)
ONE_INT(bind_vertex_array)
NO_ARG(unbind_vertex_array)
TWO_INT(unmap_vao_buffer)
NO_ARG(init_cursor_program)
NO_ARG(init_borders_program)
PYWRAP1(add_borders_rect) { unsigned int a, b, c, d, e; PA("IIIII", &a, &b, &c, &d, &e); add_borders_rect(a, b, c, d, e); Py_RETURN_NONE; }
TWO_INT(send_borders_rects)
NO_ARG(init_cell_program)
NO_ARG_INT(create_cell_vao)
NO_ARG_INT(create_graphics_vao)
NO_ARG(destroy_sprite_map)
PYWRAP1(layout_sprite_map) {
unsigned int cell_width, cell_height;
PyObject *render_cell;
PA("IIO", &cell_width, &cell_height, &render_cell);
layout_sprite_map(cell_width, cell_height, render_cell);
Py_RETURN_NONE;
}
PYWRAP1(clear_buffers) {
PyObject *swap_buffers;
unsigned int bg;
PA("OI", &swap_buffers, &bg);
#define C(shift) ((float)((bg >> shift) & 0xff)) / 255.0
glClearColor(C(16), C(8), C(0), 1);
#undef C
glClear(GL_COLOR_BUFFER_BIT);
PyObject *ret = PyObject_CallFunctionObjArgs(swap_buffers, NULL);
if (ret == NULL) return NULL;
Py_DECREF(ret);
glClear(GL_COLOR_BUFFER_BIT);
Py_RETURN_NONE;
}
PYWRAP0(check_for_extensions) {
GLint n = 0, i, left = 2;
glGetIntegerv(GL_NUM_EXTENSIONS, &n);
bool texture_storage = false;
#define CHECK(name) if (!name) { \
if (strstr((const char*)ext, "GL_ARB_" #name) == (const char *)ext) { left--; name = true; } \
}
for (i = 0; i < n; i++) {
const GLubyte *ext = glGetStringi(GL_EXTENSIONS, i);
CHECK(texture_storage);
if (left < 1) break;
}
#undef CHECK
if (left > 0) {
#define CHECK(name) if (!name) { PyErr_Format(PyExc_RuntimeError, "The OpenGL driver on this system is missing the required extension: GL_ARB_%s", #name); return NULL; }
CHECK(texture_storage);
#undef CHECK
}
Py_RETURN_NONE;
}
#define M(name, arg_type) {#name, (PyCFunction)name, arg_type, NULL}
#define MW(name, arg_type) {#name, (PyCFunction)py##name, arg_type, NULL}
static PyMethodDef module_methods[] = {
{"glewInit", (PyCFunction)glew_init, METH_NOARGS, NULL},
M(compile_program, METH_VARARGS),
MW(check_for_extensions, METH_NOARGS),
MW(create_vao, METH_NOARGS),
MW(remove_vao, METH_O),
MW(bind_vertex_array, METH_O),
MW(unbind_vertex_array, METH_NOARGS),
MW(unmap_vao_buffer, METH_VARARGS),
MW(bind_program, METH_O),
MW(unbind_program, METH_NOARGS),
MW(init_cursor_program, METH_NOARGS),
MW(init_borders_program, METH_NOARGS),
MW(add_borders_rect, METH_VARARGS),
MW(send_borders_rects, METH_VARARGS),
MW(init_cell_program, METH_NOARGS),
MW(create_cell_vao, METH_NOARGS),
MW(create_graphics_vao, METH_NOARGS),
MW(layout_sprite_map, METH_VARARGS),
MW(destroy_sprite_map, METH_NOARGS),
MW(clear_buffers, METH_VARARGS),
{NULL, NULL, 0, NULL} /* Sentinel */
};
bool
#ifdef ENABLE_DEBUG_GL
init_shaders_debug(PyObject *module) {
#else
init_shaders(PyObject *module) {
#endif
#define C(x) if (PyModule_AddIntConstant(module, #x, x) != 0) { PyErr_NoMemory(); return false; }
C(CELL_PROGRAM); C(CELL_BACKGROUND_PROGRAM); C(CELL_SPECIAL_PROGRAM); C(CELL_FOREGROUND_PROGRAM); C(CURSOR_PROGRAM); C(BORDERS_PROGRAM); C(GRAPHICS_PROGRAM);
C(GLSL_VERSION);
C(GL_VERSION);
C(GL_VENDOR);
C(GL_SHADING_LANGUAGE_VERSION);
C(GL_RENDERER);
C(GL_TRIANGLE_FAN); C(GL_TRIANGLE_STRIP); C(GL_TRIANGLES); C(GL_LINE_LOOP);
C(GL_COLOR_BUFFER_BIT);
C(GL_VERTEX_SHADER);
C(GL_FRAGMENT_SHADER);
C(GL_TRUE);
C(GL_FALSE);
C(GL_COMPILE_STATUS);
C(GL_LINK_STATUS);
C(GL_TEXTURE0); C(GL_TEXTURE1); C(GL_TEXTURE2); C(GL_TEXTURE3); C(GL_TEXTURE4); C(GL_TEXTURE5); C(GL_TEXTURE6); C(GL_TEXTURE7); C(GL_TEXTURE8);
C(GL_MAX_ARRAY_TEXTURE_LAYERS); C(GL_TEXTURE_BINDING_BUFFER); C(GL_MAX_TEXTURE_BUFFER_SIZE);
C(GL_MAX_TEXTURE_SIZE);
C(GL_TEXTURE_2D_ARRAY);
C(GL_LINEAR); C(GL_CLAMP_TO_EDGE); C(GL_NEAREST);
C(GL_TEXTURE_MIN_FILTER); C(GL_TEXTURE_MAG_FILTER);
C(GL_TEXTURE_WRAP_S); C(GL_TEXTURE_WRAP_T);
C(GL_UNPACK_ALIGNMENT);
C(GL_R8); C(GL_RED); C(GL_UNSIGNED_BYTE); C(GL_UNSIGNED_SHORT); C(GL_R32UI); C(GL_RGB32UI); C(GL_RGBA);
C(GL_TEXTURE_BUFFER); C(GL_STATIC_DRAW); C(GL_STREAM_DRAW); C(GL_DYNAMIC_DRAW);
C(GL_SRC_ALPHA); C(GL_ONE_MINUS_SRC_ALPHA);
C(GL_WRITE_ONLY); C(GL_READ_ONLY); C(GL_READ_WRITE);
C(GL_BLEND); C(GL_FLOAT); C(GL_UNSIGNED_INT); C(GL_ARRAY_BUFFER); C(GL_UNIFORM_BUFFER);
#undef C
PyModule_AddObject(module, "GL_VERSION_REQUIRED", Py_BuildValue("II", REQUIRED_VERSION_MAJOR, REQUIRED_VERSION_MINOR));
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
update_viewport_size = &update_viewport_size_impl;
draw_borders = &draw_borders_impl;
draw_cells = &draw_cells_impl;
draw_cursor = &draw_cursor_impl;
free_texture = &free_texture_impl;
send_image_to_gpu = &send_image_to_gpu_impl;
return true;
}
// }}}

View File

@@ -1,267 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import sys
from ctypes import addressof, sizeof
from functools import lru_cache
from threading import Lock
from .fonts.render import render_cell
from .fast_data_types import (
glCreateProgram, glAttachShader, GL_FRAGMENT_SHADER, GL_VERTEX_SHADER,
glLinkProgram, GL_TRUE, GL_LINK_STATUS, glGetProgramiv,
glGetProgramInfoLog, glDeleteShader, glDeleteProgram, glGenVertexArrays,
glCreateShader, glShaderSource, glCompileShader, glGetShaderiv,
GL_COMPILE_STATUS, glGetShaderInfoLog, glGetUniformLocation,
glGetAttribLocation, glUseProgram, glBindVertexArray, GL_TEXTURE0,
GL_TEXTURE1, glGetIntegerv, GL_MAX_ARRAY_TEXTURE_LAYERS, glNamedBufferData,
GL_MAX_TEXTURE_SIZE, glDeleteTexture, GL_TEXTURE_2D_ARRAY, glGenTextures,
glBindTexture, glTexParameteri, GL_CLAMP_TO_EDGE, glDeleteBuffer,
GL_TEXTURE_MAG_FILTER, GL_TEXTURE_MIN_FILTER, GL_TEXTURE_WRAP_S,
GL_NEAREST, GL_TEXTURE_WRAP_T, glGenBuffers, GL_R8, GL_RED,
GL_UNPACK_ALIGNMENT, GL_UNSIGNED_BYTE, GL_STATIC_DRAW, GL_STREAM_DRAW,
GL_TEXTURE_BUFFER, GL_RGB32UI, GL_FLOAT, GL_ARRAY_BUFFER, glBindBuffer,
glPixelStorei, glTexBuffer, glActiveTexture, glTexStorage3D,
glCopyImageSubData, glTexSubImage3D, ITALIC, BOLD, SpriteMap,
glEnableVertexAttribArray, glVertexAttribPointer, copy_image_sub_data
)
from .utils import safe_print
GL_VERSION = (3, 3)
VERSION = GL_VERSION[0] * 100 + GL_VERSION[1] * 10
ITALIC_MASK = 1 << ITALIC
BOLD_MASK = 1 << BOLD
class Sprites:
''' Maintain sprite sheets of all rendered characters on the GPU as a texture
array with each texture being a sprite sheet. '''
# TODO: Rewrite this class using the ARB_shader_image_load_store and ARB_shader_storage_buffer_object
# extensions one they become available.
def __init__(self):
self.xnum = self.ynum = 1
self.sampler_num = 0
self.buffer_sampler_num = 1
self.first_cell_cache = {}
self.second_cell_cache = {}
self.x = self.y = self.z = 0
self.texture_id = self.buffer_texture_id = None
self.last_num_of_layers = 1
self.last_ynum = -1
self.texture_unit = GL_TEXTURE0
self.backend = SpriteMap(glGetIntegerv(GL_MAX_TEXTURE_SIZE), glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS))
self.lock = Lock()
def do_layout(self, cell_width=1, cell_height=1):
self.cell_width, self.cell_height = cell_width, cell_height
self.backend.layout(cell_width or 1, cell_height or 1)
if self.texture_id is not None:
glDeleteTexture(self.texture_id)
self.texture_id = None
self.ensure_state()
self.pre_render()
def pre_render(self):
# Pre-render the basic cells to ensure they have known sprite numbers
def send(*a, **kw):
buf = render_cell(*a, **kw)[0]
x, y, z = self.backend.increment()
self.send_to_gpu(x, y, z, buf)
return x
send() # blank
send(underline=1)
send(underline=2)
if send(strikethrough=True) != 3:
raise RuntimeError('Available OpenGL texture size is too small')
@property
def layout(self):
return 1 / self.backend.xnum, 1 / self.backend.ynum
def render_cell(self, text, bold, italic, is_second):
first, second = render_cell(text, bold, italic)
ans = second if is_second else first
return ans or render_cell()[0]
def render_dirty_cells(self):
with self.lock:
self.backend.render_dirty_cells(self.render_cell, self.send_to_gpu)
def send_to_gpu(self, x, y, z, buf):
if self.backend.z >= self.last_num_of_layers:
self.realloc_texture()
else:
if self.backend.z == 0 and self.backend.ynum > self.last_ynum:
self.realloc_texture()
tgt = GL_TEXTURE_2D_ARRAY
glBindTexture(tgt, self.texture_id)
glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
x, y = x * self.cell_width, y * self.cell_height
glTexSubImage3D(tgt, 0, x, y, z, self.cell_width, self.cell_height, 1, GL_RED, GL_UNSIGNED_BYTE, addressof(buf))
glBindTexture(tgt, 0)
def realloc_texture(self):
tgt = GL_TEXTURE_2D_ARRAY
tex = glGenTextures(1)
glBindTexture(tgt, tex)
# We use GL_NEAREST otherwise glyphs that touch the edge of the cell
# often show a border between cells
glTexParameteri(tgt, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
glTexParameteri(tgt, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
glTexParameteri(tgt, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
glTexParameteri(tgt, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
znum = self.backend.z + 1
width, height = self.backend.xnum * self.cell_width, self.backend.ynum * self.cell_height
glTexStorage3D(tgt, 1, GL_R8, width, height, znum)
if self.texture_id is not None:
ynum = self.backend.ynum
if self.backend.z == 0:
ynum -= 1 # Only copy the previous rows
try:
glCopyImageSubData(self.texture_id, tgt, 0, 0, 0, 0, tex, tgt, 0, 0, 0, 0,
width, ynum * self.cell_height, self.last_num_of_layers)
except RuntimeError:
# OpenGL does not have ARB_copy_image
if not hasattr(self, 'realloc_warned'):
safe_print(
'WARNING: Your system\'s OpenGL implementation does not have glCopyImageSubData, falling back to a slower implementation',
file=sys.stderr)
self.realloc_warned = True
copy_image_sub_data(self.texture_id, tex, width, ynum * self.cell_height, self.last_num_of_layers)
glBindTexture(tgt, tex)
glDeleteTexture(self.texture_id)
self.last_num_of_layers = znum
self.last_ynum = self.backend.ynum
self.texture_id = tex
glBindTexture(tgt, 0)
def destroy(self):
if self.texture_id is not None:
glDeleteTexture(self.texture_id)
self.texture_id = None
if self.buffer_texture_id is not None:
glDeleteTexture(self.buffer_texture_id)
def ensure_state(self):
if self.texture_id is None:
self.realloc_texture()
self.buffer_texture_id = glGenTextures(1)
self.buffer_texture_unit = GL_TEXTURE1
def add_sprite_map(self):
return glGenBuffers(1)
def set_sprite_map(self, buf_id, data, usage=GL_STREAM_DRAW):
self.bind_sprite_map(buf_id)
glNamedBufferData(buf_id, sizeof(data), addressof(data), usage)
def bind_sprite_map(self, buf_id):
glBindBuffer(GL_TEXTURE_BUFFER, buf_id)
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGB32UI, buf_id)
def destroy_sprite_map(self, buf_id):
glDeleteBuffer(buf_id)
def __enter__(self):
self.ensure_state()
glActiveTexture(self.texture_unit)
glBindTexture(GL_TEXTURE_2D_ARRAY, self.texture_id)
glActiveTexture(self.buffer_texture_unit)
glBindTexture(GL_TEXTURE_BUFFER, self.buffer_texture_id)
def __exit__(self, *a):
glBindTexture(GL_TEXTURE_2D_ARRAY, 0)
glBindTexture(GL_TEXTURE_BUFFER, 0)
glBindBuffer(GL_TEXTURE_BUFFER, 0)
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGB32UI, 0)
class ShaderProgram:
""" Helper class for using GLSL shader programs """
def __init__(self, vertex, fragment):
"""
Create a shader program.
"""
self.program_id = glCreateProgram()
vs_id = self.add_shader(vertex, GL_VERTEX_SHADER)
glAttachShader(self.program_id, vs_id)
frag_id = self.add_shader(fragment, GL_FRAGMENT_SHADER)
glAttachShader(self.program_id, frag_id)
glLinkProgram(self.program_id)
if glGetProgramiv(self.program_id, GL_LINK_STATUS) != GL_TRUE:
info = glGetProgramInfoLog(self.program_id)
glDeleteProgram(self.program_id)
glDeleteShader(vs_id)
glDeleteShader(frag_id)
raise ValueError('Error linking shader program: \n%s' % info.decode('utf-8'))
glDeleteShader(vs_id)
glDeleteShader(frag_id)
self.vao_id = glGenVertexArrays(1)
self.buffers = {}
def add_vertex_array(self, name, size=3, dtype=GL_FLOAT, normalized=False, stride=0, offset=0):
glBindVertexArray(self.vao_id)
if name not in self.buffers:
self.buffers[name] = glGenBuffers(1)
glBindBuffer(GL_ARRAY_BUFFER, self.buffers[name])
aid = self.attribute_location(name)
glEnableVertexAttribArray(aid)
glVertexAttribPointer(aid, size, dtype, normalized, stride, offset)
glBindBuffer(GL_ARRAY_BUFFER, 0)
glBindVertexArray(0)
def send_vertex_data(self, name, data, usage=GL_STATIC_DRAW):
bufid = self.buffers[name]
glNamedBufferData(bufid, sizeof(data), addressof(data), usage)
def __hash__(self) -> int:
return self.program_id
def __eq__(self, other) -> bool:
return isinstance(other, ShaderProgram) and other.program_id == self.program_id
def __ne__(self, other) -> bool:
return not self.__eq__(other)
def add_shader(self, source: str, shader_type: int) -> int:
' Compile a shader and return its id, or raise an exception if compilation fails '
shader_id = glCreateShader(shader_type)
source = '#version {}\n{}'.format(VERSION, source)
try:
glShaderSource(shader_id, source)
glCompileShader(shader_id)
if glGetShaderiv(shader_id, GL_COMPILE_STATUS) != GL_TRUE:
info = glGetShaderInfoLog(shader_id)
raise ValueError('GLSL {} compilation failed: \n{}'.format(shader_type, info.decode('utf-8')))
return shader_id
except Exception:
glDeleteShader(shader_id)
raise
@lru_cache(maxsize=2**6)
def uniform_location(self, name: str) -> int:
' Return the id for the uniform variable `name` or -1 if not found. '
return glGetUniformLocation(self.program_id, name)
@lru_cache(maxsize=2**6)
def attribute_location(self, name: str) -> int:
' Return the id for the attribute variable `name` or -1 if not found. '
return glGetAttribLocation(self.program_id, name)
def __enter__(self):
glUseProgram(self.program_id)
glBindVertexArray(self.vao_id)
def __exit__(self, *args):
glUseProgram(0)
glBindVertexArray(0)

View File

@@ -6,107 +6,37 @@
*/
#include "data-types.h"
#include "lineops.h"
#include <structmember.h>
static PyObject*
new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
SpriteMap *self;
unsigned long mlen, msz;
if (!PyArg_ParseTuple(args, "kk", &msz, &mlen)) return NULL;
typedef struct SpritePosition SpritePosition;
struct SpritePosition {
SpritePosition *next;
sprite_index x, y, z;
char_type ch;
combining_type cc;
bool bold, italic;
bool is_second;
bool filled;
bool rendered;
};
self = (SpriteMap *)type->tp_alloc(type, 0);
if (self != NULL) {
self->max_array_len = mlen;
self->max_texture_size = msz;
self->dirty = true;
}
return (PyObject*) self;
}
typedef struct {
size_t max_array_len, max_texture_size, max_y;
unsigned int x, y, z, xnum, ynum;
SpritePosition cache[1024];
bool dirty;
} SpriteMap;
static void
dealloc(SpriteMap* self) {
SpritePosition *s, *t;
for (size_t i = 0; i < sizeof(self->cache)/sizeof(self->cache[0]); i++) {
s = &(self->cache[i]);
s = s->next;
while (s) {
t = s;
s = s->next;
PyMem_Free(t);
}
}
Py_TYPE(self)->tp_free((PyObject*)self);
}
static SpriteMap sprite_map = {
.max_array_len = 1000,
.max_texture_size = 1000,
.max_y = 100,
.dirty = true
};
static PyObject*
layout(SpriteMap *self, PyObject *args) {
#define layout_doc "layout(cell_width, cell_height) -> Invalidate the cache and prepare it for new cell size"
unsigned long cell_width, cell_height;
if (!PyArg_ParseTuple(args, "kk", &cell_width, &cell_height)) return NULL;
self->xnum = MAX(1, self->max_texture_size / cell_width);
self->max_y = MAX(1, self->max_texture_size / cell_height);
self->ynum = 1;
self->x = 0; self->y = 0; self->z = 0;
for (size_t i = 0; i < sizeof(self->cache)/sizeof(self->cache[0]); i++) {
SpritePosition *s = &(self->cache[i]);
do {
s->filled = false;
s->is_second = false;
s->rendered = false;
s->ch = 0; s->cc = 0;
s->x = 0; s->y = 0; s->z = 0;
s = s->next;
} while (s != NULL);
}
Py_RETURN_NONE;
}
static void
do_increment(SpriteMap *self, int *error) {
self->x++;
if (self->x >= self->xnum) {
self->x = 0; self->y++;
self->ynum = MIN(MAX(self->ynum, self->y + 1), self->max_y);
if (self->y >= self->max_y) {
self->y = 0; self->z++;
if (self->z >= self->max_array_len) *error = 2;
}
}
}
static SpritePosition*
sprite_position_for(SpriteMap *self, char_type ch, combining_type cc, bool is_second, int *error) {
char_type attrs = ch >> ATTRS_SHIFT, pos_char;
uint8_t bold = (attrs >> BOLD_SHIFT) & 1, italic = (attrs >> ITALIC_SHIFT) & 1;
size_t idx = (ch & 0xff) | (bold << 8) | (italic << 9);
attrs = bold << BOLD_SHIFT | italic << ITALIC_SHIFT;
pos_char = (ch & CHAR_MASK) | (attrs << ATTRS_SHIFT);
SpritePosition *s = &(self->cache[idx]);
while(true) {
if (s->filled) {
if (s->ch == pos_char && s->cc == cc && s->is_second == is_second) return s; // Cache hit
} else {
break;
}
if (!s->next) {
s->next = PyMem_Calloc(1, sizeof(SpritePosition));
if (s->next == NULL) { *error = 1; return NULL; }
}
s = s->next;
}
s->ch = pos_char;
s->cc = cc;
s->is_second = is_second;
s->filled = true;
s->rendered = false;
s->x = self->x; s->y = self->y; s->z = self->z;
do_increment(self, error);
self->dirty = true;
return s;
}
static void set_sprite_error(int error) {
static inline void
sprite_map_set_error(int error) {
switch(error) {
case 1:
PyErr_NoMemory(); break;
@@ -117,125 +47,182 @@ static void set_sprite_error(int error) {
}
}
static PyObject*
increment(SpriteMap *self) {
#define increment_doc "Increment the current position and return the old (x, y, z) values"
unsigned int x = self->x, y = self->y, z = self->z;
int error = 0;
do_increment(self, &error);
if (error) { set_sprite_error(error); return NULL; }
return Py_BuildValue("III", x, y, z);
void
sprite_map_set_limits(size_t max_texture_size, size_t max_array_len) {
sprite_map.max_texture_size = max_texture_size;
sprite_map.max_array_len = max_array_len;
}
static PyObject*
position_for(SpriteMap *self, PyObject *args) {
#define position_for_doc "position_for(ch, cc, is_second) -> x, y, z the sprite position for the specified text"
unsigned long ch = 0;
unsigned long long cc = 0;
int is_second = 0, error = 0;
if (!PyArg_ParseTuple(args, "|kKp", &ch, &cc, &is_second)) return NULL;
SpritePosition *pos = sprite_position_for(self, ch, cc, is_second, &error);
if (pos == NULL) {set_sprite_error(error); return NULL; }
return Py_BuildValue("III", pos->x, pos->y, pos->z);
}
bool
update_cell_range_data(ScreenModes *modes, SpriteMap *self, Line *line, unsigned int xstart, unsigned int xmax, ColorProfile *color_profile, const uint32_t default_bg, const uint32_t default_fg, unsigned int *data) {
SpritePosition *sp;
char_type previous_ch=0, ch;
uint8_t previous_width = 0;
int err = 0;
const bool screen_reversed = modes->mDECSCNM;
size_t base = line->ynum * line->xnum * DATA_CELL_SIZE;
for (size_t i = xstart, offset = base + xstart * DATA_CELL_SIZE; i <= xmax; i++, offset += DATA_CELL_SIZE) {
ch = line->chars[i];
if (previous_width == 2) sp = sprite_position_for(self, previous_ch, 0, true, &err);
else sp = sprite_position_for(self, ch, line->combining_chars[i], false, &err);
if (sp == NULL) { set_sprite_error(err); return false; }
char_type attrs = ch >> ATTRS_SHIFT;
unsigned int decoration = (attrs >> DECORATION_SHIFT) & DECORATION_MASK;
unsigned int strikethrough = ((attrs >> STRIKE_SHIFT) & 1) ? 3 : 0;
bool reverse = ((attrs >> REVERSE_SHIFT) & 1) ^ screen_reversed;
data[offset] = sp->x;
data[offset+1] = sp->y;
data[offset+2] = sp->z;
data[offset+(reverse ? 4 : 3)] = to_color(color_profile, line->fg_colors[i] & COL_MASK, default_fg);
data[offset+(reverse ? 3 : 4)] = to_color(color_profile, line->bg_colors[i] & COL_MASK, default_bg);
unsigned int decoration_fg = to_color(color_profile, line->decoration_fg[i] & COL_MASK, data[offset+3]);
data[offset+5] = (decoration_fg & COL_MASK) | (decoration << 24) | (strikethrough << 26);
previous_ch = ch; previous_width = (attrs) & WIDTH_MASK;
}
return true;
}
static PyObject*
render_dirty_cells(SpriteMap *self, PyObject *args) {
#define render_dirty_cells_doc "Render all cells that are marked as dirty"
PyObject *render_cell, *send_to_gpu;
if (!PyArg_ParseTuple(args, "OO", &render_cell, &send_to_gpu)) return NULL;
if (!self->dirty) { Py_RETURN_NONE; }
for (size_t i = 0; i < sizeof(self->cache)/sizeof(self->cache[0]); i++) {
SpritePosition *sp = &(self->cache[i]);
while (sp) {
if (sp->filled && !sp->rendered) {
PyObject *text = line_text_at(sp->ch & CHAR_MASK, sp->cc);
if (text == NULL) return NULL;
char_type attrs = sp->ch >> ATTRS_SHIFT;
bool bold = (attrs >> BOLD_SHIFT) & 1, italic = (attrs >> ITALIC_SHIFT) & 1;
PyObject *rcell = PyObject_CallFunctionObjArgs(render_cell, text, bold ? Py_True : Py_False, italic ? Py_True : Py_False, sp->is_second ? Py_True : Py_False, NULL);
Py_CLEAR(text);
if (rcell == NULL) return NULL;
PyObject *ret = PyObject_CallFunction(send_to_gpu, "IIIO", sp->x, sp->y, sp->z, rcell);
Py_CLEAR(rcell);
if (ret == NULL) return NULL;
Py_CLEAR(ret);
sp->rendered = true;
}
sp = sp->next;
}
}
self->dirty = false;
sprite_map_set_limits_py(PyObject UNUSED *self, PyObject *args) {
unsigned int w, h;
if(!PyArg_ParseTuple(args, "II", &w, &h)) return NULL;
sprite_map_set_limits(w, h);
Py_RETURN_NONE;
}
// Boilerplate {{{
void
sprite_map_free() {
SpritePosition *s, *t;
for (size_t i = 0; i < sizeof(sprite_map.cache)/sizeof(sprite_map.cache[0]); i++) {
s = &(sprite_map.cache[i]);
s = s->next;
while (s) {
t = s;
s = s->next;
PyMem_Free(t);
}
}
}
static PyMemberDef members[] = {
{"xnum", T_UINT, offsetof(SpriteMap, xnum), 0, "xnum"},
{"ynum", T_UINT, offsetof(SpriteMap, ynum), 0, "ynum"},
{"x", T_UINT, offsetof(SpriteMap, x), 0, "x"},
{"y", T_UINT, offsetof(SpriteMap, y), 0, "y"},
{"z", T_UINT, offsetof(SpriteMap, z), 0, "z"},
{NULL} /* Sentinel */
static inline void
do_increment(int *error) {
sprite_map.x++;
if (sprite_map.x >= sprite_map.xnum) {
sprite_map.x = 0; sprite_map.y++;
sprite_map.ynum = MIN(MAX(sprite_map.ynum, sprite_map.y + 1), sprite_map.max_y);
if (sprite_map.y >= sprite_map.max_y) {
sprite_map.y = 0; sprite_map.z++;
if (sprite_map.z >= MIN(UINT16_MAX, sprite_map.max_array_len)) *error = 2;
}
}
}
SpritePosition*
sprite_map_position_for(char_type ch, attrs_type attrs, combining_type cc, bool is_second, int *error) {
uint8_t bold_italic = (attrs >> BOLD_SHIFT) & 3;
unsigned int idx = (ch & 0xFF) | (bold_italic << 8); // Only bold italic bits and lowest byte of char
SpritePosition *s = &(sprite_map.cache[idx]);
// Optimize for the common case of an ASCII char already in the cache
if (LIKELY(s->ch == ch && s->filled && s->cc == cc && s->is_second == is_second)) return s; // Cache hit
while(true) {
if (s->filled) {
if (s->ch == ch && s->cc == cc && s->is_second == is_second) return s; // Cache hit
} else {
break;
}
if (!s->next) {
s->next = PyMem_Calloc(1, sizeof(SpritePosition));
if (s->next == NULL) { *error = 1; return NULL; }
}
s = s->next;
}
s->ch = ch;
s->cc = cc;
s->is_second = is_second;
s->filled = true;
s->rendered = false;
s->bold = bold_italic & 1;
s->italic = bold_italic >> 1;
s->x = sprite_map.x; s->y = sprite_map.y; s->z = sprite_map.z;
do_increment(error);
sprite_map.dirty = true;
return s;
}
void
set_sprite_position(Cell *cell, Cell *previous_cell) {
SpritePosition *sp;
static int error;
if (UNLIKELY(previous_cell != NULL && ((previous_cell->attrs) & WIDTH_MASK) == 2)) {
sp = sprite_map_position_for(previous_cell->ch, previous_cell->attrs, 0, true, &error);
} else {
sp = sprite_map_position_for(cell->ch, cell->attrs, cell->cc, false, &error);
}
cell->sprite_x = sp->x;
cell->sprite_y = sp->y;
cell->sprite_z = sp->z;
}
int
sprite_map_increment(sprite_index *x, sprite_index *y, sprite_index *z) {
int error = 0;
*x = sprite_map.x; *y = sprite_map.y; *z = sprite_map.z;
do_increment(&error);
return error;
}
void
sprite_map_set_layout(unsigned int cell_width, unsigned int cell_height) {
// Invalidate cache since cell size has changed.
SpritePosition *s;
sprite_map.xnum = MIN(MAX(1, sprite_map.max_texture_size / cell_width), UINT16_MAX);
sprite_map.max_y = MIN(MAX(1, sprite_map.max_texture_size / cell_height), UINT16_MAX);
sprite_map.ynum = 1;
sprite_map.x = 0; sprite_map.y = 0; sprite_map.z = 0;
for (size_t i = 0; i < sizeof(sprite_map.cache)/sizeof(sprite_map.cache[0]); i++) {
s = &(sprite_map.cache[i]);
do {
s->filled = false;
s->is_second = false;
s->rendered = false;
s->ch = 0; s->cc = 0;
s->x = 0; s->y = 0; s->z = 0;
s = s->next;
} while (s != NULL);
}
sprite_map.dirty = true;
}
static PyObject*
sprite_map_set_layout_py(PyObject UNUSED *self, PyObject *args) {
unsigned int w, h;
if(!PyArg_ParseTuple(args, "II", &w, &h)) return NULL;
sprite_map_set_layout(w, h);
Py_RETURN_NONE;
}
void
sprite_map_current_layout(unsigned int *x, unsigned int *y, unsigned int *z) {
*x = sprite_map.xnum; *y = sprite_map.ynum; *z = sprite_map.z;
}
PyObject*
sprite_position_for(PyObject UNUSED *self, PyObject *args) {
#define sprite_position_for_doc "sprite_position_for(ch, cc, is_second, attrs) -> x, y, z the sprite position for the specified text"
unsigned long ch = 0;
unsigned long long cc = 0;
unsigned int attrs = 1;
int is_second = 0, error = 0;
if (!PyArg_ParseTuple(args, "|kKpI", &ch, &cc, &is_second, &attrs)) return NULL;
SpritePosition *pos = sprite_map_position_for(ch, attrs, cc, is_second, &error);
if (pos == NULL) { sprite_map_set_error(error); return NULL; }
return Py_BuildValue("III", pos->x, pos->y, pos->z);
}
void
render_dirty_sprites(void (*render)(PyObject*, bool, bool, bool, sprite_index, sprite_index, sprite_index)) {
#define render_dirty_cells_doc "Render all cells that are marked as dirty"
if (!sprite_map.dirty) return;
for (size_t i = 0; i < sizeof(sprite_map.cache)/sizeof(sprite_map.cache[0]); i++) {
SpritePosition *sp = &(sprite_map.cache[i]);
do {
if (sp->filled && !sp->rendered) {
PyObject *text = line_text_at(sp->ch, sp->cc);
render(text, sp->bold, sp->italic, sp->is_second, sp->x, sp->y, sp->z);
Py_CLEAR(text);
sp->rendered = true;
}
sp = sp->next;
} while(sp);
}
sprite_map.dirty = false;
}
static PyMethodDef module_methods[] = {
{"sprite_position_for", (PyCFunction)sprite_position_for, METH_VARARGS, ""},
{"sprite_map_set_layout", (PyCFunction)sprite_map_set_layout_py, METH_VARARGS, ""},
{"sprite_map_set_limits", (PyCFunction)sprite_map_set_limits_py, METH_VARARGS, ""},
{NULL, NULL, 0, NULL} /* Sentinel */
};
static PyMethodDef methods[] = {
METHOD(layout, METH_VARARGS)
METHOD(position_for, METH_VARARGS)
METHOD(render_dirty_cells, METH_VARARGS)
METHOD(increment, METH_NOARGS)
{NULL} /* Sentinel */
};
PyTypeObject SpriteMap_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "fast_data_types.SpriteMap",
.tp_basicsize = sizeof(SpriteMap),
.tp_dealloc = (destructor)dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = "SpriteMap",
.tp_methods = methods,
.tp_members = members,
.tp_new = new,
};
INIT_TYPE(SpriteMap)
// }}}
bool
init_sprites(PyObject *module) {
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
return true;
}

15
kitty/sprites.h Normal file
View File

@@ -0,0 +1,15 @@
/*
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#pragma once
void sprite_map_current_layout(unsigned int *x, unsigned int *y, unsigned int*);
void sprite_map_set_layout(unsigned int cell_width, unsigned int cell_height);
void sprite_map_set_limits(size_t max_texture_size, size_t max_array_len);
void sprite_map_free();
int sprite_map_increment(sprite_index *x, sprite_index *y, sprite_index *z);
void render_dirty_sprites(void (*render)(PyObject*, bool, bool, bool, sprite_index, sprite_index, sprite_index));

300
kitty/state.c Normal file
View File

@@ -0,0 +1,300 @@
/*
* state.c
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "state.h"
GlobalState global_state = {{0}};
static const Tab EMPTY_TAB = {0};
static const Window EMPTY_WINDOW = {0};
#define ensure_can_add(array, count, msg) if (count >= sizeof(array)/sizeof(array[0]) - 1) fatal(msg);
#define noop(...)
#define REMOVER(array, qid, count, empty, structure, destroy) { \
size_t capacity = sizeof(array)/sizeof(array[0]); \
for (size_t i = 0; i < count; i++) { \
if (array[i].id == qid) { \
destroy(array[i]); \
array[i] = empty; \
size_t num_to_right = capacity - count - 1; \
if (num_to_right) memmove(array + i, array + i + 1, num_to_right * sizeof(structure)); \
(count)--; \
} \
}}
#define WITH_TAB(tab_id) \
for (size_t t = 0; t < global_state.num_tabs; t++) { \
if (global_state.tabs[t].id == tab_id) { \
Tab *tab = global_state.tabs + t;
#define END_WITH_TAB break; }}
static inline void
add_tab(unsigned int id) {
ensure_can_add(global_state.tabs, global_state.num_tabs, "Too many children (add_tab)");
global_state.tabs[global_state.num_tabs] = EMPTY_TAB;
global_state.tabs[global_state.num_tabs].id = id;
global_state.num_tabs++;
}
static inline void
add_window(unsigned int tab_id, unsigned int id, PyObject *title) {
WITH_TAB(tab_id);
ensure_can_add(tab->windows, tab->num_windows, "Too many children (add_window)");
tab->windows[tab->num_windows] = EMPTY_WINDOW;
tab->windows[tab->num_windows].id = id;
tab->windows[tab->num_windows].visible = true;
tab->windows[tab->num_windows].title = title;
Py_INCREF(tab->windows[tab->num_windows].title);
tab->num_windows++;
END_WITH_TAB;
}
static inline void
update_window_title(unsigned int tab_id, unsigned int window_id, PyObject *title) {
WITH_TAB(tab_id);
for (size_t i = 0; i < tab->num_windows; i++) {
if (tab->windows[i].id == window_id) {
Py_CLEAR(tab->windows[i].title);
tab->windows[i].title = title;
Py_INCREF(tab->windows[i].title);
break;
}
}
END_WITH_TAB;
}
static inline void
remove_tab(unsigned int id) {
REMOVER(global_state.tabs, id, global_state.num_tabs, EMPTY_TAB, Tab, noop);
}
static inline void
remove_window(unsigned int tab_id, unsigned int id) {
WITH_TAB(tab_id);
#define destroy_window(w) Py_CLEAR(w.render_data.screen); Py_CLEAR(w.title);
REMOVER(tab->windows, id, tab->num_windows, EMPTY_WINDOW, Window, destroy_window);
#undef destroy_window
END_WITH_TAB;
}
static inline void
set_active_tab(unsigned int idx) {
global_state.active_tab = idx;
}
static inline void
set_active_window(unsigned int tab_id, unsigned int idx) {
WITH_TAB(tab_id);
tab->active_window = idx;
END_WITH_TAB;
}
static inline void
swap_tabs(unsigned int a, unsigned int b) {
Tab t = global_state.tabs[b];
global_state.tabs[b] = global_state.tabs[a];
global_state.tabs[a] = t;
}
static inline void
swap_windows(unsigned int tab_id, unsigned int a, unsigned int b) {
WITH_TAB(tab_id);
Window w = tab->windows[b];
tab->windows[b] = tab->windows[a];
tab->windows[a] = w;
END_WITH_TAB;
}
// Python API {{{
#define PYWRAP0(name) static PyObject* py##name(PyObject UNUSED *self)
#define PYWRAP1(name) static PyObject* py##name(PyObject UNUSED *self, PyObject *args)
#define PYWRAP2(name) static PyObject* py##name(PyObject UNUSED *self, PyObject *args, PyObject *kw)
#define PA(fmt, ...) if(!PyArg_ParseTuple(args, fmt, __VA_ARGS__)) return NULL;
#define ONE_UINT(name) PYWRAP1(name) { name((unsigned int)PyLong_AsUnsignedLong(args)); Py_RETURN_NONE; }
#define TWO_UINT(name) PYWRAP1(name) { unsigned int a, b; PA("II", &a, &b); name(a, b); Py_RETURN_NONE; }
#define THREE_UINT(name) PYWRAP1(name) { unsigned int a, b, c; PA("III", &a, &b, &c); name(a, b, c); Py_RETURN_NONE; }
static inline color_type
color_as_int(PyObject *color) {
if (!PyTuple_Check(color)) { PyErr_SetString(PyExc_TypeError, "Not a color tuple"); return 0; }
#define I(n, s) ((PyLong_AsUnsignedLong(PyTuple_GET_ITEM(color, n)) & 0xff) << s)
return (I(0, 16) | I(1, 8) | I(2, 0)) & 0xffffff;
#undef I
}
static inline double
repaint_delay(PyObject *val) {
return (double)(PyLong_AsUnsignedLong(val)) / 1000.0;
}
#define dict_iter(d) { \
PyObject *key, *value; Py_ssize_t pos = 0; \
while (PyDict_Next(d, &pos, &key, &value))
static inline void
set_special_keys(PyObject *dict) {
dict_iter(dict) {
if (!PyTuple_Check(key)) { PyErr_SetString(PyExc_TypeError, "dict keys for special keys must be tuples"); return; }
int mods = PyLong_AsLong(PyTuple_GET_ITEM(key, 0));
int glfw_key = PyLong_AsLong(PyTuple_GET_ITEM(key, 1));
set_special_key_combo(glfw_key, mods);
}}
}
PYWRAP1(set_options) {
PyObject *ret;
#define GA(name) ret = PyObject_GetAttrString(args, #name); if (ret == NULL) return NULL;
#define S(name, convert) { GA(name); global_state.opts.name = convert(ret); Py_DECREF(ret); if (PyErr_Occurred()) return NULL; }
S(visual_bell_duration, PyFloat_AsDouble);
S(enable_audio_bell, PyObject_IsTrue);
S(cursor_blink_interval, PyFloat_AsDouble);
S(cursor_stop_blinking_after, PyFloat_AsDouble);
S(cursor_shape, PyLong_AsLong);
S(mouse_hide_wait, PyFloat_AsDouble);
S(wheel_scroll_multiplier, PyFloat_AsDouble);
S(open_url_modifiers, PyLong_AsUnsignedLong);
S(click_interval, PyFloat_AsDouble);
S(url_color, color_as_int);
S(repaint_delay, repaint_delay);
S(input_delay, repaint_delay);
PyObject *chars = PyObject_GetAttrString(args, "select_by_word_characters");
if (chars == NULL) return NULL;
for (size_t i = 0; i < MIN((size_t)PyUnicode_GET_LENGTH(chars), sizeof(OPT(select_by_word_characters))/sizeof(OPT(select_by_word_characters[0]))); i++) {
OPT(select_by_word_characters)[i] = PyUnicode_READ(PyUnicode_KIND(chars), PyUnicode_DATA(chars), i);
}
OPT(select_by_word_characters_count) = PyUnicode_GET_LENGTH(chars);
GA(keymap); set_special_keys(ret);
Py_DECREF(ret); if (PyErr_Occurred()) return NULL;
GA(send_text_map);
dict_iter(ret) {
set_special_keys(value);
}}
Py_DECREF(ret); if (PyErr_Occurred()) return NULL;
Py_DECREF(chars);
#undef S
Py_RETURN_NONE;
}
PYWRAP1(set_tab_bar_render_data) {
#define A(name) &(global_state.tab_bar_render_data.name)
Py_CLEAR(global_state.tab_bar_render_data.screen);
PA("iffffO", A(vao_idx), A(xstart), A(ystart), A(dx), A(dy), A(screen));
Py_INCREF(global_state.tab_bar_render_data.screen);
Py_RETURN_NONE;
#undef A
}
PYWRAP1(set_window_render_data) {
#define A(name) &(d.name)
#define B(name) &(g.name)
unsigned int window_idx, tab_id;
static ScreenRenderData d = {0};
static WindowGeometry g = {0};
PA("IIiiffffOIIII", &tab_id, &window_idx, A(vao_idx), A(gvao_idx), A(xstart), A(ystart), A(dx), A(dy), A(screen), B(left), B(top), B(right), B(bottom));
WITH_TAB(tab_id);
Py_CLEAR(tab->windows[window_idx].render_data.screen);
tab->windows[window_idx].render_data = d;
tab->windows[window_idx].geometry = g;
Py_INCREF(tab->windows[window_idx].render_data.screen);
END_WITH_TAB;
Py_RETURN_NONE;
#undef A
#undef B
}
PYWRAP1(update_window_visibility) {
unsigned int window_idx, tab_id;
int visible;
PA("IIp", &tab_id, &window_idx, &visible);
WITH_TAB(tab_id);
tab->windows[window_idx].visible = visible & 1;
END_WITH_TAB;
Py_RETURN_NONE;
}
PYWRAP1(set_logical_dpi) {
PA("dd", &global_state.logical_dpi_x, &global_state.logical_dpi_y);
Py_RETURN_NONE;
}
PYWRAP1(set_boss) {
Py_CLEAR(global_state.boss);
global_state.boss = args;
Py_INCREF(global_state.boss);
Py_RETURN_NONE;
}
PYWRAP0(destroy_global_data) {
Py_CLEAR(global_state.tab_bar_render_data.screen);
Py_CLEAR(global_state.boss);
Py_RETURN_NONE;
}
PYWRAP1(set_display_state) {
PA("iiII", &global_state.viewport_width, &global_state.viewport_height, &global_state.cell_width, &global_state.cell_height);
Py_RETURN_NONE;
}
#define WF(name) PYWRAP1(name) { \
unsigned int tab_id, window_id; \
PyObject *title; \
PA("IIO", &tab_id, &window_id, &title); \
name(tab_id, window_id, title); \
Py_RETURN_NONE; \
}
WF(add_window)
WF(update_window_title)
ONE_UINT(add_tab)
ONE_UINT(remove_tab)
TWO_UINT(remove_window)
ONE_UINT(set_active_tab)
TWO_UINT(set_active_window)
TWO_UINT(swap_tabs)
THREE_UINT(swap_windows)
#define M(name, arg_type) {#name, (PyCFunction)name, arg_type, NULL}
#define MW(name, arg_type) {#name, (PyCFunction)py##name, arg_type, NULL}
static PyMethodDef module_methods[] = {
MW(set_options, METH_O),
MW(set_logical_dpi, METH_VARARGS),
MW(add_tab, METH_O),
MW(add_window, METH_VARARGS),
MW(update_window_title, METH_VARARGS),
MW(remove_tab, METH_O),
MW(remove_window, METH_VARARGS),
MW(set_active_tab, METH_O),
MW(set_active_window, METH_VARARGS),
MW(swap_tabs, METH_VARARGS),
MW(swap_windows, METH_VARARGS),
MW(set_tab_bar_render_data, METH_VARARGS),
MW(set_window_render_data, METH_VARARGS),
MW(update_window_visibility, METH_VARARGS),
MW(set_boss, METH_O),
MW(set_display_state, METH_VARARGS),
MW(destroy_global_data, METH_NOARGS),
{NULL, NULL, 0, NULL} /* Sentinel */
};
bool
init_state(PyObject *module) {
double now = monotonic();
global_state.application_focused = true;
global_state.cursor_blink_zero_time = now;
global_state.last_mouse_activity_at = now;
global_state.cell_width = 1; global_state.cell_height = 1;
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
return true;
}
// }}}

105
kitty/state.h Normal file
View File

@@ -0,0 +1,105 @@
/*
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#pragma once
#include "data-types.h"
#include "screen.h"
#define OPT(name) global_state.opts.name
typedef struct {
double visual_bell_duration, cursor_blink_interval, cursor_stop_blinking_after, mouse_hide_wait, click_interval, wheel_scroll_multiplier;
bool enable_audio_bell;
CursorShape cursor_shape;
unsigned int open_url_modifiers;
char_type select_by_word_characters[256]; size_t select_by_word_characters_count;
color_type url_color;
double repaint_delay, input_delay;
} Options;
typedef struct {
ssize_t vao_idx, gvao_idx;
float xstart, ystart, dx, dy;
Screen *screen;
} ScreenRenderData;
typedef struct {
unsigned int left, top, right, bottom;
} WindowGeometry;
typedef struct {
double at;
int button, modifiers;
} Click;
#define CLICK_QUEUE_SZ 3
typedef struct {
Click clicks[CLICK_QUEUE_SZ];
unsigned int length;
} ClickQueue;
typedef struct {
unsigned int id;
bool visible;
PyObject *title;
ScreenRenderData render_data;
unsigned int mouse_cell_x, mouse_cell_y;
WindowGeometry geometry;
ClickQueue click_queue;
double last_drag_scroll_at;
} Window;
typedef struct {
unsigned int id, active_window, num_windows;
Window windows[MAX_CHILDREN];
} Tab;
#define MAX_KEY_COUNT 512
typedef struct {
Options opts;
Tab tabs[MAX_CHILDREN];
unsigned int active_tab, num_tabs;
ScreenRenderData tab_bar_render_data;
bool application_focused;
double cursor_blink_zero_time, last_mouse_activity_at;
double logical_dpi_x, logical_dpi_y;
double mouse_x, mouse_y;
bool mouse_button_pressed[20];
int viewport_width, viewport_height;
double viewport_x_ratio, viewport_y_ratio;
unsigned int cell_width, cell_height;
PyObject *application_title;
PyObject *boss;
bool is_key_pressed[MAX_KEY_COUNT];
} GlobalState;
extern GlobalState global_state;
typedef struct {
bool is_visible;
CursorShape shape;
double left, right, top, bottom;
color_type color;
} CursorRenderInfo;
bool drag_scroll(Window *);
#define call_boss(name, ...) { \
PyObject *cret_ = PyObject_CallMethod(global_state.boss, #name, __VA_ARGS__); \
if (cret_ == NULL) { PyErr_Print(); } \
else Py_DECREF(cret_); \
}
#define EXTERNAL_FUNC(name, ret, ...) typedef ret (*name##_func)(__VA_ARGS__); extern name##_func name
#define EXTERNAL_FUNC0(name, ret) typedef ret (*name##_func)(); extern name##_func name
EXTERNAL_FUNC0(draw_borders, void);
EXTERNAL_FUNC(draw_cells, void, ssize_t, ssize_t, float, float, float, float, Screen *, CursorRenderInfo *);
EXTERNAL_FUNC(draw_cursor, void, CursorRenderInfo *);
EXTERNAL_FUNC(update_viewport_size, void, int, int);
EXTERNAL_FUNC(free_texture, void, uint32_t*);
EXTERNAL_FUNC(send_image_to_gpu, void, uint32_t*, const void*, int32_t, int32_t, bool, bool);

View File

@@ -2,50 +2,64 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from collections import deque
from collections import deque, namedtuple
from functools import partial
from threading import Lock
from ctypes import addressof
from itertools import count
from .borders import Borders
from .child import Child
from .config import build_ansi_color_table
from .constants import get_boss, appname, shell_path, cell_size, queue_action, viewport_size, WindowGeometry, GLuint
from .fast_data_types import glfw_post_empty_event, Screen, DECAWM, DATA_CELL_SIZE, ColorProfile
from .char_grid import calculate_gl_geometry, render_cells
from .layout import all_layouts, Rect
from .constants import (
WindowGeometry, appname, cell_size, get_boss, shell_path, viewport_size
)
from .fast_data_types import (
DECAWM, Screen, add_tab, add_window, create_cell_vao,
glfw_post_empty_event, remove_tab, remove_window, set_active_tab,
set_active_window, set_tab_bar_render_data, swap_tabs, swap_windows
)
from .layout import Rect, all_layouts
from .utils import color_as_int
from .borders import Borders
from .window import Window
from .window import Window, calculate_gl_geometry
TabbarData = namedtuple('TabbarData', 'title is_active is_last')
borders = None
tab_counter = count()
next(tab_counter)
def SpecialWindow(cmd, stdin=None, override_title=None):
return (cmd, stdin, override_title)
class Tab:
class Tab: # {{{
def __init__(self, opts, args, on_title_change, session_tab=None, special_window=None):
global borders
self.id = next(tab_counter)
add_tab(self.id)
self.opts, self.args = opts, args
self.name = getattr(session_tab, 'name', '')
self.on_title_change = on_title_change
self.enabled_layouts = list((session_tab or opts).enabled_layouts)
self.borders = Borders(opts)
self.enabled_layouts = list(getattr(session_tab, 'enabled_layouts', None) or opts.enabled_layouts)
if borders is None:
borders = Borders(opts)
self.windows = deque()
self.active_window_idx = 0
for i, which in enumerate('first second third fourth fifth sixth seventh eighth ninth tenth'.split()):
setattr(self, which + '_window', partial(self.nth_window, num=i))
if session_tab is None:
self.cwd = args.directory
l = self.enabled_layouts[0]
self.current_layout = all_layouts[l](opts, borders.border_width, self.windows)
if special_window is None:
queue_action(self.new_window)
self.new_window()
else:
queue_action(self.new_special_window, special_window)
self.new_special_window(special_window)
else:
self.cwd = session_tab.cwd or args.directory
l = session_tab.layout
queue_action(self.startup, session_tab)
self.current_layout = all_layouts[l](opts, self.borders.border_width, self.windows)
for i, which in enumerate('first second third fourth fifth sixth seventh eighth ninth tenth'.split()):
setattr(self, which + '_window', partial(self.nth_window, num=i))
self.current_layout = all_layouts[l](opts, borders.border_width, self.windows)
self.startup(session_tab)
def startup(self, session_tab):
for cmd in session_tab.windows:
@@ -80,8 +94,8 @@ class Tab:
def relayout_borders(self):
tm = get_boss().tab_manager
self.borders(self.windows, self.active_window, self.current_layout, tm.blank_rects,
self.current_layout.needs_window_borders and len(self.windows) > 1)
borders(self.windows, self.active_window, self.current_layout, tm.blank_rects,
self.current_layout.needs_window_borders and len(self.windows) > 1)
def next_layout(self):
if len(self.opts.enabled_layouts) > 1:
@@ -90,9 +104,9 @@ class Tab:
except Exception:
idx = -1
nl = self.opts.enabled_layouts[(idx + 1) % len(self.opts.enabled_layouts)]
self.current_layout = all_layouts[nl](self.opts, self.borders.border_width, self.windows)
for w in self.windows:
w.is_visible_in_layout = True
self.current_layout = all_layouts[nl](self.opts, borders.border_width, self.windows)
for i, w in enumerate(self.windows):
w.set_visible_in_layout(i, True)
self.relayout()
def launch_child(self, use_shell=False, cmd=None, stdin=None):
@@ -110,8 +124,11 @@ class Tab:
window = Window(self, child, self.opts, self.args)
if override_title is not None:
window.title = window.override_title = override_title
get_boss().add_child_fd(child.child_fd, window.read_ready, window.write_ready)
# Must add child before laying out so that resize_pty succeeds
get_boss().add_child(window)
add_window(self.id, window.id, window.override_title or window.title or appname)
self.active_window_idx = self.current_layout.add_window(self.windows, window, self.active_window_idx)
set_active_window(self.id, self.active_window_idx)
self.relayout_borders()
glfw_post_empty_event()
return window
@@ -124,7 +141,9 @@ class Tab:
self.remove_window(self.windows[self.active_window_idx])
def remove_window(self, window):
remove_window(self.id, window.id)
self.active_window_idx = self.current_layout.remove_window(self.windows, window, self.active_window_idx)
set_active_window(self.id, self.active_window_idx)
self.relayout_borders()
glfw_post_empty_event()
@@ -132,6 +151,7 @@ class Tab:
if idx != self.active_window_idx:
self.current_layout.set_active_window(self.windows, idx)
self.active_window_idx = idx
set_active_window(self.id, self.active_window_idx)
self.relayout_borders()
glfw_post_empty_event()
@@ -149,6 +169,7 @@ class Tab:
def _next_window(self, delta=1):
if len(self.windows) > 1:
self.active_window_idx = self.current_layout.next_window(self.windows, self.active_window_idx, delta)
set_active_window(self.id, self.active_window_idx)
self.relayout_borders()
glfw_post_empty_event()
@@ -163,7 +184,9 @@ class Tab:
idx = self.active_window_idx
nidx = (idx + len(self.windows) + delta) % len(self.windows)
self.windows[nidx], self.windows[idx] = self.windows[idx], self.windows[nidx]
swap_windows(self.id, nidx, idx)
self.active_window_idx = nidx
set_active_window(self.id, self.active_window_idx)
self.relayout()
def move_window_to_top(self):
@@ -185,75 +208,135 @@ class Tab:
return window in self.windows
def destroy(self):
if hasattr(self, 'windows'):
for w in self.windows:
w.destroy()
del self.windows
def render(self):
self.borders.render(get_boss().borders_program)
self.windows = deque()
def __repr__(self):
return 'Tab(title={}, id={})'.format(self.name or self.title, hex(id(self)))
# }}}
class TabManager:
class TabBar: # {{{
def __init__(self, opts, args, startup_session):
self.opts, self.args = opts, args
self.buffer_id = None
self.tabbar_lock = Lock()
self.tabs = [Tab(opts, args, self.title_changed, t) for t in startup_session.tabs]
self.cell_ranges = []
self.active_tab_idx = startup_session.active_tab_idx
self.tabbar_dirty = True
self.color_profile = ColorProfile()
self.color_profile.update_ansi_color_table(build_ansi_color_table(opts))
self.default_fg = color_as_int(opts.inactive_tab_foreground)
self.default_bg = color_as_int(opts.inactive_tab_background)
self.tab_bar_blank_rects = ()
def __init__(self, opts):
self.num_tabs = 1
self.cell_width = 1
self.data_buffer_size = 0
self.vao_id = create_cell_vao()
self.layout_changed = None
self.dirty = True
self.screen = s = Screen(None, 1, 10)
s.color_profile.update_ansi_color_table(build_ansi_color_table(opts))
s.color_profile.set_configured_colors(
color_as_int(opts.inactive_tab_foreground),
color_as_int(opts.inactive_tab_background)
)
self.blank_rects = ()
def as_rgb(x):
return (x << 8) | 2
self.active_bg = as_rgb(color_as_int(opts.active_tab_background))
self.active_fg = as_rgb(color_as_int(opts.active_tab_foreground))
self.can_render = False
def layout(self, viewport_width, viewport_height, cell_width, cell_height):
self.cell_width = cell_width
s = self.screen
ncells = viewport_width // cell_width
s.resize(1, ncells)
s.reset_mode(DECAWM)
self.layout_changed = True
margin = (viewport_width - ncells * cell_width) // 2
self.window_geometry = g = WindowGeometry(
margin, viewport_height - cell_height, viewport_width - margin, viewport_height, s.columns, s.lines)
if margin > 0:
self.tab_bar_blank_rects = (Rect(0, g.top, g.left, g.bottom), Rect(g.right - 1, g.top, viewport_width, g.bottom))
else:
self.tab_bar_blank_rects = ()
self.screen_geometry = sg = calculate_gl_geometry(g, viewport_width, viewport_height, cell_width, cell_height)
set_tab_bar_render_data(self.vao_id, sg.xstart, sg.ystart, sg.dx, sg.dy, self.screen)
def update(self, data):
if self.layout_changed is None:
return
s = self.screen
s.cursor.x = 0
s.erase_in_line(2, False)
max_title_length = (self.screen_geometry.xnum // max(1, len(data))) - 1
cr = []
for t in data:
s.cursor.bg = self.active_bg if t.is_active else 0
s.cursor.fg = self.active_fg if t.is_active else 0
s.cursor.bold = s.cursor.italic = t.is_active
before = s.cursor.x
s.draw(t.title)
extra = s.cursor.x - before - max_title_length
if extra > 0:
s.cursor.x -= extra + 1
s.draw('')
cr.append((before, s.cursor.x))
s.cursor.bold = s.cursor.italic = False
s.cursor.fg = s.cursor.bg = 0
s.draw('')
if s.cursor.x > s.columns - max_title_length and not t.is_last:
s.draw('')
break
s.erase_in_line(0, False) # Ensure no long titles bleed after the last tab
self.cell_ranges = cr
glfw_post_empty_event()
def tab_at(self, x):
x = (x - self.window_geometry.left) // self.cell_width
for i, (a, b) in enumerate(self.cell_ranges):
if a <= x <= b:
return i
# }}}
class TabManager: # {{{
def __init__(self, opts, args):
self.opts, self.args = opts, args
self.tabs = []
self.tab_bar = TabBar(opts)
self.refresh_sprite_positions = self.tab_bar.screen.refresh_sprite_positions
self.tab_bar.layout(*self.tab_bar_layout_data)
self.active_tab_idx = 0
def _add_tab(self, tab):
self.tabs.append(tab)
def _remove_tab(self, tab):
remove_tab(tab.id)
self.tabs.remove(tab)
def _set_active_tab(self, idx):
self.active_tab_idx = idx
set_active_tab(idx)
def init(self, startup_session):
for t in startup_session.tabs:
self._add_tab(Tab(self.opts, self.args, self.title_changed, t))
self._set_active_tab(max(0, min(startup_session.active_tab_idx, len(self.tabs) - 1)))
if len(self.tabs) > 1:
self.layout_tab_bar()
get_boss().tabbar_visibility_changed()
self.update_tab_bar()
def update_tab_bar(self):
if len(self.tabs) > 1:
self.tab_bar.update(self.tab_bar_data)
def resize(self, only_tabs=False):
if not only_tabs:
self.layout_tab_bar()
self.tab_bar.layout(*self.tab_bar_layout_data)
self.update_tab_bar()
for tab in self.tabs:
tab.relayout()
def layout_tab_bar(self):
self.can_render = False
ncells = viewport_size.width // cell_size.width
s = Screen(None, 1, ncells)
s.reset_mode(DECAWM)
self.sprite_map_type = (GLuint * (s.lines * s.columns * DATA_CELL_SIZE))
with self.tabbar_lock:
self.sprite_map = self.sprite_map_type()
self.tab_bar_screen = s
self.tabbar_dirty = True
margin = (viewport_size.width - ncells * cell_size.width) // 2
self.window_geometry = g = WindowGeometry(
margin, viewport_size.height - cell_size.height, viewport_size.width - margin, viewport_size.height, s.columns, s.lines)
if margin > 0:
self.tab_bar_blank_rects = (Rect(0, g.top, g.left, g.bottom), Rect(g.right - 1, g.top, viewport_size.width, g.bottom))
else:
self.tab_bar_blank_rects = ()
self.screen_geometry = calculate_gl_geometry(g)
self.screen = s
self.can_render = True
def set_active_tab(self, idx):
self.active_tab_idx = idx
self.tabbar_dirty = True
self._set_active_tab(idx)
self.active_tab.relayout_borders()
glfw_post_empty_event()
self.update_tab_bar()
def next_tab(self, delta=1):
if len(self.tabs) > 1:
@@ -278,83 +361,55 @@ class TabManager:
idx = self.active_tab_idx
nidx = (idx + len(self.tabs) + delta) % len(self.tabs)
self.tabs[idx], self.tabs[nidx] = self.tabs[nidx], self.tabs[idx]
self.active_tab_idx = nidx
glfw_post_empty_event()
swap_tabs(idx, nidx)
self._set_active_tab(nidx)
self.update_tab_bar()
def title_changed(self, new_title):
with self.tabbar_lock:
self.tabbar_dirty = True
self.update_tab_bar()
def new_tab(self, special_window=None):
' Must be called in the GUI thread '
needs_resize = len(self.tabs) == 1
self.active_tab_idx = len(self.tabs)
self.tabs.append(Tab(self.opts, self.args, self.title_changed, special_window=special_window))
idx = len(self.tabs)
self._add_tab(Tab(self.opts, self.args, self.title_changed, special_window=special_window))
self._set_active_tab(idx)
self.update_tab_bar()
if needs_resize:
if not self.can_render:
queue_action(self.layout_tab_bar)
queue_action(get_boss().tabbar_visibility_changed)
get_boss().tabbar_visibility_changed()
def remove(self, tab):
' Must be called in the GUI thread '
needs_resize = len(self.tabs) == 2
self.tabs.remove(tab)
self.active_tab_idx = max(0, min(self.active_tab_idx, len(self.tabs) - 1))
self.tabbar_dirty = True
self._remove_tab(tab)
self._set_active_tab(max(0, min(self.active_tab_idx, len(self.tabs) - 1)))
self.update_tab_bar()
tab.destroy()
if needs_resize:
queue_action(get_boss().tabbar_visibility_changed)
get_boss().tabbar_visibility_changed()
def update_tab_bar_data(self, sprites):
s = self.tab_bar_screen
s.cursor.x = 0
s.erase_in_line(2, False)
@property
def tab_bar_layout_data(self):
return viewport_size.width, viewport_size.height, cell_size.width, cell_size.height
@property
def tab_bar_data(self):
at = self.active_tab
max_title_length = (self.screen_geometry.xnum // len(self.tabs)) - 1
self.cell_ranges = []
ans = []
for t in self.tabs:
title = (t.name or t.title or appname) + ' '
s.cursor.bg = self.active_bg if t is at else 0
s.cursor.fg = self.active_fg if t is at else 0
s.cursor.bold = s.cursor.italic = t is at
before = s.cursor.x
s.draw(title)
extra = s.cursor.x - before - max_title_length
if extra > 0:
s.cursor.x -= extra + 1
s.draw('')
self.cell_ranges.append((before, s.cursor.x))
s.cursor.bold = s.cursor.italic = False
s.cursor.fg = s.cursor.bg = 0
s.draw('')
if s.cursor.x > s.columns - max_title_length:
s.draw('')
break
s.update_cell_data(
sprites.backend, self.color_profile, addressof(self.sprite_map), self.default_fg, self.default_bg, True)
sprites.render_dirty_cells()
if self.buffer_id is None:
self.buffer_id = sprites.add_sprite_map()
sprites.set_sprite_map(self.buffer_id, self.sprite_map)
ans.append(TabbarData(title, t is at, t is self.tabs[-1]))
return ans
def activate_tab_at(self, x):
x = (x - self.window_geometry.left) // cell_size.width
for i, (a, b) in enumerate(self.cell_ranges):
if a <= x <= b:
queue_action(self.set_active_tab, i)
return
i = self.tab_bar.tab_at(x)
if i is not None:
self.set_active_tab(i)
@property
def blank_rects(self):
if len(self.tabs) < 2:
return ()
return self.tab_bar_blank_rects
return self.tab_bar.blank_rects if len(self.tabs) > 1 else ()
def render(self, cell_program, sprites):
if not self.can_render or len(self.tabs) < 2:
def render(self):
if len(self.tabs) < 2:
return
with self.tabbar_lock:
if self.tabbar_dirty:
self.update_tab_bar_data(sprites)
render_cells(self.buffer_id, self.screen_geometry, cell_program, sprites)
self.tab_bar.render()
# }}}

View File

@@ -1,58 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from collections import namedtuple
from operator import itemgetter
from time import monotonic
from .utils import safe_print
Event = namedtuple('Event', 'at callback args')
get_at = itemgetter(0)
class Timers:
def __init__(self):
self.timers = []
def _add(self, delay, callback, args):
self.timers.append(Event(monotonic() + delay, callback, args))
self.timers.sort(key=get_at)
def add(self, delay, callback, *args):
self.remove(callback)
self._add(delay, callback, args)
def add_if_missing(self, delay, callback, *args):
for ev in self.timers:
if ev.callback == callback:
return
self._add(delay, callback, args)
def remove(self, callback):
for i, ev in enumerate(self.timers):
if ev.callback == callback:
break
else:
return
del self.timers[i]
def timeout(self):
if self.timers:
return max(0, self.timers[0][0] - monotonic())
def __call__(self):
if self.timers:
now = monotonic()
expired_timers, waiting_timers = [], []
for ev in self.timers:
(expired_timers if ev[0] <= now else waiting_timers).append(ev)
self.timers = waiting_timers
for ev in expired_timers:
try:
ev.callback(*ev.args)
except Exception:
import traceback
safe_print(traceback.format_exc())

View File

@@ -1,267 +0,0 @@
/*
* tracker.c
* Copyright (C) 2016 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "data-types.h"
#include "tracker.h"
bool tracker_resize(ChangeTracker *self, unsigned int ynum, unsigned int xnum) {
#define ALLOC_VAR(name, sz) \
bool *name = PyMem_Calloc(sz, sizeof(bool)); \
if (name == NULL) { PyErr_NoMemory(); return false; } \
PyMem_Free(self->name); self->name = name;
self->ynum = ynum; self->xnum = xnum;
ALLOC_VAR(changed_lines, self->ynum);
ALLOC_VAR(changed_cells, self->xnum * self->ynum);
ALLOC_VAR(lines_with_changed_cells, self->ynum);
RESET_STATE_VARS(self);
return true;
}
static PyObject*
resize(ChangeTracker *self, PyObject *args) {
#define resize_doc "Resize this change tracker must be called when the screen it is tracking for is resized"
unsigned int ynum=1, xnum=1;
if (!PyArg_ParseTuple(args, "|II", &ynum, &xnum)) return NULL;
if (!tracker_resize(self, ynum, xnum)) return NULL;
Py_RETURN_NONE;
}
static PyObject*
new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
ChangeTracker *self;
self = (ChangeTracker *)type->tp_alloc(type, 0);
if (self != NULL) {
PyObject *ret = resize(self, args);
if (ret == NULL) { Py_CLEAR(self); return NULL; }
Py_CLEAR(ret);
}
return (PyObject*) self;
}
static void
dealloc(ChangeTracker* self) {
PyMem_Free(self->changed_lines); PyMem_Free(self->changed_cells); PyMem_Free(self->lines_with_changed_cells);
Py_TYPE(self)->tp_free((PyObject*)self);
}
static PyObject*
reset(ChangeTracker *self) {
#define reset_doc "Reset all changes"
tracker_reset(self);
Py_RETURN_NONE;
}
static PyObject*
cursor_changed(ChangeTracker *self) {
#define cursor_changed_doc ""
tracker_cursor_changed(self);
Py_RETURN_NONE;
}
static PyObject*
line_added_to_history(ChangeTracker *self) {
#define line_added_to_history_doc ""
tracker_line_added_to_history(self);
Py_RETURN_NONE;
}
static PyObject*
update_screen(ChangeTracker *self) {
#define update_screen_doc ""
tracker_update_screen(self);
Py_RETURN_NONE;
}
static PyObject*
update_line_range(ChangeTracker *self, PyObject *args) {
#define update_line_range_doc ""
unsigned int f, l;
if (!PyArg_ParseTuple(args, "II", &f, &l)) return NULL;
tracker_update_line_range(self, f, l);
Py_RETURN_NONE;
}
static PyObject*
update_cell_range(ChangeTracker *self, PyObject *args) {
#define update_cell_range_doc ""
unsigned int line, f, l;
if (!PyArg_ParseTuple(args, "III", &line, &f, &l)) return NULL;
tracker_update_cell_range(self, line, f, l);
Py_RETURN_NONE;
}
bool tracker_update_cell_data(ScreenModes *modes, ChangeTracker *self, LineBuf *lb, SpriteMap *spm, ColorProfile *color_profile, unsigned int *data, unsigned long default_fg, unsigned long default_bg, bool force_screen_refresh) {
unsigned int y;
Py_ssize_t start;
default_fg &= COL_MASK;
default_bg &= COL_MASK;
#define UPDATE_RANGE(xstart, xmax) \
linebuf_init_line(lb, y); \
if (!update_cell_range_data(modes, spm, lb->line, (xstart), (xmax), color_profile, default_bg, default_fg, data)) return false;
if (self->screen_changed || force_screen_refresh) {
for (y = 0; y < self->ynum; y++) {
UPDATE_RANGE(0, self->xnum - 1);
}
} else {
for (y = 0; y < self->ynum; y++) {
if (self->changed_lines[y]) {
UPDATE_RANGE(0, self->xnum - 1);
} else if (self->lines_with_changed_cells[y]) {
start = -1;
bool *line = self->changed_cells + y * self->xnum;
for (unsigned int i = 0; i < self->xnum; i++) {
if (line[i]) {
if (start == -1) {
start = i;
}
} else {
if (start != -1) {
UPDATE_RANGE(start, i - 1);
start = -1;
}
}
}
if (start != -1) {
UPDATE_RANGE(start, self->xnum - 1);
}
}
}
}
tracker_reset(self);
return true;
}
static inline PyObject*
get_ranges(bool *line, unsigned int xnum) {
PyObject *ans = PyList_New(0), *t;
Py_ssize_t start = -1;
if (ans == NULL) return PyErr_NoMemory();
#define APPEND_RANGE(x) \
t = Py_BuildValue("nI", start, x); \
if (t == NULL) { Py_CLEAR(ans); return NULL; } \
if (PyList_Append(ans, t) != 0) { Py_CLEAR(ans); Py_CLEAR(t); return NULL; } \
Py_CLEAR(t);
for (unsigned int i = 0; i < xnum; i++) {
if (line[i]) {
if (start == -1) {
start = i;
}
} else {
if (start != -1) {
APPEND_RANGE(i - 1);
start = -1;
}
}
}
if (start != -1) {
APPEND_RANGE(xnum - 1);
}
return ans;
}
PyObject*
tracker_consolidate_changes(ChangeTracker *self) {
PyObject *ans = PyDict_New();
if (ans == NULL) return PyErr_NoMemory();
if (PyDict_SetItemString(ans, "screen", self->screen_changed ? Py_True : Py_False) != 0) { Py_CLEAR(ans); return NULL; }
if (PyDict_SetItemString(ans, "cursor", self->cursor_changed ? Py_True : Py_False) != 0) { Py_CLEAR(ans); return NULL; }
PyObject *t = PyLong_FromUnsignedLong((unsigned long)self->history_line_added_count);
if (t == NULL) { Py_CLEAR(ans); return NULL; }
if (PyDict_SetItemString(ans, "history_line_added_count", t) != 0) { Py_CLEAR(t); Py_CLEAR(ans); return NULL; }
Py_CLEAR(t);
// Changed lines
Py_ssize_t num = 0;
if (!self->screen_changed) {
for (unsigned int i = 0; i < self->ynum; i++) { num += self->changed_lines[i]; }
}
t = PyTuple_New(num);
if (t == NULL) { Py_CLEAR(ans); return NULL; }
if (num > 0) {
for (unsigned int i = 0, j=0; i < self->ynum; i++) {
if (self->changed_lines[i]) {
PyObject *n = PyLong_FromUnsignedLong(i);
if (n == NULL) { Py_CLEAR(t); Py_CLEAR(ans); return NULL; }
PyTuple_SET_ITEM(t, j++, n);
}
}
}
if (PyDict_SetItemString(ans, "lines", t) != 0) { Py_CLEAR(t); Py_CLEAR(ans); return NULL; }
Py_CLEAR(t);
// Changed cells
t = PyDict_New();
if (t == NULL) { Py_CLEAR(ans); return PyErr_NoMemory(); }
if (!self->screen_changed) {
for (unsigned int i = 0; i < self->ynum; i++) {
if (self->lines_with_changed_cells[i] && !self->changed_lines[i]) {
PyObject *ranges = get_ranges(self->changed_cells + i * self->xnum, self->xnum);
if (ranges == NULL) { Py_CLEAR(t); Py_CLEAR(ans); return NULL; }
PyObject *key = PyLong_FromUnsignedLong(i);
if (key == NULL) { Py_CLEAR(t); Py_CLEAR(ans); Py_CLEAR(ranges); return NULL; }
if (PyDict_SetItem(t, key, ranges) != 0) { Py_CLEAR(key); Py_CLEAR(t); Py_CLEAR(ans); Py_CLEAR(ranges); return NULL; }
Py_CLEAR(key); Py_CLEAR(ranges);
}
}
}
if (PyDict_SetItemString(ans, "cells", t) != 0) { Py_CLEAR(t); Py_CLEAR(ans); return NULL; }
Py_CLEAR(t);
tracker_reset(self);
return ans;
}
// Boilerplate {{{
BOOL_GETSET(ChangeTracker, dirty)
static PyGetSetDef getseters[] = {
GETSET(dirty)
{NULL} /* Sentinel */
};
static PyMethodDef methods[] = {
METHOD(resize, METH_VARARGS)
METHOD(reset, METH_NOARGS)
METHOD(cursor_changed, METH_NOARGS)
{"consolidate_changes", (PyCFunction)tracker_consolidate_changes, METH_NOARGS, ""},
METHOD(line_added_to_history, METH_NOARGS)
METHOD(update_screen, METH_NOARGS)
METHOD(update_line_range, METH_VARARGS)
METHOD(update_cell_range, METH_VARARGS)
{NULL} /* Sentinel */
};
PyTypeObject ChangeTracker_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "fast_data_types.ChangeTracker",
.tp_basicsize = sizeof(ChangeTracker),
.tp_dealloc = (destructor)dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = "ChangeTracker",
.tp_methods = methods,
.tp_getset = getseters,
.tp_new = new,
};
INIT_TYPE(ChangeTracker)
// }}}
ChangeTracker* alloc_change_tracker(unsigned int ynum, unsigned int xnum) {
ChangeTracker *self = (ChangeTracker *)(&ChangeTracker_Type)->tp_alloc((&ChangeTracker_Type), 0);
if (!tracker_resize(self, ynum, xnum)) { Py_CLEAR(self); return NULL; }
return self;
}

View File

@@ -1,55 +0,0 @@
/*
* tracker.h
* Copyright (C) 2016 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#pragma once
static inline void tracker_cursor_changed(ChangeTracker *self) {
self->cursor_changed = true;
self->dirty = true;
}
static inline void tracker_line_added_to_history(ChangeTracker *self) {
self->history_line_added_count++;
self->dirty = true;
}
static inline void tracker_update_screen(ChangeTracker *self) {
self->screen_changed = true;
self->dirty = true;
}
static inline void tracker_update_line_range(ChangeTracker *self, unsigned int first_line, unsigned int last_line) {
if (!self->screen_changed) {
for (unsigned int i = first_line; i <= MIN(self->ynum - 1, last_line); i++) self->changed_lines[i] = true;
self->dirty = true;
}
}
static inline void tracker_update_cell_range(ChangeTracker *self, unsigned int line, unsigned int first_cell, unsigned int last_cell) {
if (!self->screen_changed && line < self->ynum && !self->changed_lines[line]) {
self->lines_with_changed_cells[line] = true;
unsigned int base = line * self->xnum;
for (unsigned int i = first_cell; i <= MIN(self->xnum - 1, last_cell); i++) self->changed_cells[base + i] = true;
self->dirty = true;
}
}
#define RESET_STATE_VARS(self) \
self->screen_changed = false; self->cursor_changed = false; self->dirty = false; self->history_line_added_count = 0;
static inline void tracker_reset(ChangeTracker *self) {
self->screen_changed = false; self->cursor_changed = false; self->dirty = false;
self->history_line_added_count = 0;
memset(self->changed_lines, 0, self->ynum * sizeof(bool));
memset(self->changed_cells, 0, self->ynum * self->xnum * sizeof(bool));
memset(self->lines_with_changed_cells, 0, self->ynum * sizeof(bool));
RESET_STATE_VARS(self);
}
PyObject* tracker_consolidate_changes(ChangeTracker *self);
bool tracker_resize(ChangeTracker *self, unsigned int ynum, unsigned int xnum);
bool tracker_update_cell_data(ScreenModes*, ChangeTracker *, LineBuf *, SpriteMap *, ColorProfile *, unsigned int *, unsigned long, unsigned long, bool);

View File

@@ -1,11 +1,41 @@
#pragma once
static inline bool is_combining_char(uint32_t ch) {
return (0x300 <= ch && ch <= 0x34e)||(0x350 <= ch && ch <= 0x36f)||(0x483 <= ch && ch <= 0x487)||(0x591 <= ch && ch <= 0x5bd)||(0x5c1 <= ch && ch <= 0x5c2)||(0x5c4 <= ch && ch <= 0x5c5)||(0x610 <= ch && ch <= 0x61a)||(0x64b <= ch && ch <= 0x65f)||(0x6d6 <= ch && ch <= 0x6dc)||(0x6df <= ch && ch <= 0x6e4)||(0x6e7 <= ch && ch <= 0x6e8)||(0x6ea <= ch && ch <= 0x6ed)||(0x730 <= ch && ch <= 0x74a)||(0x7eb <= ch && ch <= 0x7f3)||(0x816 <= ch && ch <= 0x819)||(0x81b <= ch && ch <= 0x823)||(0x825 <= ch && ch <= 0x827)||(0x829 <= ch && ch <= 0x82d)||(0x859 <= ch && ch <= 0x85b)||(0x8e3 <= ch && ch <= 0x8ff)||(0x951 <= ch && ch <= 0x954)||(0xc55 <= ch && ch <= 0xc56)||(0xe38 <= ch && ch <= 0xe3a)||(0xe48 <= ch && ch <= 0xe4b)||(0xeb8 <= ch && ch <= 0xeb9)||(0xec8 <= ch && ch <= 0xecb)||(0xf18 <= ch && ch <= 0xf19)||(0xf71 <= ch && ch <= 0xf72)||(0xf7a <= ch && ch <= 0xf7d)||(0xf82 <= ch && ch <= 0xf84)||(0xf86 <= ch && ch <= 0xf87)||(0x1039 <= ch && ch <= 0x103a)||(0x135d <= ch && ch <= 0x135f)||(0x1939 <= ch && ch <= 0x193b)||(0x1a17 <= ch && ch <= 0x1a18)||(0x1a75 <= ch && ch <= 0x1a7c)||(0x1ab0 <= ch && ch <= 0x1abd)||(0x1b6b <= ch && ch <= 0x1b73)||(0x1baa <= ch && ch <= 0x1bab)||(0x1bf2 <= ch && ch <= 0x1bf3)||(0x1cd0 <= ch && ch <= 0x1cd2)||(0x1cd4 <= ch && ch <= 0x1ce0)||(0x1ce2 <= ch && ch <= 0x1ce8)||(0x1cf8 <= ch && ch <= 0x1cf9)||(0x1dc0 <= ch && ch <= 0x1df5)||(0x1dfc <= ch && ch <= 0x1dff)||(0x20d0 <= ch && ch <= 0x20dc)||(0x20e5 <= ch && ch <= 0x20f0)||(0x2cef <= ch && ch <= 0x2cf1)||(0x2de0 <= ch && ch <= 0x2dff)||(0x302a <= ch && ch <= 0x302f)||(0x3099 <= ch && ch <= 0x309a)||(0xa674 <= ch && ch <= 0xa67d)||(0xa69e <= ch && ch <= 0xa69f)||(0xa6f0 <= ch && ch <= 0xa6f1)||(0xa8e0 <= ch && ch <= 0xa8f1)||(0xa92b <= ch && ch <= 0xa92d)||(0xaab2 <= ch && ch <= 0xaab4)||(0xaab7 <= ch && ch <= 0xaab8)||(0xaabe <= ch && ch <= 0xaabf)||(0xfe20 <= ch && ch <= 0xfe2f)||(0x10376 <= ch && ch <= 0x1037a)||(0x10a38 <= ch && ch <= 0x10a3a)||(0x10ae5 <= ch && ch <= 0x10ae6)||(0x110b9 <= ch && ch <= 0x110ba)||(0x11100 <= ch && ch <= 0x11102)||(0x11133 <= ch && ch <= 0x11134)||(0x11235 <= ch && ch <= 0x11236)||(0x112e9 <= ch && ch <= 0x112ea)||(0x11366 <= ch && ch <= 0x1136c)||(0x11370 <= ch && ch <= 0x11374)||(0x114c2 <= ch && ch <= 0x114c3)||(0x115bf <= ch && ch <= 0x115c0)||(0x116b6 <= ch && ch <= 0x116b7)||(0x16af0 <= ch && ch <= 0x16af4)||(0x16b30 <= ch && ch <= 0x16b36)||(0x1d165 <= ch && ch <= 0x1d169)||(0x1d16d <= ch && ch <= 0x1d172)||(0x1d17b <= ch && ch <= 0x1d182)||(0x1d185 <= ch && ch <= 0x1d18b)||(0x1d1aa <= ch && ch <= 0x1d1ad)||(0x1d242 <= ch && ch <= 0x1d244)||(0x1e8d0 <= ch && ch <= 0x1e8d6) || (ch == 0x5bf)||(ch == 0x5c7)||(ch == 0x670)||(ch == 0x711)||(ch == 0x93c)||(ch == 0x94d)||(ch == 0x9bc)||(ch == 0x9cd)||(ch == 0xa3c)||(ch == 0xa4d)||(ch == 0xabc)||(ch == 0xacd)||(ch == 0xb3c)||(ch == 0xb4d)||(ch == 0xbcd)||(ch == 0xc4d)||(ch == 0xcbc)||(ch == 0xccd)||(ch == 0xd4d)||(ch == 0xdca)||(ch == 0xf35)||(ch == 0xf37)||(ch == 0xf39)||(ch == 0xf74)||(ch == 0xf80)||(ch == 0xfc6)||(ch == 0x1037)||(ch == 0x108d)||(ch == 0x1714)||(ch == 0x1734)||(ch == 0x17d2)||(ch == 0x17dd)||(ch == 0x18a9)||(ch == 0x1a60)||(ch == 0x1a7f)||(ch == 0x1b34)||(ch == 0x1b44)||(ch == 0x1be6)||(ch == 0x1c37)||(ch == 0x1ced)||(ch == 0x1cf4)||(ch == 0x20e1)||(ch == 0x2d7f)||(ch == 0xa66f)||(ch == 0xa806)||(ch == 0xa8c4)||(ch == 0xa953)||(ch == 0xa9b3)||(ch == 0xa9c0)||(ch == 0xaab0)||(ch == 0xaac1)||(ch == 0xaaf6)||(ch == 0xabed)||(ch == 0xfb1e)||(ch == 0x101fd)||(ch == 0x102e0)||(ch == 0x10a0d)||(ch == 0x10a0f)||(ch == 0x10a3f)||(ch == 0x11046)||(ch == 0x1107f)||(ch == 0x11173)||(ch == 0x111c0)||(ch == 0x111ca)||(ch == 0x1133c)||(ch == 0x1134d)||(ch == 0x1163f)||(ch == 0x1172b)||(ch == 0x1bc9e);
#include <unictype.h>
#include <uninorm.h>
static inline bool
is_combining_char(uint32_t ch) {
return uc_combining_class(ch) != UC_CCC_NR;
}
static inline bool is_ignored_char(uint32_t ch) {
return (0x0 < ch && ch <= 0x1f)||(0x7f <= ch && ch <= 0x9f)||(0x600 <= ch && ch <= 0x605)||(0x200b <= ch && ch <= 0x200f)||(0x202a <= ch && ch <= 0x202e)||(0x2060 <= ch && ch <= 0x2064)||(0x2066 <= ch && ch <= 0x206f)||(0xd800 <= ch && ch <= 0xdfff)||(0xfff9 <= ch && ch <= 0xfffb)||(0x1bca0 <= ch && ch <= 0x1bca3)||(0x1d173 <= ch && ch <= 0x1d17a)||(0xe0020 <= ch && ch <= 0xe007f) || (ch == 0xad)||(ch == 0x61c)||(ch == 0x6dd)||(ch == 0x70f)||(ch == 0x180e)||(ch == 0xfeff)||(ch == 0x110bd)||(ch == 0xe0001);
static inline bool
is_ignored_char(uint32_t ch) {
return uc_is_general_category_withtable(ch, UC_CATEGORY_MASK_Cc | UC_CATEGORY_MASK_Cf | UC_CATEGORY_MASK_Cs);
}
static inline bool
is_word_char(uint32_t ch) {
return uc_is_general_category_withtable(ch, UC_CATEGORY_MASK_L | UC_CATEGORY_MASK_N);
}
static inline bool
is_url_char(uint32_t ch) {
return ch && !uc_is_general_category_withtable(ch, UC_CATEGORY_MASK_C | UC_CATEGORY_MASK_Z);
}
static inline uint32_t
normalize(uint32_t ch, uint32_t cc1, uint32_t cc2) {
uint32_t ans = uc_composition(ch, cc1);
if (ans && cc2) ans = uc_composition(ans, cc2);
return ans;
}
static inline bool
can_strip_from_end_of_url(uint32_t ch) {
// remove trailing punctuation
return (
(uc_is_general_category_withtable(ch, UC_CATEGORY_MASK_P) && ch != '/') ||
ch == '>'
) ? true : false;
}

View File

@@ -6,17 +6,27 @@ import math
import os
import re
import shlex
import signal
import string
import subprocess
from contextlib import contextmanager
from functools import lru_cache
from time import monotonic
from .constants import isosx
from .fast_data_types import glfw_get_physical_dpi, wcwidth as wcwidth_impl
from .constants import isosx, iswayland, selection_clipboard_funcs
from .fast_data_types import (
GLSL_VERSION, glfw_get_physical_dpi, redirect_std_streams,
wcwidth as wcwidth_impl
)
from .rgb import Color, to_color
BASE = os.path.dirname(os.path.abspath(__file__))
def load_shaders(name):
vert = open(os.path.join(BASE, '{}_vertex.glsl'.format(name))).read().replace('GLSL_VERSION', str(GLSL_VERSION), 1)
frag = open(os.path.join(BASE, '{}_fragment.glsl'.format(name))).read().replace('GLSL_VERSION', str(GLSL_VERSION), 1)
return vert, frag
def safe_print(*a, **k):
try:
@@ -37,6 +47,13 @@ def wcwidth(c: str) -> int:
return wcwidth_impl(ord(c[0]))
@lru_cache()
def pt_to_px(pts):
dpix, dpiy = get_dpi()['logical']
dpi = (dpix + dpiy) / 2
return round(pts * dpi / 72)
@contextmanager
def timeit(name, do_timing=False):
if do_timing:
@@ -50,17 +67,71 @@ def sanitize_title(x):
return re.sub(r'\s+', ' ', re.sub(r'[\0-\x19]', '', x))
@lru_cache()
def load_libx11():
import ctypes
from ctypes.util import find_library
libx11 = ctypes.CDLL(find_library('X11'))
ans = []
def cdef(name, restype, *argtypes):
f = getattr(libx11, name)
if restype is not None:
f.restype = restype
if argtypes:
f.argtypes = argtypes
ans.append(f)
cdef('XOpenDisplay', ctypes.c_void_p, ctypes.c_char_p)
cdef('XCloseDisplay', ctypes.c_int, ctypes.c_void_p)
cdef('XResourceManagerString', ctypes.c_char_p, ctypes.c_void_p)
return ans
def parse_xrdb(raw):
q = 'Xft.dpi:\t'
for line in raw.decode('utf-8').splitlines():
if line.startswith(q):
return float(line[len(q):])
def x11_dpi_native():
XOpenDisplay, XCloseDisplay, XResourceManagerString = load_libx11()
display = XOpenDisplay(None)
if display is None:
raise RuntimeError('Could not connect to the X server')
try:
raw = XResourceManagerString(display)
return parse_xrdb(raw)
finally:
XCloseDisplay(display)
def x11_dpi():
try:
return x11_dpi_native()
except Exception:
pass
try:
raw = subprocess.check_output(['xrdb', '-query'], stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
return parse_xrdb(raw)
except Exception:
pass
def get_logical_dpi():
if not hasattr(get_logical_dpi, 'ans'):
if isosx:
# TODO: Investigate if this needs a different implementation on OS X
if isosx or iswayland:
# TODO: Investigate if this needs a different implementation on OS X or Wayland
get_logical_dpi.ans = glfw_get_physical_dpi()
else:
raw = subprocess.check_output(['xdpyinfo']).decode('utf-8')
m = re.search(
r'^\s*resolution:\s*(\d+)+x(\d+)', raw, flags=re.MULTILINE
)
get_logical_dpi.ans = int(m.group(1)), int(m.group(2))
# See https://github.com/glfw/glfw/issues/1019 for why we cant use
# glfw_get_physical_dpi()
dpi = x11_dpi()
if dpi is None:
get_logical_dpi.ans = glfw_get_physical_dpi()
else:
get_logical_dpi.ans = dpi, dpi
return get_logical_dpi.ans
@@ -92,35 +163,32 @@ def parse_color_set(raw):
continue
def pipe2():
try:
read_fd, write_fd = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC)
except AttributeError:
import fcntl
read_fd, write_fd = os.pipe()
for fd in (read_fd, write_fd):
flag = fcntl.fcntl(fd, fcntl.F_GETFD)
fcntl.fcntl(fd, fcntl.F_SETFD, flag | fcntl.FD_CLOEXEC)
flag = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, flag | os.O_NONBLOCK)
return read_fd, write_fd
def handle_unix_signals():
read_fd, write_fd = pipe2()
for sig in (signal.SIGINT, signal.SIGTERM):
signal.signal(sig, lambda x, y: None)
signal.siginterrupt(sig, False)
signal.set_wakeup_fd(write_fd)
return read_fd
def set_primary_selection(text):
if isosx:
return # There is no primary selection on OS X
if isinstance(text, str):
text = text.encode('utf-8')
s = selection_clipboard_funcs()[1]
if s is None:
p = subprocess.Popen(['xsel', '-i', '-p'], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
p.stdin.write(text), p.stdin.close()
p.wait()
else:
s(text)
def get_primary_selection():
if isosx:
return '' # There is no primary selection on OS X
# glfw has no way to get the primary selection
# https://github.com/glfw/glfw/issues/894
return subprocess.check_output(['xsel', '-p']).decode('utf-8')
g = selection_clipboard_funcs()[0]
if g is None:
ans = subprocess.check_output(['xsel', '-p'], stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL).decode('utf-8')
if ans:
# Without this for some reason repeated pastes dont work
set_primary_selection(ans)
else:
ans = (g() or b'').decode('utf-8', 'replace')
return ans
def base64_encode(
@@ -137,14 +205,11 @@ def base64_encode(
return ans
def set_primary_selection(text):
if isosx:
return # There is no primary selection on OS X
if isinstance(text, str):
text = text.encode('utf-8')
p = subprocess.Popen(['xsel', '-i', '-p'], stdin=subprocess.PIPE)
p.stdin.write(text), p.stdin.close()
p.wait()
def open_cmd(cmd, arg=None):
if arg is not None:
cmd = list(cmd)
cmd.append(arg)
return subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).wait()
def open_url(url, program='default'):
@@ -152,5 +217,21 @@ def open_url(url, program='default'):
cmd = ['open'] if isosx else ['xdg-open']
else:
cmd = shlex.split(program)
cmd.append(url)
subprocess.Popen(cmd).wait()
return open_cmd(cmd, url)
def detach(fork=True, setsid=True, redirect=True):
if fork:
# Detach from the controlling process.
if os.fork() != 0:
raise SystemExit(0)
if setsid:
os.setsid()
if redirect:
redirect_std_streams(os.devnull)
def adjust_line_height(cell_height, val):
if isinstance(val, int):
return cell_height + val
return int(cell_height * val)

View File

@@ -2,25 +2,33 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import os
import sys
import weakref
from collections import deque
from functools import partial
from time import monotonic
from enum import Enum
from itertools import count
from .char_grid import CharGrid, DynamicColor
from .constants import wakeup, get_boss, appname, WindowGeometry, is_key_pressed, mouse_button_pressed, cell_size
from .fast_data_types import (
BRACKETED_PASTE_START, BRACKETED_PASTE_END, Screen, read_bytes_dump,
read_bytes, GLFW_MOD_SHIFT, GLFW_MOUSE_BUTTON_1, GLFW_PRESS,
GLFW_MOUSE_BUTTON_MIDDLE, GLFW_RELEASE, glfw_post_empty_event,
GLFW_MOUSE_BUTTON_5, ANY_MODE, MOTION_MODE, GLFW_KEY_LEFT_SHIFT,
GLFW_KEY_RIGHT_SHIFT, GLFW_KEY_UP, GLFW_KEY_DOWN, GLFW_MOUSE_BUTTON_4
from .config import build_ansi_color_table
from .constants import (
ScreenGeometry, WindowGeometry, appname, cell_size, get_boss,
viewport_size, wakeup
)
from .keys import get_key_map
from .mouse import encode_mouse_event, PRESS, RELEASE, MOVE, DRAG
from .fast_data_types import (
BRACKETED_PASTE_END, BRACKETED_PASTE_START, CELL_BACKGROUND_PROGRAM,
CELL_FOREGROUND_PROGRAM, CELL_PROGRAM, CELL_SPECIAL_PROGRAM,
CURSOR_PROGRAM, GRAPHICS_PROGRAM, SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE,
Screen, compile_program, create_cell_vao, create_graphics_vao,
glfw_post_empty_event, init_cell_program, init_cursor_program, remove_vao,
set_window_render_data, update_window_title, update_window_visibility
)
from .rgb import to_color
from .terminfo import get_capabilities
from .utils import sanitize_title, get_primary_selection, parse_color_set, safe_print
from .utils import color_as_int, load_shaders, parse_color_set, sanitize_title, open_url, open_cmd
class DynamicColor(Enum):
default_fg, default_bg, cursor_color, highlight_fg, highlight_bg = range(1, 6)
DYNAMIC_COLOR_CODES = {
10: DynamicColor.default_fg,
@@ -30,11 +38,46 @@ DYNAMIC_COLOR_CODES = {
19: DynamicColor.highlight_fg,
}
DYNAMIC_COLOR_CODES.update({k+100: v for k, v in DYNAMIC_COLOR_CODES.items()})
window_counter = count()
next(window_counter)
def calculate_gl_geometry(window_geometry, viewport_width, viewport_height, cell_width, cell_height):
dx, dy = 2 * cell_width / viewport_width, 2 * cell_height / viewport_height
xmargin = window_geometry.left / viewport_width
ymargin = window_geometry.top / viewport_height
xstart = -1 + 2 * xmargin
ystart = 1 - 2 * ymargin
return ScreenGeometry(xstart, ystart, window_geometry.xnum, window_geometry.ynum, dx, dy)
def load_shader_programs():
v, f = load_shaders('cell')
compile_program(GRAPHICS_PROGRAM, *load_shaders('graphics'))
for which, p in {
'ALL': CELL_PROGRAM, 'BACKGROUND': CELL_BACKGROUND_PROGRAM, 'SPECIAL': CELL_SPECIAL_PROGRAM,
'FOREGROUND': CELL_FOREGROUND_PROGRAM
}.items():
vv, ff = v.replace('WHICH_PROGRAM', which), f.replace('WHICH_PROGRAM', which)
compile_program(p, vv, ff)
init_cell_program()
compile_program(CURSOR_PROGRAM, *load_shaders('cursor'))
init_cursor_program()
def setup_colors(screen, opts):
screen.color_profile.update_ansi_color_table(build_ansi_color_table(opts))
screen.color_profile.set_configured_colors(*map(color_as_int, (
opts.foreground, opts.background, opts.cursor, opts.selection_foreground, opts.selection_background)))
class Window:
def __init__(self, tab, child, opts, args):
self.id = next(window_counter)
self.vao_id = create_cell_vao()
self.gvao_id = create_graphics_vao()
self.tab_id = tab.id
self.tabref = weakref.ref(tab)
self.override_title = None
self.last_mouse_cursor_pos = 0, 0
@@ -43,30 +86,19 @@ class Window:
self.geometry = WindowGeometry(0, 0, 0, 0, 0, 0)
self.needs_layout = True
self.title = appname
self._is_visible_in_layout = True
self.is_visible_in_layout = True
self.child, self.opts = child, opts
self.child_fd = child.child_fd
self.start_visual_bell_at = None
self.screen = Screen(self, 24, 80, opts.scrollback_lines)
self.read_bytes = partial(read_bytes_dump, self.dump_commands) if args.dump_commands or args.dump_bytes else read_bytes
if args.dump_bytes:
self.dump_bytes_to = open(args.dump_bytes, 'ab')
self.draw_dump_buf = []
self.write_buf = memoryview(b'')
self.char_grid = CharGrid(self.screen, opts)
self.screen = Screen(self, 24, 80, opts.scrollback_lines, self.id)
setup_colors(self.screen, opts)
def __repr__(self):
return 'Window(title={}, id={})'.format(self.title, hex(id(self)))
return 'Window(title={}, id={})'.format(self.title, self.id)
@property
def is_visible_in_layout(self):
return self._is_visible_in_layout
@is_visible_in_layout.setter
def is_visible_in_layout(self, val):
def set_visible_in_layout(self, window_idx, val):
val = bool(val)
if val != self._is_visible_in_layout:
self._is_visible_in_layout = val
if val is not self.is_visible_in_layout:
self.is_visible_in_layout = val
update_window_visibility(self.tab_id, window_idx, val)
if val:
self.refresh()
@@ -74,16 +106,26 @@ class Window:
self.screen.mark_as_dirty()
wakeup()
def set_geometry(self, new_geometry):
def update_position(self, window_geometry):
self.screen_geometry = sg = calculate_gl_geometry(window_geometry, viewport_size.width, viewport_size.height, cell_size.width, cell_size.height)
return sg
def set_geometry(self, window_idx, new_geometry):
if self.destroyed:
return
if self.needs_layout or new_geometry.xnum != self.screen.columns or new_geometry.ynum != self.screen.lines:
boss = get_boss()
self.screen.resize(new_geometry.ynum, new_geometry.xnum)
self.child.resize_pty(self.screen.columns, self.screen.lines,
max(0, new_geometry.right - new_geometry.left), max(0, new_geometry.bottom - new_geometry.top))
self.char_grid.resize(new_geometry)
current_pty_size = (
self.screen.lines, self.screen.columns,
max(0, new_geometry.right - new_geometry.left), max(0, new_geometry.bottom - new_geometry.top))
sg = self.update_position(new_geometry)
self.needs_layout = False
boss.child_monitor.resize_pty(self.id, *current_pty_size)
else:
self.char_grid.update_position(new_geometry)
self.geometry = new_geometry
sg = self.update_position(new_geometry)
self.geometry = g = new_geometry
set_window_render_data(self.tab_id, window_idx, self.vao_id, self.gvao_id, sg.xstart, sg.ystart, sg.dx, sg.dy, self.screen, *g[:4])
def contains(self, x, y):
g = self.geometry
@@ -92,51 +134,24 @@ class Window:
def close(self):
get_boss().close_window(self)
def destroy(self):
def on_child_death(self):
if self.destroyed:
return
self.destroyed = True
self.child.hangup()
self.child.get_child_status() # Ensure child does not become zombie
# At this point this window can still render to screen using its
# existing buffers in char_grid. The rest of the cleanup must be
# performed in the GUI thread.
def read_ready(self):
if self.read_bytes(self.screen, self.child_fd) is False:
self.close() # EOF
def write_ready(self):
while self.write_buf:
try:
n = os.write(self.child_fd, self.write_buf)
except BlockingIOError:
n = 0
if not n:
return
self.write_buf = self.write_buf[n:]
# Remove cycles so that screen is de-allocated immediately
boss = get_boss()
self.screen.reset_callbacks()
boss.gui_close_window(self)
self.screen = None
def write_to_child(self, data):
self.write_buf = memoryview(self.write_buf.tobytes() + data)
wakeup()
def bell(self):
if self.opts.enable_audio_bell:
try:
with open('/dev/tty', 'wb') as f:
f.write(b'\007')
except EnvironmentError:
pass # failure to beep is not critical
if self.opts.visual_bell_duration > 0:
self.start_visual_bell_at = monotonic()
tm = get_boss()
tm.queue_ui_action(tm.request_attention)
glfw_post_empty_event()
if data:
if get_boss().child_monitor.needs_write(self.id, data) is not True:
print('Failed to write to child %d as it does not exist' % self.id, file=sys.stderr)
# screen callbacks {{{
def use_utf8(self, on):
self.child.set_iutf8(on)
def update_screen(self):
self.char_grid.update_cell_data()
glfw_post_empty_event()
get_boss().child_monitor.set_iutf8(self.window_id, on)
def focus_changed(self, focused):
if focused:
@@ -149,6 +164,7 @@ class Window:
def title_changed(self, new_title):
if self.override_title is None:
self.title = sanitize_title(new_title or appname)
update_window_title(self.tab_id, self.id, self.title)
t = self.tabref()
if t is not None:
t.title_changed(self)
@@ -157,6 +173,24 @@ class Window:
def icon_changed(self, new_icon):
pass # TODO: Implement this
def change_colors(self, changes):
dirtied = False
def item(raw):
if raw is None:
return 0
val = to_color(raw)
return None if val is None else (color_as_int(val) << 8) | 2
for which, val in changes.items():
val = item(val)
if val is None:
continue
dirtied = True
setattr(self.screen.color_profile, which.name, val)
if dirtied:
self.screen.mark_as_dirty()
def set_dynamic_color(self, code, value):
if isinstance(value, bytes):
value = value.decode('utf-8')
@@ -168,17 +202,18 @@ class Window:
val = None
color_changes[w] = val
code += 1
self.char_grid.change_colors(color_changes)
self.change_colors(color_changes)
glfw_post_empty_event()
def set_color_table_color(self, code, value):
cp = self.screen.color_profile
if code == 4:
for c, val in parse_color_set(value):
self.char_grid.color_profile.set_color(c, val)
cp.set_color(c, val)
self.refresh()
elif code == 104:
if not value.strip():
self.char_grid.color_profile.reset_color_table()
cp.reset_color_table()
else:
for c in value.split(';'):
try:
@@ -186,120 +221,36 @@ class Window:
except Exception:
continue
if 0 <= c <= 255:
self.char_grid.color_profile.reset_color(c)
cp.reset_color(c)
self.refresh()
def request_capabilities(self, q):
self.write_to_child(get_capabilities(q))
def dispatch_multi_click(self, x, y):
if len(self.click_queue) > 2 and self.click_queue[-1] - self.click_queue[-3] <= 2 * self.opts.click_interval:
self.char_grid.multi_click(3, x, y)
glfw_post_empty_event()
elif len(self.click_queue) > 1 and self.click_queue[-1] - self.click_queue[-2] <= self.opts.click_interval:
self.char_grid.multi_click(2, x, y)
glfw_post_empty_event()
# }}}
def on_mouse_button(self, button, action, mods):
mode = self.screen.mouse_tracking_mode()
handle_event = mods == GLFW_MOD_SHIFT or mode == 0 or button == GLFW_MOUSE_BUTTON_MIDDLE or (
mods == self.opts.open_url_modifiers and button == GLFW_MOUSE_BUTTON_1)
x, y = self.last_mouse_cursor_pos
if handle_event:
if button == GLFW_MOUSE_BUTTON_1:
self.char_grid.update_drag(action == GLFW_PRESS, x, y)
if action == GLFW_RELEASE:
if mods == self.char_grid.opts.open_url_modifiers:
self.char_grid.click_url(x, y)
self.click_queue.append(monotonic())
self.dispatch_multi_click(x, y)
elif button == GLFW_MOUSE_BUTTON_MIDDLE:
if action == GLFW_RELEASE:
self.paste_from_selection()
else:
x, y = self.char_grid.cell_for_pos(x, y)
if x is not None:
ev = encode_mouse_event(mode, self.screen.mouse_tracking_protocol(),
button, PRESS if action == GLFW_PRESS else RELEASE, mods, x, y)
if ev:
self.write_to_child(ev)
def text_for_selection(self):
return ''.join(self.screen.text_for_selection())
def on_mouse_move(self, x, y):
button = None
for b in range(0, GLFW_MOUSE_BUTTON_5 + 1):
if mouse_button_pressed[b]:
button = b
break
action = MOVE if button is None else DRAG
mode = self.screen.mouse_tracking_mode()
send_event = (mode == ANY_MODE or (mode == MOTION_MODE and button is not None)) and not (
is_key_pressed[GLFW_KEY_LEFT_SHIFT] or is_key_pressed[GLFW_KEY_RIGHT_SHIFT])
x, y = max(0, x - self.geometry.left), max(0, y - self.geometry.top)
self.last_mouse_cursor_pos = x, y
tm = get_boss()
tm.queue_ui_action(tm.change_mouse_cursor, self.char_grid.has_url_at(x, y))
if send_event:
x, y = self.char_grid.cell_for_pos(x, y)
if x is not None:
ev = encode_mouse_event(mode, self.screen.mouse_tracking_protocol(),
button, action, 0, x, y)
if ev:
self.write_to_child(ev)
else:
if self.char_grid.current_selection.in_progress:
self.char_grid.update_drag(None, x, y)
margin = cell_size.height // 2
if y <= margin or y >= self.geometry.bottom - margin:
get_boss().timers.add(0.02, self.drag_scroll)
def destroy(self):
if self.vao_id is not None:
remove_vao(self.vao_id)
remove_vao(self.gvao_id)
self.vao_id = self.gvao_id = None
def drag_scroll(self):
x, y = self.last_mouse_cursor_pos
tm = get_boss()
margin = cell_size.height // 2
if y <= margin or y >= self.geometry.bottom - margin:
self.scroll_line_up() if y < 50 else self.scroll_line_down()
self.char_grid.update_drag(None, x, y)
tm.timers.add(0.02, self.drag_scroll)
def buffer_as_ansi(self):
data = []
self.screen.historybuf.as_ansi(data.append)
self.screen.linebuf.as_ansi(data.append)
return ''.join(data)
def on_mouse_scroll(self, x, y):
s = int(round(y * self.opts.wheel_scroll_multiplier))
if abs(s) < 0:
return
upwards = s > 0
if self.screen.is_main_linebuf():
self.char_grid.scroll(abs(s), upwards)
glfw_post_empty_event()
else:
mode = self.screen.mouse_tracking_mode()
send_event = mode > 0
if send_event:
x, y = self.last_mouse_cursor_pos
x, y = self.char_grid.cell_for_pos(x, y)
if x is not None:
ev = encode_mouse_event(mode, self.screen.mouse_tracking_protocol(),
GLFW_MOUSE_BUTTON_4 if upwards else GLFW_MOUSE_BUTTON_5, PRESS, 0, x, y)
if ev:
self.write_to_child(ev)
else:
k = get_key_map(self.screen)[GLFW_KEY_UP if upwards else GLFW_KEY_DOWN]
self.write_to_child(k * abs(s))
def buf_toggled(self, is_main_linebuf):
self.char_grid.scroll('full', False)
def render_cells(self, render_data, program, sprites):
invert_colors = False
if self.start_visual_bell_at is not None:
invert_colors = monotonic() - self.start_visual_bell_at <= self.opts.visual_bell_duration
if not invert_colors:
self.start_visual_bell_at = None
self.char_grid.render_cells(render_data, program, sprites, invert_colors)
def buffer_as_text(self):
return str(self.screen.historybuf) + '\n' + str(self.screen.linebuf)
# actions {{{
def show_scrollback(self):
data = self.char_grid.get_scrollback_as_ansi()
get_boss().display_scrollback(data)
get_boss().display_scrollback(self.buffer_as_ansi().encode('utf-8'))
def paste(self, text):
if text and not self.destroyed:
@@ -309,65 +260,40 @@ class Window:
text = BRACKETED_PASTE_START.encode('ascii') + text + BRACKETED_PASTE_END.encode('ascii')
self.write_to_child(text)
def paste_from_selection(self):
text = get_primary_selection()
if text:
if isinstance(text, bytes):
text = text.decode('utf-8')
self.paste(text)
def copy_to_clipboard(self):
text = self.char_grid.text_for_selection()
text = self.text_for_selection()
if text:
tm = get_boss()
tm.queue_ui_action(tm.glfw_window.set_clipboard_string, text)
get_boss().glfw_window.set_clipboard_string(text)
def pass_selection_to_program(self, *args):
text = self.text_for_selection()
if text:
if args:
open_cmd(args, text)
else:
open_url(text)
def scroll_line_up(self):
if self.screen.is_main_linebuf():
self.char_grid.scroll('line', True)
glfw_post_empty_event()
self.screen.scroll(SCROLL_LINE, True)
def scroll_line_down(self):
if self.screen.is_main_linebuf():
self.char_grid.scroll('line', False)
glfw_post_empty_event()
self.screen.scroll(SCROLL_LINE, False)
def scroll_page_up(self):
if self.screen.is_main_linebuf():
self.char_grid.scroll('page', True)
glfw_post_empty_event()
self.screen.scroll(SCROLL_PAGE, True)
def scroll_page_down(self):
if self.screen.is_main_linebuf():
self.char_grid.scroll('page', False)
glfw_post_empty_event()
self.screen.scroll(SCROLL_PAGE, False)
def scroll_home(self):
if self.screen.is_main_linebuf():
self.char_grid.scroll('full', True)
glfw_post_empty_event()
self.screen.scroll(SCROLL_FULL, True)
def scroll_end(self):
if self.screen.is_main_linebuf():
self.char_grid.scroll('full', False)
glfw_post_empty_event()
# }}}
def dump_commands(self, *a): # {{{
if a:
if a[0] == 'draw':
if a[1] is None:
if self.draw_dump_buf:
safe_print('draw', ''.join(self.draw_dump_buf))
self.draw_dump_buf = []
else:
self.draw_dump_buf.append(a[1])
elif a[0] == 'bytes':
self.dump_bytes_to.write(a[1])
self.dump_bytes_to.flush()
else:
if self.draw_dump_buf:
safe_print('draw', ''.join(self.draw_dump_buf))
self.draw_dump_buf = []
safe_print(*a)
self.screen.scroll(SCROLL_FULL, False)
# }}}

BIN
kitty_tests/Lenna.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

View File

@@ -12,7 +12,7 @@ class Callbacks:
def __init__(self):
self.clear()
def write_to_child(self, data):
def write(self, data):
self.wtcbuf += data
def title_changed(self, data):
@@ -30,16 +30,12 @@ class Callbacks:
def request_capabilities(self, q):
self.qbuf += q
def buf_toggled(self, is_alt):
self.is_alt = is_alt
def use_utf8(self, on):
self.iutf8 = on
def clear(self):
self.wtcbuf = b''
self.iconbuf = self.titlebuf = self.colorbuf = self.qbuf = self.ctbuf = ''
self.is_alt = False
self.iutf8 = True
@@ -72,9 +68,11 @@ def filled_history_buf(ynum=5, xnum=5, cursor=Cursor()):
class BaseTest(TestCase):
ae = TestCase.assertEqual
maxDiff = 2000
def create_screen(self, cols=5, lines=5, scrollback=5):
return Screen(Callbacks(), lines, cols, scrollback)
c = Callbacks()
return Screen(c, lines, cols, scrollback, 0, c)
def assertEqualAttributes(self, c1, c2):
x1, y1, c1.x, c1.y = c1.x, c1.y, 0, 0
@@ -83,17 +81,3 @@ class BaseTest(TestCase):
self.assertEqual(c1, c2)
finally:
c1.x, c1.y, c2.x, c2.y = x1, y1, x2, y2
def assertChanges(self, s, ignore='', **expected_changes):
actual_changes = s.consolidate_changes()
ignore = frozenset(ignore.split())
for k, v in actual_changes.items():
if k not in ignore:
if isinstance(v, dict):
v = {ky: tuple(vy) for ky, vy in v.items()}
if k == 'lines':
v = set(v)
if k in expected_changes:
self.ae(expected_changes[k], v)
else:
self.assertFalse(v, 'The property {} was expected to be empty but is: {}'.format(k, v))

View File

@@ -5,10 +5,14 @@
import os
from unittest import skipIf
from . import BaseTest, filled_line_buf, filled_cursor, filled_history_buf
from kitty.config import build_ansi_color_table, defaults
from kitty.utils import wcwidth, sanitize_title
from kitty.fast_data_types import LineBuf, Cursor as C, REVERSE, ColorProfile, SpriteMap, HistoryBuf, Cursor
from kitty.fast_data_types import (
REVERSE, ColorProfile, Cursor as C, HistoryBuf, LineBuf,
sprite_map_set_layout, sprite_map_set_limits, sprite_position_for
)
from kitty.utils import sanitize_title, wcwidth
from . import BaseTest, filled_cursor, filled_history_buf, filled_line_buf
def create_lbuf(*lines):
@@ -30,7 +34,7 @@ class TestDataTypes(BaseTest):
new.copy_old(old)
self.ae(new.line(0), old.line(1))
new.clear()
self.ae(str(new.line(0)), ' ' * new.xnum)
self.ae(str(new.line(0)), '')
old.set_attribute(REVERSE, False)
for y in range(old.ynum):
for x in range(old.xnum):
@@ -136,22 +140,23 @@ class TestDataTypes(BaseTest):
lb = LineBuf(2, 3)
for y in range(lb.ynum):
line = lb.line(y)
self.ae(str(line), ' ' * lb.xnum)
self.ae(str(line), '')
for x in range(lb.xnum):
self.ae(line[x], ' ')
self.ae(line[x], '\0')
with self.assertRaises(IndexError):
lb.line(lb.ynum)
with self.assertRaises(IndexError):
lb.line(0)[lb.xnum]
l = lb.line(0)
l.set_text(' ', 0, len(' '), C())
l.add_combining_char(0, '1')
self.ae(l[0], ' 1')
l.add_combining_char(0, '2')
self.ae(l[0], ' 12')
l.add_combining_char(0, '3')
self.ae(l[0], ' 13')
self.ae(l[1], ' ')
self.ae(str(l), ' 13 ')
self.ae(l[1], '\0')
self.ae(str(l), ' 13')
t = 'Testing with simple text'
lb = LineBuf(2, len(t))
l = lb.line(0)
@@ -210,10 +215,49 @@ class TestDataTypes(BaseTest):
l.set_char(0, 'x', 1, q)
self.assertEqualAttributes(l.cursor_from(0), q)
def test_url_at(self):
def create(t):
lb = create.lb = LineBuf(1, len(t))
l = lb.line(0)
l.set_text(t, 0, len(t), C())
return l
for trail in '.,]>)\\':
l = create("http://xyz.com" + trail)
self.ae(l.url_end_at(0), len(l) - 2)
l = create("ftp://abc/")
self.ae(l.url_end_at(0), len(l) - 1)
l = create("http://-abcd] ")
self.ae(l.url_end_at(0), len(l) - 3)
def lspace_test(n, scheme='http'):
l = create(' ' * n + scheme + '://acme.com')
for i in range(0, n):
self.ae(l.url_start_at(i), len(l))
for i in range(n, len(l)):
self.ae(l.url_start_at(i), n)
for i in range(7):
for scheme in 'http https ftp file'.split():
lspace_test(i)
l = create('b https://testing.me a')
for s in (0, 1, len(l) - 1, len(l) - 2):
self.ae(l.url_start_at(s), len(l), 'failed with start at: %d' % s)
for s in range(2, len(l) - 2):
self.ae(l.url_start_at(s), 2, 'failed with start at: %d (%s)' % (s, str(l)[s:]))
def no_url(t):
l = create(t)
for s in range(len(l)):
self.ae(l.url_start_at(s), len(l))
no_url('https:// testing.me a')
no_url('h ttp://acme.com')
no_url('http: //acme.com')
no_url('http:/ /acme.com')
def rewrap(self, lb, lb2):
hb = HistoryBuf(lb2.ynum, lb2.xnum)
cy = lb.rewrap(lb2, hb)
return hb, cy
return hb, cy[1]
def test_rewrap_simple(self):
' Same width buffers '
@@ -224,7 +268,7 @@ class TestDataTypes(BaseTest):
self.ae(lb2.line(i), lb.line(i))
lb2 = LineBuf(8, 5)
cy = self.rewrap(lb, lb2)[1]
self.ae(cy, 4)
self.ae(cy, 5)
for i in range(lb.ynum):
self.ae(lb2.line(i), lb.line(i))
empty = LineBuf(1, lb2.xnum)
@@ -232,7 +276,7 @@ class TestDataTypes(BaseTest):
self.ae(str(lb2.line(i)), str(empty.line(0)))
lb2 = LineBuf(3, 5)
cy = self.rewrap(lb, lb2)[1]
self.ae(cy, 2)
self.ae(cy, 3)
for i in range(lb2.ynum):
self.ae(lb2.line(i), lb.line(i + 2))
@@ -253,18 +297,21 @@ class TestDataTypes(BaseTest):
def test_rewrap_wider(self):
' New buffer wider '
lb = create_lbuf('0123 ', '56789')
lb2 = self.line_comparison_rewrap(lb, '0123 5', '6789 ', ' ' * 6)
lb2 = self.line_comparison_rewrap(lb, '0123 5', '6789', '')
self.assertContinued(lb2, False, True)
lb = create_lbuf('12', 'abc')
lb2 = self.line_comparison_rewrap(lb, '12 ', 'abc ')
lb2 = self.line_comparison_rewrap(lb, '12', 'abc')
self.assertContinued(lb2, False, False)
def test_rewrap_narrower(self):
' New buffer narrower '
lb = create_lbuf('123 ', 'abcde')
lb2 = self.line_comparison_rewrap(lb, '123', 'abc', 'de ')
lb = create_lbuf('123', 'abcde')
lb2 = self.line_comparison_rewrap(lb, '123', 'abc', 'de')
self.assertContinued(lb2, False, False, True)
lb = create_lbuf('123 ', 'abcde')
lb2 = self.line_comparison_rewrap(lb, '123', ' a', 'bcd', 'e')
self.assertContinued(lb2, False, True, True, True)
@skipIf('ANCIENT_WCWIDTH' in os.environ, 'wcwidth() is too old')
def test_utils(self):
@@ -280,17 +327,18 @@ class TestDataTypes(BaseTest):
self.ae(c.as_color(255 << 8 | 1), (0xee, 0xee, 0xee))
def test_sprite_map(self):
s = SpriteMap(10, 2)
s.layout(5, 5)
self.ae(s.position_for(0), (0, 0, 0))
self.ae(s.position_for(1), (1, 0, 0))
self.ae(s.position_for(2), (0, 1, 0))
self.ae(s.position_for(3), (1, 1, 0))
self.ae(s.position_for(4), (0, 0, 1))
self.ae(s.position_for(5), (1, 0, 1))
self.ae(s.position_for(0, 1), (0, 1, 1))
self.ae(s.position_for(0, 2), (1, 1, 1))
self.ae(s.position_for(0, 2), (1, 1, 1))
sprite_map_set_limits(10, 2)
sprite_map_set_layout(5, 5)
self.ae(sprite_position_for(0), (0, 0, 0))
self.ae(sprite_position_for(1), (1, 0, 0))
self.ae(sprite_position_for(2), (0, 1, 0))
self.ae(sprite_position_for(3), (1, 1, 0))
self.ae(sprite_position_for(4), (0, 0, 1))
self.ae(sprite_position_for(5), (1, 0, 1))
self.ae(sprite_position_for(0, 1), (0, 1, 1))
self.ae(sprite_position_for(0, 2), (1, 1, 1))
self.ae(sprite_position_for(0, 2), (1, 1, 1))
sprite_map_set_limits(1000, 1000)
def test_historybuf(self):
lb = filled_line_buf()
@@ -340,7 +388,7 @@ class TestDataTypes(BaseTest):
lb.as_ansi(a.append)
self.ae(a, ['\x1b[0m' + str(lb.line(i)) + '\n' for i in range(lb.ynum)])
l = lb.line(0)
c = Cursor()
c = C()
c.bold = c.italic = c.reverse = c.strikethrough = True
c.fg = (4 << 8) | 1
c.bg = (1 << 24) | (2 << 16) | (3 << 8) | 2

72
kitty_tests/gr.py Normal file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import os
import sys
import zlib
from base64 import standard_b64encode
write = getattr(sys.stdout, 'buffer', sys.stdout).write
def clear_screen():
write(b'\033[2J')
def move_cursor(x, y):
write('\033[{};{}H'.format(y, x).encode('ascii'))
def write_gr_cmd(cmd, payload):
cmd = ','.join('{}={}'.format(k, v) for k, v in cmd.items())
w = write
w(b'\033_G'), w(cmd.encode('ascii')), w(b';'), w(payload), w(b'\033\\')
sys.stdout.flush()
def display(data, width, height, x, y, z, ncols=0, nrows=0):
move_cursor(x, y)
cmd = {'a': 'T', 's': width, 'v': height, 'c': ncols, 'r': nrows, 'S': len(data), 'z': z}
data = zlib.compress(data)
cmd['o'] = 'z'
data = standard_b64encode(data)
while data:
chunk, data = data[:4096], data[4096:]
m = 1 if data else 0
cmd['m'] = m
write_gr_cmd(cmd, chunk)
cmd.clear()
def display_png_file(path):
cmd = {'a': 'T', 't': 'f', 'f': '100'}
path = os.path.abspath(path)
if not isinstance(path, bytes):
path = path.encode(sys.getfilesystemencoding() or 'utf-8')
data = standard_b64encode(path)
write_gr_cmd(cmd, data)
def main():
clear_screen()
display(b'\xdd\xdd\xdd\xff', 1, 1, 0, 0, -10, 40, 20)
display(open('logo/kitty.rgba', 'rb').read(), 256, 256, 0, 5, -9)
display(b'\0\0\0\xaa', 1, 1, 0, 7, -8, 40, 3)
move_cursor(5, 8)
print('kitty is \033[3m\033[32mawesome\033[m!')
move_cursor(0, 21)
print('Lenna...')
display_png_file('kitty_tests/Lenna.png')
try:
try:
raw_input()
except NameError:
input()
except (EOFError, KeyboardInterrupt):
pass
if __name__ == '__main__':
os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
main()

328
kitty_tests/graphics.py Normal file
View File

@@ -0,0 +1,328 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import os
import tempfile
import unittest
import zlib
from base64 import standard_b64encode
from io import BytesIO
from kitty.fast_data_types import (
parse_bytes, set_display_state, set_send_to_gpu, shm_unlink, shm_write
)
from . import BaseTest
try:
from PIL import Image
except ImportError:
Image = None
set_send_to_gpu(False)
def relpath(name):
base = os.path.dirname(os.path.abspath(__file__))
return os.path.join(base, name)
def send_command(screen, cmd, payload=b''):
cmd = '\033_G' + cmd
if payload:
if isinstance(payload, str):
payload = payload.encode('utf-8')
payload = standard_b64encode(payload).decode('ascii')
cmd += ';' + payload
cmd += '\033\\'
c = screen.callbacks
c.clear()
parse_bytes(screen, cmd.encode('ascii'))
return c.wtcbuf
def parse_response(res):
if not res:
return
return res.decode('ascii').partition(';')[2].partition('\033')[0]
all_bytes = bytes(bytearray(range(256)))
def byte_block(sz):
d, m = divmod(sz, len(all_bytes))
return (all_bytes * d) + all_bytes[:m]
def load_helpers(self):
s = self.create_screen()
g = s.grman
def l(payload, **kw):
kw.setdefault('i', 1)
cmd = ','.join('%s=%s' % (k, v) for k, v in kw.items())
res = send_command(s, cmd, payload)
return parse_response(res)
def sl(payload, **kw):
if isinstance(payload, str):
payload = payload.encode('utf-8')
data = kw.pop('expecting_data', payload)
cid = kw.setdefault('i', 1)
self.ae('OK', l(payload, **kw))
img = g.image_for_client_id(cid)
self.ae(img['client_id'], cid)
self.ae(img['data'], data)
if 's' in kw:
self.ae((kw['s'], kw['v']), (img['width'], img['height']))
self.ae(img['is_4byte_aligned'], kw.get('f') != 24)
return img
return s, g, l, sl
def put_helpers(self, cw, ch):
iid = 0
def create_screen():
s = self.create_screen(10, 5)
set_display_state(s.columns * cw, s.lines * ch, cw, ch)
return s, 2 / s.columns, 2 / s.lines
def put_cmd(z=0, num_cols=0, num_lines=0, x_off=0, y_off=0, width=0, height=0, cell_x_off=0, cell_y_off=0):
return 'z=%d,c=%d,r=%d,x=%d,y=%d,w=%d,h=%d,X=%d,Y=%d' % (z, num_cols, num_lines, x_off, y_off, width, height, cell_x_off, cell_y_off)
def put_image(screen, w, h, **kw):
nonlocal iid
iid += 1
cmd = 'a=T,f=24,i=%d,s=%d,v=%d,%s' % (iid, w, h, put_cmd(**kw))
data = b'x' * w * h * 3
res = send_command(screen, cmd, data)
return iid, parse_response(res)
def put_ref(screen, **kw):
cmd = 'a=p,i=%d,%s' % (iid, put_cmd(**kw))
send_command(screen, cmd)
def layers(screen, scrolled_by=0, xstart=-1, ystart=1):
return screen.grman.update_layers(scrolled_by, xstart, ystart, dx, dy, screen.columns, screen.lines)
def rect_eq(r, left, top, right, bottom):
for side in 'left top right bottom'.split():
a, b = r[side], locals()[side]
if abs(a - b) > 0.0001:
self.ae(a, b, 'the %s side is not equal' % side)
s, dx, dy = create_screen()
return s, dx, dy, put_image, put_ref, layers, rect_eq
class TestGraphics(BaseTest):
def test_load_images(self):
s, g, l, sl = load_helpers(self)
# Test load query
self.ae(l('abcd', s=1, v=1, a='q'), 'OK')
self.ae(g.image_count, 0)
# Test simple load
for f in 32, 24:
p = 'abc' + ('d' if f == 32 else '')
img = sl(p, s=1, v=1, f=f)
self.ae(bool(img['is_4byte_aligned']), f == 32)
# Test chuunked load
self.assertIsNone(l('abcd', s=2, v=2, m=1))
self.assertIsNone(l('efgh', m=1))
self.assertIsNone(l('ijkl', m=1))
self.ae(l('mnop', m=0), 'OK')
img = g.image_for_client_id(1)
self.ae(img['data'], b'abcdefghijklmnop')
# Test compression
random_data = byte_block(3 * 1024)
compressed_random_data = zlib.compress(random_data)
sl(
compressed_random_data,
s=24,
v=32,
o='z',
expecting_data=random_data
)
# Test chunked + compressed
b = len(compressed_random_data) // 2
self.assertIsNone(l(compressed_random_data[:b], s=24, v=32, o='z', m=1))
self.ae(l(compressed_random_data[b:], m=0), 'OK')
img = g.image_for_client_id(1)
self.ae(img['data'], random_data)
# Test loading from file
f = tempfile.NamedTemporaryFile()
f.write(random_data), f.flush()
sl(f.name, s=24, v=32, t='f', expecting_data=random_data)
self.assertTrue(os.path.exists(f.name))
f.seek(0), f.truncate(), f.write(compressed_random_data), f.flush()
sl(f.name, s=24, v=32, t='t', o='z', expecting_data=random_data)
self.assertRaises(
FileNotFoundError, f.close
) # check that file was deleted
# Test loading from POSIX SHM
name = '/kitty-test-shm'
shm_write(name, random_data)
sl(name, s=24, v=32, t='s', expecting_data=random_data)
self.assertRaises(
FileNotFoundError, shm_unlink, name
) # check that file was deleted
@unittest.skipIf(Image is None, 'PIL not available, skipping PNG tests')
def test_load_png(self):
s, g, l, sl = load_helpers(self)
w, h = 5, 3
rgba_data = byte_block(w * h * 4)
img = Image.frombytes('RGBA', (w, h), rgba_data)
rgb_data = img.convert('RGB').convert('RGBA').tobytes()
def png(mode='RGBA'):
buf = BytesIO()
i = img
if mode != i.mode:
i = img.convert(mode)
i.save(buf, 'PNG')
return buf.getvalue()
for mode in 'RGBA RGB'.split():
data = png(mode)
sl(data, f=100, expecting_data=rgb_data if mode == 'RGB' else rgba_data)
for m in 'LP':
img = img.convert(m)
rgba_data = img.convert('RGBA').tobytes()
data = png(m)
sl(data, f=100, expecting_data=rgba_data)
self.ae(l(b'a' * 20, f=100, S=20).partition(':')[0], 'EBADPNG')
def test_image_put(self):
cw, ch = 10, 20
s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch)
self.ae(put_image(s, 10, 20)[1], 'OK')
l = layers(s)
self.ae(len(l), 1)
rect_eq(l[0]['src_rect'], 0, 0, 1, 1)
rect_eq(l[0]['dest_rect'], -1, 1, -1 + dx, 1 - dy)
self.ae(l[0]['group_count'], 1)
self.ae(s.cursor.x, 1), self.ae(s.cursor.y, 0)
put_ref(s, num_cols=s.columns, x_off=2, y_off=1, width=3, height=5, cell_x_off=3, cell_y_off=1, z=-1)
l = layers(s)
self.ae(len(l), 2)
rect_eq(l[0]['src_rect'], 2 / 10, 1 / 20, (2 + 3) / 10, (1 + 5)/20)
left, top = -1 + dx + 3 * dx / cw, 1 - 1 * dy / ch
rect_eq(l[0]['dest_rect'], left, top, -1 + (1 + s.columns) * dx, top - dy * 5 / ch)
rect_eq(l[1]['src_rect'], 0, 0, 1, 1)
rect_eq(l[1]['dest_rect'], -1, 1, -1 + dx, 1 - dy)
self.ae(l[0]['group_count'], 1), self.ae(l[1]['group_count'], 1)
self.ae(s.cursor.x, 0), self.ae(s.cursor.y, 1)
def test_gr_scroll(self):
cw, ch = 10, 20
s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch)
put_image(s, 10, 20) # a one cell image at (0, 0)
self.ae(len(layers(s)), 1)
for i in range(s.lines):
s.index()
self.ae(len(layers(s)), 0), self.ae(s.grman.image_count, 1)
for i in range(s.historybuf.ynum - 1):
s.index()
self.ae(len(layers(s)), 0), self.ae(s.grman.image_count, 1)
s.index()
self.ae(s.grman.image_count, 0)
# Now test with margins
s.reset()
# Test images outside page area untouched
put_image(s, cw, ch) # a one cell image at (0, 0)
for i in range(s.lines - 1):
s.index()
put_image(s, cw, ch) # a one cell image at (0, bottom)
s.set_margins(2, 4) # 1-based indexing
self.ae(s.grman.image_count, 2)
for i in range(s.lines + s.historybuf.ynum):
s.index()
self.ae(s.grman.image_count, 2)
for i in range(s.lines): # ensure cursor is at top margin
s.reverse_index()
# Test clipped scrolling during index
put_image(s, cw, 2*ch, z=-1) # 1x2 cell image
self.ae(s.grman.image_count, 3)
self.ae(layers(s)[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 1.0})
s.index(), s.index()
l = layers(s)
self.ae(len(l), 3)
self.ae(layers(s)[0]['src_rect'], {'left': 0.0, 'top': 0.5, 'right': 1.0, 'bottom': 1.0})
s.index()
self.ae(s.grman.image_count, 2)
# Test clipped scrolling during reverse_index
for i in range(s.lines):
s.reverse_index()
put_image(s, cw, 2*ch, z=-1) # 1x2 cell image
self.ae(s.grman.image_count, 3)
self.ae(layers(s)[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 1.0})
while s.cursor.y != 1:
s.reverse_index()
s.reverse_index()
self.ae(layers(s)[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 0.5})
s.reverse_index()
self.ae(s.grman.image_count, 2)
def test_gr_reset(self):
cw, ch = 10, 20
s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch)
put_image(s, cw, ch) # a one cell image at (0, 0)
self.ae(len(layers(s)), 1)
s.reset()
self.ae(s.grman.image_count, 0)
put_image(s, cw, ch) # a one cell image at (0, 0)
self.ae(s.grman.image_count, 1)
for i in range(s.lines):
s.index()
s.reset()
self.ae(s.grman.image_count, 1)
def test_gr_delete(self):
cw, ch = 10, 20
s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch)
def delete(ac=None, **kw):
cmd = 'a=d'
if ac:
cmd += ',d={}'.format(ac)
if kw:
cmd += ',' + ','.join('{}={}'.format(k, v) for k, v in kw.items())
send_command(s, cmd)
put_image(s, cw, ch)
delete()
self.ae(len(layers(s)), 0), self.ae(s.grman.image_count, 1)
delete('A')
self.ae(s.grman.image_count, 0)
iid = put_image(s, cw, ch)[0]
delete('I', i=iid)
self.ae(s.grman.image_count, 0)
s.reset()
put_image(s, cw, ch)
put_image(s, cw, ch)
delete('C')
self.ae(s.grman.image_count, 2)
s.cursor_position(1, 1)
delete('C')
self.ae(s.grman.image_count, 1)
delete('P', x=2, y=1)
self.ae(s.grman.image_count, 0)
put_image(s, cw, ch, z=9)
delete('Z', z=9)
self.ae(s.grman.image_count, 0)

80
kitty_tests/keys.py Normal file
View File

@@ -0,0 +1,80 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from functools import partial
import kitty.fast_data_types as defines
from kitty.keys import (
interpret_key_event, modify_complex_key, modify_key_bytes, smkx_key_map
)
from . import BaseTest
class DummyWindow:
def __init__(self):
self.screen = self
self.extended_keyboard = False
self.cursor_key_mode = True
class TestParser(BaseTest):
def test_modify_complex_key(self):
self.ae(modify_complex_key('kcuu1', 4), b'\033[1;4A')
self.ae(modify_complex_key('kcuu1', 3), b'\033[1;3A')
self.ae(modify_complex_key('kf5', 3), b'\033[15;3~')
self.assertRaises(ValueError, modify_complex_key, 'kri', 3)
def test_interpret_key_event(self):
# test rmkx/smkx
w = DummyWindow()
def k(expected, key, mods=0):
actual = interpret_key_event(
getattr(defines, 'GLFW_KEY_' + key),
0,
mods,
w,
defines.GLFW_PRESS,
)
self.ae(b'\033' + expected.encode('ascii'), actual)
for ckm, mch in {True: 'O', False: '['}.items():
w.cursor_key_mode = ckm
for name, ch in {
'UP': 'A',
'DOWN': 'B',
'RIGHT': 'C',
'LEFT': 'D',
'HOME': 'H',
'END': 'F',
}.items():
k(mch + ch, name)
w.cursor_key_mode = True
# test remaining special keys
for key, num in zip('INSERT DELETE PAGE_UP PAGE_DOWN'.split(), '2356'):
k('[' + num + '~', key)
for key, num in zip('1234', 'PQRS'):
k('O' + num, 'F' + key)
for key, num in zip(range(5, 13), (15, 17, 18, 19, 20, 21, 23, 24)):
k('[' + str(num) + '~', 'F{}'.format(key))
# test modifiers
SPECIAL_KEYS = 'UP DOWN RIGHT LEFT HOME END INSERT DELETE PAGE_UP PAGE_DOWN '
for i in range(1, 13):
SPECIAL_KEYS += 'F{} '.format(i)
SPECIAL_KEYS = SPECIAL_KEYS.strip().split()
for mods, num in zip(('CONTROL', 'ALT', 'SHIFT+ALT'), '534'):
fmods = 0
num = int(num)
for m in mods.split('+'):
fmods |= getattr(defines, 'GLFW_MOD_' + m)
km = partial(k, mods=fmods)
for key in SPECIAL_KEYS:
keycode = getattr(defines, 'GLFW_KEY_' + key)
base_key = smkx_key_map[keycode]
km(modify_key_bytes(base_key, num).decode('ascii')[1:], key)

View File

@@ -45,15 +45,15 @@ class TestParser(BaseTest):
pb = partial(self.parse_bytes_dump, s)
pb('12', '12')
self.ae(str(s.line(0)), '12 ')
self.ae(str(s.line(0)), '12')
self.ae(s.cursor.x, 2)
pb('3456', '3456')
self.ae(str(s.line(0)), '12345')
self.ae(str(s.line(1)), '6 ')
self.ae(str(s.line(1)), '6')
pb(b'\n123\n\r45', ('screen_linefeed',), '123', ('screen_linefeed',), ('screen_carriage_return',), '45')
self.ae(str(s.line(1)), '6 ')
self.ae(str(s.line(2)), ' 123 ')
self.ae(str(s.line(3)), '45 ')
self.ae(str(s.line(1)), '6')
self.ae(str(s.line(2)), ' 123')
self.ae(str(s.line(3)), '45')
parse_bytes(s, b'\rabcde')
self.ae(str(s.line(3)), 'abcde')
pb('\rßxyz1', ('screen_carriage_return',), 'ßxyz1')
@@ -65,11 +65,11 @@ class TestParser(BaseTest):
s = self.create_screen()
pb = partial(self.parse_bytes_dump, s)
pb('12\033Da', '12', ('screen_index',), 'a')
self.ae(str(s.line(0)), '12 ')
self.ae(str(s.line(1)), ' a ')
self.ae(str(s.line(0)), '12')
self.ae(str(s.line(1)), ' a')
pb('\033x', ('Unknown char after ESC: 0x%x' % ord('x'),))
pb('\033c123', ('screen_reset', ), '123')
self.ae(str(s.line(0)), '123 ')
self.ae(str(s.line(0)), '123')
def test_charsets(self):
s = self.create_screen()
@@ -79,14 +79,14 @@ class TestParser(BaseTest):
s = self.create_screen()
pb = partial(self.parse_bytes_dump, s)
pb('\033)0\x0e/_', ('screen_designate_charset', 1, ord('0')), ('screen_change_charset', 1), '/_')
self.ae(str(s.line(0)), '/\xa0 ')
self.ae(str(s.line(0)), '/\xa0')
self.assertTrue(s.callbacks.iutf8)
pb('\033%@_', ('screen_use_latin1', 1), '_')
self.assertFalse(s.callbacks.iutf8)
s = self.create_screen()
pb = partial(self.parse_bytes_dump, s)
pb('\033(0/_', ('screen_designate_charset', 0, ord('0')), '/_')
self.ae(str(s.line(0)), '/\xa0 ')
self.ae(str(s.line(0)), '/\xa0')
def test_csi_codes(self):
s = self.create_screen()
@@ -173,7 +173,7 @@ class TestParser(BaseTest):
c.clear()
pb('\033]2;;;;\x07', ('set_title', ';;;'))
self.ae(c.titlebuf, ';;;')
pb('\033]110\x07', ('set_dynamic_color', ''))
pb('\033]110\x07', ('set_dynamic_color', 110, ''))
self.ae(c.colorbuf, '')
def test_dcs_codes(self):
@@ -185,6 +185,52 @@ class TestParser(BaseTest):
def test_oth_codes(self):
s = self.create_screen()
pb = partial(self.parse_bytes_dump, s)
for prefix in '\033_', '\033^', '\u009e', '\u009f':
for prefix in '\033_', '\u009f':
for suffix in '\u009c', '\033\\':
pb('a{}+\\++{}bcde'.format(prefix, suffix), 'abcde')
pb('a{}+\\++{}bcde'.format(prefix, suffix), ('draw', 'a'), ('Unrecognized APC code: 0x2b',), ('draw', 'bcde'))
for prefix in '\033^', '\u009e':
for suffix in '\u009c', '\033\\':
pb('a{}+\\++{}bcde'.format(prefix, suffix), ('draw', 'a'), ('Unrecognized PM code: 0x2b',), ('draw', 'bcde'))
def test_graphics_command(self):
from base64 import standard_b64encode
def enc(x):
return standard_b64encode(x.encode('utf-8') if isinstance(x, str) else x).decode('ascii')
def c(**k):
for p, v in tuple(k.items()):
if isinstance(v, str) and p != 'payload':
k[p] = v.encode('ascii')
for f in 'action delete_action transmission_type compressed'.split():
k.setdefault(f, b'\0')
for f in ('format more id data_sz data_offset width height x_offset y_offset data_height data_width'
' num_cells num_lines cell_x_offset cell_y_offset z_index').split():
k.setdefault(f, 0)
p = k.pop('payload', '').encode('utf-8')
k['payload_sz'] = len(p)
return ('graphics_command', k, p)
def t(cmd, **kw):
pb('\033_G{};{}\033\\'.format(cmd, enc(kw.get('payload', ''))), c(**kw))
def e(cmd, err):
pb('\033_G{}\033\\'.format(cmd), (err,))
s = self.create_screen()
pb = partial(self.parse_bytes_dump, s)
uint32_max = 2**32 - 1
t('i=%d' % uint32_max, id=uint32_max)
e('i=%d' % (uint32_max + 1), 'id is too large')
pb('\033_Gi=12\033\\', c(id=12))
t('a=t,t=d,s=100,z=-9', payload='X', action='t', transmission_type='d', data_width=100, z_index=-9, payload_sz=1)
t('a=t,t=d,s=100,z=9', payload='payload', action='t', transmission_type='d', data_width=100, z_index=9, payload_sz=7)
t('a=t,t=d,s=100,z=9', action='t', transmission_type='d', data_width=100, z_index=9)
e(',s=1', 'Malformed graphics control block, invalid key character: 0x2c')
e('W=1', 'Malformed graphics control block, invalid key character: 0x57')
e('1=1', 'Malformed graphics control block, invalid key character: 0x31')
e('a=t,,w=2', 'Malformed graphics control block, invalid key character: 0x2c')
e('s', 'Malformed graphics control block, no = after key')
e('s=', 'Malformed graphics control block, expecting an integer value')
e('s==', 'Malformed graphics control block, expecting an integer value for key: s')
e('s=1=', 'Malformed graphics control block, expecting a comma or semi-colon after a value, found: 0x3d')

View File

@@ -18,15 +18,13 @@ class TestScreen(BaseTest):
s.draw('a' * 5)
self.ae(str(s.line(0)), 'a' * 5)
self.ae(s.cursor.x, 5), self.ae(s.cursor.y, 0)
self.assertChanges(s, ignore='cursor', cells={0: ((0, 4),)})
s.draw('b' * 7)
self.assertTrue(s.linebuf.is_continued(1))
self.assertTrue(s.linebuf.is_continued(2))
self.ae(str(s.line(0)), 'a' * 5)
self.ae(str(s.line(1)), 'b' * 5)
self.ae(str(s.line(2)), 'b' * 2 + ' ' * 3)
self.ae(str(s.line(2)), 'b' * 2)
self.ae(s.cursor.x, 2), self.ae(s.cursor.y, 2)
self.assertChanges(s, ignore='cursor', cells={1: ((0, 4),), 2: ((0, 1),)})
s.draw('c' * 15)
self.ae(str(s.line(0)), 'b' * 5)
self.ae(str(s.line(1)), 'bbccc')
@@ -37,11 +35,9 @@ class TestScreen(BaseTest):
s.draw('0123456789')
self.ae(str(s.line(0)), '01239')
self.ae(s.cursor.x, 5), self.ae(s.cursor.y, 0)
self.assertChanges(s, ignore='cursor', cells={0: ((0, 4),)})
s.draw('ab')
self.ae(str(s.line(0)), '0123b')
self.ae(s.cursor.x, 5), self.ae(s.cursor.y, 0)
self.assertChanges(s, ignore='cursor', cells={0: ((4, 4),)})
# Now test in insert mode
s.reset(), s.reset_dirty()
@@ -53,7 +49,6 @@ class TestScreen(BaseTest):
s.draw('ab')
self.ae(str(s.line(4)), 'ab123')
self.ae((s.cursor.x, s.cursor.y), (2, 4))
self.assertChanges(s, ignore='cursor', cells={4: ((0, 4),)})
@skipIf('ANCIENT_WCWIDTH' in os.environ, 'wcwidth() is too old')
def test_draw_char(self):
@@ -63,23 +58,19 @@ class TestScreen(BaseTest):
self.ae(str(s.line(0)), 'ココx')
self.ae(tuple(map(s.line(0).width, range(5))), (2, 0, 2, 0, 1))
self.ae(s.cursor.x, 5), self.ae(s.cursor.y, 0)
self.assertChanges(s, ignore='cursor', cells={0: ((0, 4),)})
s.draw('ニチハ')
self.ae(str(s.line(0)), 'ココx')
self.ae(str(s.line(1)), 'ニチ ')
self.ae(str(s.line(2)), ' ')
self.assertChanges(s, ignore='cursor', cells={1: ((0, 3),), 2: ((0, 1),)})
self.ae(str(s.line(1)), 'ニチ')
self.ae(str(s.line(2)), '')
self.ae(s.cursor.x, 2), self.ae(s.cursor.y, 2)
s.draw('Ƶ̧\u0308')
self.ae(str(s.line(2)), 'ハƵ̧\u0308 ')
self.ae(str(s.line(2)), 'ハƵ̧\u0308')
self.ae(s.cursor.x, 3), self.ae(s.cursor.y, 2)
self.assertChanges(s, ignore='cursor', cells={2: ((2, 2),)})
s.draw('xy'), s.draw('\u0306')
self.ae(str(s.line(2)), 'ハƵ̧\u0308xy\u0306')
self.ae(s.cursor.x, 5), self.ae(s.cursor.y, 2)
self.assertChanges(s, ignore='cursor', cells={2: ((3, 4),)})
s.draw('c' * 15)
self.ae(str(s.line(0)), 'ニチ ')
self.ae(str(s.line(0)), 'ニチ')
# Now test without line-wrap
s.reset(), s.reset_dirty()
@@ -87,11 +78,9 @@ class TestScreen(BaseTest):
s.draw('0\u030612345\u03066789\u0306')
self.ae(str(s.line(0)), '0\u03061239\u0306')
self.ae(s.cursor.x, 5), self.ae(s.cursor.y, 0)
self.assertChanges(s, ignore='cursor', cells={0: ((0, 4),)})
s.draw('ab\u0306')
self.ae(str(s.line(0)), '0\u0306123b\u0306')
self.ae(s.cursor.x, 5), self.ae(s.cursor.y, 0)
self.assertChanges(s, ignore='cursor', cells={0: ((4, 4),)})
# Now test in insert mode
s.reset(), s.reset_dirty()
@@ -103,7 +92,6 @@ class TestScreen(BaseTest):
s.draw('a\u0306b')
self.ae(str(s.line(4)), 'a\u0306b1\u030623')
self.ae((s.cursor.x, s.cursor.y), (2, 4))
self.assertChanges(s, ignore='cursor', cells={4: ((0, 4),)})
@skipIf('ANCIENT_WCWIDTH' in os.environ, 'wcwidth() is too old')
def test_char_manipulation(self):
@@ -121,17 +109,14 @@ class TestScreen(BaseTest):
s.insert_characters(2)
self.ae(str(s.line(0)), 'a bc')
self.assertTrue(s.line(0).cursor_from(1).bold)
self.assertChanges(s, ignore='cursor', cells={0: ((1, 4),)})
s.cursor_back(1)
s.insert_characters(20)
self.ae(str(s.line(0)), ' ')
self.assertChanges(s, ignore='cursor', cells={0: ((0, 4),)})
self.ae(str(s.line(0)), '')
s.draw('xココ')
s.cursor_back(5)
s.reset_dirty()
s.insert_characters(1)
self.ae(str(s.line(0)), ' xコ ')
self.assertChanges(s, ignore='cursor', cells={0: ((0, 4),)})
self.ae(str(s.line(0)), ' xコ')
c = Cursor()
c.italic = True
s.line(0).apply_cursor(c, 0, 5)
@@ -141,34 +126,29 @@ class TestScreen(BaseTest):
init()
s.delete_characters(2)
self.ae(str(s.line(0)), 'ade ')
self.ae(str(s.line(0)), 'ade')
self.assertTrue(s.line(0).cursor_from(4).bold)
self.assertFalse(s.line(0).cursor_from(2).bold)
self.assertChanges(s, ignore='cursor', cells={0: ((1, 4),)})
init()
s.erase_characters(2)
self.ae(str(s.line(0)), 'a de')
self.assertTrue(s.line(0).cursor_from(1).bold)
self.assertFalse(s.line(0).cursor_from(4).bold)
self.assertChanges(s, cells={0: ((1, 2),)})
s.erase_characters(20)
self.ae(str(s.line(0)), 'a ')
self.ae(str(s.line(0)), 'a')
init()
s.erase_in_line()
self.ae(str(s.line(0)), 'a ')
self.ae(str(s.line(0)), 'a')
self.assertTrue(s.line(0).cursor_from(1).bold)
self.assertFalse(s.line(0).cursor_from(0).bold)
self.assertChanges(s, cells={0: ((1, 4),)})
init()
s.erase_in_line(1)
self.ae(str(s.line(0)), ' cde')
self.assertChanges(s, cells={0: ((0, 1),)})
init()
s.erase_in_line(2)
self.ae(str(s.line(0)), ' ')
self.assertChanges(s, cells={0: ((0, 4),)})
self.ae(str(s.line(0)), '')
init()
s.erase_in_line(2, True)
self.ae((False, False, False, False, False), tuple(map(lambda i: s.line(0).cursor_from(i).bold, range(5))))
@@ -188,23 +168,19 @@ class TestScreen(BaseTest):
init()
s.erase_in_display()
self.ae(all_lines(s), ('12345', '12 ', ' ', ' ', ' '))
self.assertChanges(s, lines={2, 3, 4}, cells={1: ((2, 4),)})
self.ae(all_lines(s), ('12345', '12', '', '', ''))
init()
s.erase_in_display(1)
self.ae(all_lines(s), (' ', ' 45', '12345', '12345', '12345'))
self.assertChanges(s, lines={0}, cells={1: ((0, 2),)})
self.ae(all_lines(s), ('', ' 45', '12345', '12345', '12345'))
init()
s.erase_in_display(2)
self.ae(all_lines(s), (' ', ' ', ' ', ' ', ' '))
self.assertChanges(s, lines=set(range(5)))
self.ae(all_lines(s), ('', '', '', '', ''))
self.assertTrue(s.line(0).cursor_from(1).bold)
init()
s.erase_in_display(2, True)
self.ae(all_lines(s), (' ', ' ', ' ', ' ', ' '))
self.assertChanges(s, lines=set(range(5)))
self.ae(all_lines(s), ('', '', '', '', ''))
self.assertFalse(s.line(0).cursor_from(1).bold)
def test_cursor_movement(self):
@@ -227,13 +203,13 @@ class TestScreen(BaseTest):
s = self.create_screen()
s.draw('12345' * 5)
s.index()
self.ae(str(s.line(4)), ' ' * 5)
self.ae(str(s.line(4)), '')
for i in range(4):
self.ae(str(s.line(i)), '12345')
s.draw('12345' * 5)
s.cursor_up(5)
s.reverse_index()
self.ae(str(s.line(0)), ' ' * 5)
self.ae(str(s.line(0)), '')
for i in range(1, 5):
self.ae(str(s.line(i)), '12345')
@@ -241,20 +217,25 @@ class TestScreen(BaseTest):
s = self.create_screen(scrollback=6)
s.draw(''.join([str(i) * s.columns for i in range(s.lines)]))
s.resize(3, 10)
self.ae(str(s.line(0)), '0'*5 + '1'*5)
self.ae(str(s.line(1)), '2'*5 + '3'*5)
self.ae(str(s.line(2)), '4'*5 + ' '*5)
self.ae(str(s.line(0)), '2'*5 + '3'*5)
self.ae(str(s.line(1)), '4'*5)
self.ae(str(s.line(2)), ''*5)
s.resize(5, 1)
self.ae(str(s.line(0)), '4')
hb = s.historybuf
for i in range(hb.ynum):
self.ae(str(hb.line(i)), '4' if i == 0 else '3')
s = self.create_screen(scrollback=6)
s.draw(''.join([str(i) * s.columns for i in range(s.lines*2)]))
self.ae(str(s.line(4)), '9'*5)
self.ae(str(hb), '4\n3\n3\n3\n3\n3')
s = self.create_screen(scrollback=20)
s.draw(''.join(str(i) * s.columns for i in range(s.lines*2)))
self.ae(str(s.linebuf), '55555\n66666\n77777\n88888\n99999')
s.resize(5, 2)
self.ae(str(s.line(3)), '9 ')
self.ae(str(s.line(4)), ' ')
self.ae(str(s.linebuf), '88\n99\n99\n9\n')
def test_cursor_after_resize(self):
s = self.create_screen()
s.draw('123'), s.linefeed(), s.carriage_return(), s.draw('123'), s.linefeed(), s.carriage_return()
y_before = s.cursor.y
s.resize(s.lines, s.columns-1)
self.ae(y_before, s.cursor.y)
def test_tab_stops(self):
# Taken from vttest/main.c

View File

@@ -84,9 +84,13 @@ int main(int argc, char *argv[]) {
char lib[PATH_MAX+1] = {0};
char *final_argv[MAX_ARGC + 1] = {0};
wchar_t *argvw[MAX_ARGC + 1] = {0};
#ifdef WITH_PROFILER
num = snprintf(lib, PATH_MAX, "%s%s", exe_dir, "/");
#else
num = snprintf(lib, PATH_MAX, "%s%s", exe_dir, "/../lib/kitty");
#endif
if (num < 0 || num >= PATH_MAX) { fprintf(stderr, "Failed to create path to /../lib/kitty\n"); return 1; }
if (num < 0 || num >= PATH_MAX) { fprintf(stderr, "Failed to create path to kitty lib\n"); return 1; }
final_argv[0] = exe;
final_argv[1] = lib;
for (i = 1, num_args=2; i < argc && i + 1 <= MAX_ARGC; i++) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@@ -70,260 +70,8 @@ link:http://vt100.net/docs/vt510-rm/DECRPM[DECRPM]
== Graphics rendering
The goal of this specification is to create a flexible and performant protocol
that allows the program running in the terminal, hereafter called the _client_,
to render arbitrary pixel (raster) graphics to the screen of the terminal
emulator. The major design goals are
* Should not require terminal emulators to understand image formats.
* Should allow specifying graphics to be drawn per individual character cell. This allows graphics to mix with text using
the existing cursor based protocols.
* Should use optimizations when the client is running on the same computer as the terminal emulator.
For some discussion regarding the design choices, see link:../../issues/33[#33].
=== Getting the window size
In order to know what size of images to display and how to position them, the client must be able to get the
window size in pixels and the number of cells per row and column. This can be done by using the `TIOCGWINSZ` ioctl.
Some C code to demonstrate its use
```C
struct ttysize ts;
ioctl(0, TIOCGWINSZ, &ts);
printf("number of columns: %i, number of rows: %i, screen width: %i, screen height: %i\n", sz.ws_col, sz.ws_row, sz.ws_xpixel, sz.ws_ypixel);
```
Note that some terminals return `0` for the width and height values. Such terminals should be modified to return the correct values.
Examples of terminals that return correct values: `kitty, xterm`
=== Transferring pixel data
```
<ESC>_G<control data>;<payload><ESC>\
```
Before describing this escape code in detail, lets see some quick examples to get a flavor of it in action.
```
# Draw 10x20 pixels starting at the top-left corner of the current cell.
<ESC>_Gw=10,h=20,s=100;<pixel data><ESC>\
# Ditto, getting the pixel data from /tmp/pixel_data
<ESC>_Gw=10,h=20,t=f,s=100;<encoded /tmp/pixel_data><ESC>\
# Ditto, getting the pixel data from /dev/shm/pixel_data, deleting the file after reading data
<ESC>_Gw=10,h=20,t=t,s=100;<encoded /dev/shm/pixel_data><ESC>\
# Draw 10x20 pixels starting at the top-left corner of the current cell, ignoring the first 4 rows and 3 columns of the pixel data
<ESC>_Gw=10,h=20,x=3,y=4,s=100;<pixel data><ESC>\
```
This control code is an _Application-Programming Command (APC)_, indicated by
the leading `<ESC>_`. No modern terminals that I know of use APC codes, and
well-behaved terminals are supposed to ignore APC codes they do not understand.
The next character `G` indicates this APC code is for graphics data. In the future, we might
have different first letters for different needs.
The control data is a comma-separated list of key-value pairs with the restriction that
keys and values must contain only the characters `0-9a-zA-Z_-+/*`. The payload is arbitrary binary
data interpreted based on the control codes. The binary data must be base-64 encoded so as to minimize
the probability of problems with legacy systems that might interpret control
codes in the binary data incorrectly.
The key to the operation of this escape code is understanding the way the control data works.
The control data's keys are split up into categories for easier reference.
==== Controlling drawing
|===
| Key | Default | Meaning
| w | full width | width -- number of columns of the pixel data to draw
| h | full height | height -- number of rows of the pixel data to draw
| x | zero | x-offset -- the column in the pixel data to start from (0-based)
| y | zero | y-offset -- the row in the pixel data to start from (0-based)
|===
The origin for `(x, y)` is the top left corner of the pixel data, with `x`
increasing from left-to-right and `y` increasing downwards. The terminal
emulator will draw the specified region starting at the top-left corner of the
current cell. If the width is greater than a single cell, the cursor will be
moved one cell to the right and drawing will continue. If the cursor reaches
the end of the line, it moves to the next line and starts drawing the next row
of data. This means that the displayed image will be truncated at the right
edge of the screen. If the cursor needs to move past the bottom of the screen,
the screen is scrolled. After the entire region is drawn, the cursor will be
positioned at the first cell after the image.
Setting the width and/or height to zero means that no drawing is done and the
cursor position remains unchanged.
==== Transmitting data
The first consideration when transferring data between the client and the
terminal emulator is the format in which to do so. Since there is a vast and
growing number of image formats in existence, it does not make sense to have
every terminal emulator implement support for them. Instead, the client should
send simple pixel data to the terminal emulator. The obvious downside to this
is performance, especially when the client is running on a remote machine.
Techniques for remedying this limitation are discussed later. The terminal
emulator must understand pixel data in two formats, 24-bit RGB and 32-bit RGBA.
This is specified using the `f` key in the control data. `f=32` (which is the
default) indicates 32-bit RGBA data and `f=24` indicates 24-bit RGB data.
One additional parameter is needed to describe the pixel data, the _stride_,
that is the number of pixels per row. This is encoded using the `s` key, which
is **required**. For example, `s=100` means there are one hundred pixels per
row in the pixel data.
Now let us turn to considering how the data is actually transmitted.
===== Local client
When the client and the terminal emulator are on the same computer and share a
filesystem or shared memory, transfer can happen efficiently using files or
shared memory objects to pass the data around. The type of transfer is
controlled by the `t` key. When sending data via files/shared memory, `t` can
take three values, described below:
|===
| Value of `t` | Meaning
| f | A simple file
| t | A temporary file, the terminal emulator will delete the file after reading the pixel data
| s | A http://man7.org/linux/man-pages/man7/shm_overview.7.html[POSIX shared memory object]. The terminal emulator will delete it after reading the pixel data
|===
In all these cases, the payload data must be the base-64 encoded absolute file path.
[[query]]An important consideration is how the client can tell if the terminal emulator
and it share a filesystem. This can be done by using the _response mode_, specifying
the `q` key, with some unique id as the value. For example,
```
<ESC>_Gt=t,s=100,q=33;<encoded /tmp/pixel_data><ESC>\
```
When the terminal emulator receives this escape code, it will read and display
the pixel data as normal, and also send an escape code back to the client
indicating whether the reading of the data was successful or not. The returned
escape code will look like:
```
<ESC>_Gq=33;<encoded error message or OK><ESC>\
```
Here the `q` value will be the same as was sent by the client in the original
request. The payload data will be a base-64 encoded UTF-8 string. The string
will be `OK` if reading the pixel data succeeded or an error message. Clients
can set the width and height to zero to avoid actually drawing anything on
screen during the test.
===== Remote client
Remote clients, those that are unable to use the filesystem/shared memory to
transmit data, must send the pixel data directly using escape codes. Since
escape codes are of limited maximum length, the data will need to be chunked up
for transfer. This is done using the `m` key. The pixel data must first be
base64 encoded then chunked up into chunks no larger than `4096` bytes. The client
then sends the graphics escape code as usual, with the addition of an `m` key that
must have the value `1` for all but the last chunk, where it must be `0`. For example,
if the data is split into three chunks, the client would send the following
sequence of escape codes to the terminal emulator:
```
<ESC>_Gw=100,h=30,s=100,m=1;<base-64 pixel data first chunk><ESC>\
<ESC>_Gm=1;<base-64 pixel data second chunk><ESC>\
<ESC>_Gm=0;<base-64 pixel data last chunk><ESC>\
```
Note that only the first escape code needs to have the full set of control
codes such as stride, width, height, format etc. Subsequent chunks must have
only the `m` key. The client must finish sending all chunks for a single image
before sending any other graphics related escape codes.
=== Image persistence
Full screen applications may need to render the same image multiple times or
even render different parts of an image, in different locations, for example,
if the image is sprite map. Resending the image data each time this happens is
wasteful. Instead this protocol allows the client to have the terminal emulator
manage a persistent store of images.
Persistence is implemented by simply assigning an id to transmitted pixel data using the
key `i`. So for example,
```
<ESC>_Gt=t,s=100,i=some-id;<encoded /tmp/pixel_data><ESC>\
```
Now, if the client wants to redraw that image in the future, all it has to do is send
a code with the keys `t=i,i=some-id`, and no payload, like this:
```
<ESC>_Gt=i,i=some-id;<ESC>\
```
The client can use the `w, h, x, y` keys to draw different parts of the image
and draw it at different locations by positioning the cursor before sending the
code.
Saved images can be deleted, to free up resources, by using the code:
```
<ESC>_Gt=d,i=some-id;<ESC>\
```
The special value of `i=*` will cause the terminal emulator to delete all
stored images. Well behaved clients should send this code before terminating.
Terminal emulators may limit the maximum amount of saved data to avoid denial-of-service
attacks. Terminal emulators should make the limit fairly generous, at least a
few hundred, full screen, RGBA images worth of data should be allowed.
Client applications can check if an image is still stored by sending the `q`
key, as described <<query,above>>. For example,
```
<ESC>_Gt=i,i=some-id,q=some-id;<ESC>\
```
The terminal emulator will respond with:
```
<ESC>_Gq=some-id;<encoded OK or error message><ESC>\
```
If `OK` is sent the image was successfully loaded from the persistent storage, if not,
then it must be resent.
Note that when using the local filesystem to send data (`t=f`) mode, there is
no need to use this persistence mechanism, as the client can directly refer to
the file repeatedly with no overhead.
=== A summary of the control keys used
|===
|Key | Description
| f | The _format_ of the transmitted pixel data
| h | _height_ -- number of rows of the pixel data to draw
| i | _id_ to save transmitted data in persistent storage
| m | indicates whether there is _more_ data to come during a chunked transfer
| q | _query_ the terminal emulator to see if transmission succeeded
| s | The _stride_ of the transmitted pixel data
| t | The _type_ of transmission medium used
| w | _width_ -- number of columns of the pixel data to draw
| x | _x-offset_ -- the column in the pixel data to start from (0-based)
| y | _y-offset_ -- the row in the pixel data to start from (0-based)
|===
See link:graphics-protocol.asciidoc[Graphics Protocol] for a description
of this protocol to enable drawing of arbitrary raster images in the terminal.
== Keyboard handling
@@ -332,6 +80,7 @@ There are various problems with the current state of keyboard handling. They
include:
* No way to use modifiers other than `Ctrl` and `Alt`
* No way to use multiple modifier keys, other than, `Shift+Alt`.
* No way to handle different types of keyboard events, such as press, release or repeat
* No reliable way to distinguish single `Esc` keypresses from the
start of a escape sequence. Currently, client programs use

114
setup.py
View File

@@ -99,7 +99,7 @@ def get_sanitize_args(cc, ccver):
return sanitize_args
def init_env(debug=False, sanitize=False, native_optimizations=True):
def init_env(debug=False, sanitize=False, native_optimizations=True, profile=False):
global cflags, ldflags, cc, ldpaths
native_optimizations = native_optimizations and not sanitize and not debug
cc, ccver = cc_version()
@@ -115,10 +115,10 @@ def init_env(debug=False, sanitize=False, native_optimizations=True):
cflags = os.environ.get(
'OVERRIDE_CFLAGS', (
'-Wextra -Wno-missing-field-initializers -Wall -std=c99 -D_XOPEN_SOURCE=700'
' -pedantic-errors -Werror {} {} -DNDEBUG -fwrapv {} {} -pipe {}'
' -pedantic-errors -Werror {} {} -D{}DEBUG -fwrapv {} {} -pipe {} -fvisibility=hidden'
).format(
optimize, ' '.join(sanitize_args), stack_protector, missing_braces, '-march=native'
if native_optimizations else ''
optimize, ' '.join(sanitize_args), ('' if debug else 'N'), stack_protector, missing_braces,
'-march=native' if native_optimizations else '',
)
)
cflags = shlex.split(cflags
@@ -129,9 +129,17 @@ def init_env(debug=False, sanitize=False, native_optimizations=True):
ldflags = shlex.split(ldflags)
cflags += shlex.split(os.environ.get('CFLAGS', ''))
ldflags += shlex.split(os.environ.get('LDFLAGS', ''))
if not debug:
cflags.append('-flto'), ldflags.append('-flto')
if profile:
cflags.append('-DWITH_PROFILER')
cflags.append('-g')
ldflags.append('-lprofiler')
cflags.append('-pthread')
cflags.append('-DPRIMARY_VERSION={}'.format(version[0]))
# We add 4000 to the primary version because vim turns on SGR mouse mode
# automatically if this version is high enough
cflags.append('-DPRIMARY_VERSION={}'.format(version[0] + 4000))
cflags.append('-DSECONDARY_VERSION={}'.format(version[1]))
if not is_travis and not isosx and subprocess.Popen(
[PKGCONFIG, 'glew', '--atleast-version=2']
@@ -147,6 +155,7 @@ def init_env(debug=False, sanitize=False, native_optimizations=True):
raise SystemExit(
'glew >= 2.0.0 is required, found version: ' + ver
)
cflags.extend(pkg_config('libpng', '--cflags-only-I'))
if not isosx:
cflags.extend(pkg_config('glew', '--cflags-only-I'))
if isosx:
@@ -164,7 +173,12 @@ def init_env(debug=False, sanitize=False, native_optimizations=True):
else:
glfw_ldflags = pkg_config('glfw3', '--libs')
glew_libs = pkg_config('glew', '--libs')
ldpaths = pylib + glew_libs + font_libs + glfw_ldflags
libpng = pkg_config('libpng', '--libs')
ldpaths = pylib + glew_libs + font_libs + glfw_ldflags + libpng + ['-lunistring']
if not isosx:
ldpaths += ['-lrt']
if '-lz' not in ldpaths:
ldpaths.append('-lz')
try:
os.mkdir(build_dir)
@@ -188,6 +202,7 @@ def run_tool(cmd):
SPECIAL_SOURCES = {
'kitty/parser_dump.c': ('kitty/parser.c', ['DUMP_COMMANDS']),
'kitty/shaders_debug.c': ('kitty/shaders.c', ['ENABLE_DEBUG_GL']),
}
@@ -202,6 +217,26 @@ def newer(dest, *sources):
return False
def dependecies_for(src, obj, all_headers):
dep_file = obj.rpartition('.')[0] + '.d'
try:
deps = open(dep_file).read()
except FileNotFoundError:
yield src
yield from iter(all_headers)
else:
RE_INC = re.compile(r'^(?P<target>.+?):\s+(?P<deps>.+?)$', re.MULTILINE)
SPACE_TOK = '\x1B'
text = deps.replace('\\\n', ' ').replace('\\ ', SPACE_TOK)
for match in RE_INC.finditer(text):
files = (f.replace(SPACE_TOK, ' ') for f in match.group('deps').split())
for path in files:
path = os.path.abspath(path)
if path.startswith(base):
yield path
def compile_c_extension(module, incremental, sources, headers):
prefix = os.path.basename(module)
objects = [
@@ -216,8 +251,8 @@ def compile_c_extension(module, incremental, sources, headers):
cflgs.extend(map(define, defines))
src = os.path.join(base, src)
if not incremental or newer(dest, src, *headers):
run_tool([cc] + cflgs + ['-c', src] + ['-o', dest])
if not incremental or newer(dest, *dependecies_for(src, dest, headers)):
run_tool([cc, '-MMD'] + cflgs + ['-c', src] + ['-o', dest])
dest = os.path.join(base, module + '.so')
if not incremental or newer(dest, *objects):
run_tool([cc] + ldflags + objects + ldpaths + ['-o', dest])
@@ -229,7 +264,7 @@ def option_parser():
'action',
nargs='?',
default='build',
choices='build test linux-package osx-bundle'.split(),
choices='build test linux-package osx-bundle clean'.split(),
help='Action to perform (default is build)'
)
p.add_argument(
@@ -258,13 +293,19 @@ def option_parser():
action='store_true',
help='Only build changed files'
)
p.add_argument(
'--profile',
default=False,
action='store_true',
help='Use the -pg compile flag to add profiling information'
)
return p
def find_c_files():
ans, headers = [], []
d = os.path.join(base, 'kitty')
exclude = {'freetype.c', 'fontconfig.c'} if isosx else {'core_text.m'}
exclude = {'freetype.c', 'fontconfig.c'} if isosx else {'core_text.m', 'cocoa_window.m'}
for x in os.listdir(d):
ext = os.path.splitext(x)[1]
if ext in ('.c', '.m') and os.path.basename(x) not in exclude:
@@ -275,21 +316,19 @@ def find_c_files():
key=lambda x: os.path.getmtime(os.path.join(base, x)), reverse=True
)
ans.append('kitty/parser_dump.c')
ans.append('kitty/shaders_debug.c')
return tuple(ans), tuple(headers)
def build(args, native_optimizations=True):
init_env(args.debug, args.sanitize, native_optimizations)
init_env(args.debug, args.sanitize, native_optimizations, args.profile)
compile_c_extension(
'kitty/fast_data_types', args.incremental, *find_c_files()
)
def safe_makedirs(path):
try:
os.makedirs(path)
except FileExistsError:
pass
os.makedirs(path, exist_ok=True)
def build_test_launcher(args):
@@ -304,6 +343,25 @@ def build_test_launcher(args):
run_tool(cmd)
def build_linux_launcher(args, launcher_dir='.', for_bundle=False):
cflags = '-Wall -Werror -fpie'.split()
libs = []
if args.profile:
cflags.append('-DWITH_PROFILER'), cflags.append('-g')
libs.append('-lprofiler')
else:
cflags.append('-O3')
if for_bundle:
cflags.append('-DFOR_BUNDLE')
cflags.append('-DPYVER="{}"'.format(sysconfig.get_python_version()))
pylib = get_python_flags(cflags)
exe = 'kitty-profile' if args.profile else 'kitty'
cmd = [cc] + cflags + [
'linux-launcher.c', '-o', os.path.join(launcher_dir, exe)
] + libs + pylib
run_tool(cmd)
def package(args, for_bundle=False): # {{{
ddir = args.prefix
libdir = os.path.join(ddir, 'lib', 'kitty')
@@ -320,7 +378,7 @@ def package(args, for_bundle=False): # {{{
def src_ignore(parent, entries):
return [
x for x in entries
if '.' in x and x.rpartition('.')[2] not in ('py', 'so', 'conf')
if '.' in x and x.rpartition('.')[2] not in ('py', 'so', 'conf', 'glsl')
]
shutil.copytree('kitty', os.path.join(libdir, 'kitty'), ignore=src_ignore)
@@ -332,15 +390,7 @@ def package(args, for_bundle=False): # {{{
os.chmod(path, 0o755 if f.endswith('.so') else 0o644)
launcher_dir = os.path.join(ddir, 'bin')
safe_makedirs(launcher_dir)
cflags = '-O3 -Wall -Werror -fpie'.split()
if for_bundle:
cflags.append('-DFOR_BUNDLE')
cflags.append('-DPYVER="{}"'.format(sysconfig.get_python_version()))
pylib = get_python_flags(cflags)
cmd = [cc] + cflags + [
'linux-launcher.c', '-o', os.path.join(launcher_dir, 'kitty')
] + pylib
run_tool(cmd)
build_linux_launcher(args, launcher_dir, for_bundle)
if not isosx: # {{{ linux desktop gunk
icdir = os.path.join(ddir, 'share', 'icons', 'hicolor', '256x256')
safe_makedirs(icdir)
@@ -375,6 +425,15 @@ Categories=System;
# }}}
def clean():
for f in subprocess.check_output('git ls-files --others --ignored --exclude-from=.gitignore'.split()).decode('utf-8').splitlines():
if f.startswith('logo/kitty.iconset') or f.startswith('dev/'):
continue
os.unlink(f)
if os.sep in f and not os.listdir(os.path.dirname(f)):
os.rmdir(os.path.dirname(f))
def main():
if sys.version_info < (3, 5):
raise SystemExit('python >= 3.5 required')
@@ -384,6 +443,9 @@ def main():
if args.action == 'build':
build(args)
build_test_launcher(args)
if args.profile:
build_linux_launcher(args)
print('kitty profile executable is', 'kitty-profile')
elif args.action == 'test':
os.execlp(
sys.executable, sys.executable, os.path.join(base, 'test.py')
@@ -394,6 +456,8 @@ def main():
elif args.action == 'osx-bundle':
build(args, native_optimizations=False)
package(args, for_bundle=True)
elif args.action == 'clean':
clean()
if __name__ == '__main__':