Compare commits
880 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d32202b3a | ||
|
|
4f4d562670 | ||
|
|
28fc9c69da | ||
|
|
6edf00e699 | ||
|
|
a0d6445dda | ||
|
|
e74797037c | ||
|
|
3fb125dba7 | ||
|
|
3c8640f9ad | ||
|
|
2e63a4c83f | ||
|
|
9da5bee178 | ||
|
|
2fc6f3b542 | ||
|
|
4754f42e51 | ||
|
|
941fe918fb | ||
|
|
f4e82e972e | ||
|
|
f66dbf111d | ||
|
|
1a2da5c49d | ||
|
|
8327dabb29 | ||
|
|
195ea6c140 | ||
|
|
fcc0707174 | ||
|
|
aa9c3cd634 | ||
|
|
b541341681 | ||
|
|
e5d50aca6d | ||
|
|
b738f4d4ff | ||
|
|
5c38aabf0e | ||
|
|
03e9ed6947 | ||
|
|
63493fad22 | ||
|
|
696b857f91 | ||
|
|
53ae2bbe0d | ||
|
|
01069e919d | ||
|
|
fc0095f505 | ||
|
|
421528e130 | ||
|
|
7b70356515 | ||
|
|
e803505aad | ||
|
|
ad1ff455a0 | ||
|
|
7692cb5c3d | ||
|
|
a94e208db3 | ||
|
|
ba9d5cf3ff | ||
|
|
0dff0d03f9 | ||
|
|
c92aca5d19 | ||
|
|
5762baeed7 | ||
|
|
601cf01222 | ||
|
|
0eef48b516 | ||
|
|
3c1954f329 | ||
|
|
cc29640604 | ||
|
|
d0329460b8 | ||
|
|
717c0c96de | ||
|
|
b3f161efce | ||
|
|
0286940163 | ||
|
|
d4a4ee386f | ||
|
|
33498f9133 | ||
|
|
da3d4d1bef | ||
|
|
c69b8870d2 | ||
|
|
85b55b31b6 | ||
|
|
948919e42b | ||
|
|
85c40090ed | ||
|
|
98b50ec145 | ||
|
|
1ab4f7d2d0 | ||
|
|
98801b6fa6 | ||
|
|
bdd68284b5 | ||
|
|
a61d70ea9a | ||
|
|
e271719dd2 | ||
|
|
a08efa1fc5 | ||
|
|
ffac3600e0 | ||
|
|
6499f8ac24 | ||
|
|
63307cb1d8 | ||
|
|
0a6fb362f6 | ||
|
|
457fcbfb90 | ||
|
|
3b6277b9e2 | ||
|
|
e6e61028b1 | ||
|
|
e9be66126b | ||
|
|
681fe7f2f2 | ||
|
|
0196920533 | ||
|
|
5f5bb3ace0 | ||
|
|
3c970e0122 | ||
|
|
43af6e3b8a | ||
|
|
ac8cfc12fb | ||
|
|
b19472e0f5 | ||
|
|
b4d08044a0 | ||
|
|
b2fe02e63f | ||
|
|
d1a5d30233 | ||
|
|
bba30138e1 | ||
|
|
082546a1e7 | ||
|
|
41ffd58e26 | ||
|
|
b4f24e4e1e | ||
|
|
75c8ffd715 | ||
|
|
688606ff7c | ||
|
|
2e3f9dffa7 | ||
|
|
859033415b | ||
|
|
1f1b52b580 | ||
|
|
4e7bf80447 | ||
|
|
e39df50884 | ||
|
|
8d6a9d45c2 | ||
|
|
0862e85577 | ||
|
|
0b9a37139e | ||
|
|
6357f7b811 | ||
|
|
24197dc422 | ||
|
|
9bc2ab3245 | ||
|
|
bf4e8c490c | ||
|
|
590a4d70ee | ||
|
|
40341d2ddd | ||
|
|
0cd2c41ee3 | ||
|
|
5d65cdb6a8 | ||
|
|
759a15ccbb | ||
|
|
5aead5eebe | ||
|
|
a325a5fdee | ||
|
|
931e91f1a7 | ||
|
|
6e13509720 | ||
|
|
5b4844e71e | ||
|
|
66cbbea9bb | ||
|
|
b65119b8a9 | ||
|
|
86703da917 | ||
|
|
610e9afdf0 | ||
|
|
e3af9f68d3 | ||
|
|
747ac85e7c | ||
|
|
8c23f9e526 | ||
|
|
e6e339fcd3 | ||
|
|
f04680ac47 | ||
|
|
90985cc846 | ||
|
|
63917944f2 | ||
|
|
793605f80f | ||
|
|
93a281c7e3 | ||
|
|
8f1c6c4d74 | ||
|
|
1610dba7ab | ||
|
|
d9d419991c | ||
|
|
f9e86b19aa | ||
|
|
fffb976e43 | ||
|
|
0da566b49f | ||
|
|
da55717d20 | ||
|
|
254836902c | ||
|
|
d01f8d1865 | ||
|
|
a46c3f7007 | ||
|
|
4e58062025 | ||
|
|
aa3be0535f | ||
|
|
8ebf5e007c | ||
|
|
c2957e7f12 | ||
|
|
bf7c26d357 | ||
|
|
0547102bdd | ||
|
|
a07369ce69 | ||
|
|
21c5610431 | ||
|
|
6dbdf72f40 | ||
|
|
76a6bba643 | ||
|
|
b0b8222a96 | ||
|
|
0d87b8fac5 | ||
|
|
5227b1f98b | ||
|
|
c4f7c5c1cd | ||
|
|
33a84a609c | ||
|
|
037111715a | ||
|
|
93271b93e6 | ||
|
|
04488ada06 | ||
|
|
21c7002c68 | ||
|
|
97cae0e355 | ||
|
|
85fc0a59b1 | ||
|
|
eddd45bbc3 | ||
|
|
e2339697b9 | ||
|
|
cfe9b408ce | ||
|
|
83e8018787 | ||
|
|
372d2008a6 | ||
|
|
ab9a36f8c1 | ||
|
|
2458c3a7c6 | ||
|
|
e827e6fa21 | ||
|
|
342f0981d8 | ||
|
|
9c19e88288 | ||
|
|
7329bd4910 | ||
|
|
b9ad62226e | ||
|
|
3bff3bcbe4 | ||
|
|
a06fdad034 | ||
|
|
a49bd98ec6 | ||
|
|
90f41108d3 | ||
|
|
382c31ddf2 | ||
|
|
871ca4dda6 | ||
|
|
fa003f8233 | ||
|
|
12f37bcfad | ||
|
|
e86c712424 | ||
|
|
e8a9935cb2 | ||
|
|
f353131b44 | ||
|
|
7c4a2ba329 | ||
|
|
dfb543bd94 | ||
|
|
c912eef040 | ||
|
|
5f06d3a427 | ||
|
|
a70fa9abc5 | ||
|
|
5956277863 | ||
|
|
d3f37eeba4 | ||
|
|
ea48332f46 | ||
|
|
eb12511646 | ||
|
|
aa11ddf32c | ||
|
|
a348f64833 | ||
|
|
626a96e20f | ||
|
|
60995ff04b | ||
|
|
ba4e711715 | ||
|
|
891b184195 | ||
|
|
5414dc398e | ||
|
|
917559f883 | ||
|
|
9f2fb76309 | ||
|
|
b6692849d6 | ||
|
|
b27f6d5957 | ||
|
|
cda1e28b32 | ||
|
|
8a3e71d3d0 | ||
|
|
149ae2866a | ||
|
|
21707171bb | ||
|
|
2ebdf738ca | ||
|
|
0e871a89aa | ||
|
|
2d4f194f0d | ||
|
|
ce94a9b2df | ||
|
|
bfbb3c7068 | ||
|
|
6d1fd20542 | ||
|
|
bb8cab3a02 | ||
|
|
10435c23c2 | ||
|
|
01142cdc8c | ||
|
|
fc0adfd965 | ||
|
|
8803eeb890 | ||
|
|
25be705bcf | ||
|
|
d8f214a267 | ||
|
|
f74fb846e5 | ||
|
|
8ae110691e | ||
|
|
abff2292c1 | ||
|
|
7d690bbad2 | ||
|
|
7298de646a | ||
|
|
3c2f3ecd8e | ||
|
|
ac2c21e046 | ||
|
|
c817ba9eae | ||
|
|
92827ae647 | ||
|
|
5bdb405635 | ||
|
|
9beae321d7 | ||
|
|
353db678a2 | ||
|
|
9b973ef99c | ||
|
|
308d171dae | ||
|
|
7057bc663e | ||
|
|
d84cff0baf | ||
|
|
645890ccc8 | ||
|
|
cc1336a616 | ||
|
|
9b32f18109 | ||
|
|
a0321376d5 | ||
|
|
0a6e264d09 | ||
|
|
afec07b124 | ||
|
|
8a34fede55 | ||
|
|
686b5f297e | ||
|
|
6609d219f4 | ||
|
|
804998ca69 | ||
|
|
de5254469c | ||
|
|
ef879bab9c | ||
|
|
f729985346 | ||
|
|
1e998b6a3f | ||
|
|
77e9460eea | ||
|
|
81a58186c6 | ||
|
|
ac149be2bb | ||
|
|
a76a163db5 | ||
|
|
4ae59d3f77 | ||
|
|
d83a450260 | ||
|
|
61acc0784e | ||
|
|
f05890719d | ||
|
|
0f4e7921ee | ||
|
|
ec8c96b8e6 | ||
|
|
f0b29e15c3 | ||
|
|
8ad62106e0 | ||
|
|
36eb52424f | ||
|
|
c9ce2f47dc | ||
|
|
5035ed61ee | ||
|
|
a4cc10c41b | ||
|
|
5730ce5f53 | ||
|
|
27524bfc3d | ||
|
|
8843ded1c9 | ||
|
|
64b497589f | ||
|
|
ee48fd7151 | ||
|
|
291cd5e23f | ||
|
|
be668cfd9a | ||
|
|
f889c1c75a | ||
|
|
55c5f3e9fd | ||
|
|
242d335095 | ||
|
|
c763db94ce | ||
|
|
a391732600 | ||
|
|
06b45fcace | ||
|
|
bdac5f7d5b | ||
|
|
74865efddb | ||
|
|
b1f4d16692 | ||
|
|
01b8e2c3f5 | ||
|
|
8c1ef94b26 | ||
|
|
507fbc7abf | ||
|
|
2f8002c567 | ||
|
|
551e28d8e0 | ||
|
|
e84163e838 | ||
|
|
3a64b835fb | ||
|
|
08a44ed4dd | ||
|
|
4b8cc23a86 | ||
|
|
c4b2de8939 | ||
|
|
e2a603dcda | ||
|
|
294d01def6 | ||
|
|
05f289f912 | ||
|
|
4343c4067e | ||
|
|
c167dcc4e9 | ||
|
|
9a67176719 | ||
|
|
42d1a4755a | ||
|
|
1690e6933f | ||
|
|
5bbc15583e | ||
|
|
e03aabd972 | ||
|
|
6f5968eeb0 | ||
|
|
71b6e8f12a | ||
|
|
9c486f6e69 | ||
|
|
2f24588c66 | ||
|
|
6b1ea326a0 | ||
|
|
4512d0dbac | ||
|
|
511c4da6a3 | ||
|
|
af8fdf1dbe | ||
|
|
b7d12de0a7 | ||
|
|
2d870fc84a | ||
|
|
6e87929603 | ||
|
|
2b441df9e9 | ||
|
|
3e672ac178 | ||
|
|
12a24d5c86 | ||
|
|
dec1a0886d | ||
|
|
a82e45ad71 | ||
|
|
82e5750d71 | ||
|
|
6a3eb3f3d7 | ||
|
|
533a739e2b | ||
|
|
b12a8331f1 | ||
|
|
8f4ec036e7 | ||
|
|
cba36f2240 | ||
|
|
b54ffbefe4 | ||
|
|
6d470e4da0 | ||
|
|
4e1ebec8c2 | ||
|
|
e9779258e9 | ||
|
|
5c83f45bc6 | ||
|
|
eb65aca1f4 | ||
|
|
5d7e393c93 | ||
|
|
d6d01102fe | ||
|
|
1ad802b194 | ||
|
|
4a191b65bb | ||
|
|
65cc9f4e1b | ||
|
|
9f4b13b523 | ||
|
|
97e1b62a1e | ||
|
|
3ac966a38b | ||
|
|
6f1fda0a48 | ||
|
|
c00b486864 | ||
|
|
7928fff645 | ||
|
|
0285f5a1ad | ||
|
|
56e5c8be32 | ||
|
|
ef569976cb | ||
|
|
f8ae048d60 | ||
|
|
c51f4df0ca | ||
|
|
84453bf15c | ||
|
|
917350f058 | ||
|
|
0c58662eb6 | ||
|
|
52ca75855e | ||
|
|
49a07651b9 | ||
|
|
90a8ba3789 | ||
|
|
734c3199f0 | ||
|
|
64567646d9 | ||
|
|
1a1638cc7f | ||
|
|
fdf055312c | ||
|
|
b0b8b06b56 | ||
|
|
9c1ffb1adf | ||
|
|
563bafea08 | ||
|
|
efe4c32ffd | ||
|
|
0f003a9a77 | ||
|
|
3eff929652 | ||
|
|
3010ebfef5 | ||
|
|
ff14f47781 | ||
|
|
0b99a5caae | ||
|
|
467b96f2f2 | ||
|
|
18751a05c0 | ||
|
|
9007623d0c | ||
|
|
c1abb7038d | ||
|
|
53f7eebc4d | ||
|
|
d54d29122d | ||
|
|
1d71791b3c | ||
|
|
0c6145d4f2 | ||
|
|
62e273c5bb | ||
|
|
d82f399be7 | ||
|
|
20a7050564 | ||
|
|
b235f411b0 | ||
|
|
99faeeacfc | ||
|
|
08ad4f389d | ||
|
|
edba63d0d9 | ||
|
|
e33f882abc | ||
|
|
53e059f4f7 | ||
|
|
830364e026 | ||
|
|
332714a925 | ||
|
|
1eee6deb22 | ||
|
|
547f138703 | ||
|
|
b6c345fc3c | ||
|
|
29c40a4957 | ||
|
|
72af821e1b | ||
|
|
5fe4d10f38 | ||
|
|
3b36470e84 | ||
|
|
f9e8a4050c | ||
|
|
ce308286df | ||
|
|
b670973226 | ||
|
|
01b11b8a53 | ||
|
|
75c47a8659 | ||
|
|
5b4e0ed483 | ||
|
|
7432a7f346 | ||
|
|
0830fc3b5a | ||
|
|
ab2c901e85 | ||
|
|
3774cc54c7 | ||
|
|
330ed8e4ae | ||
|
|
eddae44d6d | ||
|
|
226488717a | ||
|
|
2f9dabd344 | ||
|
|
cf1b7c0adb | ||
|
|
eb57d0d431 | ||
|
|
8116ee015a | ||
|
|
0fa3cec61f | ||
|
|
eb60464177 | ||
|
|
44b2651a0f | ||
|
|
f2e91b5ed2 | ||
|
|
e72b60c882 | ||
|
|
5263babc9a | ||
|
|
9034b50b3a | ||
|
|
fdd7fe1948 | ||
|
|
8e7b8b70dc | ||
|
|
9bf24bdff5 | ||
|
|
ee53edd96d | ||
|
|
1bc5d7038d | ||
|
|
8a6b8b415c | ||
|
|
86174c81c8 | ||
|
|
374500d7b0 | ||
|
|
319a141967 | ||
|
|
c0d0062ef8 | ||
|
|
aaaa34e119 | ||
|
|
fba9502a9d | ||
|
|
6670cc1534 | ||
|
|
970729dca0 | ||
|
|
6627586d6e | ||
|
|
9f364209af | ||
|
|
c31a39a052 | ||
|
|
36fda90814 | ||
|
|
7ccfbc16e9 | ||
|
|
ad3b527bc4 | ||
|
|
f7eef3f456 | ||
|
|
1870364888 | ||
|
|
d7dfc85cf7 | ||
|
|
c047e5f16a | ||
|
|
8d21f63ec2 | ||
|
|
85b6a91e5c | ||
|
|
c788e30b66 | ||
|
|
a99caed693 | ||
|
|
9589fb5275 | ||
|
|
1a6dc530a4 | ||
|
|
60835871eb | ||
|
|
644ee646a2 | ||
|
|
8f1ec053ef | ||
|
|
2e3037ce3a | ||
|
|
109a856b91 | ||
|
|
eaec854bfb | ||
|
|
2c6cc6f047 | ||
|
|
f32ae10339 | ||
|
|
c46317f96f | ||
|
|
a67e4e550e | ||
|
|
65a7b19f40 | ||
|
|
1b5daf930a | ||
|
|
748ca81d4b | ||
|
|
0ea7d24ba4 | ||
|
|
6c9861cb76 | ||
|
|
a1125b5cc8 | ||
|
|
b4e189d2bf | ||
|
|
dbc763fca0 | ||
|
|
4c03ffeae2 | ||
|
|
2719ac7d01 | ||
|
|
219b81b5ee | ||
|
|
8e1443360f | ||
|
|
d55242aa82 | ||
|
|
ba531969ec | ||
|
|
c12e7587cb | ||
|
|
3aac0860a2 | ||
|
|
e4fd12001c | ||
|
|
3581ffe04b | ||
|
|
f6901e4a3d | ||
|
|
5a2cc4578e | ||
|
|
6b5acc4d3d | ||
|
|
43326c9bd0 | ||
|
|
96f3253e6d | ||
|
|
f5090c951a | ||
|
|
edacdc5e25 | ||
|
|
c67661af61 | ||
|
|
5e60329adf | ||
|
|
6b4f8408de | ||
|
|
ea1ef709c6 | ||
|
|
bbf247c5c0 | ||
|
|
8243fd505f | ||
|
|
ed9f9db1b0 | ||
|
|
74bed658e1 | ||
|
|
2f7bdc274d | ||
|
|
1347ff27d9 | ||
|
|
5bf4d520f5 | ||
|
|
cf8d665eb7 | ||
|
|
b2eee6df5c | ||
|
|
e8121b39ec | ||
|
|
5359abab76 | ||
|
|
01644ac3f0 | ||
|
|
8fbf552494 | ||
|
|
9c3390c5e6 | ||
|
|
24d45b99e3 | ||
|
|
936f2186cc | ||
|
|
c68b302fa2 | ||
|
|
ca7cab1a2d | ||
|
|
083b294659 | ||
|
|
bc24716476 | ||
|
|
4acab65016 | ||
|
|
f6b03f106c | ||
|
|
8f9bbeabd7 | ||
|
|
ac72ac7af2 | ||
|
|
68e00bc8e1 | ||
|
|
922d6b3b32 | ||
|
|
4dde98305e | ||
|
|
3593189a75 | ||
|
|
4f135be86c | ||
|
|
605b32f78a | ||
|
|
476a8b1429 | ||
|
|
da9165aebf | ||
|
|
455a1e9532 | ||
|
|
f12031e284 | ||
|
|
df551bc992 | ||
|
|
35fb702833 | ||
|
|
072cd29e3c | ||
|
|
624dd78460 | ||
|
|
51c4737a29 | ||
|
|
d63ea11cac | ||
|
|
3d307794be | ||
|
|
d32a05925f | ||
|
|
04006ea81e | ||
|
|
1723616968 | ||
|
|
23bc2171c9 | ||
|
|
0b3602f764 | ||
|
|
0e448347cd | ||
|
|
e0c66ea5ba | ||
|
|
268de9a9d5 | ||
|
|
74eb523178 | ||
|
|
8c540d5a46 | ||
|
|
2045055ae3 | ||
|
|
d8ed135b46 | ||
|
|
3a4ba7e834 | ||
|
|
fab57fbdf1 | ||
|
|
76a93b6cd6 | ||
|
|
f44c3a4606 | ||
|
|
714bee7f45 | ||
|
|
91673642b3 | ||
|
|
fee08d746c | ||
|
|
527ff0238a | ||
|
|
789d649b5c | ||
|
|
8020657d8c | ||
|
|
a15d3ff12d | ||
|
|
fbce95aeda | ||
|
|
9bc1b5a2d9 | ||
|
|
6a8b7bf92f | ||
|
|
48728bdfdc | ||
|
|
b6416cf8a9 | ||
|
|
053de5763a | ||
|
|
fdb915d0c8 | ||
|
|
c2769c9aed | ||
|
|
57deb19d0b | ||
|
|
321771b150 | ||
|
|
61dd2011f5 | ||
|
|
8a5f189213 | ||
|
|
14560b008a | ||
|
|
db85e07d41 | ||
|
|
07c838fe24 | ||
|
|
1583b1b0c6 | ||
|
|
d250555cd0 | ||
|
|
dbd0dab154 | ||
|
|
b479ea410d | ||
|
|
1db613e95b | ||
|
|
70071fe1f6 | ||
|
|
58688dda38 | ||
|
|
756d65c41e | ||
|
|
b9a08eacb0 | ||
|
|
b653711446 | ||
|
|
e725c647ef | ||
|
|
003f77b257 | ||
|
|
10b578e6d2 | ||
|
|
151c6c2c70 | ||
|
|
53e6de9bd9 | ||
|
|
66a455a079 | ||
|
|
f8855410e0 | ||
|
|
93e8074325 | ||
|
|
296f512bfb | ||
|
|
c8f20d0edf | ||
|
|
286f3630d7 | ||
|
|
38423505b1 | ||
|
|
9a3c621eb6 | ||
|
|
bcf847fe98 | ||
|
|
ee7a5eef99 | ||
|
|
7bed66a458 | ||
|
|
b26229c44e | ||
|
|
c497c0f96a | ||
|
|
6df64613f2 | ||
|
|
46dd648380 | ||
|
|
4cd5de78c7 | ||
|
|
32fa46d6eb | ||
|
|
902c2f6c88 | ||
|
|
2a5196873a | ||
|
|
d2067141d3 | ||
|
|
b5229ec73c | ||
|
|
3a2a16f54c | ||
|
|
b2d428618c | ||
|
|
97af84a063 | ||
|
|
18faee6250 | ||
|
|
fd90a3ddc3 | ||
|
|
a08931d84d | ||
|
|
2f0b6e24c9 | ||
|
|
33c5fc0fb6 | ||
|
|
71adb2dcf8 | ||
|
|
1ae324691d | ||
|
|
689d059517 | ||
|
|
18f0ab9e02 | ||
|
|
088ab5f137 | ||
|
|
3dd83975ba | ||
|
|
a796a6cbef | ||
|
|
4766d7de82 | ||
|
|
4a3686c16c | ||
|
|
b49105495c | ||
|
|
ca2eda2c5b | ||
|
|
4fc7c4aafc | ||
|
|
0981828493 | ||
|
|
6bc4f18a59 | ||
|
|
79532a315c | ||
|
|
7472a992ed | ||
|
|
e40616a0db | ||
|
|
dd8b97a86c | ||
|
|
5eff3897de | ||
|
|
530cf2eb14 | ||
|
|
2baa34beb8 | ||
|
|
87e2f7f86d | ||
|
|
e142083d53 | ||
|
|
2e850a0d0d | ||
|
|
aec9c31bca | ||
|
|
598a147500 | ||
|
|
ba201c4c92 | ||
|
|
5bc7cfaa43 | ||
|
|
2804e1ff81 | ||
|
|
ae27d36902 | ||
|
|
08182fbe4d | ||
|
|
778474f436 | ||
|
|
c6698ce305 | ||
|
|
21bc215313 | ||
|
|
88a9cc42d2 | ||
|
|
28f33a67cf | ||
|
|
6742fabfba | ||
|
|
b3806f4533 | ||
|
|
6d5fb4ccc6 | ||
|
|
2487f18f24 | ||
|
|
f59afff1d1 | ||
|
|
b92f95b092 | ||
|
|
d68ae01448 | ||
|
|
87eda834d3 | ||
|
|
c28ff5259a | ||
|
|
4619259e26 | ||
|
|
7bf0afa621 | ||
|
|
e3e02c7271 | ||
|
|
24e17cb7d8 | ||
|
|
769998adca | ||
|
|
9b5d88e92d | ||
|
|
6d7bebee3a | ||
|
|
1c38654321 | ||
|
|
da7a78691f | ||
|
|
1eaf7083ef | ||
|
|
79a6fcdc5c | ||
|
|
90bcbbf426 | ||
|
|
41049e2a40 | ||
|
|
c9b8510e9c | ||
|
|
d854fc1cd0 | ||
|
|
5f7bcae072 | ||
|
|
652eec3033 | ||
|
|
a9928ec98a | ||
|
|
e1dfb608ca | ||
|
|
2cee3a8809 | ||
|
|
804f72b31a | ||
|
|
e106dcc83b | ||
|
|
9347be639b | ||
|
|
ffc0c42a63 | ||
|
|
f5e855b3ad | ||
|
|
9af4dc4bff | ||
|
|
6f6180c34a | ||
|
|
6ddd751713 | ||
|
|
2c72b06012 | ||
|
|
88854c1aa4 | ||
|
|
5ad704469f | ||
|
|
c140e17a6f | ||
|
|
4fc733a9c8 | ||
|
|
c5c59e4c55 | ||
|
|
e0edef6719 | ||
|
|
0f46ce49e2 | ||
|
|
f9fd39b8c1 | ||
|
|
bd75157428 | ||
|
|
f987f3ba77 | ||
|
|
1c16806efd | ||
|
|
29155fef23 | ||
|
|
c05cbed5b2 | ||
|
|
4f85d4db71 | ||
|
|
468468ab9f | ||
|
|
1b4a6da577 | ||
|
|
aed7a197c7 | ||
|
|
c257b7ecfd | ||
|
|
d6072f6218 | ||
|
|
4ea72a2892 | ||
|
|
89b07a1c6b | ||
|
|
db0cd61807 | ||
|
|
6feb906953 | ||
|
|
7cbf8c63bc | ||
|
|
fae51d4e00 | ||
|
|
c510584afd | ||
|
|
738878c2ff | ||
|
|
d5682fe49a | ||
|
|
d8dd22048a | ||
|
|
737d8b96b6 | ||
|
|
c8627c9713 | ||
|
|
65fa7da24a | ||
|
|
c4c6ef799f | ||
|
|
0d18b91895 | ||
|
|
42089aaadc | ||
|
|
432d0f3318 | ||
|
|
4ade6587e8 | ||
|
|
1724f11429 | ||
|
|
75d66d1048 | ||
|
|
feccef67b9 | ||
|
|
a7d30a199d | ||
|
|
37bbfb11d3 | ||
|
|
79c80b4043 | ||
|
|
18a1402ae2 | ||
|
|
5d639b91b0 | ||
|
|
080c09d22e | ||
|
|
00c2670039 | ||
|
|
f113184165 | ||
|
|
d69f8af107 | ||
|
|
ad2bd1bf63 | ||
|
|
059786ab66 | ||
|
|
72dfa5e5e6 | ||
|
|
d84da6b7b8 | ||
|
|
6aa82d82ad | ||
|
|
a649eb2a48 | ||
|
|
83932c8099 | ||
|
|
95e36cece1 | ||
|
|
7075a7f1c0 | ||
|
|
3093610d26 | ||
|
|
86eed7f895 | ||
|
|
5dc14c5861 | ||
|
|
cab87f6fb2 | ||
|
|
fb3acd1f4a | ||
|
|
b247cef85c | ||
|
|
70445f33e9 | ||
|
|
cadb4f1389 | ||
|
|
bf75a4de61 | ||
|
|
400ab584ac | ||
|
|
9edad62144 | ||
|
|
e94205098f | ||
|
|
35ab7eb472 | ||
|
|
18619b14ac | ||
|
|
97e0b9b65a | ||
|
|
5175d9f4a6 | ||
|
|
a5aed0f4ab | ||
|
|
14d693a8ff | ||
|
|
645963cc24 | ||
|
|
20751c2807 | ||
|
|
dda886b97c | ||
|
|
b796ceecdd | ||
|
|
4def89ebb5 | ||
|
|
bd1c1839dd | ||
|
|
8739f698f9 | ||
|
|
b8c8482f20 | ||
|
|
4348ca8cc5 | ||
|
|
d1aa59080f | ||
|
|
bf0ffa80be | ||
|
|
2a89a0c87e | ||
|
|
de84489a20 | ||
|
|
40901778ad | ||
|
|
c1febea659 | ||
|
|
b42f4ac288 | ||
|
|
fae57a5868 | ||
|
|
7a02d09508 | ||
|
|
8ec6fe960d | ||
|
|
61a77e19ef | ||
|
|
82e88b54c7 | ||
|
|
ab8975f4da | ||
|
|
5e7fd21c8d | ||
|
|
30cd9b2ae4 | ||
|
|
4f7d12f3fd | ||
|
|
62da158823 | ||
|
|
2d19b87208 | ||
|
|
54ca775006 | ||
|
|
a64a0d64cb | ||
|
|
6634dc81fe | ||
|
|
a2d5eef398 | ||
|
|
8184ba246a | ||
|
|
54ad1d103a | ||
|
|
d92f8a90ef | ||
|
|
d33c287344 | ||
|
|
782eebf976 | ||
|
|
0fbf75b95a | ||
|
|
5781facf4b | ||
|
|
b710ffc403 | ||
|
|
7d8edccb04 | ||
|
|
3750d70173 | ||
|
|
025e0bb546 | ||
|
|
033367de91 | ||
|
|
f93b605b75 | ||
|
|
7b4141b0c8 | ||
|
|
90acef72dc | ||
|
|
43f200be77 | ||
|
|
2fbb167803 | ||
|
|
2b396262f0 | ||
|
|
74deca52b9 | ||
|
|
058d956464 | ||
|
|
0f38dc1bd1 | ||
|
|
576b03c047 | ||
|
|
59cb1ad1e0 | ||
|
|
f77acdce14 | ||
|
|
43a98a1df1 | ||
|
|
d1cf771c14 | ||
|
|
10e5fcc375 | ||
|
|
2112932d04 | ||
|
|
be505ce6e0 | ||
|
|
4d6472128c | ||
|
|
26b7b1ec63 | ||
|
|
9a8f0a9b5a | ||
|
|
16dbbf3a1e | ||
|
|
8492d0a264 | ||
|
|
d8f8f1f590 | ||
|
|
b762e81da0 | ||
|
|
8b3b68ea69 | ||
|
|
28525766a5 | ||
|
|
d8fc23c38f | ||
|
|
8cae70fa36 | ||
|
|
7f2b98fad7 | ||
|
|
2b6dde2ac5 | ||
|
|
0c254fa7c7 | ||
|
|
fe7375becc | ||
|
|
b6ee88056e | ||
|
|
96f691ad64 | ||
|
|
8cf3380a77 | ||
|
|
e8d7e3e98b | ||
|
|
8af0a8476f | ||
|
|
2de4248f76 | ||
|
|
8c73ab6afb | ||
|
|
ee9bd77dc4 | ||
|
|
81f31ca215 | ||
|
|
51eff372f8 | ||
|
|
087376b44d | ||
|
|
e49047ef7d | ||
|
|
c117f36009 | ||
|
|
5ec83ee759 | ||
|
|
410165c14d | ||
|
|
973ef9fcda | ||
|
|
396bfb8540 | ||
|
|
aaf597bffd | ||
|
|
63c4f64bf5 | ||
|
|
5745dc34f7 | ||
|
|
114b8cff93 | ||
|
|
261571bb69 | ||
|
|
497e2c9163 | ||
|
|
ac01ffaa54 | ||
|
|
eb9fe06d10 | ||
|
|
506f2f7db7 | ||
|
|
f476c56f91 | ||
|
|
474688888d | ||
|
|
7ddbb613fe | ||
|
|
adc1c06089 | ||
|
|
1f8631cc99 | ||
|
|
f9e4e71285 | ||
|
|
5529194c8d | ||
|
|
5b008163af | ||
|
|
802da7087c | ||
|
|
53ab7617ca | ||
|
|
4369255a25 | ||
|
|
b709ee6842 | ||
|
|
4a8f9efe79 | ||
|
|
19d36fbe69 | ||
|
|
d37e73db54 | ||
|
|
a8f6615380 | ||
|
|
97fe38fbbc | ||
|
|
3378175450 | ||
|
|
9f60d2aea3 | ||
|
|
46d49e18c0 | ||
|
|
43fc335be2 | ||
|
|
f68339c571 | ||
|
|
0d68b7078c | ||
|
|
2e71dde780 | ||
|
|
51ddd584d6 | ||
|
|
4b090259cd | ||
|
|
f3b9ff5f9f | ||
|
|
4ec1a8d9c3 | ||
|
|
aadab38487 | ||
|
|
53275c9137 |
@@ -1,104 +0,0 @@
|
||||
version: 2
|
||||
|
||||
jobs:
|
||||
|
||||
lin-sys:
|
||||
docker:
|
||||
- image: kovidgoyal/kitty-test:latest
|
||||
steps:
|
||||
- checkout
|
||||
- run: if grep -Inr '\s$' kitty kitty_tests kittens docs *.py *.asciidoc *.rst .gitattributes .gitignore; then echo Trailing whitespace found, aborting.; exit 1; fi
|
||||
- run: python3 -m flake8 --count .
|
||||
- run: python3 setup.py build --debug --verbose
|
||||
- run: ./kitty/launcher/kitty +launch test.py
|
||||
- run: make FAIL_WARN=-W man
|
||||
- run: make FAIL_WARN=-W html
|
||||
|
||||
lin-package:
|
||||
docker:
|
||||
- image: kovidgoyal/kitty-test:latest
|
||||
steps:
|
||||
- checkout
|
||||
- run: python3 setup.py linux-package --update-check-interval=0
|
||||
|
||||
lin-35:
|
||||
docker:
|
||||
- image: kovidgoyal/kitty-test:latest
|
||||
environment:
|
||||
CC: gcc
|
||||
CFLAGS: -funsigned-char
|
||||
LD_LIBRARY_PATH: /opt/py3.5/lib
|
||||
ASAN_OPTIONS: leak_check_at_exit=0
|
||||
steps:
|
||||
- checkout
|
||||
- run: /opt/py3.5/bin/python3 setup.py build --debug --verbose --sanitize
|
||||
- run: ./kitty/launcher/kitty +launch test.py
|
||||
|
||||
lin-37:
|
||||
docker:
|
||||
- image: kovidgoyal/kitty-test:latest
|
||||
environment:
|
||||
CC: clang
|
||||
LD_LIBRARY_PATH: /opt/py3.7/lib
|
||||
ASAN_OPTIONS: leak_check_at_exit=0
|
||||
steps:
|
||||
- checkout
|
||||
- run: /opt/py3.7/bin/python3 setup.py build --debug --verbose --sanitize
|
||||
- run: ./kitty/launcher/kitty +launch test.py
|
||||
|
||||
lin-bundle:
|
||||
docker:
|
||||
- image: kovidgoyal/kitty-test:latest
|
||||
environment:
|
||||
ASAN_OPTIONS: leak_check_at_exit=0
|
||||
steps:
|
||||
- checkout
|
||||
- run: echo "export LD_LIBRARY_PATH=$SW/lib" >> $BASH_ENV
|
||||
- run: echo "export PKG_CONFIG_PATH=$SW/lib/pkgconfig" >> $BASH_ENV
|
||||
- run: echo "export PYTHONHOME=$SW" >> $BASH_ENV
|
||||
- run: $SW/bin/python3 setup.py build --debug --verbose
|
||||
- run: ./kitty/launcher/kitty +launch test.py
|
||||
|
||||
mac-bundle:
|
||||
macos:
|
||||
xcode: "10.1.0"
|
||||
environment:
|
||||
LC_ALL: en_US.UTF-8
|
||||
LANG: en_US.UTF-8
|
||||
SW: /Users/Shared/buildbot/sw/sw
|
||||
steps:
|
||||
- checkout
|
||||
- run: mkdir -p $SW
|
||||
- run: curl https://download.calibre-ebook.com/travis/kitty/osx.tar.xz | tar xJ -C $SW
|
||||
- run: echo "export PATH=$SW/bin:$PATH" >> $BASH_ENV
|
||||
- run: python3 setup.py build --debug --verbose
|
||||
- run: ./kitty/launcher/kitty +launch test.py
|
||||
|
||||
mac-brew:
|
||||
macos:
|
||||
xcode: "10.2.0"
|
||||
environment:
|
||||
LC_ALL: en_US.UTF-8
|
||||
LANG: en_US.UTF-8
|
||||
steps:
|
||||
- checkout
|
||||
- run: brew bundle
|
||||
- run: python3 setup.py build --debug --verbose
|
||||
- run: ./kitty/launcher/kitty +launch test.py
|
||||
- run: echo "export PATH=/usr/local/opt/sphinx-doc/bin:$PATH" >> $BASH_ENV
|
||||
- run: python3 setup.py linux-package --verbose
|
||||
- run: python3 setup.py kitty.app --verbose
|
||||
- run: kitty.app/Contents/MacOS/kitty +runpy "from kitty.constants import *; print(kitty_exe())"
|
||||
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
all_jobs:
|
||||
jobs:
|
||||
- mac-bundle
|
||||
- lin-sys
|
||||
- lin-35
|
||||
- lin-37
|
||||
- lin-package
|
||||
- lin-bundle
|
||||
- mac-brew
|
||||
@@ -1,55 +0,0 @@
|
||||
# When changing this file, go to
|
||||
# https://cloud.docker.com/repository/docker/kovidgoyal/kitty-test/builds and
|
||||
# click the "Trigger" button under Automated builds to rebuild
|
||||
FROM ubuntu:latest
|
||||
|
||||
# make Apt non-interactive
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN echo 'APT::Get::Assume-Yes "true";' > /etc/apt/apt.conf.d/90circleci \
|
||||
&& echo 'DPkg::Options "--force-confnew";' >> /etc/apt/apt.conf.d/90circleci \
|
||||
&& echo 'Acquire::Languages "none";' >> /etc/apt/apt.conf.d/90circleci
|
||||
|
||||
# System setup
|
||||
RUN apt-get update && mkdir -p /usr/share/man/man1 && apt-get install -y python3-flake8 python3-pip python3-pil clang git apt locales sudo openssh-client ca-certificates tar gzip parallel net-tools netcat unzip zip bzip2 gnupg curl wget build-essential apt-utils
|
||||
|
||||
# Setup circleci user
|
||||
RUN groupadd --gid 3434 circleci \
|
||||
&& useradd --uid 3434 --gid circleci --shell /bin/bash --create-home circleci \
|
||||
&& echo 'circleci ALL=NOPASSWD: ALL' >> /etc/sudoers.d/50-circleci \
|
||||
&& echo 'Defaults env_keep += "DEBIAN_FRONTEND"' >> /etc/sudoers.d/env_keep \
|
||||
&& echo 'Defaults env_keep += "SW"' >> /etc/sudoers.d/env_keep \
|
||||
&& echo 'Defaults env_keep += "LANG"' >> /etc/sudoers.d/env_keep
|
||||
|
||||
# Set timezone to UTC by default
|
||||
RUN ln -sf /usr/share/zoneinfo/Etc/UTC /etc/localtime
|
||||
|
||||
# Use unicode
|
||||
RUN locale-gen en_US.UTF-8 || true
|
||||
ENV LANG=en_US.UTF-8
|
||||
|
||||
# Deps needed for building python
|
||||
RUN apt-get install -y libreadline-dev zlib1g-dev libssl-dev libbz2-dev libsqlite3-dev libffi-dev
|
||||
|
||||
# kitty deps
|
||||
RUN apt-get install -y libgl1-mesa-dev libxi-dev libxrandr-dev libxinerama-dev libxcursor-dev libxcb-xkb-dev libdbus-1-dev libxkbcommon-dev libharfbuzz-dev libpng-dev libfontconfig-dev libpython3-dev libxkbcommon-x11-dev python3-pygments libcanberra-dev
|
||||
|
||||
# Needed to build kitty docs
|
||||
RUN pip3 install sphinx
|
||||
|
||||
# Install multiple pythons
|
||||
ADD install-python.py /tmp/install-python.py
|
||||
RUN python3 /tmp/install-python.py py3.5 https://www.python.org/ftp/python/3.5.6/Python-3.5.6.tar.xz
|
||||
RUN python3 /tmp/install-python.py py3.7 https://www.python.org/ftp/python/3.7.2/Python-3.7.2.tar.xz
|
||||
|
||||
# Install kitty bundle
|
||||
ENV SW=/home/circleci/sw
|
||||
RUN python3 /tmp/install-python.py bundle https://download.calibre-ebook.com/travis/kitty/linux-64.tar.xz
|
||||
RUN echo SW=$SW >> /etc/environment
|
||||
|
||||
# Clean unused files
|
||||
RUN apt-get clean -y
|
||||
|
||||
USER circleci
|
||||
|
||||
LABEL com.circleci.preserve-entrypoint=true
|
||||
ENTRYPOINT sleep 2h
|
||||
@@ -1,60 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import io
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
from urllib.request import urlopen
|
||||
|
||||
PY, URL = sys.argv[1], sys.argv[2]
|
||||
if PY == 'bundle':
|
||||
SW = os.environ['SW']
|
||||
os.mkdir(SW)
|
||||
os.chdir(SW)
|
||||
else:
|
||||
os.chdir('/usr/src')
|
||||
|
||||
|
||||
def run(cmd):
|
||||
cmd = shlex.split(cmd)
|
||||
p = subprocess.Popen(cmd)
|
||||
if p.wait() != 0:
|
||||
raise SystemExit(p.returncode)
|
||||
|
||||
|
||||
with urlopen(URL) as f:
|
||||
data = f.read()
|
||||
|
||||
with tarfile.open(fileobj=io.BytesIO(data), mode='r:xz') as tf:
|
||||
tf.extractall()
|
||||
|
||||
|
||||
def replace_in_file(path, src, dest):
|
||||
with open(path, 'r+') as f:
|
||||
n = f.read().replace(src, dest)
|
||||
f.seek(0), f.truncate()
|
||||
f.write(n)
|
||||
|
||||
|
||||
if PY == 'bundle':
|
||||
replaced = 0
|
||||
for dirpath, dirnames, filenames in os.walk(SW):
|
||||
for f in filenames:
|
||||
if f.endswith('.pc') or (f.endswith('.py') and f.startswith('_sysconfig')):
|
||||
replace_in_file(os.path.join(dirpath, f), '/sw/sw', SW)
|
||||
replaced += 1
|
||||
if replaced < 2:
|
||||
raise SystemExit('Failed to replace path to SW in bundle')
|
||||
else:
|
||||
src = os.path.abspath(tuple(os.listdir('.'))[0])
|
||||
os.chdir(src)
|
||||
run(f'./configure --prefix=/opt/{PY} --enable-shared --with-system-expat --without-ensurepip')
|
||||
run(f'make -j {os.cpu_count()}')
|
||||
run('make install')
|
||||
os.chdir('/')
|
||||
shutil.rmtree(src)
|
||||
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -3,11 +3,11 @@ kitty/emoji.h linguist-generated=true
|
||||
kitty/keys.h linguist-generated=true
|
||||
kitty/charsets.c linguist-generated=true
|
||||
kitty/key_encoding.py linguist-generated=true
|
||||
kitty/unicode-data.c
|
||||
kitty/unicode-data.c linguist-generated=true
|
||||
kitty/rgb.py linguist-generated=true
|
||||
kitty/gl-wrapper.* linguist-generated=true
|
||||
kitty/khrplatform.h linguist-generated=true
|
||||
kitty/glfw-wrapper.* linguist-generated=true
|
||||
kitty/parse-graphics-command.h linguist-generated=true
|
||||
glfw/*.c linguist-vendored=true
|
||||
glfw/*.h linguist-vendored=true
|
||||
kittens/unicode_input/names.h linguist-generated=true
|
||||
|
||||
125
.github/workflows/ci.py
vendored
Normal file
125
.github/workflows/ci.py
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import io
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
from urllib.request import urlopen
|
||||
|
||||
is_bundle = os.environ.get('KITTY_BUNDLE') == '1'
|
||||
is_macos = 'darwin' in sys.platform.lower()
|
||||
SW = None
|
||||
|
||||
|
||||
def run(*a):
|
||||
if len(a) == 1:
|
||||
a = shlex.split(a[0])
|
||||
print(' '.join(map(shlex.quote, a)))
|
||||
sys.stdout.flush()
|
||||
ret = subprocess.Popen(a).wait()
|
||||
if ret != 0:
|
||||
raise SystemExit(ret)
|
||||
|
||||
|
||||
def install_deps():
|
||||
print('Installing kitty dependencies...')
|
||||
sys.stdout.flush()
|
||||
if is_macos:
|
||||
items = (x.strip() for x in open('Brewfile').readlines() if not x.startswith('#'))
|
||||
run('brew', 'install', *items)
|
||||
else:
|
||||
run('sudo apt-get update')
|
||||
run('sudo apt-get install -y libgl1-mesa-dev libxi-dev libxrandr-dev libxinerama-dev'
|
||||
' libxcursor-dev libxcb-xkb-dev libdbus-1-dev libxkbcommon-dev libharfbuzz-dev'
|
||||
' libpng-dev libfontconfig-dev libxkbcommon-x11-dev libcanberra-dev')
|
||||
if is_bundle:
|
||||
install_bundle()
|
||||
else:
|
||||
run('pip install Pillow pygments')
|
||||
|
||||
|
||||
def build_kitty():
|
||||
python = 'python3' if is_bundle else sys.executable
|
||||
cmd = '{} setup.py build --verbose'.format(python)
|
||||
if os.environ.get('KITTY_SANITIZE') == '1':
|
||||
cmd += ' --debug --sanitize'
|
||||
run(cmd)
|
||||
|
||||
|
||||
def test_kitty():
|
||||
run('./kitty/launcher/kitty +launch test.py')
|
||||
|
||||
|
||||
def package_kitty():
|
||||
py = 'python3' if is_macos else 'python'
|
||||
run(py + ' setup.py linux-package --update-check-interval=0 --verbose')
|
||||
if is_macos:
|
||||
run('python3 setup.py kitty.app --update-check-interval=0 --verbose')
|
||||
run('kitty.app/Contents/MacOS/kitty +runpy "from kitty.constants import *; print(kitty_exe())"')
|
||||
|
||||
|
||||
def replace_in_file(path, src, dest):
|
||||
with open(path, 'r+') as f:
|
||||
n = f.read().replace(src, dest)
|
||||
f.seek(0), f.truncate()
|
||||
f.write(n)
|
||||
|
||||
|
||||
def setup_bundle_env():
|
||||
global SW
|
||||
os.environ['SW'] = SW = '/Users/Shared/buildbot/sw/sw' if is_macos else os.path.join(os.environ['GITHUB_WORKSPACE'], 'sw')
|
||||
os.environ['PKG_CONFIG_PATH'] = SW + '/lib/pkgconfig'
|
||||
if is_macos:
|
||||
os.environ['PATH'] = '{}:{}'.format('/usr/local/opt/sphinx-doc/bin', os.environ['PATH'])
|
||||
else:
|
||||
os.environ['LD_LIBRARY_PATH'] = SW + '/lib'
|
||||
os.environ['PYTHONHOME'] = SW
|
||||
os.environ['PATH'] = '{}:{}'.format(os.path.join(SW, 'bin'), os.environ['PATH'])
|
||||
|
||||
|
||||
def install_bundle():
|
||||
cwd = os.getcwd()
|
||||
os.makedirs(SW)
|
||||
os.chdir(SW)
|
||||
with urlopen('https://download.calibre-ebook.com/travis/kitty/{}.tar.xz'.format(
|
||||
'osx' if is_macos else 'linux-64')) as f:
|
||||
data = f.read()
|
||||
with tarfile.open(fileobj=io.BytesIO(data), mode='r:xz') as tf:
|
||||
tf.extractall()
|
||||
if not is_macos:
|
||||
replaced = 0
|
||||
for dirpath, dirnames, filenames in os.walk('.'):
|
||||
for f in filenames:
|
||||
if f.endswith('.pc') or (f.endswith('.py') and f.startswith('_sysconfig')):
|
||||
replace_in_file(os.path.join(dirpath, f), '/sw/sw', SW)
|
||||
replaced += 1
|
||||
if replaced < 2:
|
||||
raise SystemExit('Failed to replace path to SW in bundle')
|
||||
os.chdir(cwd)
|
||||
|
||||
|
||||
def main():
|
||||
if is_bundle:
|
||||
setup_bundle_env()
|
||||
else:
|
||||
if not is_macos:
|
||||
os.environ['LD_LIBRARY_PATH'] = '{}/lib'.format(os.environ['pythonLocation'])
|
||||
action = sys.argv[-1]
|
||||
if action in ('build', 'package'):
|
||||
install_deps()
|
||||
if action == 'build':
|
||||
build_kitty()
|
||||
elif action == 'package':
|
||||
package_kitty()
|
||||
elif action == 'test':
|
||||
test_kitty()
|
||||
else:
|
||||
raise SystemExit('Unknown action: ' + action)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
132
.github/workflows/ci.yml
vendored
Normal file
132
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
name: CI
|
||||
on: [push, pull_request]
|
||||
env:
|
||||
CI: 'true'
|
||||
ASAN_OPTIONS: leak_check_at_exit=0
|
||||
LC_ALL: en_US.UTF-8
|
||||
LANG: en_US.UTF-8
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Linux (python=${{ matrix.pyver }} cc=${{ matrix.cc }} sanitize=${{ matrix.sanitize }})
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CC: ${{ matrix.cc }}
|
||||
KITTY_SANITIZE: ${{ matrix.sanitize }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
python: [a, b, c]
|
||||
cc: [gcc, clang]
|
||||
include:
|
||||
- python: a
|
||||
pyver: 3.6
|
||||
sanitize: 0
|
||||
|
||||
- python: b
|
||||
pyver: 3.7
|
||||
sanitize: 1
|
||||
|
||||
- python: c
|
||||
pyver: 3.8
|
||||
sanitize: 1
|
||||
|
||||
|
||||
exclude:
|
||||
- python: a
|
||||
cc: clang
|
||||
- python: b
|
||||
cc: clang
|
||||
- python: c
|
||||
cc: gcc
|
||||
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@master
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Set up Python ${{ matrix.pyver }}
|
||||
uses: actions/setup-python@master
|
||||
with:
|
||||
python-version: ${{ matrix.pyver }}
|
||||
|
||||
- name: Build kitty
|
||||
run: python .github/workflows/ci.py build
|
||||
|
||||
- name: Test kitty
|
||||
run: python .github/workflows/ci.py test
|
||||
|
||||
linux-package:
|
||||
name: Linux package
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CFLAGS: -funsigned-char
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@master
|
||||
with:
|
||||
fetch-depth: 10
|
||||
|
||||
- name: Test for trailing whitespace
|
||||
run: if grep -Inr '\s$' kitty kitty_tests kittens docs *.py *.asciidoc *.rst .gitattributes .gitignore; then echo Trailing whitespace found, aborting.; exit 1; fi
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@master
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Install build-only deps
|
||||
run: pip install flake8 mypy sphinx
|
||||
|
||||
- name: Run pyflakes
|
||||
run: python -m flake8 --count .
|
||||
|
||||
- name: Build kitty package
|
||||
run: python .github/workflows/ci.py package
|
||||
|
||||
- name: Run mypy
|
||||
run: python test.py mypy
|
||||
|
||||
- name: Build man page
|
||||
run: make FAIL_WARN=-W man
|
||||
|
||||
- name: Build HTML docs
|
||||
run: make FAIL_WARN=-W html
|
||||
|
||||
bundle:
|
||||
name: Bundle test (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
env:
|
||||
KITTY_BUNDLE: 1
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@master
|
||||
with:
|
||||
fetch-depth: 10
|
||||
|
||||
- name: Build kitty
|
||||
run: which python3 && python3 .github/workflows/ci.py build
|
||||
|
||||
- name: Test kitty
|
||||
run: python3 .github/workflows/ci.py test
|
||||
|
||||
brew:
|
||||
name: macOS Brew
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@master
|
||||
with:
|
||||
fetch-depth: 10
|
||||
|
||||
- name: Build kitty
|
||||
run: python3 .github/workflows/ci.py build
|
||||
|
||||
- name: Test kitty
|
||||
run: python3 .github/workflows/ci.py test
|
||||
|
||||
- name: Build kitty package
|
||||
run: python3 .github/workflows/ci.py package
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
*.so
|
||||
*.pyc
|
||||
*.pyo
|
||||
*_stub.pyi
|
||||
/.dmypy.json
|
||||
/tags
|
||||
/build/
|
||||
/linux-package/
|
||||
@@ -14,4 +16,5 @@ __pycache__/
|
||||
/glfw/wayland-*-client-protocol.[ch]
|
||||
/docs/_build/
|
||||
/docs/generated/
|
||||
/.mypy_cache
|
||||
.DS_Store
|
||||
|
||||
10
Brewfile
10
Brewfile
@@ -1,5 +1,5 @@
|
||||
brew 'pkg-config'
|
||||
brew 'imagemagick'
|
||||
brew 'harfbuzz'
|
||||
brew 'python'
|
||||
brew 'sphinx-doc'
|
||||
pkg-config
|
||||
python
|
||||
imagemagick
|
||||
harfbuzz
|
||||
sphinx-doc
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
To build from source:
|
||||
To build from source: <https://sw.kovidgoyal.net/kitty/build.html>
|
||||
|
||||
https://sw.kovidgoyal.net/kitty/build.html
|
||||
|
||||
Pre-built binaries:
|
||||
|
||||
https://sw.kovidgoyal.net/kitty/binary.html
|
||||
Pre-built binaries: <https://sw.kovidgoyal.net/kitty/binary.html>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
= kitty - the fast, featureful, GPU based, terminal emulator
|
||||
|
||||
See https://sw.kovidgoyal.net/kitty
|
||||
See https://sw.kovidgoyal.net/kitty/
|
||||
|
||||
image:https://circleci.com/gh/kovidgoyal/kitty.svg?style=svg["Build status", link="https://circleci.com/gh/kovidgoyal/kitty"]
|
||||
image:https://github.com/kovidgoyal/kitty/workflows/CI/badge.svg["Build status", link="https://github.com/kovidgoyal/kitty/actions?query=workflow%3ACI"]
|
||||
|
||||
54
__main__.py
54
__main__.py
@@ -4,54 +4,56 @@
|
||||
|
||||
import sys
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
|
||||
def icat(args):
|
||||
from kittens.runner import run_kitten
|
||||
def icat(args: List[str]) -> None:
|
||||
from kittens.runner import run_kitten as rk
|
||||
sys.argv = args
|
||||
run_kitten('icat')
|
||||
rk('icat')
|
||||
|
||||
|
||||
def list_fonts(args):
|
||||
from kitty.fonts.list import main
|
||||
main(args)
|
||||
def list_fonts(args: List[str]) -> None:
|
||||
from kitty.fonts.list import main as list_main
|
||||
list_main(args)
|
||||
|
||||
|
||||
def remote_control(args):
|
||||
from kitty.remote_control import main
|
||||
main(args)
|
||||
def remote_control(args: List[str]) -> None:
|
||||
from kitty.remote_control import main as rc_main
|
||||
rc_main(args)
|
||||
|
||||
|
||||
def runpy(args):
|
||||
def runpy(args: List[str]) -> None:
|
||||
sys.argv = ['kitty'] + args[2:]
|
||||
exec(args[1])
|
||||
|
||||
|
||||
def hold(args):
|
||||
def hold(args: List[str]) -> None:
|
||||
import subprocess
|
||||
ret = subprocess.Popen(args[1:]).wait()
|
||||
sys.stdin.read()
|
||||
raise SystemExit(ret)
|
||||
|
||||
|
||||
def complete(args):
|
||||
from kitty.complete import main
|
||||
main(args[1:], entry_points, namespaced_entry_points)
|
||||
def complete(args: List[str]) -> None:
|
||||
from kitty.complete import main as complete_main
|
||||
complete_main(args[1:], entry_points, namespaced_entry_points)
|
||||
|
||||
|
||||
def launch(args):
|
||||
def launch(args: List[str]) -> None:
|
||||
import runpy
|
||||
sys.argv = args[1:]
|
||||
exe = args[1]
|
||||
if exe.startswith(':'):
|
||||
import shutil
|
||||
exe = shutil.which(exe[1:])
|
||||
if not exe:
|
||||
q = shutil.which(exe[1:])
|
||||
if not q:
|
||||
raise SystemExit('{} not found in PATH'.format(args[1][1:]))
|
||||
exe = q
|
||||
runpy.run_path(exe, run_name='__main__')
|
||||
|
||||
|
||||
def run_kitten(args):
|
||||
def run_kitten(args: List[str]) -> None:
|
||||
try:
|
||||
kitten = args[1]
|
||||
except IndexError:
|
||||
@@ -59,11 +61,11 @@ def run_kitten(args):
|
||||
list_kittens()
|
||||
raise SystemExit(1)
|
||||
sys.argv = args[1:]
|
||||
from kittens.runner import run_kitten
|
||||
run_kitten(kitten)
|
||||
from kittens.runner import run_kitten as rk
|
||||
rk(kitten)
|
||||
|
||||
|
||||
def namespaced(args):
|
||||
def namespaced(args: List[str]) -> None:
|
||||
func = namespaced_entry_points[args[1]]
|
||||
func(args[1:])
|
||||
|
||||
@@ -84,7 +86,7 @@ namespaced_entry_points['hold'] = hold
|
||||
namespaced_entry_points['complete'] = complete
|
||||
|
||||
|
||||
def setup_openssl_environment():
|
||||
def setup_openssl_environment() -> None:
|
||||
# Workaround for Linux distros that have still failed to get their heads
|
||||
# out of their asses and implement a common location for SSL certificates.
|
||||
# It's not that hard people, there exists a wonderful tool called the symlink
|
||||
@@ -92,11 +94,13 @@ def setup_openssl_environment():
|
||||
if 'SSL_CERT_FILE' not in os.environ and 'SSL_CERT_DIR' not in os.environ:
|
||||
if os.access('/etc/pki/tls/certs/ca-bundle.crt', os.R_OK):
|
||||
os.environ['SSL_CERT_FILE'] = '/etc/pki/tls/certs/ca-bundle.crt'
|
||||
setattr(sys, 'kitty_ssl_env_var', 'SSL_CERT_FILE')
|
||||
elif os.path.isdir('/etc/ssl/certs'):
|
||||
os.environ['SSL_CERT_DIR'] = '/etc/ssl/certs'
|
||||
setattr(sys, 'kitty_ssl_env_var', 'SSL_CERT_DIR')
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> None:
|
||||
if getattr(sys, 'frozen', False) and 'darwin' not in sys.platform.lower():
|
||||
setup_openssl_environment()
|
||||
first_arg = '' if len(sys.argv) < 2 else sys.argv[1]
|
||||
@@ -107,8 +111,8 @@ def main():
|
||||
elif first_arg.startswith('+'):
|
||||
namespaced(['+', first_arg[1:]] + sys.argv[2:])
|
||||
else:
|
||||
from kitty.main import main
|
||||
main()
|
||||
from kitty.main import main as kitty_main
|
||||
kitty_main()
|
||||
else:
|
||||
func(sys.argv[1:])
|
||||
|
||||
|
||||
@@ -1,2 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
cloc --exclude-list-file <(echo -e 'kitty/wcwidth-std.h\nkitty/glfw.c\nkitty/keys.h\nkitty/charsets.c\nkitty/unicode-data.c\nkitty/key_encoding.py\nkitty/rgb.py\nkitty/gl.h\nkitty/gl-wrapper.h\nkitty/gl-wrapper.c\nkitty/khrplatform.h\nkitty/glfw-wrapper.h\nkitty/glfw-wrapper.c\nkitty/emoji.h\nkittens/unicode_input/names.h') kitty kittens
|
||||
#!/usr/bin/env python
|
||||
|
||||
import subprocess
|
||||
|
||||
files_to_exclude = '''\
|
||||
kitty/wcwidth-std.h
|
||||
kitty/glfw.c
|
||||
kitty/keys.h
|
||||
kitty/charsets.c
|
||||
kitty/unicode-data.c
|
||||
kitty/key_encoding.py
|
||||
kitty/rgb.py
|
||||
kitty/gl.h
|
||||
kitty/gl-wrapper.h
|
||||
kitty/gl-wrapper.c
|
||||
kitty/glfw-wrapper.h
|
||||
kitty/glfw-wrapper.c
|
||||
kitty/emoji.h
|
||||
kittens/unicode_input/names.h
|
||||
kitty/parse-graphics-command.h
|
||||
'''
|
||||
|
||||
p = subprocess.Popen([
|
||||
'cloc', '--exclude-list-file', '/dev/stdin', 'kitty', 'kittens'
|
||||
], stdin=subprocess.PIPE)
|
||||
p.communicate(files_to_exclude.encode('utf-8'))
|
||||
raise SystemExit(p.wait())
|
||||
|
||||
2
docs/_static/custom.css
vendored
2
docs/_static/custom.css
vendored
@@ -78,6 +78,8 @@ body div.document {
|
||||
div.sphinxsidebar {
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#sidebartoc li {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
Building kitty from source
|
||||
==============================
|
||||
|
||||
.. image:: https://circleci.com/gh/kovidgoyal/kitty.svg?style=svg
|
||||
.. image:: https://github.com/kovidgoyal/kitty/workflows/CI/badge.svg
|
||||
:alt: Build status
|
||||
:target: https://circleci.com/gh/kovidgoyal/kitty
|
||||
:target: https://github.com/kovidgoyal/kitty/actions?query=workflow%3ACI
|
||||
|
||||
|
||||
|kitty| is designed to run from source, for easy hackability. Make sure
|
||||
@@ -29,7 +29,7 @@ Build-time dependencies:
|
||||
* gcc or clang
|
||||
* pkg-config
|
||||
* For building on Linux in addition to the above dependencies you might also need to install the ``-dev`` packages for:
|
||||
``libdbus-1-dev``, ``libxcursor-dev``, ``libxrandr-dev``, ``libxi-dev``, ``libxinerama-dev``, ``libgl1-mesa-dev``, ``libxkbcommon-x11-dev``, ``libfontconfig-dev``, ``libcanberra-dev`` and ``libpython-dev``.
|
||||
``libdbus-1-dev``, ``libxcursor-dev``, ``libxrandr-dev``, ``libxi-dev``, ``libxinerama-dev``, ``libgl1-mesa-dev``, ``libxkbcommon-x11-dev``, ``libfontconfig-dev``, and ``libpython-dev``,
|
||||
if they are not already installed by your distro.
|
||||
|
||||
Install and run from source
|
||||
|
||||
@@ -4,6 +4,347 @@ Changelog
|
||||
|kitty| is a feature full, cross-platform, *fast*, GPU based terminal emulator.
|
||||
To update |kitty|, :doc:`follow the instructions <binary>`.
|
||||
|
||||
|
||||
0.17.4 [2020-05-09]
|
||||
--------------------
|
||||
|
||||
- Allow showing the name of the current layout and the number of windows
|
||||
in tab titles (:iss:`2634`)
|
||||
|
||||
- macOS: Fix a regression in the previous release that caused ligatures to be
|
||||
not be centered horizontally (:iss:`2591`)
|
||||
|
||||
- By default, double clicking no longer considers the : as part of words, see
|
||||
:opt:`select_by_word_characters` (:iss:`2602`)
|
||||
|
||||
- Fix a regression that caused clicking in the padding/margins of windows in
|
||||
the stack layout to switch the window to the first window (:iss:`2604`)
|
||||
|
||||
- macOS: Fix a regression that broke drag and drop (:iss:`2605`)
|
||||
|
||||
- Report modifier key state when sending wheel events to the terminal program
|
||||
|
||||
- Fix kitty @ send-text not working with text larger than 1024 bytes when using
|
||||
:option:`kitty --listen-on` (:iss:`2607`)
|
||||
|
||||
- Wayland: Fix OS window title not updating for hidden windows (:iss:`2629`)
|
||||
|
||||
- Fix :opt:`background_tint` making the window semi-transparent (:iss:`2618`)
|
||||
|
||||
|
||||
0.17.3 [2020-04-23]
|
||||
--------------------
|
||||
|
||||
- Allow individually setting margins and padding for each edge (left, right,
|
||||
top, bottom). Margins can also be controlled per window via remote control
|
||||
(:iss:`2546`)
|
||||
|
||||
- Fix reverse video not being rendered correctly when using transparency or a
|
||||
background image (:iss:`2419`)
|
||||
|
||||
- Allow mapping arbitrary remote control commands to key presses in
|
||||
:file:`kitty.conf`
|
||||
|
||||
- X11: Fix crash when doing drag and drop from some applications (:iss:`2505`)
|
||||
|
||||
- Fix :option:`launch --stdin-add-formatting` not working (:iss:`2512`)
|
||||
|
||||
- Update to Unicode 13.0 (:iss:`2513`)
|
||||
|
||||
- Render country flags designated by a pair of unicode codepoints
|
||||
in two cells instead of four.
|
||||
|
||||
- diff kitten: New option to control the background color for filler lines in
|
||||
the margin (:iss:`2518`)
|
||||
|
||||
- Fix specifying options for layouts in the startup session file not working
|
||||
(:iss:`2520`)
|
||||
|
||||
- macOS: Fix incorrect horizontal positioning of some full-width East Asian characters
|
||||
(:iss:`1457`)
|
||||
|
||||
- macOS: Render multi-cell PUA characters centered, matching behavior on other
|
||||
platforms
|
||||
|
||||
- Linux: Ignore keys if they are designated as layout/group/mode switch keys
|
||||
(:iss:`2519`)
|
||||
|
||||
- Marks: Fix marks not handling wide characters and tab characters correctly
|
||||
(:iss:`2534`)
|
||||
|
||||
- Add a new :opt:`listen_on` option in kitty.conf to set :option:`kitty --listen-on`
|
||||
globally. Also allow using environment variables in this option (:iss:`2569`).
|
||||
|
||||
- Allow sending mouse events in kittens (:pull:`2538`)
|
||||
|
||||
- icat kitten: Fix display of 16-bit depth images (:iss:`2542`)
|
||||
|
||||
- Add ncurses specific terminfo definitions for strikethrough (:pull:`2567`)
|
||||
|
||||
- Fix a regression in 0.17 that broke displaying graphics over SSH
|
||||
(:iss:`2568`)
|
||||
|
||||
- Fix :option:`--title` not being applied at window creation time (:iss:`2570`)
|
||||
|
||||
0.17.2 [2020-03-29]
|
||||
--------------------
|
||||
|
||||
- Add a :option:`launch --watcher` option that allows defining callbacks
|
||||
that are called for various events in the window's life-cycle (:iss:`2440`)
|
||||
|
||||
- Fix a regression in 0.17 that broke drawing of borders with non-minimal
|
||||
borders (:iss:`2474`)
|
||||
|
||||
- Hints kitten: Allow copying to primary selection as well as clipboard
|
||||
(:pull:`2487`)
|
||||
|
||||
- Add a new mappable action ``close_other_windows_in_tab`` to close all but the
|
||||
active window (:iss:`2484`)
|
||||
|
||||
- Hints kitten: Adjust the default regex used to detect line numbers to handle
|
||||
line+column numbers (:iss:`2268`)
|
||||
|
||||
- Fix blank space at the start of tab bar in the powerline style when first tab is
|
||||
inactive (:iss:`2478`)
|
||||
|
||||
- Fix regression causing incorrect rendering of separators in tab bar when
|
||||
defining a tab bar background color (:pull:`2480`)
|
||||
|
||||
- Fix a regression in 0.17 that broke the kitty @ launch remote command and
|
||||
also broke the --tab-title option when creating a new tab. (:iss:`2488`)
|
||||
|
||||
- Linux: Fix selection of fonts with multiple width variants not preferring
|
||||
the normal width faces (:iss:`2491`)
|
||||
|
||||
|
||||
0.17.1 [2020-03-24]
|
||||
--------------------
|
||||
|
||||
- Fix :opt:`cursor_underline_thickness` not working (:iss:`2465`)
|
||||
|
||||
- Fix a regression in 0.17 that caused tab bar background to be rendered after
|
||||
the last tab as well (:iss:`2464`)
|
||||
|
||||
- macOS: Fix a regression in 0.17 that caused incorrect variants to be
|
||||
automatically selected for some fonts (:iss:`2462`)
|
||||
|
||||
- Fix a regression in 0.17 that caused kitty @ set-colors to require setting
|
||||
cursor_text_color (:iss:`2470`)
|
||||
|
||||
|
||||
0.17.0 [2020-03-24]
|
||||
--------------------
|
||||
|
||||
- :ref:`splits_layout` to arrange windows in arbitrary splits
|
||||
(:iss:`2308`)
|
||||
|
||||
- Add support for specifying a background image, see :opt:`background_image`
|
||||
(:iss:`163` and :pull:`2326`; thanks to Fredrick Brennan.)
|
||||
|
||||
- A new :opt:`background_tint` option to darken the background under the text
|
||||
area when using background images and/or transparent windows.
|
||||
|
||||
- Allow selection of single cells with the mouse. Also improve mouse selection
|
||||
to follow semantics common to most programs (:iss:`945`)
|
||||
|
||||
- New options :opt:`cursor_beam_thickness` and :opt:`cursor_underline_thickness` to control the thickness of the
|
||||
beam and underline cursors (:iss:`2337` and :pull:`2342`)
|
||||
|
||||
- When the application running in the terminal grabs the mouse, pass middle
|
||||
clicks to the application unless :opt:`terminal_select_modifiers` are
|
||||
pressed (:iss:`2368`)
|
||||
|
||||
- A new ``copy_and_clear_or_interrupt`` function (:iss:`2403`)
|
||||
|
||||
- X11: Fix arrow mouse cursor using right pointing instead of the default left
|
||||
pointing arrow (:iss:`2341`)
|
||||
|
||||
- Allow passing the currently active kitty window id in the launch command
|
||||
(:iss:`2391`)
|
||||
|
||||
- unicode input kitten: Allow pressing :kbd:`ctrl+tab` to change the input mode
|
||||
(:iss:`2343`)
|
||||
|
||||
- Fix a bug that prevented using custom functions with the new marks feature
|
||||
(:iss:`2344`)
|
||||
|
||||
- Make the set of URL prefixes that are recognized while hovering with the
|
||||
mouse configurable (:iss:`2416`)
|
||||
|
||||
- Fix border/margin/padding sizes not being recalculated on DPI change
|
||||
(:iss:`2346`)
|
||||
|
||||
- diff kitten: Fix directory diffing with removed binary files failing
|
||||
(:iss:`2378`)
|
||||
|
||||
- macOS: Fix menubar title not updating on OS Window focus change (:iss:`2350`)
|
||||
|
||||
- Fix rendering of combining characters with fonts that have glyphs for
|
||||
precomposed characters but not decomposed versions (:iss:`2365`)
|
||||
|
||||
- Fix incorrect rendering of selection when using rectangular select and
|
||||
scrolling (:iss:`2351`)
|
||||
|
||||
- Allow setting WM_CLASS and WM_NAME when creating new OS windows with the
|
||||
launch command (:option:`launch --os-window-class`)
|
||||
|
||||
- macOS: When switching input method while a pending multi-key input is in
|
||||
progress, clear the pending input (:iss:`2358`)
|
||||
|
||||
- Fix a regression in the previous release that broke switching to neighboring windows
|
||||
in the Grid layout when there are less than four windows (:iss:`2377`)
|
||||
|
||||
- Fix colors in scrollback pager off if the window has redefined terminal
|
||||
colors using escape codes (:iss:`2381`)
|
||||
|
||||
- Fix selection not updating properly while scrolling (:iss:`2442`)
|
||||
|
||||
- Allow extending selections by dragging with right button pressed
|
||||
(:iss:`2445`)
|
||||
|
||||
- Workaround for bug in less that causes colors to reset at wrapped lines
|
||||
(:iss:`2381`)
|
||||
|
||||
- X11/Wayland: Allow drag and drop of text/plain in addition to text/uri-list
|
||||
(:iss:`2441`)
|
||||
|
||||
- Dont strip :code:`&` and :code:`-` from the end of URLs (:iss:`2436`)
|
||||
|
||||
- Fix ``@selection`` placeholder not working with launch command (:iss:`2417`)
|
||||
|
||||
- Drop support for python 3.5
|
||||
|
||||
- Wayland: Fix a crash when drag and dropping into kitty (:iss:`2432`)
|
||||
|
||||
- diff kitten: Fix images lingering as blank rectangles after the kitten quits
|
||||
(:iss:`2449`)
|
||||
|
||||
- diff kitten: Fix images losing position when scrolling using mouse
|
||||
wheel/touchpad
|
||||
|
||||
|
||||
0.16.0 [2020-01-28]
|
||||
--------------------
|
||||
|
||||
- A new :doc:`marks` feature that allows highlighting and scrolling to arbitrary
|
||||
text in the terminal window.
|
||||
|
||||
- hints kitten: Allow pressing :sc:`goto_file_line` to quickly open
|
||||
the selected file at the selected line in vim or a configurable editor (:iss:`2268`)
|
||||
|
||||
- Allow having more than one full height window in the :code:`tall` layout
|
||||
(:iss:`2276`)
|
||||
|
||||
- Allow choosing OpenType features for individual fonts via the
|
||||
:opt:`font_features` option. (:pull:`2248`)
|
||||
|
||||
- Wayland: Fix a freeze in rare circumstances when having multiple OS Windows
|
||||
(:iss:`2307` and :iss:`1722`)
|
||||
|
||||
- Wayland: Fix window titles being set to very long strings on the order of 8KB
|
||||
causing a crash (:iss:`1526`)
|
||||
|
||||
- Add an option :opt:`force_ltr` to turn off the display of text in RTL scripts
|
||||
in right-to-left order (:pull:`2293`)
|
||||
|
||||
- Allow opening new tabs/windows before the current tab/window as well as after
|
||||
it with the :option:`launch --location` option.
|
||||
|
||||
- Add a :opt:`resize_in_steps` option that can be used to resize the OS window
|
||||
in steps as large as character cells (:pull:`2131`)
|
||||
|
||||
- When triple-click+dragging to select multiple lines, extend the selection
|
||||
of the first line to match the rest on the left (:pull:`2284`)
|
||||
|
||||
- macOS: Add a :code:`titlebar-only` setting to
|
||||
:opt:`hide_window_decorations` to only hide the title bar (:pull:`2286`)
|
||||
|
||||
- Fix a segfault when using :option:`kitty --debug-config` with maps (:iss:`2270`)
|
||||
|
||||
- ``goto_tab`` now maps numbers larger than the last tab to the last tab
|
||||
(:iss:`2291`)
|
||||
|
||||
- Fix URL detection not working for urls of the form scheme:///url
|
||||
(:iss:`2292`)
|
||||
|
||||
- When windows are semi-transparent and all contain graphics, correctly render
|
||||
them. (:iss:`2310`)
|
||||
|
||||
0.15.1 [2019-12-21]
|
||||
--------------------
|
||||
|
||||
- Fix a crash/incorrect rendering when detaching a window in some circumstances
|
||||
(:iss:`2173`)
|
||||
|
||||
- hints kitten: Add an option :option:`kitty +kitten hints --ascending` to
|
||||
control if the hints numbers increase or decrease from top to bottom
|
||||
|
||||
- Fix :opt:`background_opacity` incorrectly applying to selected text and
|
||||
reverse video text (:iss:`2177`)
|
||||
|
||||
- Add a new option :opt:`tab_bar_background` to specify a different color
|
||||
for the tab bar (:iss:`2198`)
|
||||
|
||||
- Add a new option :opt:`active_tab_title_template` to specify a different
|
||||
template for active tab titles (:iss:`2198`)
|
||||
|
||||
- Fix lines at the edge of the window at certain windows sizes when drawing
|
||||
images on a transparent window (:iss:`2079`)
|
||||
|
||||
- Fix window not being rendered for the first time until some input has been
|
||||
received from child process (:iss:`2216`)
|
||||
|
||||
|
||||
0.15.0 [2019-11-27]
|
||||
--------------------
|
||||
|
||||
- Add a new action :ref:`detach_window <detach_window>` that can be used to move the current
|
||||
window into a different tab (:iss:`1310`)
|
||||
|
||||
- Add a new action :doc:`launch <launch>` that unifies launching of processes
|
||||
in new kitty windows/tabs.
|
||||
|
||||
- Add a new style ``powerline`` for tab bar rendering, see :opt:`tab_bar_style` (:pull:`2021`)
|
||||
|
||||
- Allow changing colors by mapping a keyboard shortcut to read a kitty config
|
||||
file with color definitions. See the :doc:`FAQ <faq>` for details
|
||||
(:iss:`2083`)
|
||||
|
||||
- hints kitten: Allow completely customizing the matching and actions performed
|
||||
by the kitten using your own script (:iss:`2124`)
|
||||
|
||||
- Wayland: Fix key repeat not being stopped when focus leaves window. This is
|
||||
expected behavior on Wayland, apparently (:iss:`2014`)
|
||||
|
||||
- When drawing unicode symbols that are followed by spaces, use multiple cells
|
||||
to avoid resized or cut-off glyphs (:iss:`1452`)
|
||||
|
||||
- diff kitten: Allow diffing remote files easily via ssh (:iss:`727`)
|
||||
|
||||
- unicode input kitten: Add an option :option:`kitty +kitten unicode_input
|
||||
--emoji-variation` to control the presentation variant of selected emojis
|
||||
(:iss:`2139`)
|
||||
|
||||
- Add specialised rendering for a few more box powerline and unicode symbols
|
||||
(:pull:`2074` and :pull:`2021`)
|
||||
|
||||
- Add a new socket only mode for :opt:`allow_remote_control`. This makes
|
||||
it possible for programs running on the local machine to control kitty
|
||||
but not programs running over ssh.
|
||||
|
||||
- hints kitten: Allow using named groups in the regular expression. The named
|
||||
groups are passed to the invoked program for further processing.
|
||||
|
||||
- Fix a regression in 0.14.5 that caused rendering of private use glyphs
|
||||
with and without spaces to be identical (:iss:`2117`)
|
||||
|
||||
- Wayland: Fix incorrect scale used when first creating an OS window
|
||||
(:iss:`2133`)
|
||||
|
||||
- macOS: Disable mouse hiding by default as getting it to work robustly
|
||||
on Cocoa is too much effort (:iss:`2158`)
|
||||
|
||||
|
||||
0.14.6 [2019-09-25]
|
||||
---------------------
|
||||
|
||||
|
||||
128
docs/conf.py
128
docs/conf.py
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim:fileencoding=utf-8
|
||||
#
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
@@ -7,28 +7,34 @@
|
||||
# full list see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/config
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from functools import partial
|
||||
from typing import (
|
||||
Any, Callable, Dict, Iterable, List, Match, Optional, Sequence, Tuple,
|
||||
Union
|
||||
)
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.parsers.rst.roles import set_classes
|
||||
from pygments.lexer import RegexLexer, bygroups
|
||||
from pygments.token import (
|
||||
from pygments.lexer import RegexLexer, bygroups # type: ignore
|
||||
from pygments.token import ( # type: ignore
|
||||
Comment, Keyword, Literal, Name, Number, String, Whitespace
|
||||
)
|
||||
from sphinx import addnodes
|
||||
from sphinx.environment.adapters.toctree import TocTree
|
||||
from sphinx.util.logging import getLogger
|
||||
from sphinx import addnodes # type: ignore
|
||||
from sphinx.environment.adapters.toctree import TocTree # type: ignore
|
||||
from sphinx.util.logging import getLogger # type: ignore
|
||||
|
||||
kitty_src = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if kitty_src not in sys.path:
|
||||
sys.path.insert(0, kitty_src)
|
||||
str_version = importlib.import_module('kitty.constants').str_version
|
||||
|
||||
from kitty.conf.definition import Option, Shortcut # noqa
|
||||
from kitty.constants import str_version # noqa
|
||||
|
||||
|
||||
# config {{{
|
||||
# -- Project information -----------------------------------------------------
|
||||
@@ -77,7 +83,7 @@ master_doc = 'index'
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
language: Optional[str] = None
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
@@ -184,7 +190,7 @@ texinfo_documents = [
|
||||
|
||||
# GitHub linking inline roles {{{
|
||||
|
||||
def num_role(which, name, rawtext, text, lineno, inliner, options={}, content=[]):
|
||||
def num_role(which: str, name: str, rawtext: str, text: str, lineno: int, inliner: Any, options: Any = {}, content: Any = []) -> Tuple[List, List]:
|
||||
' Link to a github issue '
|
||||
try:
|
||||
issue_num = int(text)
|
||||
@@ -202,10 +208,11 @@ def num_role(which, name, rawtext, text, lineno, inliner, options={}, content=[]
|
||||
return [node], []
|
||||
|
||||
|
||||
def commit_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
|
||||
def commit_role(name: str, rawtext: str, text: str, lineno: int, inliner: Any, options: Any = {}, content: Any = []) -> Tuple[List, List]:
|
||||
' Link to a github commit '
|
||||
try:
|
||||
commit_id = subprocess.check_output(f'git rev-list --max-count=1 --skip=# {text}'.split()).decode('utf-8').strip()
|
||||
commit_id = subprocess.check_output(
|
||||
f'git rev-list --max-count=1 --skip=# {text}'.split()).decode('utf-8').strip()
|
||||
except Exception:
|
||||
msg = inliner.reporter.error(
|
||||
f'GitHub commit id "{text}" not recognized.', line=lineno)
|
||||
@@ -213,21 +220,22 @@ def commit_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
|
||||
return [prb], [msg]
|
||||
url = f'https://github.com/kovidgoyal/kitty/commit/{commit_id}'
|
||||
set_classes(options)
|
||||
short_id = subprocess.check_output(f'git rev-list --max-count=1 --abbrev-commit --skip=# {commit_id}'.split()).decode('utf-8').strip()
|
||||
short_id = subprocess.check_output(
|
||||
f'git rev-list --max-count=1 --abbrev-commit --skip=# {commit_id}'.split()).decode('utf-8').strip()
|
||||
node = nodes.reference(rawtext, f'commit: {short_id}', refuri=url, **options)
|
||||
return [node], []
|
||||
# }}}
|
||||
|
||||
|
||||
# Sidebar ToC {{{
|
||||
def create_toc(app, pagename):
|
||||
def create_toc(app: Any, pagename: str) -> Optional[Any]:
|
||||
tt = TocTree(app.env)
|
||||
toctree = tt.get_toc_for(pagename, app.builder)
|
||||
if toctree is not None:
|
||||
subtree = toctree[toctree.first_child_matching_class(nodes.list_item)]
|
||||
bl = subtree.first_child_matching_class(nodes.bullet_list)
|
||||
if bl is None:
|
||||
return # Empty ToC
|
||||
return None # Empty ToC
|
||||
subtree = subtree[bl]
|
||||
# for li in subtree.traverse(nodes.list_item):
|
||||
# modify_li(li)
|
||||
@@ -235,29 +243,40 @@ def create_toc(app, pagename):
|
||||
return app.builder.render_partial(subtree)['fragment']
|
||||
|
||||
|
||||
def add_html_context(app, pagename, templatename, context, *args):
|
||||
def add_html_context(app: Any, pagename: str, templatename: str, context: Any, *args: Any) -> None:
|
||||
if 'toc' in context:
|
||||
context['toc'] = create_toc(app, pagename) or context['toc']
|
||||
# }}}
|
||||
|
||||
|
||||
# CLI docs {{{
|
||||
def write_cli_docs(all_kitten_names):
|
||||
def write_cli_docs(all_kitten_names: Iterable[str]) -> None:
|
||||
from kitty.launch import options_spec as launch_options_spec
|
||||
from kitty.cli import option_spec_as_rst
|
||||
with open('generated/launch.rst', 'w') as f:
|
||||
f.write(option_spec_as_rst(
|
||||
appname='launch', ospec=launch_options_spec, heading_char='_',
|
||||
message='''\
|
||||
Launch an arbitrary program in a new kitty window/tab. Note that
|
||||
if you specify a program-to-run you can use the special placeholder
|
||||
:code:`@selection` which will be replaced by the current selection.
|
||||
'''
|
||||
))
|
||||
with open('generated/cli-kitty.rst', 'w') as f:
|
||||
f.write(option_spec_as_rst(appname='kitty').replace(
|
||||
'kitty --to', 'kitty @ --to'))
|
||||
as_rst = partial(option_spec_as_rst, heading_char='_')
|
||||
from kitty.remote_control import global_options_spec, cli_msg, cmap, all_commands
|
||||
from kitty.rc.base import all_command_names, command_for_name
|
||||
from kitty.remote_control import global_options_spec, cli_msg
|
||||
with open('generated/cli-kitty-at.rst', 'w') as f:
|
||||
p = partial(print, file=f)
|
||||
p('kitty @\n' + '-' * 80)
|
||||
p('.. program::', 'kitty @')
|
||||
p('\n\n' + as_rst(
|
||||
global_options_spec, message=cli_msg, usage='command ...', appname='kitty @'))
|
||||
from kitty.cmds import cli_params_for
|
||||
for cmd_name in all_commands:
|
||||
func = cmap[cmd_name]
|
||||
from kitty.rc.base import cli_params_for
|
||||
for cmd_name in all_command_names():
|
||||
func = command_for_name(cmd_name)
|
||||
p(f'.. _at_{func.name}:\n')
|
||||
p('kitty @', func.name + '\n' + '-' * 120)
|
||||
p('.. program::', 'kitty @', func.name)
|
||||
@@ -276,14 +295,14 @@ def write_cli_docs(all_kitten_names):
|
||||
# }}}
|
||||
|
||||
|
||||
def write_remote_control_protocol_docs(): # {{{
|
||||
from kitty.cmds import cmap
|
||||
def write_remote_control_protocol_docs() -> None: # {{{
|
||||
from kitty.rc.base import all_command_names, command_for_name, RemoteCommand
|
||||
field_pat = re.compile(r'\s*([a-zA-Z0-9_+]+)\s*:\s*(.+)')
|
||||
|
||||
def format_cmd(p, name, cmd):
|
||||
def format_cmd(p: Callable, name: str, cmd: RemoteCommand) -> None:
|
||||
p(name)
|
||||
p('-' * 80)
|
||||
lines = cmd.__doc__.strip().splitlines()
|
||||
lines = (cmd.__doc__ or '').strip().splitlines()
|
||||
fields = []
|
||||
for line in lines:
|
||||
m = field_pat.match(line)
|
||||
@@ -309,8 +328,8 @@ def write_remote_control_protocol_docs(): # {{{
|
||||
|
||||
with open(f'generated/rc.rst', 'w') as f:
|
||||
p = partial(print, file=f)
|
||||
for name in sorted(cmap):
|
||||
cmd = cmap[name]
|
||||
for name in sorted(all_command_names()):
|
||||
cmd = command_for_name(name)
|
||||
if not cmd.__doc__:
|
||||
continue
|
||||
name = name.replace('_', '-')
|
||||
@@ -373,7 +392,7 @@ class SessionLexer(RegexLexer):
|
||||
}
|
||||
|
||||
|
||||
def link_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
|
||||
def link_role(name: str, rawtext: str, text: str, lineno: int, inliner: Any, options: Any = {}, content: Any = []) -> Tuple[List, List]:
|
||||
m = re.match(r'(.+)\s+<(.+?)>', text)
|
||||
if m is None:
|
||||
msg = inliner.reporter.error(f'link "{text}" not recognized', line=lineno)
|
||||
@@ -385,24 +404,24 @@ def link_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
|
||||
return [node], []
|
||||
|
||||
|
||||
def expand_opt_references(conf_name, text):
|
||||
def expand_opt_references(conf_name: str, text: str) -> str:
|
||||
conf_name += '.'
|
||||
|
||||
def expand(m):
|
||||
def expand(m: Match) -> str:
|
||||
ref = m.group(1)
|
||||
if '<' not in ref and '.' not in ref:
|
||||
full_ref = conf_name + ref
|
||||
return ':opt:`{} <{}>`'.format(ref, full_ref)
|
||||
return m.group()
|
||||
return str(m.group())
|
||||
|
||||
return re.sub(r':opt:`(.+?)`', expand, text)
|
||||
|
||||
|
||||
opt_aliases = {}
|
||||
shortcut_slugs = {}
|
||||
opt_aliases: Dict[str, str] = {}
|
||||
shortcut_slugs: Dict[str, Tuple[str, str]] = {}
|
||||
|
||||
|
||||
def parse_opt_node(env, sig, signode):
|
||||
def parse_opt_node(env: Any, sig: str, signode: Any) -> str:
|
||||
"""Transform an option description into RST nodes."""
|
||||
count = 0
|
||||
firstname = ''
|
||||
@@ -424,22 +443,22 @@ def parse_opt_node(env, sig, signode):
|
||||
return firstname
|
||||
|
||||
|
||||
def parse_shortcut_node(env, sig, signode):
|
||||
def parse_shortcut_node(env: Any, sig: str, signode: Any) -> str:
|
||||
"""Transform a shortcut description into RST nodes."""
|
||||
conf_name, text = sig.split('.', 1)
|
||||
signode += addnodes.desc_name(text, text)
|
||||
return sig
|
||||
|
||||
|
||||
def render_conf(conf_name, all_options):
|
||||
from kitty.conf.definition import merged_opts, Option
|
||||
def render_conf(conf_name: str, all_options: Iterable[Union['Option', Sequence['Shortcut']]]) -> str:
|
||||
from kitty.conf.definition import merged_opts, Option, Group
|
||||
ans = ['.. default-domain:: conf', '']
|
||||
a = ans.append
|
||||
current_group = None
|
||||
all_options = list(all_options)
|
||||
current_group: Optional[Group] = None
|
||||
all_options_ = list(all_options)
|
||||
kitty_mod = 'kitty_mod'
|
||||
|
||||
def render_group(group):
|
||||
def render_group(group: Group) -> None:
|
||||
a('')
|
||||
a(f'.. _conf-{conf_name}-{group.name}:')
|
||||
a('')
|
||||
@@ -451,11 +470,12 @@ def render_conf(conf_name, all_options):
|
||||
a(group.start_text)
|
||||
a('')
|
||||
|
||||
def handle_group_end(group):
|
||||
def handle_group_end(group: Group) -> None:
|
||||
if group.end_text:
|
||||
assert current_group is not None
|
||||
a(''), a(current_group.end_text)
|
||||
|
||||
def handle_group(new_group, new_group_is_shortcut=False):
|
||||
def handle_group(new_group: Group, new_group_is_shortcut: bool = False) -> None:
|
||||
nonlocal current_group
|
||||
if new_group is not current_group:
|
||||
if current_group:
|
||||
@@ -463,14 +483,14 @@ def render_conf(conf_name, all_options):
|
||||
current_group = new_group
|
||||
render_group(current_group)
|
||||
|
||||
def handle_option(i, opt):
|
||||
def handle_option(i: int, opt: Option) -> None:
|
||||
nonlocal kitty_mod
|
||||
if not opt.long_text or not opt.add_to_docs:
|
||||
return
|
||||
handle_group(opt.group)
|
||||
if opt.name == 'kitty_mod':
|
||||
kitty_mod = opt.defval_as_string
|
||||
mopts = list(merged_opts(all_options, opt, i))
|
||||
mopts = list(merged_opts(all_options_, opt, i))
|
||||
a('.. opt:: ' + ', '.join(conf_name + '.' + mo.name for mo in mopts))
|
||||
a('.. code-block:: conf')
|
||||
a('')
|
||||
@@ -482,7 +502,7 @@ def render_conf(conf_name, all_options):
|
||||
a(expand_opt_references(conf_name, opt.long_text))
|
||||
a('')
|
||||
|
||||
def handle_shortcuts(shortcuts):
|
||||
def handle_shortcuts(shortcuts: Sequence[Shortcut]) -> None:
|
||||
sc = shortcuts[0]
|
||||
handle_group(sc.group, True)
|
||||
sc_text = f'{conf_name}.{sc.short_text}'
|
||||
@@ -500,7 +520,7 @@ def render_conf(conf_name, all_options):
|
||||
a(expand_opt_references(conf_name, sc.long_text))
|
||||
a('')
|
||||
|
||||
for i, opt in enumerate(all_options):
|
||||
for i, opt in enumerate(all_options_):
|
||||
if isinstance(opt, Option):
|
||||
handle_option(i, opt)
|
||||
else:
|
||||
@@ -511,7 +531,7 @@ def render_conf(conf_name, all_options):
|
||||
return '\n'.join(ans)
|
||||
|
||||
|
||||
def process_opt_link(env, refnode, has_explicit_title, title, target):
|
||||
def process_opt_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
|
||||
conf_name, opt = target.partition('.')[::2]
|
||||
if not opt:
|
||||
conf_name, opt = 'kitty', conf_name
|
||||
@@ -519,7 +539,7 @@ def process_opt_link(env, refnode, has_explicit_title, title, target):
|
||||
return title, opt_aliases.get(full_name, full_name)
|
||||
|
||||
|
||||
def process_shortcut_link(env, refnode, has_explicit_title, title, target):
|
||||
def process_shortcut_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
|
||||
conf_name, slug = target.partition('.')[::2]
|
||||
if not slug:
|
||||
conf_name, slug = 'kitty', conf_name
|
||||
@@ -534,7 +554,7 @@ def process_shortcut_link(env, refnode, has_explicit_title, title, target):
|
||||
return title, target
|
||||
|
||||
|
||||
def write_conf_docs(app, all_kitten_names):
|
||||
def write_conf_docs(app: Any, all_kitten_names: Iterable[str]) -> None:
|
||||
app.add_lexer('conf', ConfLexer())
|
||||
app.add_object_type(
|
||||
'opt', 'opt',
|
||||
@@ -555,7 +575,7 @@ def write_conf_docs(app, all_kitten_names):
|
||||
sc_role.warn_dangling = True
|
||||
sc_role.process_link = process_shortcut_link
|
||||
|
||||
def generate_default_config(all_options, name):
|
||||
def generate_default_config(all_options: Dict[str, Union[Option, Sequence[Shortcut]]], name: str) -> None:
|
||||
from kitty.conf.definition import as_conf_file
|
||||
with open(f'generated/conf-{name}.rst', 'w', encoding='utf-8') as f:
|
||||
print('.. highlight:: conf\n', file=f)
|
||||
@@ -577,13 +597,13 @@ def write_conf_docs(app, all_kitten_names):
|
||||
# }}}
|
||||
|
||||
|
||||
def setup(app):
|
||||
def setup(app: Any) -> None:
|
||||
os.makedirs('generated/conf', exist_ok=True)
|
||||
from kittens.runner import all_kitten_names
|
||||
all_kitten_names = all_kitten_names()
|
||||
write_cli_docs(all_kitten_names)
|
||||
kn = all_kitten_names()
|
||||
write_cli_docs(kn)
|
||||
write_remote_control_protocol_docs()
|
||||
write_conf_docs(app, all_kitten_names)
|
||||
write_conf_docs(app, kn)
|
||||
app.add_lexer('session', SessionLexer())
|
||||
app.add_role('link', link_role)
|
||||
app.add_role('iss', partial(num_role, 'issues'))
|
||||
|
||||
16
docs/faq.rst
16
docs/faq.rst
@@ -109,8 +109,13 @@ How do I change the colors in a running kitty instance?
|
||||
|
||||
You can either use the
|
||||
`OSC terminal escape codes <https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands>`_
|
||||
to set colors or you can enable :doc:`remote control <remote-control>`
|
||||
for |kitty| and use :ref:`at_set-colors`.
|
||||
to set colors or you can define keyboard shortcuts to set colors, for example::
|
||||
|
||||
map f1 set_colors --configured /path/to/some/config/file/colors.conf
|
||||
|
||||
Or you can enable :doc:`remote control <remote-control>` for |kitty| and use :ref:`at_set-colors`.
|
||||
The shortcut mapping technique has the same syntax as the remote control
|
||||
command, for details, see :ref:`at_set-colors`.
|
||||
|
||||
A list of pre-made color themes for kitty is available at:
|
||||
`kitty-themes <https://github.com/dexpota/kitty-themes>`_
|
||||
@@ -174,7 +179,7 @@ following :file:`~/.config/fontconfig/fonts.conf`::
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
|
||||
<fontconfig>
|
||||
<match target="scan">
|
||||
<match target="font">
|
||||
<test name="family">
|
||||
<string>Your Font Family Name</string>
|
||||
</test>
|
||||
@@ -184,6 +189,11 @@ following :file:`~/.config/fontconfig/fonts.conf`::
|
||||
</match>
|
||||
</fontconfig>
|
||||
|
||||
After creating (or modifying) this file, you may need to run the following
|
||||
command to rebuild your fontconfig cache::
|
||||
|
||||
fc-cache -r
|
||||
|
||||
Then, the font will be available in ``kitty list-fonts``.
|
||||
|
||||
|
||||
|
||||
@@ -334,7 +334,9 @@ Finally, you can specify the image *z-index*, i.e. the vertical stacking order.
|
||||
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.
|
||||
allows rendering of text on top of images. Negative z-index values below
|
||||
INT32_MIN/2 (-1,073,741,824) will be drawn under cells with non-default background
|
||||
colors.
|
||||
|
||||
Deleting images
|
||||
---------------------
|
||||
@@ -417,7 +419,7 @@ Key Value Default Description
|
||||
``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
|
||||
``z`` 32-bit integer ``0`` The *z-index* vertical stacking order of the image
|
||||
**Keys for deleting images**
|
||||
-----------------------------------------------------------
|
||||
``d`` Single character. ``a`` What to delete.
|
||||
|
||||
@@ -53,19 +53,19 @@ Quickstart
|
||||
--------------
|
||||
|
||||
Pre-built binaries of |kitty| are available for both macOS and Linux.
|
||||
See the :doc:`binary install instructions </binary>`. You can also
|
||||
See the :doc:`binary install instructions </binary>`. You can
|
||||
:doc:`build from source </build>`.
|
||||
|
||||
You can also use your favorite package manager to install the |kitty| package.
|
||||
|kitty| packages are available for:
|
||||
`macOS with Homebrew (Cask) <https://formulae.brew.sh/cask/kitty>`_,
|
||||
`macOS and Linux with Nix <https://nixos.org/nixos/packages.html#kitty>`_,
|
||||
`macOS and Linux with Nix <https://nixos.org/nixos/packages.html?channel=nixpkgs-unstable&query=kitty>`_,
|
||||
`Ubuntu <https://launchpad.net/ubuntu/+source/kitty>`_,
|
||||
`Debian <https://packages.debian.org/buster/kitty>`_,
|
||||
`openSUSE <https://build.opensuse.org/package/show/X11:terminals/kitty>`_,
|
||||
`Arch Linux <https://www.archlinux.org/packages/community/x86_64/kitty/>`_,
|
||||
`Gentoo <https://packages.gentoo.org/packages/x11-terms/kitty>`_,
|
||||
`Fedora <https://copr.fedorainfracloud.org/coprs/gagbo/kitty-latest/>`_,
|
||||
`Fedora <https://src.fedoraproject.org/rpms/kitty>`_,
|
||||
`Void Linux <https://github.com/void-linux/void-packages/blob/master/srcpkgs/kitty/template>`_,
|
||||
and `Solus <https://dev.getsol.us/source/kitty/>`_.
|
||||
|
||||
@@ -173,6 +173,30 @@ You can also define a shortcut to switch to the previously active window::
|
||||
``nth_window`` will focus the nth window for positive numbers and the
|
||||
previously active windows for negative numbers.
|
||||
|
||||
.. _detach_window:
|
||||
|
||||
You can define shortcuts to detach the current window and
|
||||
move it to another tab or another OS window::
|
||||
|
||||
# moves the window into a new OS window
|
||||
map ctrl+f2 detach_window
|
||||
# moves the window into a new Tab
|
||||
map ctrl+f3 detach_window new-tab
|
||||
# asks which tab to move the window into
|
||||
map ctrl+f4 detach_window ask
|
||||
|
||||
Similarly, you can detach the current tab, with::
|
||||
|
||||
# moves the tab into a new OS window
|
||||
map ctrl+f2 detach_tab
|
||||
# asks which OS Window to move the tab into
|
||||
map ctrl+f4 detach_tab ask
|
||||
|
||||
Finally, you can define a shortcut to close all windows in a tab other than
|
||||
the currently active window::
|
||||
|
||||
map f9 close_other_windows_in_tab
|
||||
|
||||
|
||||
Other keyboard shortcuts
|
||||
----------------------------------
|
||||
@@ -206,55 +230,26 @@ Reset background opacity :sc:`reset_background_opacity`
|
||||
Layouts
|
||||
----------
|
||||
|
||||
A layout is an arrangement of multiple *windows*. You can create a new window
|
||||
A layout is an arrangement of multiple kitty *windows* inside a top-level OS window. You can create a new window
|
||||
using the :sc:`new_window` key combination.
|
||||
|
||||
Currently, there are five layouts available,
|
||||
Currently, there are six layouts available:
|
||||
|
||||
* **Stack** -- Only a single maximized window is shown at a time
|
||||
* **Tall** -- One window is shown full height on the left, the rest of the windows are shown one below the other on the right
|
||||
* **Fat** -- One window is shown full width on the top, the rest of the windows are shown side-by-side on the bottom
|
||||
* **Fat** -- One (or optionally more) windows are shown full width on the top, the rest of the windows are shown side-by-side on the bottom
|
||||
* **Grid** -- All windows are shown in a grid
|
||||
* **Horizontal** -- All windows are shown side-by-side
|
||||
* **Splits** -- Windows arranged in arbitrary patterns created using horizontal and vertical splits
|
||||
* **Stack** -- Only a single maximized window is shown at a time
|
||||
* **Tall** -- One (or optionally more) windows are shown full height on the left, the rest of the windows are shown one below the other on the right
|
||||
* **Vertical** -- All windows are shown one below the other
|
||||
|
||||
You can switch between layouts using the :sc:`next_layout` key combination. You can
|
||||
also create shortcuts to select particular layouts, and choose which layouts
|
||||
you want to enable/disable, see :ref:`conf-kitty-shortcuts.layout` for examples.
|
||||
By default, all layouts are enabled and you can switch between layouts using
|
||||
the :sc:`next_layout` key combination. You can also create shortcuts to select
|
||||
particular layouts, and choose which layouts you want to enable/disable, see
|
||||
:ref:`conf-kitty-shortcuts.layout` for examples. The first layout listed in
|
||||
:opt:`enabled_layouts` becomes the default layout.
|
||||
|
||||
You can resize windows inside layouts. Press :sc:`start_resizing_window` (also :kbd:`⌘+r` on macOS) to
|
||||
enter resizing mode and follow the on-screen instructions. In a given window
|
||||
layout only some operations may be possible for a particular window. For
|
||||
example, in the Tall layout you can make the first window wider/narrower, but
|
||||
not taller/shorter. Note that what you are resizing is actually not a window,
|
||||
but a row/column in the layout, all windows in that row/column will be resized.
|
||||
|
||||
You can also define shortcuts in :file:`kitty.conf` to make the active window
|
||||
wider, narrower, taller, or shorter by mapping to the ``resize_window``
|
||||
action, for example::
|
||||
|
||||
map ctrl+left resize_window narrower
|
||||
map ctrl+right resize_window wider
|
||||
map ctrl+up resize_window taller
|
||||
map ctrl+down resize_window shorter 3
|
||||
|
||||
The ``resize_window`` action has a second, optional argument to control
|
||||
the resizing increment (a positive integer that defaults to 1).
|
||||
|
||||
|
||||
Some layouts take options to control their behavior. For example, the ``fat``
|
||||
and ``tall`` layouts accept the ``bias`` option to control how the available
|
||||
space is split up. To specify the option, in :opt:`kitty.conf <enabled_layouts>` use::
|
||||
|
||||
enabled_layouts tall:bias=70
|
||||
|
||||
This will make the tall window occupy ``70%`` of available width. ``bias`` can be
|
||||
any number between 10 and 90.
|
||||
|
||||
Writing a new layout only requires about a hundred lines of code, so if there
|
||||
is some layout you want, take a look at `layout.py
|
||||
<https://github.com/kovidgoyal/kitty/blob/master/kitty/layout.py>`_ and submit
|
||||
a pull request!
|
||||
For more details on the layouts and how to use them see :doc:`layouts`.
|
||||
|
||||
.. _kittens:
|
||||
|
||||
@@ -402,7 +397,7 @@ comfortably within the pager.
|
||||
Additionally, you can pipe the contents of the scrollback buffer to an
|
||||
arbitrary, command running in a new window, tab or overlay, for example::
|
||||
|
||||
map f1 pipe @ansi window less +G -R
|
||||
map f1 launch --stdin-source=@screen_scrollback --stdin-add-formatting less +G -R
|
||||
|
||||
Would open the scrollback buffer in a new window when you press the :kbd:`F1`
|
||||
key. See :sc:`show_scrollback` for details.
|
||||
@@ -428,6 +423,14 @@ internal buffer named ``a`` and :kbd:`F2` to paste from that buffer. The buffer
|
||||
names are arbitrary strings, so you can define as many such buffers as you
|
||||
need.
|
||||
|
||||
Marks
|
||||
-------------
|
||||
|
||||
kitty has the ability to mark text on the screen based on regular expressions.
|
||||
This can be useful to highlight words or phrases when browsing output from long
|
||||
running programs or similar. To learn how this feature works, see :doc:`marks`.
|
||||
|
||||
|
||||
Frequently Asked Questions
|
||||
---------------------------------
|
||||
|
||||
@@ -478,7 +481,8 @@ completions happens after the call to :file:`compinit`.
|
||||
fish
|
||||
~~~~~~~~
|
||||
|
||||
Add the following to your :file:`~/.config/fish/config.fish`
|
||||
For versions of fish earlier than 3.0.0, add the following to your
|
||||
:file:`~/.config/fish/config.fish`. Later versions source completions by default.
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
|
||||
@@ -20,24 +20,24 @@ your machine).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def main(args):
|
||||
# this is the main entry point of the kitten, it will be executed in
|
||||
# the overlay window when the kitten is launched
|
||||
answer = input('Enter some text: ')
|
||||
# whatever this function returns will be available in the
|
||||
# handle_result() function
|
||||
return answer
|
||||
def main(args: List[str]) -> str:
|
||||
# this is the main entry point of the kitten, it will be executed in
|
||||
# the overlay window when the kitten is launched
|
||||
answer = input('Enter some text: ')
|
||||
# whatever this function returns will be available in the
|
||||
# handle_result() function
|
||||
return answer
|
||||
|
||||
def handle_result(args, answer, target_window_id, boss):
|
||||
# get the kitty window into which to paste answer
|
||||
w = boss.window_id_map.get(target_window_id)
|
||||
if w is not None:
|
||||
w.paste(answer)
|
||||
def handle_result(args: List[str], answer: str, target_window_id: int, boss: kitty.boss.Boss) -> None:
|
||||
# get the kitty window into which to paste answer
|
||||
w = boss.window_id_map.get(target_window_id)
|
||||
if w is not None:
|
||||
w.paste(answer)
|
||||
|
||||
|
||||
Now in :file:`kitty.conf` add the lines::
|
||||
|
||||
map ctrl+k kitten mykitten.py
|
||||
map ctrl+k kitten mykitten.py
|
||||
|
||||
|
||||
Start kitty and press :kbd:`ctrl+k` and you should see the kitten running.
|
||||
@@ -53,7 +53,7 @@ Passing arguments to kittens
|
||||
You can pass arguments to kittens by defining them in the map directive in
|
||||
:file:`kitty.conf`. For example::
|
||||
|
||||
map ctrl+k kitten mykitten.py arg1 arg2
|
||||
map ctrl+k kitten mykitten.py arg1 arg2
|
||||
|
||||
These will be available as the ``args`` parameter in the ``main()`` and
|
||||
``handle_result()`` functions. Note also that the current working directory
|
||||
@@ -70,17 +70,18 @@ function, telling kitty what kind of input your kitten would like. For example:
|
||||
|
||||
.. code-block:: py
|
||||
|
||||
# in main, STDIN is for the kitten process and will contain
|
||||
# the contents of the screen
|
||||
def main(args):
|
||||
return sys.stdin.read()
|
||||
# in main, STDIN is for the kitten process and will contain
|
||||
# the contents of the screen
|
||||
def main(args):
|
||||
return sys.stdin.read()
|
||||
|
||||
# in handle_result, STDIN is for the kitty process itself, rather
|
||||
# than the kitten process and should not be read from.
|
||||
def handle_result(args, stdin_data, target_window_id, boss):
|
||||
pass
|
||||
# in handle_result, STDIN is for the kitty process itself, rather
|
||||
# than the kitten process and should not be read from.
|
||||
from kittens.tui.handler import result_handler
|
||||
@result_handler(type_of_input='text')
|
||||
def handle_result(args, stdin_data, target_window_id, boss):
|
||||
pass
|
||||
|
||||
handle_result.type_of_input = 'text'
|
||||
|
||||
This will send the plain text of the active window to the kitten's
|
||||
:file:`STDIN`. For text with formatting escape codes, use ``ansi``
|
||||
@@ -103,23 +104,23 @@ Create a file in the kitty config folder, :file:`~/.config/kitty/zoom_toggle.py`
|
||||
|
||||
.. code-block:: py
|
||||
|
||||
def main(args):
|
||||
pass
|
||||
def main(args):
|
||||
pass
|
||||
|
||||
def handle_result(args, answer, target_window_id, boss):
|
||||
tab = boss.active_tab
|
||||
if tab is not None:
|
||||
if tab.current_layout.name == 'stack':
|
||||
tab.last_used_layout()
|
||||
else:
|
||||
tab.goto_layout('stack')
|
||||
|
||||
handle_result.no_ui = True
|
||||
from kittens.tui.handler import result_handler
|
||||
@result_handler(no_ui=True)
|
||||
def handle_result(args, answer, target_window_id, boss):
|
||||
tab = boss.active_tab
|
||||
if tab is not None:
|
||||
if tab.current_layout.name == 'stack':
|
||||
tab.last_used_layout()
|
||||
else:
|
||||
tab.goto_layout('stack')
|
||||
|
||||
|
||||
Now in kitty.conf add::
|
||||
|
||||
map f11 kitten zoom_toggle.py
|
||||
map f11 kitten zoom_toggle.py
|
||||
|
||||
Pressing :kbd:`F11` will now act as a zoom toggle function. You can get even
|
||||
more fancy, switching the kitty OS window to fullscreen as well as changing the
|
||||
@@ -128,7 +129,53 @@ layout, by simply adding the line::
|
||||
boss.toggle_fullscreen()
|
||||
|
||||
|
||||
to the ``handle_result()`` function, above.
|
||||
To the ``handle_result()`` function, above.
|
||||
|
||||
|
||||
Sending mouse events
|
||||
--------------------
|
||||
|
||||
If the program running in a window is receiving mouse events you can simulate
|
||||
those using::
|
||||
|
||||
from kitty.fast_data_types import send_mouse_event
|
||||
send_mouse_event(screen, x, y, button, action, mods)
|
||||
|
||||
``screen`` is the ``screen`` attribute of the window you want to send the event
|
||||
to. ``x`` and ``y`` are the 0-indexed coordinates. ``button`` is
|
||||
``GLFW_MOUSE_BUTTON_{button}`` where ``{button}`` is one of ``LEFT``,
|
||||
``RIGHT``, ``MIDDLE`` or a digit from ``1`` to ``8``. ``action`` is one of
|
||||
``PRESS``, ``RELEASE``, ``DRAG`` or ``MOVE``. ``mods`` is a bitmask of
|
||||
``GLFW_MOD_{mod}`` where ``{mod}`` is one of ``SHIFT``, ``CONTROL`` or ``ALT``.
|
||||
All the mentioned constants are imported from ``kitty.fast_data_types``.
|
||||
|
||||
For example, to send a left click at position x: 2, y: 3 to the active window::
|
||||
|
||||
from kitty.fast_data_types import send_mouse_event, GLFW_MOUSE_BUTTON_LEFT, PRESS
|
||||
send_mouse_event(boss.active_window.screen, 2, 3, GLFW_MOUSE_BUTTON_LEFT, PRESS, 0)
|
||||
|
||||
The function will only send the event if the program is receiving events of
|
||||
that type, and will return ``True`` if it sent the event, and ``False`` if not.
|
||||
|
||||
|
||||
Debugging kittens
|
||||
--------------------
|
||||
|
||||
The part of the kitten that runs in ``main()`` is just a normal program and
|
||||
the output of print statements will be visible in the kitten window. Or
|
||||
alternately, you can use::
|
||||
|
||||
from kittens.tui.loop import debug
|
||||
debug('whatever')
|
||||
|
||||
The ``debug()`` function is just like ``print()`` except that the output
|
||||
will appear in the ``STDOUT`` of the kitty process inside which the kitten is
|
||||
running.
|
||||
|
||||
The ``handle_result()`` part of the kitten runs inside the kitty process.
|
||||
The output of print statements will go to the ``STDOUT`` of the kitty process.
|
||||
So if you run kitty from another kitty instance, the output will be visible
|
||||
in the first kitty instance.
|
||||
|
||||
.. _external_kittens:
|
||||
|
||||
@@ -136,11 +183,11 @@ Kittens created by kitty users
|
||||
---------------------------------------------
|
||||
|
||||
`vim-kitty-navigator <https://github.com/knubie/vim-kitty-navigator>`_
|
||||
Allows you to navigate seamlessly between vim and kitty splits using a consistent set of hotkeys.
|
||||
Allows you to navigate seamlessly between vim and kitty splits using a consistent set of hotkeys.
|
||||
|
||||
`smart-scroll <https://github.com/yurikhan/kitty-smart-scroll>`_
|
||||
Makes the kitty scroll bindings work in full screen applications
|
||||
Makes the kitty scroll bindings work in full screen applications
|
||||
|
||||
`insert password <https://github.com/kovidgoyal/kitty/issues/1222>`_
|
||||
Insert a password from a CLI password manager, taking care to only do it at
|
||||
a password prompt.
|
||||
Insert a password from a CLI password manager, taking care to only do it at
|
||||
a password prompt.
|
||||
|
||||
@@ -18,11 +18,71 @@ select anything that looks like a path or filename and then insert it into the
|
||||
terminal, very useful for picking files from the output of a ``git`` or ``ls`` command and
|
||||
adding them to the command line for the next command.
|
||||
|
||||
You can also press :sc:`goto_file_line` to select anything that looks
|
||||
like a path or filename followed by a colon and a line number and open
|
||||
the file in vim at the specified line number. The patterns and editor
|
||||
to be used can be modified using options passed to the kitten. For example::
|
||||
|
||||
map ctrl+g kitten hints --type=linenum --linenum-action=tab nvim +{line} +{path}
|
||||
|
||||
will open the selected file in a new tab inside neovim when you press
|
||||
:kbd:`ctrl+g`.
|
||||
|
||||
The hints kitten is very powerful to see more detailed help on its various
|
||||
options and modes of operation, see below. You can use these options to
|
||||
create mappings in :file:`kitty.conf` to select various different text
|
||||
snippets. See :sc:`insert_selected_path` for examples.
|
||||
|
||||
Completely customizing the matching and actions of the kitten
|
||||
---------------------------------------------------------------
|
||||
|
||||
The hints kitten supports writing simple python scripts that can be used to
|
||||
completely customize how it finds matches and what happens when a match is
|
||||
selected. This allows the hints kitten to provide the user interface, while
|
||||
you can provide the logic for finding matches and performing actions on them.
|
||||
This is best illustrated with an example. Create the file
|
||||
:file:`custom-hints.py` in the kitty config directory with the following
|
||||
contents:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import re
|
||||
|
||||
def mark(text, args, Mark, extra_cli_args, *a):
|
||||
# This function is responsible for finding all
|
||||
# matching text. extra_cli_args are any extra arguments
|
||||
# passed on the command line when invoking the kitten.
|
||||
# We mark all individual word for potential selection
|
||||
for idx, m in enumerate(re.finditer(r'\w+', text)):
|
||||
start, end = m.span()
|
||||
mark_text = text[start:end].replace('\n', '').replace('\0', '')
|
||||
# The empty dictionary below will be available as groupdicts
|
||||
# in handle_result() and can contain arbitrary data.
|
||||
yield Mark(idx, start, end, mark_text, {})
|
||||
|
||||
|
||||
def handle_result(args, data, target_window_id, boss, extra_cli_args, *a):
|
||||
# This function is responsible for performing some
|
||||
# action on the selected text.
|
||||
# matches is a list of the selected entries and groupdicts contains
|
||||
# the arbitrary data associated with each entry in mark() above
|
||||
matches, groupdicts = [], []
|
||||
for m, g in zip(data['match'], data['groupdicts']):
|
||||
if m:
|
||||
matches.append(m), groupdicts.append(g)
|
||||
for word, match_data in zip(matches, groupdicts):
|
||||
# Lookup the word in a dictionary, the open_url function
|
||||
# will open the provided url in the system browser
|
||||
boss.open_url(f'https://www.google.com/search?q=define:{word}')
|
||||
|
||||
Now run kitty with::
|
||||
|
||||
kitty -o 'map f1 kitten hints --customize-processing custom-hints.py'
|
||||
|
||||
When you press the :kbd:`F1` key you will be able to select a word to
|
||||
look it up in the Google dictionary.
|
||||
|
||||
|
||||
Command Line Interface
|
||||
-------------------------
|
||||
|
||||
|
||||
@@ -21,6 +21,12 @@ Then you can simply use ``icat image.png`` to view images.
|
||||
`ImageMagick <https://www.imagemagick.org>`_ must be installed for ``icat`` to
|
||||
work.
|
||||
|
||||
.. note::
|
||||
|
||||
kitty's image display protocol may not work when used within a terminal
|
||||
multiplexer such as ``screen`` or ``tmux``, depending on whether the
|
||||
multiplexer has added support for it or not.
|
||||
|
||||
|
||||
.. program:: kitty +kitten icat
|
||||
|
||||
|
||||
@@ -22,3 +22,12 @@ In :guilabel:`Name` mode you instead type words from the character name and use
|
||||
keys/tab to select the character from the displayed matches. You can also type
|
||||
a leading period and the index for the match if you don't like to use arrow
|
||||
keys.
|
||||
|
||||
You can switch between modes using either the function keys or by pressing
|
||||
:kbd:`Ctrl+Tab` and :kbd:`Ctrl+Shift+Tab`.
|
||||
|
||||
|
||||
Command Line Interface
|
||||
-------------------------
|
||||
|
||||
.. include:: ../generated/cli-kitten-unicode_input.rst
|
||||
|
||||
96
docs/launch.rst
Normal file
96
docs/launch.rst
Normal file
@@ -0,0 +1,96 @@
|
||||
Launching programs in new windows/tabs
|
||||
========================================
|
||||
|
||||
.. program:: launch
|
||||
|
||||
|
||||
|kitty| has a :code:`launch` action that can be used to run arbitrary programs
|
||||
in news windows/tabs. It can be mapped to user defined shortcuts in kitty.conf.
|
||||
It is very powerful and allows sending the contents of
|
||||
the current window to the launched program, as well as many other options.
|
||||
|
||||
In the simplest form, you can use it to open a new kitty window running the
|
||||
shell, as shown below::
|
||||
|
||||
map f1 launch
|
||||
|
||||
To run a different program simply pass the command line as arguments to
|
||||
launch::
|
||||
|
||||
map f1 launch vim path/to/some/file
|
||||
|
||||
|
||||
To open a new window with the same working directory as the currently
|
||||
active window::
|
||||
|
||||
map f1 launch --cwd=current
|
||||
|
||||
To open the new window in a new tab::
|
||||
|
||||
map f1 launch --type=tab
|
||||
|
||||
To pass the contents of the current screen and scrollback to the started process::
|
||||
|
||||
map f1 launch --stdin-source=@screen_scrollback less
|
||||
|
||||
There are many more powerful options, refer to the complete list below.
|
||||
|
||||
The piping environment
|
||||
--------------------------
|
||||
|
||||
When using :option:`launch --stdin-source`, the program to which the data is
|
||||
piped has a special environment variable declared, ``KITTY_PIPE_DATA`` whose
|
||||
contents are::
|
||||
|
||||
KITTY_PIPE_DATA={scrolled_by}:{cursor_x},{cursor_y}:{lines},{columns}
|
||||
|
||||
where ``scrolled_by`` is the number of lines kitty is currently scrolled by,
|
||||
``cursor_(x|y)`` is the position of the cursor on the screen with ``(1,1)``
|
||||
being the top left corner and ``{lines},{columns}`` being the number of rows
|
||||
and columns of the screen.
|
||||
|
||||
|
||||
Special arguments
|
||||
-------------------
|
||||
|
||||
There are a few special placeholder arguments that can be specified as part of
|
||||
the command line. Namely ``@selection`` which is replaced by the current
|
||||
selection and ``@active-kitty-window-id`` which is replaced by the id of the
|
||||
currently active kitty window. For example::
|
||||
|
||||
map f1 launch my-program @active-kitty-window-id
|
||||
|
||||
|
||||
Watching launched windows
|
||||
---------------------------
|
||||
|
||||
The :option:`launch --watcher` option allows you to specify python functions
|
||||
that will be called at specific events, such as when the window is resized or
|
||||
closed. Simply specify the path to a python module that specifies callback
|
||||
functions for the events you are interested in, for example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def on_resize(boss, window, data):
|
||||
# Here data will contain old_geometry and new_geometry
|
||||
|
||||
def on_close(boss, window, data):
|
||||
# called when window is closed, typically when the program running in
|
||||
# it exits.
|
||||
|
||||
|
||||
Every callback is passed a reference to the global ``Boss`` object as well as
|
||||
the ``Window`` object the action is occurring on. The ``data`` object is
|
||||
mapping that contains event dependent data. Some useful methods and attributes
|
||||
for the ``Window`` object are: ``as_text(as_ans=False, add_history=False,
|
||||
add_wrap_markers=False, alternate_screen=False)`` with which you can get the
|
||||
contents of the window and its scrollback buffer. Similarly,
|
||||
``window.child.pid`` is the PID of the processes that was launched
|
||||
in the window and ``window.id`` is the internal kitty ``id`` of the
|
||||
window.
|
||||
|
||||
|
||||
Syntax reference
|
||||
------------------
|
||||
|
||||
.. include:: /generated/launch.rst
|
||||
247
docs/layouts.rst
Normal file
247
docs/layouts.rst
Normal file
@@ -0,0 +1,247 @@
|
||||
Layouts
|
||||
============
|
||||
|
||||
kitty has the ability to define its own windows that can be tiled next to each
|
||||
other in arbitrary arrangements, based on *Layouts*, see below for examples:
|
||||
|
||||
|
||||
.. figure:: screenshots/screenshot.png
|
||||
:alt: Screenshot, showing three programs in the 'Tall' layout
|
||||
:align: center
|
||||
:scale: 100%
|
||||
|
||||
Screenshot, showing vim, tig and git running in |kitty| with the 'Tall' layout
|
||||
|
||||
|
||||
.. figure:: screenshots/splits.png
|
||||
:alt: Screenshot, showing windows in the 'Splits' layout
|
||||
:align: center
|
||||
:scale: 100%
|
||||
|
||||
Screenshot, showing windows with arbitrary arrangement in the 'Splits'
|
||||
layout
|
||||
|
||||
|
||||
There are many different layouts available. They are all enabled by default,
|
||||
you can switch layouts using :sc:`next_layout`. To control which layouts
|
||||
are available use :opt:`enabled_layouts`, the first listed layout becomes
|
||||
the default. Individual layouts and how to use them are described below.
|
||||
|
||||
.. contents::
|
||||
|
||||
|
||||
The Stack Layout
|
||||
------------------
|
||||
|
||||
This is the simplest layout it displays a single window using all available
|
||||
space, other windows are hidden behind it. It has no options::
|
||||
|
||||
enabled_layouts stack
|
||||
|
||||
|
||||
The Tall Layout
|
||||
------------------
|
||||
|
||||
Displays one (or optionally more) full height windows on the left half of the
|
||||
screen. Remaining windows are tiled vertically on the right half of the screen.
|
||||
There are options to control how the screen is split horizontally ``bias``
|
||||
(an integer between ``10`` and ``90``) and options to control how many
|
||||
full-height windows there are ``full_size`` (a positive integer). The syntax
|
||||
for the options is shown below::
|
||||
|
||||
enabled_layouts tall:bias=50;full_size=1
|
||||
|
||||
┌──────────────┬───────────────┐
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ ├───────────────┤
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ ├───────────────┤
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
└──────────────┴───────────────┘
|
||||
|
||||
The Fat Layout
|
||||
----------------
|
||||
|
||||
Displays one (or optionally more) full width windows on the top half of the
|
||||
screen. Remaining windows are tiled horizontally on the bottom half of the screen.
|
||||
There are options to control how the screen is split vertically ``bias``
|
||||
(an integer between ``10`` and ``90``) and options to control how many
|
||||
full-height windows there are ``full_size`` (a positive integer). The syntax
|
||||
for the options is shown below::
|
||||
|
||||
enabled_layouts fat:bias=50;full_size=1
|
||||
|
||||
┌──────────────────────────────┐
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
├─────────┬──────────┬─────────┤
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
└─────────┴──────────┴─────────┘
|
||||
|
||||
|
||||
The Grid Layout
|
||||
--------------------
|
||||
|
||||
Display windows in a balanced grid with all windows the same size except the
|
||||
last column if there are not enough windows to fill the grid. Has no options::
|
||||
|
||||
enabled_layouts grid
|
||||
|
||||
┌─────────┬──────────┬─────────┐
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
├─────────┼──────────┼─────────┤
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
└─────────┴──────────┴─────────┘
|
||||
|
||||
|
||||
.. _splits_layout:
|
||||
|
||||
The Splits Layout
|
||||
--------------------
|
||||
|
||||
This is the most flexible layout. You can create any arrangement of windows
|
||||
by splitting exiting windows repeatedly. To best use this layout you should
|
||||
define a few extra keybindings in :file:`kitty.conf`::
|
||||
|
||||
map F5 launch --location=hsplit
|
||||
map F6 launch --location=vsplit
|
||||
map F7 layout_action rotate
|
||||
|
||||
map shift+up move_window up
|
||||
map shift+left move_window left
|
||||
map shift+right move_window right
|
||||
map shift+down move_window down
|
||||
|
||||
map ctrl+left neighboring_window left
|
||||
map ctrl+right neighboring_window right
|
||||
map ctrl+up neighboring_window up
|
||||
map ctrl+down neighboring_window down
|
||||
|
||||
Now you can create horizontal and vertical splits by using :kbd:`F5` and
|
||||
:kbd:`F6`. You can move them around using :kbd:`shift+arrow keys`
|
||||
and you can move focus to neighboring windows using :kbd:`ctrl+arrow keys`.
|
||||
You can switch an existing split from horizontal to vertical and vice versa
|
||||
using :kbd:`F7`. Finally, windows can be resized using :ref:`window_resizing`.
|
||||
|
||||
This layout takes one option, ``split_axis`` that controls whether new windows
|
||||
are placed into vertical or horizontal splits, by default::
|
||||
|
||||
enabled_layouts splits:split_axis=horizontal
|
||||
|
||||
┌──────────────┬───────────────┐
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ ├───────┬───────┤
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ ├───────┴───────┤
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
└──────────────┴───────────────┘
|
||||
|
||||
|
||||
|
||||
The Horizontal Layout
|
||||
------------------------
|
||||
|
||||
All windows are shown side by side. Has no options::
|
||||
|
||||
enabled_layouts horizontal
|
||||
|
||||
┌─────────┬──────────┬─────────┐
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
└─────────┴──────────┴─────────┘
|
||||
|
||||
|
||||
The Vertical Layout
|
||||
-----------------------
|
||||
|
||||
All windows are shown one below the other. Has no options::
|
||||
|
||||
enabled_layouts vertical
|
||||
|
||||
┌──────────────────────────────┐
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
├──────────────────────────────┤
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
├──────────────────────────────┤
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────┘
|
||||
|
||||
|
||||
.. _window_resizing:
|
||||
|
||||
Resizing windows
|
||||
------------------
|
||||
|
||||
You can resize windows inside layouts. Press :sc:`start_resizing_window` (also
|
||||
:kbd:`⌘+r` on macOS) to enter resizing mode and follow the on-screen
|
||||
instructions. In a given window layout only some operations may be possible
|
||||
for a particular window. For example, in the Tall layout you can make the first
|
||||
window wider/narrower, but not taller/shorter. Note that what you are resizing
|
||||
is actually not a window, but a row/column in the layout, all windows in that
|
||||
row/column will be resized.
|
||||
|
||||
You can also define shortcuts in :file:`kitty.conf` to make the active window
|
||||
wider, narrower, taller, or shorter by mapping to the ``resize_window``
|
||||
action, for example::
|
||||
|
||||
map ctrl+left resize_window narrower
|
||||
map ctrl+right resize_window wider
|
||||
map ctrl+up resize_window taller
|
||||
map ctrl+down resize_window shorter 3
|
||||
|
||||
The ``resize_window`` action has a second, optional argument to control
|
||||
the resizing increment (a positive integer that defaults to 1).
|
||||
|
||||
|
||||
Some layouts take options to control their behavior. For example, the ``fat``
|
||||
and ``tall`` layouts accept the ``bias`` and ``full_size`` options to control
|
||||
how the available space is split up.
|
||||
To specify the option, in :opt:`kitty.conf <enabled_layouts>` use::
|
||||
|
||||
enabled_layouts tall:bias=70;full_size=2
|
||||
|
||||
This will have ``2`` instead of a single tall window, that occupy ``70%``
|
||||
instead of ``50%`` of available width. ``bias`` can be any number between 10
|
||||
and 90.
|
||||
|
||||
Writing a new layout only requires about a hundred lines of code, so if there
|
||||
is some layout you want, take a look at `layout.py
|
||||
<https://github.com/kovidgoyal/kitty/blob/master/kitty/layout.py>`_ and submit
|
||||
a pull request!
|
||||
125
docs/marks.rst
Normal file
125
docs/marks.rst
Normal file
@@ -0,0 +1,125 @@
|
||||
Marks
|
||||
=================
|
||||
|
||||
|
||||
kitty has the ability to mark text on the screen based on regular expressions.
|
||||
This can be useful to highlight words or phrases when browsing output from long
|
||||
running programs or similar. Lets start with a few examples:
|
||||
|
||||
Suppose we want to be able to highlight the word ERROR in the current window.
|
||||
Add the following to :file:`kitty.conf`::
|
||||
|
||||
map f1 toggle_marker text 1 ERROR
|
||||
|
||||
Now when you press :kbd:`F1` all instances of the word :code:`ERROR` will be
|
||||
highlighted. To turn off the highlighting, press :kbd:`F1` again.
|
||||
If you want to make it case-insensitive, use::
|
||||
|
||||
map f1 toggle_marker itext 1 ERROR
|
||||
|
||||
To make it match only complete words, use::
|
||||
|
||||
map f1 toggle_marker regex 1 \bERROR\b
|
||||
|
||||
Suppose you want to highlight both :code:`ERROR` and :code:`WARNING`, case
|
||||
insensitively::
|
||||
|
||||
map f1 toggle_marker iregex 1 \bERROR\b 2 \bWARNING\b
|
||||
|
||||
kitty supports up to 3 mark groups (the numbers in the commands above). You
|
||||
can control the colors used for these groups in :file:`kitty.conf` with::
|
||||
|
||||
mark1_foreground red
|
||||
mark1_background gray
|
||||
mark2_foreground green
|
||||
...
|
||||
|
||||
|
||||
.. note::
|
||||
For performance reasons, matching is done per line only, and only when that line is
|
||||
altered in any way. So you cannot match text that stretches across multiple
|
||||
lines.
|
||||
|
||||
|
||||
Creating markers dynamically
|
||||
---------------------------------
|
||||
|
||||
If you want to create markers dynamically rather than pre-defining them in
|
||||
:file:`kitty.conf` you can do so as follows::
|
||||
|
||||
map f1 create_marker
|
||||
map f2 remove_marker
|
||||
|
||||
Then pressing :kbd:`F1` will allow you to enter the marker definition and set
|
||||
it and pressing :kbd:`F2` will remove the marker. ``create_marker`` accepts
|
||||
the same syntax as ``toggle_marker`` above. Note that while creating
|
||||
markers, the prompt has history so you can easily re-use previous marker
|
||||
expressions.
|
||||
|
||||
You can also use the facilities for :doc:`remote-control` to dynamically
|
||||
add/remove markers.
|
||||
|
||||
|
||||
Scrolling to marks
|
||||
--------------------
|
||||
|
||||
kitty has an action to scroll to the next line that contains a mark. You can
|
||||
use it by mapping it to some shortcut in :file:`kitty.conf`::
|
||||
|
||||
map ctrl+p scroll_to_mark prev
|
||||
map ctrl+n scroll_to_mark next
|
||||
|
||||
Then pressing :kbd:`ctrl+p` will scroll to the first line in the scrollback
|
||||
buffer above the current top line that contains a mark. Pressing :kbd:`ctrl+n`
|
||||
will scroll to show the first line below the current last line that contains
|
||||
a mark. If you wish to jump to a mark of a specific type, you can add that to
|
||||
the mapping::
|
||||
|
||||
map ctrl+1 scroll_to_mark prev 1
|
||||
|
||||
Which will scroll only to marks of type 1.
|
||||
|
||||
|
||||
The full syntax for creating marks
|
||||
-------------------------------------
|
||||
|
||||
The syntax of the :code:`toggle_marker` command is::
|
||||
|
||||
toggle_marker <marker-type> <specification>
|
||||
|
||||
Here :code:`marker-type` is one of:
|
||||
|
||||
* :code:`text` - simple substring matching
|
||||
* :code:`itext` - case-insensitive substring matching
|
||||
* :code:`regex` - A python regular expression
|
||||
* :code:`iregex` - A case-insensitive python regular expression
|
||||
* :code:`function` - An arbitrary function defined in a python file, see
|
||||
:ref:`marker_funcs`.
|
||||
|
||||
.. _marker_funcs:
|
||||
|
||||
Arbitrary marker functions
|
||||
-----------------------------
|
||||
|
||||
You can create your own marker functions. Create a python file named
|
||||
:file:`mymarker.py` and in it create a :code:`marker` function. This
|
||||
function receives the text of the line as input and must yield three numbers,
|
||||
the starting character position, the ending character position and the mark
|
||||
group (1-3). For example:
|
||||
|
||||
.. code-block::
|
||||
|
||||
def marker(text):
|
||||
# Function to highlight the letter X
|
||||
for i, ch in enumerate(text):
|
||||
if ch.lower() == 'x':
|
||||
yield i, i, 3
|
||||
|
||||
|
||||
Save this file somewhere and in :file:`kitty.conf`, use::
|
||||
|
||||
map f1 toggle_marker function /path/to/mymarker.py
|
||||
|
||||
If you save the file in the kitty config directory, you can use::
|
||||
|
||||
map f1 toggle_marker function mymarker.py
|
||||
@@ -1,6 +1,9 @@
|
||||
Working with the screen and history buffer contents
|
||||
======================================================
|
||||
|
||||
.. warning::
|
||||
The pipe action has been deprecated in favor of the
|
||||
:doc:`launch <launch>` action which is more powerful.
|
||||
|
||||
You can pipe the contents of the current screen and history buffer as
|
||||
:file:`STDIN` to an arbitrary program using the ``pipe`` function. The program
|
||||
@@ -56,6 +59,9 @@ Input placeholders
|
||||
|
||||
There are various different kinds of placeholders
|
||||
|
||||
``@selection``
|
||||
Plain text, currently selected text
|
||||
|
||||
``@text``
|
||||
Plain text, current screen + scrollback buffer
|
||||
|
||||
|
||||
@@ -219,6 +219,11 @@ first, for example::
|
||||
<ESC>]52;c;!<ESC>\
|
||||
|
||||
Here ``!`` is not valid base64 encoded text, so it clears the clipboard.
|
||||
Further, since it is invalid, it should be ignored by terminal emulators
|
||||
that do not support this extension, thereby making it safe to use, simply
|
||||
always send it before starting a new OSC 52 paste, even if you aren't chunking
|
||||
up large pastes, that way kitty wont concatenate your paste, and it will have
|
||||
no ill-effects in other terminal emulators.
|
||||
|
||||
In case you're using software that can't be easily adapted to this
|
||||
protocol extension, it can be disabled by specifying ``no-append`` to the
|
||||
|
||||
@@ -21,7 +21,7 @@ In order for control to work, :opt:`allow_remote_control` must be enabled in
|
||||
|
||||
Now, in the new |kitty| window, enter the command::
|
||||
|
||||
kitty @ new-window --title Output --keep-focus cat
|
||||
kitty @ launch --title Output --keep-focus cat
|
||||
|
||||
This will open a new window, running the ``cat`` program that will appear next
|
||||
to the current window.
|
||||
@@ -54,7 +54,7 @@ window. Type ``Ctrl+D`` when you are ready to stop.
|
||||
|
||||
Now, let's open a new tab::
|
||||
|
||||
kitty @ new-window --new-tab --tab-title "My Tab" --keep-focus bash
|
||||
kitty @ launch --type=tab --tab-title "My Tab" --keep-focus bash
|
||||
|
||||
This will open a new tab running the bash shell with the title "My Tab".
|
||||
We can change the title of the tab with::
|
||||
@@ -126,7 +126,7 @@ If you do not want to allow all programs running in |kitty| to control it, you c
|
||||
enable remote control for only some |kitty| windows. Simply create a shortcut
|
||||
such as::
|
||||
|
||||
map ctrl+k new_window @ some_program
|
||||
map ctrl+k launch --allow-remote-control some_program
|
||||
|
||||
Then programs running in windows created with that shortcut can use ``kitty @``
|
||||
to control kitty. Note that any program with the right level of permissions can
|
||||
@@ -134,6 +134,19 @@ still write to the pipes of any other program on the same computer and
|
||||
therefore can control |kitty|. It can, however, be useful to block programs
|
||||
running on other computers (for example, over ssh) or as other users.
|
||||
|
||||
Mapping key presses to remote control commands
|
||||
--------------------------------------------------
|
||||
|
||||
If you wish to trigger a remote control command easily with just a keypress,
|
||||
you can map it in :file:`kitty.conf`. For example::
|
||||
|
||||
map F1 remote_control set-spacing margin=30
|
||||
|
||||
Then pressing the :kbd:`F1` key will set the active window margins to 30.
|
||||
The syntax for what follows :code:`remote_control` is exactly the same
|
||||
as the syntax for what follows :code:`kitty @` above.
|
||||
|
||||
|
||||
Documentation for the remote control protocol
|
||||
-----------------------------------------------
|
||||
|
||||
|
||||
BIN
docs/screenshots/splits.png
Normal file
BIN
docs/screenshots/splits.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@@ -4,18 +4,23 @@
|
||||
|
||||
import subprocess
|
||||
from collections import defaultdict
|
||||
from typing import Any, DefaultDict, Dict, FrozenSet, List, Tuple, Union
|
||||
|
||||
KeymapType = Dict[str, Tuple[str, Union[FrozenSet[str], str]]]
|
||||
|
||||
|
||||
def resolve_keys(keymap):
|
||||
ans = defaultdict(list)
|
||||
def resolve_keys(keymap: KeymapType) -> DefaultDict[str, List[str]]:
|
||||
ans: DefaultDict[str, List[str]] = defaultdict(list)
|
||||
for ch, (attr, atype) in keymap.items():
|
||||
if atype not in ('int', 'uint'):
|
||||
atype = 'flag'
|
||||
ans[atype].append(ch)
|
||||
if isinstance(atype, str) and atype in ('int', 'uint'):
|
||||
q = atype
|
||||
else:
|
||||
q = 'flag'
|
||||
ans[q].append(ch)
|
||||
return ans
|
||||
|
||||
|
||||
def enum(keymap):
|
||||
def enum(keymap: KeymapType) -> str:
|
||||
lines = []
|
||||
for ch, (attr, atype) in keymap.items():
|
||||
lines.append(f"{attr}='{ch}'")
|
||||
@@ -26,15 +31,15 @@ def enum(keymap):
|
||||
'''.format(',\n'.join(lines))
|
||||
|
||||
|
||||
def parse_key(keymap):
|
||||
def parse_key(keymap: KeymapType) -> str:
|
||||
lines = []
|
||||
for attr, atype in keymap.values():
|
||||
vs = atype.upper() if atype in ('uint', 'int') else 'FLAG'
|
||||
vs = atype.upper() if isinstance(atype, str) and atype in ('uint', 'int') else 'FLAG'
|
||||
lines.append(f'case {attr}: value_state = {vs}; break;')
|
||||
return ' \n'.join(lines)
|
||||
|
||||
|
||||
def parse_flag(keymap, type_map, command_class):
|
||||
def parse_flag(keymap: KeymapType, type_map: Dict[str, Any], command_class: str) -> str:
|
||||
lines = []
|
||||
for ch in type_map['flag']:
|
||||
attr, allowed_values = keymap[ch]
|
||||
@@ -52,14 +57,14 @@ def parse_flag(keymap, type_map, command_class):
|
||||
return ' \n'.join(lines)
|
||||
|
||||
|
||||
def parse_number(keymap):
|
||||
def parse_number(keymap: KeymapType) -> Tuple[str, str]:
|
||||
int_keys = [f'I({attr})' for attr, atype in keymap.values() if atype == 'int']
|
||||
uint_keys = [f'U({attr})' for attr, atype in keymap.values() if atype == 'uint']
|
||||
return '; '.join(int_keys), '; '.join(uint_keys)
|
||||
|
||||
|
||||
def cmd_for_report(report_name, keymap, type_map, payload_allowed):
|
||||
def group(atype, conv):
|
||||
def cmd_for_report(report_name: str, keymap: KeymapType, type_map: Dict[str, Any], payload_allowed: bool) -> str:
|
||||
def group(atype: str, conv: str) -> Tuple[str, str]:
|
||||
flag_fmt, flag_attrs = [], []
|
||||
cv = {'flag': 'c', 'int': 'i', 'uint': 'I'}[atype]
|
||||
for ch in type_map[atype]:
|
||||
@@ -84,7 +89,15 @@ def cmd_for_report(report_name, keymap, type_map, payload_allowed):
|
||||
return '\n'.join(ans)
|
||||
|
||||
|
||||
def generate(function_name, callback_name, report_name, keymap, command_class, initial_key='a', payload_allowed=True):
|
||||
def generate(
|
||||
function_name: str,
|
||||
callback_name: str,
|
||||
report_name: str,
|
||||
keymap: KeymapType,
|
||||
command_class: str,
|
||||
initial_key: str = 'a',
|
||||
payload_allowed: bool = True
|
||||
) -> str:
|
||||
type_map = resolve_keys(keymap)
|
||||
keys_enum = enum(keymap)
|
||||
handle_key = parse_key(keymap)
|
||||
@@ -124,6 +137,7 @@ static inline void
|
||||
{parr}
|
||||
{keys_enum}
|
||||
enum KEYS key = '{initial_key}';
|
||||
if (screen->parser_buf[pos] == ';') state = AFTER_VALUE;
|
||||
|
||||
while (pos < screen->parser_buf_pos) {{
|
||||
switch(state) {{
|
||||
@@ -225,7 +239,7 @@ static inline void
|
||||
'''
|
||||
|
||||
|
||||
def write_header(text, path):
|
||||
def write_header(text: str, path: str) -> None:
|
||||
with open(path, 'w') as f:
|
||||
print(f'// This file is generated by {__file__} do not edit!', file=f, end='\n\n')
|
||||
print('#pragma once', file=f)
|
||||
@@ -233,9 +247,9 @@ def write_header(text, path):
|
||||
subprocess.check_call(['clang-format', '-i', path])
|
||||
|
||||
|
||||
def graphics_parser():
|
||||
def graphics_parser() -> None:
|
||||
flag = frozenset
|
||||
keymap = {
|
||||
keymap: KeymapType = {
|
||||
'a': ('action', flag('tTqpd')),
|
||||
'd': ('delete_action', flag('aAiIcCpPqQxXyYzZ')),
|
||||
't': ('transmission_type', flag('dfts')),
|
||||
|
||||
198
gen-wcwidth.py
198
gen-wcwidth.py
@@ -12,6 +12,10 @@ from functools import partial
|
||||
from html.entities import html5
|
||||
from itertools import groupby
|
||||
from operator import itemgetter
|
||||
from typing import (
|
||||
Callable, DefaultDict, Dict, FrozenSet, Generator, Iterable, List,
|
||||
Optional, Set, Tuple, Union
|
||||
)
|
||||
from urllib.request import urlopen
|
||||
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -24,7 +28,7 @@ if len(non_characters) != 66:
|
||||
emoji_skin_tone_modifiers = frozenset(range(0x1f3fb, 0x1F3FF + 1))
|
||||
|
||||
|
||||
def get_data(fname, folder='UCD'):
|
||||
def get_data(fname: str, folder: str = 'UCD') -> Iterable[str]:
|
||||
url = f'https://www.unicode.org/Public/{folder}/latest/{fname}'
|
||||
bn = os.path.basename(url)
|
||||
local = os.path.join('/tmp', bn)
|
||||
@@ -42,23 +46,25 @@ def get_data(fname, folder='UCD'):
|
||||
|
||||
|
||||
# Map of class names to set of codepoints in class
|
||||
class_maps = {}
|
||||
name_map = {}
|
||||
word_search_map = defaultdict(set)
|
||||
class_maps: Dict[str, Set[int]] = {}
|
||||
all_symbols: Set[int] = set()
|
||||
name_map: Dict[int, str] = {}
|
||||
word_search_map: DefaultDict[str, Set[int]] = defaultdict(set)
|
||||
zwj = 0x200d
|
||||
marks = set(emoji_skin_tone_modifiers) | {zwj}
|
||||
flag_codepoints = frozenset(range(0x1F1E6, 0x1F1E6 + 26))
|
||||
marks = set(emoji_skin_tone_modifiers) | {zwj} | flag_codepoints
|
||||
not_assigned = set(range(0, sys.maxunicode))
|
||||
|
||||
|
||||
def parse_ucd():
|
||||
def parse_ucd() -> None:
|
||||
|
||||
def add_word(w, c):
|
||||
def add_word(w: str, c: int) -> None:
|
||||
if c <= 32 or c == 127 or 128 <= c <= 159:
|
||||
return
|
||||
if len(w) > 1:
|
||||
word_search_map[w.lower()].add(c)
|
||||
|
||||
first = None
|
||||
first: Optional[int] = None
|
||||
for word, c in html5.items():
|
||||
if len(c) == 1:
|
||||
add_word(word.rstrip(';'), ord(c))
|
||||
@@ -76,7 +82,7 @@ def parse_ucd():
|
||||
category = parts[2]
|
||||
s = class_maps.setdefault(category, set())
|
||||
desc = parts[1]
|
||||
codepoints = (codepoint,)
|
||||
codepoints: Union[Tuple[int, ...], Iterable[int]] = (codepoint,)
|
||||
if first is None:
|
||||
if desc.endswith(', First>'):
|
||||
first = codepoint
|
||||
@@ -89,6 +95,8 @@ def parse_ucd():
|
||||
not_assigned.discard(codepoint)
|
||||
if category.startswith('M'):
|
||||
marks.add(codepoint)
|
||||
elif category.startswith('S'):
|
||||
all_symbols.add(codepoint)
|
||||
|
||||
# Some common synonyms
|
||||
word_search_map['bee'] |= word_search_map['honeybee']
|
||||
@@ -96,43 +104,97 @@ def parse_ucd():
|
||||
word_search_map['lamda'] |= word_search_map['lambda']
|
||||
|
||||
|
||||
def split_two(line):
|
||||
spec, rest = line.split(';', 1)
|
||||
spec, rest = spec.strip(), rest.strip().split(' ', 1)[0].strip()
|
||||
def parse_range_spec(spec: str) -> Set[int]:
|
||||
spec = spec.strip()
|
||||
if '..' in spec:
|
||||
chars = tuple(map(lambda x: int(x, 16), filter(None, spec.split('.'))))
|
||||
chars = set(range(chars[0], chars[1] + 1))
|
||||
chars_ = tuple(map(lambda x: int(x, 16), filter(None, spec.split('.'))))
|
||||
chars = set(range(chars_[0], chars_[1] + 1))
|
||||
else:
|
||||
chars = {int(spec, 16)}
|
||||
return chars, rest
|
||||
return chars
|
||||
|
||||
|
||||
all_emoji = set()
|
||||
emoji_categories = {}
|
||||
emoji_presentation_bases = set()
|
||||
def split_two(line: str) -> Tuple[Set[int], str]:
|
||||
spec, rest = line.split(';', 1)
|
||||
spec, rest = spec.strip(), rest.strip().split(' ', 1)[0].strip()
|
||||
return parse_range_spec(spec), rest
|
||||
|
||||
|
||||
def parse_emoji():
|
||||
for line in get_data('emoji-data.txt', 'emoji'):
|
||||
chars, rest = split_two(line)
|
||||
s = emoji_categories.setdefault(rest, set())
|
||||
s.update(chars)
|
||||
all_emoji.update(chars)
|
||||
for line in get_data('emoji-variation-sequences.txt', 'emoji'):
|
||||
base, var, *rest = line.split()
|
||||
if base.startswith('#'):
|
||||
all_emoji: Set[int] = set()
|
||||
emoji_presentation_bases: Set[int] = set()
|
||||
narrow_emoji: Set[int] = set()
|
||||
wide_emoji: Set[int] = set()
|
||||
flags: Dict[int, List[int]] = {}
|
||||
|
||||
|
||||
def parse_basic_emoji(spec: str) -> None:
|
||||
parts = list(filter(None, spec.split()))
|
||||
has_emoji_presentation = len(parts) < 2
|
||||
chars = parse_range_spec(parts[0])
|
||||
all_emoji.update(chars)
|
||||
emoji_presentation_bases.update(chars)
|
||||
(wide_emoji if has_emoji_presentation else narrow_emoji).update(chars)
|
||||
|
||||
|
||||
def parse_keycap_sequence(spec: str) -> None:
|
||||
base, fe0f, cc = list(filter(None, spec.split()))
|
||||
chars = parse_range_spec(base)
|
||||
all_emoji.update(chars)
|
||||
emoji_presentation_bases.update(chars)
|
||||
narrow_emoji.update(chars)
|
||||
|
||||
|
||||
def parse_flag_emoji_sequence(spec: str) -> None:
|
||||
a, b = list(filter(None, spec.split()))
|
||||
left, right = int(a, 16), int(b, 16)
|
||||
chars = {left, right}
|
||||
all_emoji.update(chars)
|
||||
wide_emoji.update(chars)
|
||||
emoji_presentation_bases.update(chars)
|
||||
flags.setdefault(left, []).append(right)
|
||||
|
||||
|
||||
def parse_emoji_tag_sequence(spec: str) -> None:
|
||||
a = int(spec.split()[0], 16)
|
||||
all_emoji.add(a)
|
||||
wide_emoji.add(a)
|
||||
emoji_presentation_bases.add(a)
|
||||
|
||||
|
||||
def parse_emoji_modifier_sequence(spec: str) -> None:
|
||||
a, b = list(filter(None, spec.split()))
|
||||
char, mod = int(a, 16), int(b, 16)
|
||||
mod
|
||||
all_emoji.add(char)
|
||||
wide_emoji.add(char)
|
||||
emoji_presentation_bases.add(char)
|
||||
|
||||
|
||||
def parse_emoji() -> None:
|
||||
for line in get_data('emoji-sequences.txt', 'emoji'):
|
||||
parts = [x.strip() for x in line.split(';')]
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
base = int(base, 16)
|
||||
if var.upper() == 'FE0F':
|
||||
emoji_presentation_bases.add(base)
|
||||
data, etype = parts[:2]
|
||||
if etype == 'Basic_Emoji':
|
||||
parse_basic_emoji(data)
|
||||
elif etype == 'Emoji_Keycap_Sequence':
|
||||
parse_keycap_sequence(data)
|
||||
elif etype == 'RGI_Emoji_Flag_Sequence':
|
||||
parse_flag_emoji_sequence(data)
|
||||
elif etype == 'RGI_Emoji_Tag_Sequence':
|
||||
parse_emoji_tag_sequence(data)
|
||||
elif etype == 'RGI_Emoji_Modifier_Sequence':
|
||||
parse_emoji_modifier_sequence(data)
|
||||
|
||||
|
||||
doublewidth, ambiguous = set(), set()
|
||||
doublewidth: Set[int] = set()
|
||||
ambiguous: Set[int] = set()
|
||||
|
||||
|
||||
def parse_eaw():
|
||||
def parse_eaw() -> None:
|
||||
global doublewidth, ambiguous
|
||||
seen = set()
|
||||
seen: Set[int] = set()
|
||||
for line in get_data('ucd/EastAsianWidth.txt'):
|
||||
chars, eaw = split_two(line)
|
||||
if eaw == 'A':
|
||||
@@ -148,7 +210,7 @@ def parse_eaw():
|
||||
doublewidth |= set(range(0x30000, 0x3FFFD + 1)) - seen
|
||||
|
||||
|
||||
def get_ranges(items):
|
||||
def get_ranges(items: List[int]) -> Generator[Union[int, Tuple[int, int]], None, None]:
|
||||
items.sort()
|
||||
for k, g in groupby(enumerate(items), lambda m: m[0]-m[1]):
|
||||
group = tuple(map(itemgetter(1), g))
|
||||
@@ -159,7 +221,7 @@ def get_ranges(items):
|
||||
yield a, b
|
||||
|
||||
|
||||
def write_case(spec, p):
|
||||
def write_case(spec: Union[Tuple, int], p: Callable) -> None:
|
||||
if isinstance(spec, tuple):
|
||||
p('\t\tcase 0x{:x} ... 0x{:x}:'.format(*spec))
|
||||
else:
|
||||
@@ -167,7 +229,7 @@ def write_case(spec, p):
|
||||
|
||||
|
||||
@contextmanager
|
||||
def create_header(path, include_data_types=True):
|
||||
def create_header(path: str, include_data_types: bool = True) -> Generator[Callable, None, None]:
|
||||
with open(path, 'w') as f:
|
||||
p = partial(print, file=f)
|
||||
p('// unicode data, built from the unicode standard on:', date.today())
|
||||
@@ -184,7 +246,7 @@ def create_header(path, include_data_types=True):
|
||||
p('END_ALLOW_CASE_RANGE')
|
||||
|
||||
|
||||
def gen_emoji():
|
||||
def gen_emoji() -> None:
|
||||
with create_header('kitty/emoji.h') as p:
|
||||
p('static inline bool\nis_emoji(char_type code) {')
|
||||
p('\tswitch(code) {')
|
||||
@@ -194,9 +256,10 @@ def gen_emoji():
|
||||
p('\t\tdefault: return false;')
|
||||
p('\t}')
|
||||
p('\treturn false;\n}')
|
||||
p('static inline bool\nis_emoji_modifier(char_type code) {')
|
||||
|
||||
p('static inline bool\nis_symbol(char_type code) {')
|
||||
p('\tswitch(code) {')
|
||||
for spec in get_ranges(list(emoji_categories['Emoji_Modifier'])):
|
||||
for spec in get_ranges(list(all_symbols)):
|
||||
write_case(spec, p)
|
||||
p('\t\t\treturn true;')
|
||||
p('\t\tdefault: return false;')
|
||||
@@ -204,9 +267,17 @@ def gen_emoji():
|
||||
p('\treturn false;\n}')
|
||||
|
||||
|
||||
def category_test(name, p, classes, comment, static=False, extra_chars=frozenset(), exclude=frozenset()):
|
||||
static = 'static inline ' if static else ''
|
||||
chars = set()
|
||||
def category_test(
|
||||
name: str,
|
||||
p: Callable,
|
||||
classes: Iterable[str],
|
||||
comment: str,
|
||||
use_static: bool = False,
|
||||
extra_chars: Union[FrozenSet[int], Set[int]] = frozenset(),
|
||||
exclude: Union[Set[int], FrozenSet[int]] = frozenset()
|
||||
) -> None:
|
||||
static = 'static inline ' if use_static else ''
|
||||
chars: Set[int] = set()
|
||||
for c in classes:
|
||||
chars |= class_maps[c]
|
||||
chars |= extra_chars
|
||||
@@ -221,7 +292,7 @@ def category_test(name, p, classes, comment, static=False, extra_chars=frozenset
|
||||
p('\treturn false;\n}\n')
|
||||
|
||||
|
||||
def codepoint_to_mark_map(p, mark_map):
|
||||
def codepoint_to_mark_map(p: Callable, mark_map: List[int]) -> Dict[int, int]:
|
||||
p('\tswitch(c) { // {{{')
|
||||
rmap = {c: m for m, c in enumerate(mark_map)}
|
||||
for spec in get_ranges(mark_map):
|
||||
@@ -236,14 +307,14 @@ def codepoint_to_mark_map(p, mark_map):
|
||||
return rmap
|
||||
|
||||
|
||||
def classes_to_regex(classes, exclude=''):
|
||||
chars = set()
|
||||
def classes_to_regex(classes: Iterable[str], exclude: str = '') -> Iterable[str]:
|
||||
chars: Set[int] = set()
|
||||
for c in classes:
|
||||
chars |= class_maps[c]
|
||||
for c in map(ord, exclude):
|
||||
chars.discard(c)
|
||||
for x in map(ord, exclude):
|
||||
chars.discard(x)
|
||||
|
||||
def as_string(codepoint):
|
||||
def as_string(codepoint: int) -> str:
|
||||
if codepoint < 256:
|
||||
return r'\x{:02x}'.format(codepoint)
|
||||
if codepoint <= 0xffff:
|
||||
@@ -257,7 +328,7 @@ def classes_to_regex(classes, exclude=''):
|
||||
yield as_string(spec)
|
||||
|
||||
|
||||
def gen_ucd():
|
||||
def gen_ucd() -> None:
|
||||
cz = {c for c in class_maps if c[0] in 'CZ'}
|
||||
with create_header('kitty/unicode-data.c') as p:
|
||||
p('#include "unicode-data.h"')
|
||||
@@ -285,7 +356,9 @@ def gen_ucd():
|
||||
p('}\n')
|
||||
with open('kitty/unicode-data.h') as f:
|
||||
unicode_data = f.read()
|
||||
expected = int(re.search(r'^#define VS15 (\d+)', unicode_data, re.M).group(1))
|
||||
m = re.search(r'^#define VS15 (\d+)', unicode_data, re.M)
|
||||
if m is not None:
|
||||
expected = int(m.group(1))
|
||||
if rmap[0xfe0e] != expected:
|
||||
raise ValueError('The mark for 0xfe0e has changed, you have to update VS15 to {} and VS16 to {} in unicode-data.h'.format(
|
||||
rmap[0xfe0e], rmap[0xfe0f]
|
||||
@@ -294,7 +367,7 @@ def gen_ucd():
|
||||
f.write("url_delimiters = '{}' # noqa".format(''.join(classes_to_regex(cz, exclude='\n'))))
|
||||
|
||||
|
||||
def gen_names():
|
||||
def gen_names() -> None:
|
||||
with create_header('kittens/unicode_input/names.h') as p:
|
||||
mark_to_cp = list(sorted(name_map))
|
||||
cp_to_mark = {cp: m for m, cp in enumerate(mark_to_cp)}
|
||||
@@ -349,28 +422,28 @@ def gen_names():
|
||||
|
||||
# The trie
|
||||
p(f'typedef struct {{ uint32_t children_offset; uint32_t match_offset; }} word_trie;\n')
|
||||
all_trie_nodes = []
|
||||
all_trie_nodes: List['TrieNode'] = []
|
||||
|
||||
class TrieNode:
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.match_offset = 0
|
||||
self.children_offset = 0
|
||||
self.children = {}
|
||||
self.children: Dict[int, int] = {}
|
||||
|
||||
def add_letter(self, letter):
|
||||
def add_letter(self, letter: int) -> int:
|
||||
if letter not in self.children:
|
||||
self.children[letter] = len(all_trie_nodes)
|
||||
all_trie_nodes.append(TrieNode())
|
||||
return self.children[letter]
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f'{{ .children_offset={self.children_offset}, .match_offset={self.match_offset} }}'
|
||||
|
||||
root = TrieNode()
|
||||
all_trie_nodes.append(root)
|
||||
|
||||
def add_word(word_idx, word):
|
||||
def add_word(word_idx: int, word: str) -> None:
|
||||
parent = root
|
||||
for letter in map(ord, word):
|
||||
idx = parent.add_letter(letter)
|
||||
@@ -395,10 +468,10 @@ def gen_names():
|
||||
p('}; // }}}\n')
|
||||
|
||||
|
||||
def gen_wcwidth():
|
||||
seen = set()
|
||||
def gen_wcwidth() -> None:
|
||||
seen: Set[int] = set()
|
||||
|
||||
def add(p, comment, chars_, ret):
|
||||
def add(p: Callable, comment: str, chars_: Union[Set[int], FrozenSet[int]], ret: int) -> None:
|
||||
chars = chars_ - seen
|
||||
seen.update(chars)
|
||||
p(f'\t\t// {comment} ({len(chars)} codepoints)' + ' {{' '{')
|
||||
@@ -412,13 +485,14 @@ def gen_wcwidth():
|
||||
p('\tswitch(code) {')
|
||||
|
||||
non_printing = class_maps['Cc'] | class_maps['Cf'] | class_maps['Cs']
|
||||
add(p, 'Flags', flag_codepoints, 2)
|
||||
add(p, 'Marks', marks | {0}, 0)
|
||||
add(p, 'Non-printing characters', non_printing, -1)
|
||||
add(p, 'Private use', class_maps['Co'], -3)
|
||||
add(p, 'Text Presentation', emoji_categories['Emoji'] - emoji_categories['Emoji_Presentation'], 1)
|
||||
add(p, 'Text Presentation', narrow_emoji, 1)
|
||||
add(p, 'East Asian ambiguous width', ambiguous, -2)
|
||||
add(p, 'East Asian double width', doublewidth, 2)
|
||||
add(p, 'Emoji Presentation', emoji_categories['Emoji_Presentation'], 2)
|
||||
add(p, 'Emoji Presentation', wide_emoji, 2)
|
||||
|
||||
add(p, 'Not assigned in the unicode character database', not_assigned, -4)
|
||||
|
||||
|
||||
0
glfw/__init__.py
Normal file
0
glfw/__init__.py
Normal file
106
glfw/backend_utils.c
vendored
106
glfw/backend_utils.c
vendored
@@ -22,19 +22,6 @@
|
||||
#define ppoll pollts
|
||||
#endif
|
||||
|
||||
static inline double
|
||||
monotonic(void) {
|
||||
struct timespec ts = {0};
|
||||
#ifdef CLOCK_HIGHRES
|
||||
clock_gettime(CLOCK_HIGHRES, &ts);
|
||||
#elif CLOCK_MONOTONIC_RAW
|
||||
clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
|
||||
#else
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
#endif
|
||||
return (((double)ts.tv_nsec) / 1e9) + (double)ts.tv_sec;
|
||||
}
|
||||
|
||||
void
|
||||
update_fds(EventLoopData *eld) {
|
||||
for (nfds_t i = 0; i < eld->watches_count; i++) {
|
||||
@@ -109,7 +96,7 @@ update_timers(EventLoopData *eld) {
|
||||
}
|
||||
|
||||
id_type
|
||||
addTimer(EventLoopData *eld, const char *name, double interval, int enabled, bool repeats, timer_callback_func cb, void *cb_data, GLFWuserdatafreefun free) {
|
||||
addTimer(EventLoopData *eld, const char *name, monotonic_t interval, int enabled, bool repeats, timer_callback_func cb, void *cb_data, GLFWuserdatafreefun free) {
|
||||
if (eld->timers_count >= sizeof(eld->timers)/sizeof(eld->timers[0])) {
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR, "Too many timers added");
|
||||
return 0;
|
||||
@@ -117,7 +104,7 @@ addTimer(EventLoopData *eld, const char *name, double interval, int enabled, boo
|
||||
Timer *t = eld->timers + eld->timers_count++;
|
||||
t->interval = interval;
|
||||
t->name = name;
|
||||
t->trigger_at = enabled ? monotonic() + interval : DBL_MAX;
|
||||
t->trigger_at = enabled ? monotonic() + interval : MONOTONIC_T_MAX;
|
||||
t->repeats = repeats;
|
||||
t->callback = cb;
|
||||
t->callback_data = cb_data;
|
||||
@@ -144,7 +131,7 @@ void
|
||||
toggleTimer(EventLoopData *eld, id_type timer_id, int enabled) {
|
||||
for (nfds_t i = 0; i < eld->timers_count; i++) {
|
||||
if (eld->timers[i].id == timer_id) {
|
||||
double trigger_at = enabled ? (monotonic() + eld->timers[i].interval) : DBL_MAX;
|
||||
monotonic_t trigger_at = enabled ? (monotonic() + eld->timers[i].interval) : MONOTONIC_T_MAX;
|
||||
if (trigger_at != eld->timers[i].trigger_at) {
|
||||
eld->timers[i].trigger_at = trigger_at;
|
||||
update_timers(eld);
|
||||
@@ -155,7 +142,7 @@ toggleTimer(EventLoopData *eld, id_type timer_id, int enabled) {
|
||||
}
|
||||
|
||||
void
|
||||
changeTimerInterval(EventLoopData *eld, id_type timer_id, double interval) {
|
||||
changeTimerInterval(EventLoopData *eld, id_type timer_id, monotonic_t interval) {
|
||||
for (nfds_t i = 0; i < eld->timers_count; i++) {
|
||||
if (eld->timers[i].id == timer_id) {
|
||||
eld->timers[i].interval = interval;
|
||||
@@ -165,22 +152,28 @@ changeTimerInterval(EventLoopData *eld, id_type timer_id, double interval) {
|
||||
}
|
||||
|
||||
|
||||
double
|
||||
prepareForPoll(EventLoopData *eld, double timeout) {
|
||||
monotonic_t
|
||||
prepareForPoll(EventLoopData *eld, monotonic_t timeout) {
|
||||
for (nfds_t i = 0; i < eld->watches_count; i++) eld->fds[i].revents = 0;
|
||||
if (!eld->timers_count || eld->timers[0].trigger_at == DBL_MAX) return timeout;
|
||||
double now = monotonic(), next_repeat_at = eld->timers[0].trigger_at;
|
||||
if (!eld->timers_count || eld->timers[0].trigger_at == MONOTONIC_T_MAX) return timeout;
|
||||
monotonic_t now = monotonic(), next_repeat_at = eld->timers[0].trigger_at;
|
||||
if (timeout < 0 || now + timeout > next_repeat_at) {
|
||||
timeout = next_repeat_at <= now ? 0 : next_repeat_at - now;
|
||||
}
|
||||
return timeout;
|
||||
}
|
||||
|
||||
static inline struct timespec
|
||||
calc_time(monotonic_t nsec) {
|
||||
struct timespec result;
|
||||
result.tv_sec = nsec / (1000LL * 1000LL * 1000LL);
|
||||
result.tv_nsec = nsec % (1000LL * 1000LL * 1000LL);
|
||||
return result;
|
||||
}
|
||||
|
||||
int
|
||||
pollWithTimeout(struct pollfd *fds, nfds_t nfds, double timeout) {
|
||||
const long seconds = (long) timeout;
|
||||
const long nanoseconds = (long) ((timeout - seconds) * 1e9);
|
||||
struct timespec tv = { seconds, nanoseconds };
|
||||
pollWithTimeout(struct pollfd *fds, nfds_t nfds, monotonic_t timeout) {
|
||||
struct timespec tv = calc_time(timeout);
|
||||
return ppoll(fds, nfds, &tv, NULL);
|
||||
}
|
||||
|
||||
@@ -198,10 +191,10 @@ dispatchEvents(EventLoopData *eld) {
|
||||
|
||||
unsigned
|
||||
dispatchTimers(EventLoopData *eld) {
|
||||
if (!eld->timers_count || eld->timers[0].trigger_at == DBL_MAX) return 0;
|
||||
if (!eld->timers_count || eld->timers[0].trigger_at == MONOTONIC_T_MAX) return 0;
|
||||
static struct { timer_callback_func func; id_type id; void* data; bool repeats; } dispatches[sizeof(eld->timers)/sizeof(eld->timers[0])];
|
||||
unsigned num_dispatches = 0;
|
||||
double now = monotonic();
|
||||
monotonic_t now = monotonic();
|
||||
for (nfds_t i = 0; i < eld->timers_count && eld->timers[i].trigger_at <= now; i++) {
|
||||
eld->timers[i].trigger_at = now + eld->timers[i].interval;
|
||||
dispatches[num_dispatches].func = eld->timers[i].callback;
|
||||
@@ -299,12 +292,12 @@ finalizePollData(EventLoopData *eld) {
|
||||
}
|
||||
|
||||
int
|
||||
pollForEvents(EventLoopData *eld, double timeout) {
|
||||
pollForEvents(EventLoopData *eld, monotonic_t timeout, watch_callback_func display_callback) {
|
||||
int read_ok = 0;
|
||||
timeout = prepareForPoll(eld, timeout);
|
||||
EVDBG("pollForEvents final timeout: %.3f", timeout);
|
||||
EVDBG("pollForEvents final timeout: %.3f", monotonic_t_to_s_double(timeout));
|
||||
int result;
|
||||
double end_time = monotonic() + timeout;
|
||||
monotonic_t end_time = monotonic() + timeout;
|
||||
eld->wakeup_fd_ready = false;
|
||||
|
||||
while(1) {
|
||||
@@ -312,6 +305,7 @@ pollForEvents(EventLoopData *eld, double timeout) {
|
||||
errno = 0;
|
||||
result = pollWithTimeout(eld->fds, eld->watches_count, timeout);
|
||||
int saved_errno = errno;
|
||||
if (display_callback) display_callback(result, eld->fds[0].revents && eld->watches[0].events, NULL);
|
||||
dispatchTimers(eld);
|
||||
if (result > 0) {
|
||||
dispatchEvents(eld);
|
||||
@@ -326,6 +320,7 @@ pollForEvents(EventLoopData *eld, double timeout) {
|
||||
errno = 0;
|
||||
result = poll(eld->fds, eld->watches_count, -1);
|
||||
int saved_errno = errno;
|
||||
if (display_callback) display_callback(result, eld->fds[0].revents && eld->watches[0].events, NULL);
|
||||
dispatchTimers(eld);
|
||||
if (result > 0) {
|
||||
dispatchEvents(eld);
|
||||
@@ -337,54 +332,3 @@ pollForEvents(EventLoopData *eld, double timeout) {
|
||||
}
|
||||
return read_ok;
|
||||
}
|
||||
|
||||
// Splits and translates a text/uri-list into separate file paths
|
||||
// NOTE: This function destroys the provided string
|
||||
//
|
||||
char** parseUriList(char* text, int* count)
|
||||
{
|
||||
const char* prefix = "file://";
|
||||
char** paths = NULL;
|
||||
char* line;
|
||||
|
||||
*count = 0;
|
||||
|
||||
while ((line = strtok(text, "\r\n")))
|
||||
{
|
||||
text = NULL;
|
||||
|
||||
if (line[0] == '#')
|
||||
continue;
|
||||
|
||||
if (strncmp(line, prefix, strlen(prefix)) == 0)
|
||||
{
|
||||
line += strlen(prefix);
|
||||
// TODO: Validate hostname
|
||||
while (*line != '/')
|
||||
line++;
|
||||
}
|
||||
|
||||
(*count)++;
|
||||
|
||||
char* path = calloc(strlen(line) + 1, 1);
|
||||
paths = realloc(paths, *count * sizeof(char*));
|
||||
paths[*count - 1] = path;
|
||||
|
||||
while (*line)
|
||||
{
|
||||
if (line[0] == '%' && line[1] && line[2])
|
||||
{
|
||||
const char digits[3] = { line[1], line[2], '\0' };
|
||||
*path = strtol(digits, NULL, 16);
|
||||
line += 2;
|
||||
}
|
||||
else
|
||||
*path = *line;
|
||||
|
||||
path++;
|
||||
line++;
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
14
glfw/backend_utils.h
vendored
14
glfw/backend_utils.h
vendored
@@ -25,6 +25,7 @@
|
||||
//========================================================================
|
||||
|
||||
#pragma once
|
||||
#include "../kitty/monotonic.h"
|
||||
#include <poll.h>
|
||||
#include <unistd.h>
|
||||
#include <stdbool.h>
|
||||
@@ -55,7 +56,7 @@ typedef struct {
|
||||
|
||||
typedef struct {
|
||||
id_type id;
|
||||
double interval, trigger_at;
|
||||
monotonic_t interval, trigger_at;
|
||||
timer_callback_func callback;
|
||||
void *callback_data;
|
||||
GLFWuserdatafreefun free;
|
||||
@@ -82,16 +83,15 @@ void check_for_wakeup_events(EventLoopData *eld);
|
||||
id_type addWatch(EventLoopData *eld, const char *name, int fd, int events, int enabled, watch_callback_func cb, void *cb_data);
|
||||
void removeWatch(EventLoopData *eld, id_type watch_id);
|
||||
void toggleWatch(EventLoopData *eld, id_type watch_id, int enabled);
|
||||
id_type addTimer(EventLoopData *eld, const char *name, double interval, int enabled, bool repeats, timer_callback_func cb, void *cb_data, GLFWuserdatafreefun free);
|
||||
id_type addTimer(EventLoopData *eld, const char *name, monotonic_t interval, int enabled, bool repeats, timer_callback_func cb, void *cb_data, GLFWuserdatafreefun free);
|
||||
void removeTimer(EventLoopData *eld, id_type timer_id);
|
||||
void removeAllTimers(EventLoopData *eld);
|
||||
void toggleTimer(EventLoopData *eld, id_type timer_id, int enabled);
|
||||
void changeTimerInterval(EventLoopData *eld, id_type timer_id, double interval);
|
||||
double prepareForPoll(EventLoopData *eld, double timeout);
|
||||
int pollWithTimeout(struct pollfd *fds, nfds_t nfds, double timeout);
|
||||
int pollForEvents(EventLoopData *eld, double timeout);
|
||||
void changeTimerInterval(EventLoopData *eld, id_type timer_id, monotonic_t interval);
|
||||
monotonic_t prepareForPoll(EventLoopData *eld, monotonic_t timeout);
|
||||
int pollWithTimeout(struct pollfd *fds, nfds_t nfds, monotonic_t timeout);
|
||||
int pollForEvents(EventLoopData *eld, monotonic_t timeout, watch_callback_func);
|
||||
unsigned dispatchTimers(EventLoopData *eld);
|
||||
void finalizePollData(EventLoopData *eld);
|
||||
bool initPollData(EventLoopData *eld, int display_fd);
|
||||
char** parseUriList(char* text, int* count);
|
||||
void wakeupEventLoop(EventLoopData *eld);
|
||||
|
||||
@@ -27,9 +27,13 @@
|
||||
//========================================================================
|
||||
|
||||
#include "internal.h"
|
||||
#include "../kitty/monotonic.h"
|
||||
#include <sys/param.h> // For MAXPATHLEN
|
||||
#include <pthread.h>
|
||||
|
||||
// Needed for _NSGetProgname
|
||||
#include <crt_externs.h>
|
||||
|
||||
// Change to our application bundle's resources directory, if present
|
||||
//
|
||||
static void changeToResourcesDirectory(void)
|
||||
@@ -66,14 +70,119 @@ static void changeToResourcesDirectory(void)
|
||||
chdir(resourcesPath);
|
||||
}
|
||||
|
||||
// Set up the menu bar (manually)
|
||||
// This is nasty, nasty stuff -- calls to undocumented semi-private APIs that
|
||||
// could go away at any moment, lots of stuff that really should be
|
||||
// localize(d|able), etc. Add a nib to save us this horror.
|
||||
//
|
||||
static void createMenuBar(void)
|
||||
{
|
||||
size_t i;
|
||||
NSString* appName = nil;
|
||||
NSDictionary* bundleInfo = [[NSBundle mainBundle] infoDictionary];
|
||||
NSString* nameKeys[] =
|
||||
{
|
||||
@"CFBundleDisplayName",
|
||||
@"CFBundleName",
|
||||
@"CFBundleExecutable",
|
||||
};
|
||||
|
||||
// Try to figure out what the calling application is called
|
||||
|
||||
for (i = 0; i < sizeof(nameKeys) / sizeof(nameKeys[0]); i++)
|
||||
{
|
||||
id name = bundleInfo[nameKeys[i]];
|
||||
if (name &&
|
||||
[name isKindOfClass:[NSString class]] &&
|
||||
![name isEqualToString:@""])
|
||||
{
|
||||
appName = name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!appName)
|
||||
{
|
||||
char** progname = _NSGetProgname();
|
||||
if (progname && *progname)
|
||||
appName = @(*progname);
|
||||
else
|
||||
appName = @"GLFW Application";
|
||||
}
|
||||
|
||||
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 %@", appName]
|
||||
action:@selector(orderFrontStandardAboutPanel:)
|
||||
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:@"Hide %@", appName]
|
||||
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]];
|
||||
[appMenu addItemWithTitle:[NSString stringWithFormat:@"Quit %@", appName]
|
||||
action:@selector(terminate:)
|
||||
keyEquivalent:@"q"];
|
||||
|
||||
NSMenuItem* windowMenuItem =
|
||||
[bar addItemWithTitle:@"" action:NULL keyEquivalent:@""];
|
||||
[bar release];
|
||||
NSMenu* windowMenu = [[NSMenu alloc] initWithTitle:@"Window"];
|
||||
[NSApp setWindowsMenu:windowMenu];
|
||||
[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:@""];
|
||||
|
||||
// TODO: Make this appear at the bottom of the menu (for consistency)
|
||||
[windowMenu addItem:[NSMenuItem separatorItem]];
|
||||
[[windowMenu addItemWithTitle:@"Enter Full Screen"
|
||||
action:@selector(toggleFullScreen:)
|
||||
keyEquivalent:@"f"]
|
||||
setKeyEquivalentModifierMask:NSEventModifierFlagControl | NSEventModifierFlagCommand];
|
||||
|
||||
// Prior to Snow Leopard, we need to use this oddly-named semi-private API
|
||||
// to get the application menu working properly.
|
||||
SEL setAppleMenuSelector = NSSelectorFromString(@"setAppleMenu:");
|
||||
[NSApp performSelector:setAppleMenuSelector withObject:appMenu];
|
||||
}
|
||||
|
||||
// Create key code translation tables
|
||||
//
|
||||
static void createKeyTables(void)
|
||||
{
|
||||
int scancode;
|
||||
int keycode;
|
||||
|
||||
memset(_glfw.ns.keycodes, -1, sizeof(_glfw.ns.keycodes));
|
||||
memset(_glfw.ns.scancodes, -1, sizeof(_glfw.ns.scancodes));
|
||||
memset(_glfw.ns.key_to_keycode, -1, sizeof(_glfw.ns.key_to_keycode));
|
||||
|
||||
_glfw.ns.keycodes[0x1D] = GLFW_KEY_0;
|
||||
_glfw.ns.keycodes[0x12] = GLFW_KEY_1;
|
||||
@@ -190,11 +299,11 @@ static void createKeyTables(void)
|
||||
_glfw.ns.keycodes[0x43] = GLFW_KEY_KP_MULTIPLY;
|
||||
_glfw.ns.keycodes[0x4E] = GLFW_KEY_KP_SUBTRACT;
|
||||
|
||||
for (scancode = 0; scancode < 256; scancode++)
|
||||
for (keycode = 0; keycode < 256; keycode++)
|
||||
{
|
||||
// Store the reverse translation for faster key name lookup
|
||||
if (_glfw.ns.keycodes[scancode] >= 0)
|
||||
_glfw.ns.scancodes[_glfw.ns.keycodes[scancode]] = scancode;
|
||||
if (_glfw.ns.keycodes[keycode] >= 0)
|
||||
_glfw.ns.key_to_keycode[_glfw.ns.keycodes[keycode]] = keycode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,6 +385,17 @@ static bool initializeTIS(void)
|
||||
return updateUnicodeDataNS();
|
||||
}
|
||||
|
||||
static void
|
||||
display_reconfigured(CGDirectDisplayID display UNUSED, CGDisplayChangeSummaryFlags flags, void *userInfo UNUSED)
|
||||
{
|
||||
if (flags & kCGDisplayBeginConfigurationFlag) {
|
||||
return;
|
||||
}
|
||||
if (flags & kCGDisplaySetModeFlag) {
|
||||
// GPU possibly changed
|
||||
}
|
||||
}
|
||||
|
||||
@interface GLFWHelper : NSObject
|
||||
@end
|
||||
|
||||
@@ -294,6 +414,102 @@ static bool initializeTIS(void)
|
||||
|
||||
@end // GLFWHelper
|
||||
|
||||
// Delegate for application related notifications {{{
|
||||
|
||||
@interface GLFWApplicationDelegate : NSObject <NSApplicationDelegate>
|
||||
@end
|
||||
|
||||
@implementation GLFWApplicationDelegate
|
||||
|
||||
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
|
||||
{
|
||||
(void)sender;
|
||||
_GLFWwindow* window;
|
||||
|
||||
for (window = _glfw.windowListHead; window; window = window->next)
|
||||
_glfwInputWindowCloseRequest(window);
|
||||
|
||||
return NSTerminateCancel;
|
||||
}
|
||||
|
||||
static GLFWapplicationshouldhandlereopenfun handle_reopen_callback = NULL;
|
||||
|
||||
- (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag
|
||||
{
|
||||
(void)sender;
|
||||
if (!handle_reopen_callback) return YES;
|
||||
if (handle_reopen_callback(flag)) return YES;
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)applicationDidChangeScreenParameters:(NSNotification *) notification
|
||||
{
|
||||
(void)notification;
|
||||
_GLFWwindow* window;
|
||||
|
||||
for (window = _glfw.windowListHead; window; window = window->next)
|
||||
{
|
||||
if (window->context.client != GLFW_NO_API)
|
||||
[window->context.nsgl.object update];
|
||||
}
|
||||
|
||||
_glfwPollMonitorsNS();
|
||||
}
|
||||
|
||||
static GLFWapplicationwillfinishlaunchingfun finish_launching_callback = NULL;
|
||||
|
||||
- (void)applicationWillFinishLaunching:(NSNotification *)notification
|
||||
{
|
||||
(void)notification;
|
||||
if (_glfw.hints.init.ns.menubar)
|
||||
{
|
||||
// In case we are unbundled, make us a proper UI application
|
||||
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
||||
|
||||
// Menu bar setup must go between sharedApplication and finishLaunching
|
||||
// in order to properly emulate the behavior of NSApplicationMain
|
||||
|
||||
if ([[NSBundle mainBundle] pathForResource:@"MainMenu" ofType:@"nib"])
|
||||
{
|
||||
[[NSBundle mainBundle] loadNibNamed:@"MainMenu"
|
||||
owner:NSApp
|
||||
topLevelObjects:&_glfw.ns.nibObjects];
|
||||
}
|
||||
else
|
||||
createMenuBar();
|
||||
}
|
||||
if (finish_launching_callback)
|
||||
finish_launching_callback();
|
||||
}
|
||||
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)notification
|
||||
{
|
||||
(void)notification;
|
||||
[NSApp stop:nil];
|
||||
|
||||
CGDisplayRegisterReconfigurationCallback(display_reconfigured, NULL);
|
||||
_glfwCocoaPostEmptyEvent();
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(NSNotification *)aNotification
|
||||
{
|
||||
(void)aNotification;
|
||||
CGDisplayRemoveReconfigurationCallback(display_reconfigured, NULL);
|
||||
}
|
||||
|
||||
- (void)applicationDidHide:(NSNotification *)notification
|
||||
{
|
||||
(void)notification;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < _glfw.monitorCount; i++)
|
||||
_glfwRestoreVideoModeNS(_glfw.monitors[i]);
|
||||
}
|
||||
|
||||
@end // GLFWApplicationDelegate
|
||||
// }}}
|
||||
|
||||
|
||||
@interface GLFWApplication : NSApplication
|
||||
- (void)tick_callback;
|
||||
- (void)render_frame_received:(id)displayIDAsID;
|
||||
@@ -313,6 +529,32 @@ static bool initializeTIS(void)
|
||||
@end
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
////// GLFW internal API //////
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void* _glfwLoadLocalVulkanLoaderNS(void)
|
||||
{
|
||||
CFBundleRef bundle = CFBundleGetMainBundle();
|
||||
if (!bundle)
|
||||
return NULL;
|
||||
|
||||
CFURLRef url =
|
||||
CFBundleCopyAuxiliaryExecutableURL(bundle, CFSTR("libvulkan.1.dylib"));
|
||||
if (!url)
|
||||
return NULL;
|
||||
|
||||
char path[PATH_MAX];
|
||||
void* handle = NULL;
|
||||
|
||||
if (CFURLGetFileSystemRepresentation(url, true, (UInt8*) path, sizeof(path) - 1))
|
||||
handle = _glfw_dlopen(path);
|
||||
|
||||
CFRelease(url);
|
||||
return handle;
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
////// GLFW platform API //////
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
@@ -335,6 +577,18 @@ is_cmd_period(NSEvent *event, NSEventModifierFlags modifierFlags) {
|
||||
return false;
|
||||
}
|
||||
|
||||
GLFWAPI GLFWapplicationshouldhandlereopenfun glfwSetApplicationShouldHandleReopen(GLFWapplicationshouldhandlereopenfun callback) {
|
||||
GLFWapplicationshouldhandlereopenfun previous = handle_reopen_callback;
|
||||
handle_reopen_callback = callback;
|
||||
return previous;
|
||||
}
|
||||
|
||||
GLFWAPI GLFWapplicationwillfinishlaunchingfun glfwSetApplicationWillFinishLaunching(GLFWapplicationwillfinishlaunchingfun callback) {
|
||||
GLFWapplicationwillfinishlaunchingfun previous = finish_launching_callback;
|
||||
finish_launching_callback = callback;
|
||||
return previous;
|
||||
}
|
||||
|
||||
int _glfwPlatformInit(void)
|
||||
{
|
||||
@autoreleasepool {
|
||||
@@ -345,8 +599,21 @@ int _glfwPlatformInit(void)
|
||||
toTarget:_glfw.ns.helper
|
||||
withObject:nil];
|
||||
|
||||
if (NSApp)
|
||||
_glfw.ns.finishedLaunching = true;
|
||||
|
||||
[GLFWApplication sharedApplication];
|
||||
|
||||
_glfw.ns.delegate = [[GLFWApplicationDelegate alloc] init];
|
||||
if (_glfw.ns.delegate == nil)
|
||||
{
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||
"Cocoa: Failed to create application delegate");
|
||||
return false;
|
||||
}
|
||||
|
||||
[NSApp setDelegate:_glfw.ns.delegate];
|
||||
|
||||
NSEvent* (^keydown_block)(NSEvent*) = ^ NSEvent* (NSEvent* event)
|
||||
{
|
||||
NSEventModifierFlags modifierFlags = [event modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask;
|
||||
@@ -385,6 +652,10 @@ int _glfwPlatformInit(void)
|
||||
if (_glfw.hints.init.ns.chdir)
|
||||
changeToResourcesDirectory();
|
||||
|
||||
// Press and Hold prevents some keys from emitting repeated characters
|
||||
NSDictionary* defaults = @{@"ApplePressAndHoldEnabled":@NO};
|
||||
[[NSUserDefaults standardUserDefaults] registerDefaults:defaults];
|
||||
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
addObserver:_glfw.ns.helper
|
||||
selector:@selector(selectedKeyboardInputSourceChanged:)
|
||||
@@ -402,7 +673,6 @@ int _glfwPlatformInit(void)
|
||||
if (!initializeTIS())
|
||||
return false;
|
||||
|
||||
_glfwInitTimerNS();
|
||||
_glfwInitJoysticksNS();
|
||||
|
||||
_glfwPollMonitorsNS();
|
||||
@@ -531,7 +801,7 @@ typedef struct {
|
||||
NSTimer *os_timer;
|
||||
unsigned long long id;
|
||||
bool repeats;
|
||||
double interval;
|
||||
monotonic_t interval;
|
||||
GLFWuserdatafun callback;
|
||||
void *callback_data;
|
||||
GLFWuserdatafun free_callback_data;
|
||||
@@ -551,7 +821,7 @@ remove_timer_at(size_t idx) {
|
||||
}
|
||||
|
||||
static void schedule_timer(Timer *t) {
|
||||
t->os_timer = [NSTimer scheduledTimerWithTimeInterval:t->interval repeats:(t->repeats ? YES: NO) block:^(NSTimer *os_timer) {
|
||||
t->os_timer = [NSTimer scheduledTimerWithTimeInterval:monotonic_t_to_s_double(t->interval) repeats:(t->repeats ? YES: NO) block:^(NSTimer *os_timer) {
|
||||
for (size_t i = 0; i < num_timers; i++) {
|
||||
if (timers[i].os_timer == os_timer) {
|
||||
timers[i].callback(timers[i].id, timers[i].callback_data);
|
||||
@@ -562,7 +832,7 @@ static void schedule_timer(Timer *t) {
|
||||
}];
|
||||
}
|
||||
|
||||
unsigned long long _glfwPlatformAddTimer(double interval, bool repeats, GLFWuserdatafun callback, void *callback_data, GLFWuserdatafun free_callback) {
|
||||
unsigned long long _glfwPlatformAddTimer(monotonic_t interval, bool repeats, GLFWuserdatafun callback, void *callback_data, GLFWuserdatafun free_callback) {
|
||||
static unsigned long long timer_counter = 0;
|
||||
if (num_timers >= sizeof(timers)/sizeof(timers[0]) - 1) {
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR, "Too many timers added");
|
||||
@@ -588,7 +858,7 @@ void _glfwPlatformRemoveTimer(unsigned long long timer_id) {
|
||||
}
|
||||
}
|
||||
|
||||
void _glfwPlatformUpdateTimer(unsigned long long timer_id, double interval, bool enabled) {
|
||||
void _glfwPlatformUpdateTimer(unsigned long long timer_id, monotonic_t interval, bool enabled) {
|
||||
for (size_t i = 0; i < num_timers; i++) {
|
||||
if (timers[i].id == timer_id) {
|
||||
Timer *t = timers + i;
|
||||
|
||||
@@ -146,7 +146,7 @@ static bool modeIsGood(CGDisplayModeRef mode)
|
||||
// Convert Core Graphics display mode to GLFW video mode
|
||||
//
|
||||
static GLFWvidmode vidmodeFromCGDisplayMode(CGDisplayModeRef mode,
|
||||
CVDisplayLinkRef link)
|
||||
double fallbackRefreshRate)
|
||||
{
|
||||
GLFWvidmode result;
|
||||
result.width = (int) CGDisplayModeGetWidth(mode);
|
||||
@@ -154,11 +154,7 @@ static GLFWvidmode vidmodeFromCGDisplayMode(CGDisplayModeRef mode,
|
||||
result.refreshRate = (int) round(CGDisplayModeGetRefreshRate(mode));
|
||||
|
||||
if (result.refreshRate == 0)
|
||||
{
|
||||
const CVTime time = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(link);
|
||||
if (!(time.flags & kCVTimeIsIndefinite))
|
||||
result.refreshRate = (int) (time.timeScale / (double) time.timeValue);
|
||||
}
|
||||
result.refreshRate = (int) round(fallbackRefreshRate);
|
||||
|
||||
#if MAC_OS_X_VERSION_MAX_ALLOWED <= 101100
|
||||
CFStringRef format = CGDisplayModeCopyPixelEncoding(mode);
|
||||
@@ -240,6 +236,30 @@ static bool refreshMonitorScreen(_GLFWmonitor* monitor)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns a fallback refresh rate for when Core Graphics says it is zero
|
||||
//
|
||||
static double getFallbackRefreshRate(_GLFWmonitor* monitor)
|
||||
{
|
||||
CGDisplayModeRef mode = CGDisplayCopyDisplayMode(monitor->ns.displayID);
|
||||
double refreshRate = CGDisplayModeGetRefreshRate(mode);
|
||||
CGDisplayModeRelease(mode);
|
||||
|
||||
if (refreshRate == 0.0)
|
||||
{
|
||||
CVDisplayLinkRef link = NULL;
|
||||
CVDisplayLinkCreateWithCGDisplay(monitor->ns.displayID, &link);
|
||||
|
||||
const CVTime time = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(link);
|
||||
if (!(time.flags & kCVTimeIsIndefinite))
|
||||
refreshRate = (int) (time.timeScale / (double) time.timeValue);
|
||||
|
||||
CVDisplayLinkRelease(link);
|
||||
}
|
||||
|
||||
return refreshRate;
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
////// GLFW internal API //////
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
@@ -328,9 +348,10 @@ void _glfwPollMonitorsNS(void)
|
||||
if (!name)
|
||||
name = _glfw_strdup("Unknown");
|
||||
|
||||
_GLFWmonitor* monitor = _glfwAllocMonitor(name, size.width, size.height);
|
||||
_GLFWmonitor* monitor = _glfwAllocMonitor(name, (int)size.width, (int)size.height);
|
||||
monitor->ns.displayID = displays[i];
|
||||
monitor->ns.unitNumber = unitNumber;
|
||||
monitor->ns.fallbackRefreshRate = getFallbackRefreshRate(monitor);
|
||||
createDisplayLink(monitor->ns.displayID);
|
||||
|
||||
free(name);
|
||||
@@ -354,14 +375,12 @@ void _glfwPollMonitorsNS(void)
|
||||
void _glfwSetVideoModeNS(_GLFWmonitor* monitor, const GLFWvidmode* desired)
|
||||
{
|
||||
GLFWvidmode current;
|
||||
const GLFWvidmode* best = _glfwChooseVideoMode(monitor, desired);
|
||||
_glfwPlatformGetVideoMode(monitor, ¤t);
|
||||
|
||||
const GLFWvidmode* best = _glfwChooseVideoMode(monitor, desired);
|
||||
if (_glfwCompareVideoModes(¤t, best) == 0)
|
||||
return;
|
||||
|
||||
CVDisplayLinkRef link;
|
||||
CVDisplayLinkCreateWithCGDisplay(monitor->ns.displayID, &link);
|
||||
|
||||
CFArrayRef modes = CGDisplayCopyAllDisplayModes(monitor->ns.displayID, NULL);
|
||||
const CFIndex count = CFArrayGetCount(modes);
|
||||
CGDisplayModeRef native = NULL;
|
||||
@@ -372,7 +391,8 @@ void _glfwSetVideoModeNS(_GLFWmonitor* monitor, const GLFWvidmode* desired)
|
||||
if (!modeIsGood(dm))
|
||||
continue;
|
||||
|
||||
const GLFWvidmode mode = vidmodeFromCGDisplayMode(dm, link);
|
||||
const GLFWvidmode mode =
|
||||
vidmodeFromCGDisplayMode(dm, monitor->ns.fallbackRefreshRate);
|
||||
if (_glfwCompareVideoModes(best, &mode) == 0)
|
||||
{
|
||||
native = dm;
|
||||
@@ -391,7 +411,6 @@ void _glfwSetVideoModeNS(_GLFWmonitor* monitor, const GLFWvidmode* desired)
|
||||
}
|
||||
|
||||
CFRelease(modes);
|
||||
CVDisplayLinkRelease(link);
|
||||
}
|
||||
|
||||
// Restore the previously saved (original) video mode
|
||||
@@ -454,13 +473,13 @@ void _glfwPlatformGetMonitorWorkarea(_GLFWmonitor* monitor,
|
||||
const NSRect frameRect = [monitor->ns.screen visibleFrame];
|
||||
|
||||
if (xpos)
|
||||
*xpos = frameRect.origin.x;
|
||||
*xpos = (int)frameRect.origin.x;
|
||||
if (ypos)
|
||||
*ypos = _glfwTransformYNS(frameRect.origin.y + frameRect.size.height - 1);
|
||||
*ypos = (int)_glfwTransformYNS(frameRect.origin.y + frameRect.size.height - 1);
|
||||
if (width)
|
||||
*width = frameRect.size.width;
|
||||
*width = (int)frameRect.size.width;
|
||||
if (height)
|
||||
*height = frameRect.size.height;
|
||||
*height = (int)frameRect.size.height;
|
||||
|
||||
}
|
||||
|
||||
@@ -468,9 +487,6 @@ GLFWvidmode* _glfwPlatformGetVideoModes(_GLFWmonitor* monitor, int* count)
|
||||
{
|
||||
*count = 0;
|
||||
|
||||
CVDisplayLinkRef link;
|
||||
CVDisplayLinkCreateWithCGDisplay(monitor->ns.displayID, &link);
|
||||
|
||||
CFArrayRef modes = CGDisplayCopyAllDisplayModes(monitor->ns.displayID, NULL);
|
||||
const CFIndex found = CFArrayGetCount(modes);
|
||||
GLFWvidmode* result = calloc(found, sizeof(GLFWvidmode));
|
||||
@@ -481,7 +497,8 @@ GLFWvidmode* _glfwPlatformGetVideoModes(_GLFWmonitor* monitor, int* count)
|
||||
if (!modeIsGood(dm))
|
||||
continue;
|
||||
|
||||
const GLFWvidmode mode = vidmodeFromCGDisplayMode(dm, link);
|
||||
const GLFWvidmode mode =
|
||||
vidmodeFromCGDisplayMode(dm, monitor->ns.fallbackRefreshRate);
|
||||
|
||||
for (CFIndex j = 0; j < *count; j++)
|
||||
{
|
||||
@@ -498,21 +515,14 @@ GLFWvidmode* _glfwPlatformGetVideoModes(_GLFWmonitor* monitor, int* count)
|
||||
}
|
||||
|
||||
CFRelease(modes);
|
||||
CVDisplayLinkRelease(link);
|
||||
return result;
|
||||
}
|
||||
|
||||
void _glfwPlatformGetVideoMode(_GLFWmonitor* monitor, GLFWvidmode *mode)
|
||||
{
|
||||
CVDisplayLinkRef link;
|
||||
|
||||
CVDisplayLinkCreateWithCGDisplay(monitor->ns.displayID, &link);
|
||||
|
||||
CGDisplayModeRef native = CGDisplayCopyDisplayMode(monitor->ns.displayID);
|
||||
*mode = vidmodeFromCGDisplayMode(native, link);
|
||||
*mode = vidmodeFromCGDisplayMode(native, monitor->ns.fallbackRefreshRate);
|
||||
CGDisplayModeRelease(native);
|
||||
|
||||
CVDisplayLinkRelease(link);
|
||||
}
|
||||
|
||||
bool _glfwPlatformGetGammaRamp(_GLFWmonitor* monitor, GLFWgammaramp* ramp)
|
||||
|
||||
32
glfw/cocoa_platform.h
vendored
32
glfw/cocoa_platform.h
vendored
@@ -36,6 +36,9 @@ typedef void* id;
|
||||
typedef void* CVDisplayLinkRef;
|
||||
#endif
|
||||
|
||||
// NOTE: Many Cocoa enum values have been renamed and we need to build across
|
||||
// SDK versions where one is unavailable or the other deprecated
|
||||
// We use the newer names in code and these macros to handle compatibility
|
||||
#if MAC_OS_X_VERSION_MAX_ALLOWED < 101200
|
||||
#define NSBitmapFormatAlphaNonpremultiplied NSAlphaNonpremultipliedBitmapFormat
|
||||
#define NSEventMaskAny NSAnyEventMask
|
||||
@@ -62,12 +65,15 @@ typedef void* CVDisplayLinkRef;
|
||||
#endif
|
||||
|
||||
|
||||
typedef VkFlags VkMacOSSurfaceCreateFlagsMVK;
|
||||
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int, unsigned long);
|
||||
typedef int (* GLFWapplicationshouldhandlereopenfun)(int);
|
||||
typedef int (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
|
||||
typedef bool (* GLFWapplicationshouldhandlereopenfun)(int);
|
||||
typedef void (* GLFWapplicationwillfinishlaunchingfun)(void);
|
||||
typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
|
||||
typedef void (* GLFWcocoarenderframefun)(GLFWwindow*);
|
||||
|
||||
typedef VkFlags VkMacOSSurfaceCreateFlagsMVK;
|
||||
typedef VkFlags VkMetalSurfaceCreateFlagsEXT;
|
||||
|
||||
typedef struct VkMacOSSurfaceCreateInfoMVK
|
||||
{
|
||||
VkStructureType sType;
|
||||
@@ -76,7 +82,16 @@ typedef struct VkMacOSSurfaceCreateInfoMVK
|
||||
const void* pView;
|
||||
} VkMacOSSurfaceCreateInfoMVK;
|
||||
|
||||
typedef struct VkMetalSurfaceCreateInfoEXT
|
||||
{
|
||||
VkStructureType sType;
|
||||
const void* pNext;
|
||||
VkMetalSurfaceCreateFlagsEXT flags;
|
||||
const void* pLayer;
|
||||
} VkMetalSurfaceCreateInfoEXT;
|
||||
|
||||
typedef VkResult (APIENTRY *PFN_vkCreateMacOSSurfaceMVK)(VkInstance,const VkMacOSSurfaceCreateInfoMVK*,const VkAllocationCallbacks*,VkSurfaceKHR*);
|
||||
typedef VkResult (APIENTRY *PFN_vkCreateMetalSurfaceEXT)(VkInstance,const VkMetalSurfaceCreateInfoEXT*,const VkAllocationCallbacks*,VkSurfaceKHR*);
|
||||
|
||||
#include "posix_thread.h"
|
||||
#include "cocoa_joystick.h"
|
||||
@@ -144,7 +159,7 @@ typedef struct _GLFWDisplayLinkNS
|
||||
{
|
||||
CVDisplayLinkRef displayLink;
|
||||
CGDirectDisplayID displayID;
|
||||
double lastRenderFrameRequestedAt;
|
||||
monotonic_t lastRenderFrameRequestedAt;
|
||||
} _GLFWDisplayLinkNS;
|
||||
|
||||
// Cocoa-specific global data
|
||||
@@ -153,6 +168,7 @@ typedef struct _GLFWlibraryNS
|
||||
{
|
||||
CGEventSourceRef eventSource;
|
||||
id delegate;
|
||||
bool finishedLaunching;
|
||||
bool cursorHidden;
|
||||
TISInputSourceRef inputSource;
|
||||
IOHIDManagerRef hidManager;
|
||||
@@ -160,11 +176,12 @@ typedef struct _GLFWlibraryNS
|
||||
id helper;
|
||||
id keyUpMonitor;
|
||||
id keyDownMonitor;
|
||||
id nibObjects;
|
||||
|
||||
char keyName[64];
|
||||
char text[256];
|
||||
short int keycodes[256];
|
||||
short int scancodes[GLFW_KEY_LAST + 1];
|
||||
short int key_to_keycode[GLFW_KEY_LAST + 1];
|
||||
char* clipboardString;
|
||||
CGPoint cascadePoint;
|
||||
// Where to place the cursor when re-enabled
|
||||
@@ -195,6 +212,7 @@ typedef struct _GLFWmonitorNS
|
||||
CGDisplayModeRef previousMode;
|
||||
uint32_t unitNumber;
|
||||
id screen;
|
||||
double fallbackRefreshRate;
|
||||
|
||||
} _GLFWmonitorNS;
|
||||
|
||||
@@ -215,14 +233,14 @@ typedef struct _GLFWtimerNS
|
||||
} _GLFWtimerNS;
|
||||
|
||||
|
||||
void _glfwInitTimerNS(void);
|
||||
|
||||
void _glfwPollMonitorsNS(void);
|
||||
void _glfwSetVideoModeNS(_GLFWmonitor* monitor, const GLFWvidmode* desired);
|
||||
void _glfwRestoreVideoModeNS(_GLFWmonitor* monitor);
|
||||
|
||||
float _glfwTransformYNS(float y);
|
||||
|
||||
void* _glfwLoadLocalVulkanLoaderNS(void);
|
||||
|
||||
void _glfwClearDisplayLinks(void);
|
||||
void _glfwRestartDisplayLinks(void);
|
||||
void _glfwDispatchTickCallback(void);
|
||||
|
||||
62
glfw/cocoa_time.c
vendored
62
glfw/cocoa_time.c
vendored
@@ -1,62 +0,0 @@
|
||||
//========================================================================
|
||||
// GLFW 3.4 macOS - www.glfw.org
|
||||
//------------------------------------------------------------------------
|
||||
// Copyright (c) 2009-2016 Camilla Löwy <elmindreda@glfw.org>
|
||||
//
|
||||
// This software is provided 'as-is', without any express or implied
|
||||
// warranty. In no event will the authors be held liable for any damages
|
||||
// arising from the use of this software.
|
||||
//
|
||||
// Permission is granted to anyone to use this software for any purpose,
|
||||
// including commercial applications, and to alter it and redistribute it
|
||||
// freely, subject to the following restrictions:
|
||||
//
|
||||
// 1. The origin of this software must not be misrepresented; you must not
|
||||
// claim that you wrote the original software. If you use this software
|
||||
// in a product, an acknowledgment in the product documentation would
|
||||
// be appreciated but is not required.
|
||||
//
|
||||
// 2. Altered source versions must be plainly marked as such, and must not
|
||||
// be misrepresented as being the original software.
|
||||
//
|
||||
// 3. This notice may not be removed or altered from any source
|
||||
// distribution.
|
||||
//
|
||||
//========================================================================
|
||||
// It is fine to use C99 in this file because it will not be built with VS
|
||||
//========================================================================
|
||||
|
||||
#include "internal.h"
|
||||
|
||||
#include <mach/mach_time.h>
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
////// GLFW internal API //////
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Initialise timer
|
||||
//
|
||||
void _glfwInitTimerNS(void)
|
||||
{
|
||||
mach_timebase_info_data_t info;
|
||||
mach_timebase_info(&info);
|
||||
|
||||
_glfw.timer.ns.frequency = (info.denom * 1e9) / info.numer;
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
////// GLFW platform API //////
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
uint64_t _glfwPlatformGetTimerValue(void)
|
||||
{
|
||||
return mach_absolute_time();
|
||||
}
|
||||
|
||||
uint64_t _glfwPlatformGetTimerFrequency(void)
|
||||
{
|
||||
return _glfw.timer.ns.frequency;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
3
glfw/dbus_glfw.c
vendored
3
glfw/dbus_glfw.c
vendored
@@ -27,6 +27,7 @@
|
||||
|
||||
#include "internal.h"
|
||||
#include "dbus_glfw.h"
|
||||
#include "../kitty/monotonic.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -107,7 +108,7 @@ on_dbus_timer_ready(id_type timer_id UNUSED, void *data) {
|
||||
static dbus_bool_t
|
||||
add_dbus_timeout(DBusTimeout *timeout, void *data) {
|
||||
int enabled = dbus_timeout_get_enabled(timeout) ? 1 : 0;
|
||||
double interval = ((double)dbus_timeout_get_interval(timeout)) / 1000.0;
|
||||
monotonic_t interval = ms_to_monotonic_t(dbus_timeout_get_interval(timeout));
|
||||
if (interval < 0) return FALSE;
|
||||
id_type timer_id = addTimer(dbus_data->eld, data, interval, enabled, true, on_dbus_timer_ready, timeout, NULL);
|
||||
if (!timer_id) return FALSE;
|
||||
|
||||
31
glfw/egl_context.c
vendored
31
glfw/egl_context.c
vendored
@@ -123,23 +123,24 @@ static bool chooseEGLConfig(const _GLFWctxconfig* ctxconfig,
|
||||
continue;
|
||||
|
||||
#if defined(_GLFW_X11)
|
||||
XVisualInfo vi = {0};
|
||||
|
||||
// Only consider EGLConfigs with associated Visuals
|
||||
vi.visualid = getEGLConfigAttrib(n, EGL_NATIVE_VISUAL_ID);
|
||||
if (!vi.visualid)
|
||||
continue;
|
||||
|
||||
if (desired->transparent)
|
||||
{
|
||||
int count;
|
||||
XVisualInfo* vis = XGetVisualInfo(_glfw.x11.display,
|
||||
VisualIDMask, &vi,
|
||||
&count);
|
||||
if (vis)
|
||||
XVisualInfo vi = {0};
|
||||
|
||||
// Only consider EGLConfigs with associated Visuals
|
||||
vi.visualid = getEGLConfigAttrib(n, EGL_NATIVE_VISUAL_ID);
|
||||
if (!vi.visualid)
|
||||
continue;
|
||||
|
||||
if (desired->transparent)
|
||||
{
|
||||
u->transparent = _glfwIsVisualTransparentX11(vis[0].visual);
|
||||
XFree(vis);
|
||||
int count;
|
||||
XVisualInfo* vis =
|
||||
XGetVisualInfo(_glfw.x11.display, VisualIDMask, &vi, &count);
|
||||
if (vis)
|
||||
{
|
||||
u->transparent = _glfwIsVisualTransparentX11(vis[0].visual);
|
||||
XFree(vis);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // _GLFW_X11
|
||||
|
||||
87
glfw/glfw.py
87
glfw/glfw.py
@@ -6,20 +6,56 @@ import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from typing import Callable, List, Optional, Tuple
|
||||
|
||||
_plat = sys.platform.lower()
|
||||
is_linux = 'linux' in _plat
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def wayland_protocol_file_name(base, ext='c'):
|
||||
class Env:
|
||||
|
||||
cc: str = ''
|
||||
cppflags: List[str] = []
|
||||
cflags: List[str] = []
|
||||
ldflags: List[str] = []
|
||||
ldpaths: List[str] = []
|
||||
ccver: Tuple[int, int]
|
||||
|
||||
# glfw stuff
|
||||
all_headers: List[str] = []
|
||||
sources: List[str] = []
|
||||
wayland_packagedir: str = ''
|
||||
wayland_scanner: str = ''
|
||||
wayland_scanner_code: str = ''
|
||||
wayland_protocols: Tuple[str, ...] = ()
|
||||
|
||||
def __init__(
|
||||
self, cc: str = '', cppflags: List[str] = [], cflags: List[str] = [], ldflags: List[str] = [],
|
||||
ldpaths: Optional[List[str]] = None, ccver: Tuple[int, int] = (0, 0)
|
||||
):
|
||||
self.cc, self.cppflags, self.cflags, self.ldflags, self.ldpaths = cc, cppflags, cflags, ldflags, [] if ldpaths is None else ldpaths
|
||||
self.ccver = ccver
|
||||
|
||||
def copy(self) -> 'Env':
|
||||
ans = Env(self.cc, list(self.cppflags), list(self.cflags), list(self.ldflags), list(self.ldpaths), self.ccver)
|
||||
ans.all_headers = list(self.all_headers)
|
||||
ans.sources = list(self.sources)
|
||||
ans.wayland_packagedir = self.wayland_packagedir
|
||||
ans.wayland_scanner = self.wayland_scanner
|
||||
ans.wayland_scanner_code = self.wayland_scanner_code
|
||||
ans.wayland_protocols = self.wayland_protocols
|
||||
return ans
|
||||
|
||||
|
||||
def wayland_protocol_file_name(base: str, ext: str = 'c') -> str:
|
||||
base = os.path.basename(base).rpartition('.')[0]
|
||||
return 'wayland-{}-client-protocol.{}'.format(base, ext)
|
||||
|
||||
|
||||
def init_env(env, pkg_config, at_least_version, test_compile, module='x11'):
|
||||
def init_env(env: Env, pkg_config: Callable, at_least_version: Callable, test_compile: Callable, module: str = 'x11') -> Env:
|
||||
ans = env.copy()
|
||||
ans.cflags.append('-fpic')
|
||||
ans.cflags.append('-fPIC')
|
||||
ans.cppflags.append('-D_GLFW_' + module.upper())
|
||||
ans.cppflags.append('-D_GLFW_BUILD_DLL')
|
||||
|
||||
@@ -46,8 +82,8 @@ def init_env(env, pkg_config, at_least_version, test_compile, module='x11'):
|
||||
|
||||
elif module == 'cocoa':
|
||||
ans.cppflags.append('-DGL_SILENCE_DEPRECATION')
|
||||
for f in 'Cocoa IOKit CoreFoundation CoreVideo'.split():
|
||||
ans.ldpaths.extend(('-framework', f))
|
||||
for f_ in 'Cocoa IOKit CoreFoundation CoreVideo'.split():
|
||||
ans.ldpaths.extend(('-framework', f_))
|
||||
|
||||
elif module == 'wayland':
|
||||
at_least_version('wayland-protocols', *sinfo['wayland_protocols'])
|
||||
@@ -59,7 +95,7 @@ def init_env(env, pkg_config, at_least_version, test_compile, module='x11'):
|
||||
for p in ans.wayland_protocols:
|
||||
ans.sources.append(wayland_protocol_file_name(p))
|
||||
ans.all_headers.append(wayland_protocol_file_name(p, 'h'))
|
||||
for dep in 'wayland-egl wayland-client wayland-cursor xkbcommon dbus-1'.split():
|
||||
for dep in 'wayland-client wayland-cursor xkbcommon dbus-1'.split():
|
||||
ans.cflags.extend(pkg_config(dep, '--cflags-only-I'))
|
||||
ans.ldpaths.extend(pkg_config(dep, '--libs'))
|
||||
has_memfd_create = test_compile(env.cc, '-Werror', src='''#define _GNU_SOURCE
|
||||
@@ -74,7 +110,7 @@ def init_env(env, pkg_config, at_least_version, test_compile, module='x11'):
|
||||
return ans
|
||||
|
||||
|
||||
def build_wayland_protocols(env, Command, parallel_run, emphasis, newer, dest_dir):
|
||||
def build_wayland_protocols(env: Env, Command: Callable, parallel_run: Callable, emphasis: Callable, newer: Callable, dest_dir: str) -> None:
|
||||
items = []
|
||||
for protocol in env.wayland_protocols:
|
||||
src = os.path.join(env.wayland_packagedir, protocol)
|
||||
@@ -95,7 +131,7 @@ def build_wayland_protocols(env, Command, parallel_run, emphasis, newer, dest_di
|
||||
|
||||
class Arg:
|
||||
|
||||
def __init__(self, decl):
|
||||
def __init__(self, decl: str):
|
||||
self.type, self.name = decl.rsplit(' ', 1)
|
||||
self.type = self.type.strip()
|
||||
self.name = self.name.strip()
|
||||
@@ -103,13 +139,13 @@ class Arg:
|
||||
self.name = self.name[1:]
|
||||
self.type = self.type + '*'
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return 'Arg({}, {})'.format(self.type, self.name)
|
||||
|
||||
|
||||
class Function:
|
||||
|
||||
def __init__(self, declaration, check_fail=True):
|
||||
def __init__(self, declaration: str, check_fail: bool = True):
|
||||
self.check_fail = check_fail
|
||||
m = re.match(
|
||||
r'(.+?)\s+(glfw[A-Z][a-zA-Z0-9]+)[(](.+)[)]$', declaration
|
||||
@@ -128,14 +164,14 @@ class Function:
|
||||
if not self.args:
|
||||
self.args = [Arg('void v')]
|
||||
|
||||
def declaration(self):
|
||||
return 'typedef {restype} (*{name}_func)({args});\n{name}_func {name}_impl;\n#define {name} {name}_impl'.format(
|
||||
def declaration(self) -> str:
|
||||
return 'typedef {restype} (*{name}_func)({args});\nGFW_EXTERN {name}_func {name}_impl;\n#define {name} {name}_impl'.format(
|
||||
restype=self.restype,
|
||||
name=self.name,
|
||||
args=', '.join(a.type for a in self.args)
|
||||
)
|
||||
|
||||
def load(self):
|
||||
def load(self) -> str:
|
||||
ans = '*(void **) (&{name}_impl) = dlsym(handle, "{name}");'.format(
|
||||
name=self.name
|
||||
)
|
||||
@@ -146,7 +182,7 @@ class Function:
|
||||
return ans
|
||||
|
||||
|
||||
def generate_wrappers(glfw_header):
|
||||
def generate_wrappers(glfw_header: str) -> None:
|
||||
with open(glfw_header) as f:
|
||||
src = f.read()
|
||||
functions = []
|
||||
@@ -165,13 +201,14 @@ def generate_wrappers(glfw_header):
|
||||
GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow* window, GLFWcocoatextinputfilterfun callback)
|
||||
GLFWcocoatogglefullscreenfun glfwSetCocoaToggleFullscreenIntercept(GLFWwindow *window, GLFWcocoatogglefullscreenfun callback)
|
||||
GLFWapplicationshouldhandlereopenfun glfwSetApplicationShouldHandleReopen(GLFWapplicationshouldhandlereopenfun callback)
|
||||
void glfwGetCocoaKeyEquivalent(int glfw_key, int glfw_mods, void* cocoa_key, void* cocoa_mods)
|
||||
GLFWapplicationwillfinishlaunchingfun glfwSetApplicationWillFinishLaunching(GLFWapplicationwillfinishlaunchingfun callback)
|
||||
void glfwGetCocoaKeyEquivalent(int glfw_key, int glfw_mods, char* cocoa_key, size_t key_sz, int* cocoa_mods)
|
||||
void glfwCocoaRequestRenderFrame(GLFWwindow *w, GLFWcocoarenderframefun callback)
|
||||
void* glfwGetX11Display(void)
|
||||
int32_t glfwGetX11Window(GLFWwindow* window)
|
||||
void glfwSetPrimarySelectionString(GLFWwindow* window, const char* string)
|
||||
const char* glfwGetPrimarySelectionString(GLFWwindow* window, void)
|
||||
int glfwGetXKBScancode(const char* key_name, int case_sensitive)
|
||||
int glfwGetNativeKeyForName(const char* key_name, int case_sensitive)
|
||||
void glfwRequestWaylandFrameEvent(GLFWwindow *handle, unsigned long long id, GLFWwaylandframecallbackfunc callback)
|
||||
unsigned long long glfwDBusUserNotify(const char *app_name, const char* icon, const char *summary, const char *body, \
|
||||
const char *action_text, int32_t timeout, GLFWDBusnotificationcreatedfun callback, void *data)
|
||||
@@ -185,15 +222,26 @@ const char *action_text, int32_t timeout, GLFWDBusnotificationcreatedfun callbac
|
||||
p = src.find('*/', p)
|
||||
preamble = src[p + 2:first]
|
||||
header = '''\
|
||||
//
|
||||
// THIS FILE IS GENERATED BY glfw.py
|
||||
//
|
||||
// SAVE YOURSELF SOME TIME, DO NOT MANUALLY EDIT
|
||||
//
|
||||
|
||||
#pragma once
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "monotonic.h"
|
||||
|
||||
#ifndef GFW_EXTERN
|
||||
#define GFW_EXTERN extern
|
||||
#endif
|
||||
{}
|
||||
|
||||
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int,unsigned long);
|
||||
typedef int (* GLFWapplicationshouldhandlereopenfun)(int);
|
||||
typedef int (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
|
||||
typedef bool (* GLFWapplicationshouldhandlereopenfun)(int);
|
||||
typedef void (* GLFWapplicationwillfinishlaunchingfun)(void);
|
||||
typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
|
||||
typedef void (* GLFWcocoarenderframefun)(GLFWwindow*);
|
||||
typedef void (*GLFWwaylandframecallbackfunc)(unsigned long long id);
|
||||
typedef void (*GLFWDBusnotificationcreatedfun)(unsigned long long, uint32_t, void*);
|
||||
@@ -206,6 +254,7 @@ const char* load_glfw(const char* path);
|
||||
f.write(header)
|
||||
|
||||
code = '''
|
||||
#define GFW_EXTERN
|
||||
#include "data-types.h"
|
||||
#include "glfw-wrapper.h"
|
||||
#include <dlfcn.h>
|
||||
@@ -235,7 +284,7 @@ unload_glfw(void) {
|
||||
f.write(code)
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> None:
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
generate_wrappers('glfw3.h')
|
||||
|
||||
|
||||
750
glfw/glfw3.h
vendored
750
glfw/glfw3.h
vendored
File diff suppressed because it is too large
Load Diff
29
glfw/ibus_glfw.c
vendored
29
glfw/ibus_glfw.c
vendored
@@ -107,10 +107,14 @@ get_ibus_text_from_message(DBusMessage *msg) {
|
||||
}
|
||||
|
||||
static inline void
|
||||
send_text(const char *text, int state) {
|
||||
send_text(const char *text, int ime_state) {
|
||||
_GLFWwindow *w = _glfwFocusedWindow();
|
||||
if (w && w->callbacks.keyboard) {
|
||||
w->callbacks.keyboard((GLFWwindow*) w, GLFW_KEY_UNKNOWN, 0, GLFW_PRESS, 0, text, state);
|
||||
GLFWkeyevent fake_ev;
|
||||
_glfwInitializeKeyEvent(&fake_ev, GLFW_KEY_UNKNOWN, 0, GLFW_PRESS, 0);
|
||||
fake_ev.text = text;
|
||||
fake_ev.ime_state = ime_state;
|
||||
w->callbacks.keyboard((GLFWwindow*) w, &fake_ev);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,31 +397,36 @@ ibus_key_state(unsigned int glfw_modifiers, int action) {
|
||||
void
|
||||
key_event_processed(DBusMessage *msg, const char* errmsg, void *data) {
|
||||
uint32_t handled = 0;
|
||||
KeyEvent *ev = (KeyEvent*)data;
|
||||
bool is_release = ev->action == GLFW_RELEASE;
|
||||
_GLFWIBUSKeyEvent *ev = (_GLFWIBUSKeyEvent*)data;
|
||||
// Restore key's text from the text embedded in the structure.
|
||||
ev->glfw_ev.text = ev->__embedded_text;
|
||||
bool is_release = ev->glfw_ev.action == GLFW_RELEASE;
|
||||
bool failed = false;
|
||||
if (errmsg) {
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR, "IBUS: Failed to process key with error: %s", errmsg);
|
||||
failed = true;
|
||||
} else {
|
||||
glfw_dbus_get_args(msg, "Failed to get IBUS handled key from reply", DBUS_TYPE_BOOLEAN, &handled, DBUS_TYPE_INVALID);
|
||||
debug("IBUS processed scancode: 0x%x release: %d handled: %u\n", ev->keycode, is_release, handled);
|
||||
debug("IBUS processed native_key: 0x%x release: %d handled: %u\n", ev->glfw_ev.native_key, is_release, handled);
|
||||
}
|
||||
glfw_xkb_key_from_ime(ev, handled ? true : false, failed);
|
||||
free(ev);
|
||||
}
|
||||
|
||||
bool
|
||||
ibus_process_key(const KeyEvent *ev_, _GLFWIBUSData *ibus) {
|
||||
ibus_process_key(const _GLFWIBUSKeyEvent *ev_, _GLFWIBUSData *ibus) {
|
||||
if (!check_connection(ibus)) return false;
|
||||
KeyEvent *ev = malloc(sizeof(KeyEvent));
|
||||
_GLFWIBUSKeyEvent *ev = calloc(1, sizeof(_GLFWIBUSKeyEvent));
|
||||
if (!ev) return false;
|
||||
memcpy(ev, ev_, sizeof(KeyEvent));
|
||||
uint32_t state = ibus_key_state(ev->glfw_modifiers, ev->action);
|
||||
memcpy(ev, ev_, sizeof(_GLFWIBUSKeyEvent));
|
||||
// Put the key's text in a field IN the structure, for proper serialization.
|
||||
if (ev->glfw_ev.text) strncpy(ev->__embedded_text, ev->glfw_ev.text, sizeof(ev->__embedded_text) - 1);
|
||||
ev->glfw_ev.text = NULL;
|
||||
uint32_t state = ibus_key_state(ev->glfw_ev.mods, ev->glfw_ev.action);
|
||||
if (!glfw_dbus_call_method_with_reply(
|
||||
ibus->conn, IBUS_SERVICE, ibus->input_ctx_path, IBUS_INPUT_INTERFACE, "ProcessKeyEvent",
|
||||
3000, key_event_processed, ev,
|
||||
DBUS_TYPE_UINT32, &ev->ibus_sym, DBUS_TYPE_UINT32, &ev->ibus_keycode, DBUS_TYPE_UINT32,
|
||||
DBUS_TYPE_UINT32, &ev->ibus_keysym, DBUS_TYPE_UINT32, &ev->ibus_keycode, DBUS_TYPE_UINT32,
|
||||
&state, DBUS_TYPE_INVALID)) {
|
||||
free(ev);
|
||||
return false;
|
||||
|
||||
15
glfw/ibus_glfw.h
vendored
15
glfw/ibus_glfw.h
vendored
@@ -27,6 +27,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "internal.h"
|
||||
#include "dbus_glfw.h"
|
||||
#include <xkbcommon/xkbcommon.h>
|
||||
|
||||
@@ -38,18 +39,16 @@ typedef struct {
|
||||
} _GLFWIBUSData;
|
||||
|
||||
typedef struct {
|
||||
xkb_keycode_t keycode, ibus_keycode;
|
||||
xkb_keysym_t keysym, ibus_sym;
|
||||
unsigned int glfw_modifiers;
|
||||
int action;
|
||||
xkb_keycode_t ibus_keycode;
|
||||
xkb_keysym_t ibus_keysym;
|
||||
GLFWid window_id;
|
||||
int glfw_keycode;
|
||||
char text[64];
|
||||
} KeyEvent;
|
||||
GLFWkeyevent glfw_ev;
|
||||
char __embedded_text[64];
|
||||
} _GLFWIBUSKeyEvent;
|
||||
|
||||
void glfw_connect_to_ibus(_GLFWIBUSData *ibus);
|
||||
void glfw_ibus_terminate(_GLFWIBUSData *ibus);
|
||||
void glfw_ibus_set_focused(_GLFWIBUSData *ibus, bool focused);
|
||||
void glfw_ibus_dispatch(_GLFWIBUSData *ibus);
|
||||
bool ibus_process_key(const KeyEvent *ev_, _GLFWIBUSData *ibus);
|
||||
bool ibus_process_key(const _GLFWIBUSKeyEvent *ev_, _GLFWIBUSData *ibus);
|
||||
void glfw_ibus_set_cursor_geometry(_GLFWIBUSData *ibus, int x, int y, int w, int h);
|
||||
|
||||
10
glfw/init.c
vendored
10
glfw/init.c
vendored
@@ -201,7 +201,7 @@ _glfwDebug(const char *format, ...) {
|
||||
{
|
||||
va_list vl;
|
||||
|
||||
fprintf(stderr, "[%.4f] ", glfwGetTime());
|
||||
fprintf(stderr, "[%.4f] ", monotonic_t_to_s_double(glfwGetTime()));
|
||||
va_start(vl, format);
|
||||
vfprintf(stderr, format, vl);
|
||||
va_end(vl);
|
||||
@@ -214,10 +214,11 @@ _glfwDebug(const char *format, ...) {
|
||||
////// GLFW public API //////
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
GLFWAPI int glfwInit(void)
|
||||
GLFWAPI int glfwInit(monotonic_t start_time)
|
||||
{
|
||||
if (_glfw.initialized)
|
||||
return true;
|
||||
monotonic_start_time = start_time;
|
||||
|
||||
memset(&_glfw, 0, sizeof(_glfw));
|
||||
_glfw.hints.init = _glfwInitHints;
|
||||
@@ -239,7 +240,6 @@ GLFWAPI int glfwInit(void)
|
||||
_glfwPlatformSetTls(&_glfw.errorSlot, &_glfwMainThreadError);
|
||||
|
||||
_glfw.initialized = true;
|
||||
_glfw.timer.offset = _glfwPlatformGetTimerValue();
|
||||
|
||||
glfwDefaultWindowHints();
|
||||
|
||||
@@ -350,13 +350,13 @@ GLFWAPI void glfwStopMainLoop(void) {
|
||||
}
|
||||
|
||||
GLFWAPI unsigned long long glfwAddTimer(
|
||||
double interval, bool repeats, GLFWuserdatafun callback,
|
||||
monotonic_t interval, bool repeats, GLFWuserdatafun callback,
|
||||
void *callback_data, GLFWuserdatafun free_callback)
|
||||
{
|
||||
return _glfwPlatformAddTimer(interval, repeats, callback, callback_data, free_callback);
|
||||
}
|
||||
|
||||
GLFWAPI void glfwUpdateTimer(unsigned long long timer_id, double interval, bool enabled) {
|
||||
GLFWAPI void glfwUpdateTimer(unsigned long long timer_id, monotonic_t interval, bool enabled) {
|
||||
_glfwPlatformUpdateTimer(timer_id, interval, enabled);
|
||||
}
|
||||
|
||||
|
||||
83
glfw/input.c
vendored
83
glfw/input.c
vendored
@@ -28,6 +28,7 @@
|
||||
//========================================================================
|
||||
|
||||
#include "internal.h"
|
||||
#include "../kitty/monotonic.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <float.h>
|
||||
@@ -256,42 +257,53 @@ static bool parseMapping(_GLFWmapping* mapping, const char* string)
|
||||
////// GLFW event API //////
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void _glfwInitializeKeyEvent(GLFWkeyevent *ev, int key, int native_key, int action, int mods)
|
||||
{
|
||||
ev->key = key;
|
||||
ev->native_key = native_key;
|
||||
ev->action = action;
|
||||
ev->mods = mods;
|
||||
ev->text = NULL;
|
||||
ev->ime_state = 0;
|
||||
}
|
||||
|
||||
// Notifies shared code of a physical key event
|
||||
//
|
||||
void _glfwInputKeyboard(_GLFWwindow* window, int key, int scancode, int action, int mods, const char* text, int state)
|
||||
void _glfwInputKeyboard(_GLFWwindow* window, GLFWkeyevent* ev)
|
||||
{
|
||||
if (key >= 0 && key <= GLFW_KEY_LAST)
|
||||
if (ev->key >= 0 && ev->key <= GLFW_KEY_LAST)
|
||||
{
|
||||
bool repeated = false;
|
||||
|
||||
if (action == GLFW_RELEASE && window->keys[key] == GLFW_RELEASE)
|
||||
if (ev->action == GLFW_RELEASE && window->keys[ev->key] == GLFW_RELEASE)
|
||||
return;
|
||||
|
||||
if (action == GLFW_PRESS && window->keys[key] == GLFW_PRESS)
|
||||
if (ev->action == GLFW_PRESS && window->keys[ev->key] == GLFW_PRESS)
|
||||
repeated = true;
|
||||
|
||||
if (action == GLFW_RELEASE && window->stickyKeys)
|
||||
window->keys[key] = _GLFW_STICK;
|
||||
if (ev->action == GLFW_RELEASE && window->stickyKeys)
|
||||
window->keys[ev->key] = _GLFW_STICK;
|
||||
else
|
||||
window->keys[key] = (char) action;
|
||||
window->keys[ev->key] = (char) ev->action;
|
||||
|
||||
if (repeated)
|
||||
action = GLFW_REPEAT;
|
||||
ev->action = GLFW_REPEAT;
|
||||
}
|
||||
|
||||
|
||||
// FIXME: will need to update ev->virtual_mods here too?
|
||||
if (window->callbacks.keyboard) {
|
||||
if (!window->lockKeyMods) mods &= ~(GLFW_MOD_CAPS_LOCK | GLFW_MOD_NUM_LOCK);
|
||||
window->callbacks.keyboard((GLFWwindow*) window, key, scancode, action, mods, text, state);
|
||||
if (!window->lockKeyMods) ev->mods &= ~(GLFW_MOD_CAPS_LOCK | GLFW_MOD_NUM_LOCK);
|
||||
window->callbacks.keyboard((GLFWwindow*) window, ev);
|
||||
}
|
||||
}
|
||||
|
||||
// Notifies shared code of a scroll event
|
||||
//
|
||||
void _glfwInputScroll(_GLFWwindow* window, double xoffset, double yoffset, int flags)
|
||||
void _glfwInputScroll(_GLFWwindow* window, double xoffset, double yoffset, int flags, int mods)
|
||||
{
|
||||
if (window->callbacks.scroll)
|
||||
window->callbacks.scroll((GLFWwindow*) window, xoffset, yoffset, flags);
|
||||
window->callbacks.scroll((GLFWwindow*) window, xoffset, yoffset, flags, mods);
|
||||
}
|
||||
|
||||
// Notifies shared code of a mouse button click event
|
||||
@@ -338,10 +350,11 @@ void _glfwInputCursorEnter(_GLFWwindow* window, bool entered)
|
||||
|
||||
// Notifies shared code of files or directories dropped on a window
|
||||
//
|
||||
void _glfwInputDrop(_GLFWwindow* window, int count, const char** paths)
|
||||
int _glfwInputDrop(_GLFWwindow* window, const char *mime, const char *text, size_t sz)
|
||||
{
|
||||
if (window->callbacks.drop)
|
||||
window->callbacks.drop((GLFWwindow*) window, count, paths);
|
||||
return window->callbacks.drop((GLFWwindow*) window, mime, text, sz);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Notifies shared code of a joystick connection or disconnection
|
||||
@@ -498,6 +511,7 @@ const char* _glfwGetKeyName(int key)
|
||||
case GLFW_KEY_LEFT_BRACKET: return "LEFT_BRACKET";
|
||||
case GLFW_KEY_BACKSLASH: return "BACKSLASH";
|
||||
case GLFW_KEY_RIGHT_BRACKET: return "RIGHT_BRACKET";
|
||||
case GLFW_KEY_CIRCUMFLEX: return "CIRCUMFLEX";
|
||||
case GLFW_KEY_UNDERSCORE: return "UNDERSCORE";
|
||||
case GLFW_KEY_GRAVE_ACCENT: return "GRAVE_ACCENT";
|
||||
case GLFW_KEY_WORLD_1: return "WORLD_1";
|
||||
@@ -744,7 +758,7 @@ GLFWAPI void glfwSetInputMode(GLFWwindow* handle, int mode, int value)
|
||||
_glfwInputError(GLFW_INVALID_ENUM, "Invalid input mode 0x%08X", mode);
|
||||
}
|
||||
|
||||
GLFWAPI const char* glfwGetKeyName(int key, int scancode)
|
||||
GLFWAPI const char* glfwGetKeyName(int key, int native_key)
|
||||
{
|
||||
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
|
||||
|
||||
@@ -757,13 +771,13 @@ GLFWAPI const char* glfwGetKeyName(int key, int scancode)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
scancode = _glfwPlatformGetKeyScancode(key);
|
||||
native_key = _glfwPlatformGetNativeKeyForKey(key);
|
||||
}
|
||||
|
||||
return _glfwPlatformGetScancodeName(scancode);
|
||||
return _glfwPlatformGetNativeKeyName(native_key);
|
||||
}
|
||||
|
||||
GLFWAPI int glfwGetKeyScancode(int key)
|
||||
GLFWAPI int glfwGetNativeKeyForKey(int key)
|
||||
{
|
||||
_GLFW_REQUIRE_INIT_OR_RETURN(-1);
|
||||
|
||||
@@ -773,7 +787,7 @@ GLFWAPI int glfwGetKeyScancode(int key)
|
||||
return GLFW_RELEASE;
|
||||
}
|
||||
|
||||
return _glfwPlatformGetKeyScancode(key);
|
||||
return _glfwPlatformGetNativeKeyForKey(key);
|
||||
}
|
||||
|
||||
GLFWAPI int glfwGetKey(GLFWwindow* handle, int key)
|
||||
@@ -1466,35 +1480,8 @@ GLFWAPI const char* glfwGetPrimarySelectionString(GLFWwindow* handle UNUSED)
|
||||
}
|
||||
#endif
|
||||
|
||||
GLFWAPI double glfwGetTime(void)
|
||||
{
|
||||
_GLFW_REQUIRE_INIT_OR_RETURN(0.0);
|
||||
return (double) (_glfwPlatformGetTimerValue() - _glfw.timer.offset) /
|
||||
_glfwPlatformGetTimerFrequency();
|
||||
}
|
||||
|
||||
GLFWAPI void glfwSetTime(double time)
|
||||
{
|
||||
_GLFW_REQUIRE_INIT();
|
||||
|
||||
if (time != time || time < 0.0 || time > 18446744073.0)
|
||||
{
|
||||
_glfwInputError(GLFW_INVALID_VALUE, "Invalid time %f", time);
|
||||
return;
|
||||
}
|
||||
|
||||
_glfw.timer.offset = _glfwPlatformGetTimerValue() -
|
||||
(uint64_t) (time * _glfwPlatformGetTimerFrequency());
|
||||
}
|
||||
|
||||
GLFWAPI uint64_t glfwGetTimerValue(void)
|
||||
GLFWAPI monotonic_t glfwGetTime(void)
|
||||
{
|
||||
_GLFW_REQUIRE_INIT_OR_RETURN(0);
|
||||
return _glfwPlatformGetTimerValue();
|
||||
}
|
||||
|
||||
GLFWAPI uint64_t glfwGetTimerFrequency(void)
|
||||
{
|
||||
_GLFW_REQUIRE_INIT_OR_RETURN(0);
|
||||
return _glfwPlatformGetTimerFrequency();
|
||||
return monotonic();
|
||||
}
|
||||
|
||||
34
glfw/internal.h
vendored
34
glfw/internal.h
vendored
@@ -27,6 +27,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../kitty/monotonic.h"
|
||||
|
||||
#if defined(_GLFW_USE_CONFIG_H)
|
||||
#include "glfw_config.h"
|
||||
#endif
|
||||
@@ -130,6 +132,7 @@ typedef enum VkStructureType
|
||||
VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR = 1000006000,
|
||||
VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR = 1000009000,
|
||||
VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK = 1000123000,
|
||||
VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT = 1000217000,
|
||||
VK_STRUCTURE_TYPE_MAX_ENUM = 0x7FFFFFFF
|
||||
} VkStructureType;
|
||||
|
||||
@@ -416,6 +419,7 @@ struct _GLFWwindow
|
||||
int minwidth, minheight;
|
||||
int maxwidth, maxheight;
|
||||
int numer, denom;
|
||||
int widthincr, heightincr;
|
||||
|
||||
bool stickyKeys;
|
||||
bool stickyMouseButtons;
|
||||
@@ -572,12 +576,6 @@ struct _GLFWlibrary
|
||||
_GLFWtls contextSlot;
|
||||
_GLFWmutex errorLock;
|
||||
|
||||
struct {
|
||||
uint64_t offset;
|
||||
// This is defined in the platform's time.h
|
||||
_GLFW_PLATFORM_LIBRARY_TIMER_STATE;
|
||||
} timer;
|
||||
|
||||
struct {
|
||||
bool available;
|
||||
void* handle;
|
||||
@@ -591,6 +589,7 @@ struct _GLFWlibrary
|
||||
bool KHR_win32_surface;
|
||||
#elif defined(_GLFW_COCOA)
|
||||
bool MVK_macos_surface;
|
||||
bool EXT_metal_surface;
|
||||
#elif defined(_GLFW_X11)
|
||||
bool KHR_xlib_surface;
|
||||
bool KHR_xcb_surface;
|
||||
@@ -638,8 +637,8 @@ int _glfwPlatformCreateStandardCursor(_GLFWcursor* cursor, GLFWCursorShape shape
|
||||
void _glfwPlatformDestroyCursor(_GLFWcursor* cursor);
|
||||
void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor);
|
||||
|
||||
const char* _glfwPlatformGetScancodeName(int scancode);
|
||||
int _glfwPlatformGetKeyScancode(int key);
|
||||
const char* _glfwPlatformGetNativeKeyName(int native_key);
|
||||
int _glfwPlatformGetNativeKeyForKey(int key);
|
||||
|
||||
void _glfwPlatformFreeMonitor(_GLFWmonitor* monitor);
|
||||
void _glfwPlatformGetMonitorPos(_GLFWmonitor* monitor, int* xpos, int* ypos);
|
||||
@@ -661,9 +660,6 @@ const char* _glfwPlatformGetPrimarySelectionString(void);
|
||||
int _glfwPlatformPollJoystick(_GLFWjoystick* js, int mode);
|
||||
void _glfwPlatformUpdateGamepadGUID(char* guid);
|
||||
|
||||
uint64_t _glfwPlatformGetTimerValue(void);
|
||||
uint64_t _glfwPlatformGetTimerFrequency(void);
|
||||
|
||||
int _glfwPlatformCreateWindow(_GLFWwindow* window,
|
||||
const _GLFWwndconfig* wndconfig,
|
||||
const _GLFWctxconfig* ctxconfig,
|
||||
@@ -680,6 +676,7 @@ void _glfwPlatformSetWindowSizeLimits(_GLFWwindow* window,
|
||||
int minwidth, int minheight,
|
||||
int maxwidth, int maxheight);
|
||||
void _glfwPlatformSetWindowAspectRatio(_GLFWwindow* window, int numer, int denom);
|
||||
void _glfwPlatformSetWindowSizeIncrements(_GLFWwindow* window, int widthincr, int heightincr);
|
||||
void _glfwPlatformGetFramebufferSize(_GLFWwindow* window, int* width, int* height);
|
||||
void _glfwInputLiveResize(_GLFWwindow* window, bool started);
|
||||
void _glfwPlatformGetWindowFrameSize(_GLFWwindow* window,
|
||||
@@ -687,7 +684,7 @@ void _glfwPlatformGetWindowFrameSize(_GLFWwindow* window,
|
||||
int* right, int* bottom);
|
||||
void _glfwPlatformGetWindowContentScale(_GLFWwindow* window,
|
||||
float* xscale, float* yscale);
|
||||
double _glfwPlatformGetDoubleClickInterval(_GLFWwindow* window);
|
||||
monotonic_t _glfwPlatformGetDoubleClickInterval(_GLFWwindow* window);
|
||||
void _glfwPlatformIconifyWindow(_GLFWwindow* window);
|
||||
void _glfwPlatformRestoreWindow(_GLFWwindow* window);
|
||||
void _glfwPlatformMaximizeWindow(_GLFWwindow* window);
|
||||
@@ -716,7 +713,7 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, int which, int a, int b, int c,
|
||||
|
||||
void _glfwPlatformPollEvents(void);
|
||||
void _glfwPlatformWaitEvents(void);
|
||||
void _glfwPlatformWaitEventsTimeout(double timeout);
|
||||
void _glfwPlatformWaitEventsTimeout(monotonic_t timeout);
|
||||
void _glfwPlatformPostEmptyEvent(void);
|
||||
|
||||
void _glfwPlatformGetRequiredInstanceExtensions(char** extensions);
|
||||
@@ -756,12 +753,13 @@ void _glfwInputWindowDamage(_GLFWwindow* window);
|
||||
void _glfwInputWindowCloseRequest(_GLFWwindow* window);
|
||||
void _glfwInputWindowMonitor(_GLFWwindow* window, _GLFWmonitor* monitor);
|
||||
|
||||
void _glfwInputKeyboard(_GLFWwindow* window, int key, int scancode, int action, int mods, const char* text, int state);
|
||||
void _glfwInputScroll(_GLFWwindow* window, double xoffset, double yoffset, int flags);
|
||||
void _glfwInitializeKeyEvent(GLFWkeyevent *ev, int key, int native_key, int action, int mods);
|
||||
void _glfwInputKeyboard(_GLFWwindow *window, GLFWkeyevent *ev);
|
||||
void _glfwInputScroll(_GLFWwindow* window, double xoffset, double yoffset, int flags, int mods);
|
||||
void _glfwInputMouseClick(_GLFWwindow* window, int button, int action, int mods);
|
||||
void _glfwInputCursorPos(_GLFWwindow* window, double xpos, double ypos);
|
||||
void _glfwInputCursorEnter(_GLFWwindow* window, bool entered);
|
||||
void _glfwInputDrop(_GLFWwindow* window, int count, const char** names);
|
||||
int _glfwInputDrop(_GLFWwindow* window, const char *mime, const char *text, size_t sz);
|
||||
void _glfwInputJoystick(_GLFWjoystick* js, int event);
|
||||
void _glfwInputJoystickAxis(_GLFWjoystick* js, int axis, float value);
|
||||
void _glfwInputJoystickButton(_GLFWjoystick* js, int button, char value);
|
||||
@@ -823,8 +821,8 @@ _GLFWwindow* _glfwFocusedWindow(void);
|
||||
_GLFWwindow* _glfwWindowForId(GLFWid id);
|
||||
void _glfwPlatformRunMainLoop(GLFWtickcallback, void*);
|
||||
void _glfwPlatformStopMainLoop(void);
|
||||
unsigned long long _glfwPlatformAddTimer(double interval, bool repeats, GLFWuserdatafun callback, void *callback_data, GLFWuserdatafun free_callback);
|
||||
void _glfwPlatformUpdateTimer(unsigned long long timer_id, double interval, bool enabled);
|
||||
unsigned long long _glfwPlatformAddTimer(monotonic_t interval, bool repeats, GLFWuserdatafun callback, void *callback_data, GLFWuserdatafun free_callback);
|
||||
void _glfwPlatformUpdateTimer(unsigned long long timer_id, monotonic_t interval, bool enabled);
|
||||
void _glfwPlatformRemoveTimer(unsigned long long timer_id);
|
||||
|
||||
char* _glfw_strdup(const char* source);
|
||||
|
||||
3
glfw/linux_joystick.c
vendored
3
glfw/linux_joystick.c
vendored
@@ -222,7 +222,8 @@ static bool openJoystickDevice(const char* path)
|
||||
}
|
||||
}
|
||||
|
||||
_GLFWjoystick* js = _glfwAllocJoystick(name, guid, axisCount, buttonCount, hatCount);
|
||||
_GLFWjoystick* js =
|
||||
_glfwAllocJoystick(name, guid, axisCount, buttonCount, hatCount);
|
||||
if (!js)
|
||||
{
|
||||
close(linjs.fd);
|
||||
|
||||
5
glfw/main_loop.h
vendored
5
glfw/main_loop.h
vendored
@@ -7,6 +7,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "internal.h"
|
||||
#include "../kitty/monotonic.h"
|
||||
|
||||
#ifndef GLFW_LOOP_BACKEND
|
||||
#define GLFW_LOOP_BACKEND x11
|
||||
@@ -36,7 +37,7 @@ void _glfwPlatformRunMainLoop(GLFWtickcallback tick_callback, void* data) {
|
||||
EVDBG("main loop exiting");
|
||||
}
|
||||
|
||||
unsigned long long _glfwPlatformAddTimer(double interval, bool repeats, GLFWuserdatafreefun callback, void *callback_data, GLFWuserdatafreefun free_callback) {
|
||||
unsigned long long _glfwPlatformAddTimer(monotonic_t interval, bool repeats, GLFWuserdatafreefun callback, void *callback_data, GLFWuserdatafreefun free_callback) {
|
||||
return addTimer(&_glfw.GLFW_LOOP_BACKEND.eventLoopData, "user timer", interval, 1, repeats, callback, callback_data, free_callback);
|
||||
}
|
||||
|
||||
@@ -44,7 +45,7 @@ void _glfwPlatformRemoveTimer(unsigned long long timer_id) {
|
||||
removeTimer(&_glfw.GLFW_LOOP_BACKEND.eventLoopData, timer_id);
|
||||
}
|
||||
|
||||
void _glfwPlatformUpdateTimer(unsigned long long timer_id, double interval, bool enabled) {
|
||||
void _glfwPlatformUpdateTimer(unsigned long long timer_id, monotonic_t interval, bool enabled) {
|
||||
changeTimerInterval(&_glfw.GLFW_LOOP_BACKEND.eventLoopData, timer_id, interval);
|
||||
toggleTimer(&_glfw.GLFW_LOOP_BACKEND.eventLoopData, timer_id, enabled);
|
||||
}
|
||||
|
||||
10
glfw/monotonic.c
vendored
Normal file
10
glfw/monotonic.c
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* monotonic.c
|
||||
* Copyright (C) 2019 Kovid Goyal <kovid at kovidgoyal.net>
|
||||
*
|
||||
* Distributed under terms of the GPL3 license.
|
||||
*/
|
||||
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#define MONOTONIC_IMPLEMENTATION
|
||||
#include "../kitty/monotonic.h"
|
||||
4
glfw/nsgl_context.h
vendored
4
glfw/nsgl_context.h
vendored
@@ -32,8 +32,8 @@
|
||||
//
|
||||
typedef struct _GLFWcontextNSGL
|
||||
{
|
||||
id pixelFormat;
|
||||
id object;
|
||||
id pixelFormat;
|
||||
id object;
|
||||
|
||||
} _GLFWcontextNSGL;
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@ bool _glfwCreateContextNSGL(_GLFWwindow* window,
|
||||
return false;
|
||||
}
|
||||
|
||||
NSOpenGLContext* share = NULL;
|
||||
NSOpenGLContext* share = nil;
|
||||
|
||||
if (ctxconfig->share)
|
||||
share = ctxconfig->share->context.nsgl.object;
|
||||
@@ -293,8 +293,7 @@ bool _glfwCreateContextNSGL(_GLFWwindow* window,
|
||||
forParameter:NSOpenGLContextParameterSurfaceOpacity];
|
||||
}
|
||||
|
||||
if (window->ns.retina)
|
||||
[window->ns.view setWantsBestResolutionOpenGLSurface:YES];
|
||||
[window->ns.view setWantsBestResolutionOpenGLSurface:window->ns.retina];
|
||||
|
||||
GLint interval = 0;
|
||||
[window->context.nsgl.object setValues:&interval
|
||||
@@ -325,7 +324,7 @@ GLFWAPI id glfwGetNSGLContext(GLFWwindow* handle)
|
||||
if (window->context.client == GLFW_NO_API)
|
||||
{
|
||||
_glfwInputError(GLFW_NO_WINDOW_CONTEXT, NULL);
|
||||
return NULL;
|
||||
return nil;
|
||||
}
|
||||
|
||||
return window->context.nsgl.object;
|
||||
|
||||
2
glfw/null_init.c
vendored
2
glfw/null_init.c
vendored
@@ -36,7 +36,6 @@
|
||||
|
||||
int _glfwPlatformInit(void)
|
||||
{
|
||||
_glfwInitTimerPOSIX();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -49,4 +48,3 @@ const char* _glfwPlatformGetVersionString(void)
|
||||
{
|
||||
return _GLFW_VERSION_NUMBER " null OSMesa";
|
||||
}
|
||||
|
||||
|
||||
15
glfw/null_window.c
vendored
15
glfw/null_window.c
vendored
@@ -28,6 +28,7 @@
|
||||
//========================================================================
|
||||
|
||||
#include "internal.h"
|
||||
#include "../kitty/monotonic.h"
|
||||
|
||||
|
||||
static int createNativeWindow(_GLFWwindow* window,
|
||||
@@ -127,6 +128,10 @@ void _glfwPlatformSetWindowAspectRatio(_GLFWwindow* window UNUSED, int n UNUSED,
|
||||
{
|
||||
}
|
||||
|
||||
void _glfwPlatformSetWindowSizeIncrements(_GLFWwindow* window UNUSED, int widthincr UNUSED, int heightincr UNUSED)
|
||||
{
|
||||
}
|
||||
|
||||
void _glfwPlatformGetFramebufferSize(_GLFWwindow* window, int* width, int* height)
|
||||
{
|
||||
if (width)
|
||||
@@ -150,9 +155,9 @@ void _glfwPlatformGetWindowContentScale(_GLFWwindow* window UNUSED,
|
||||
*yscale = 1.f;
|
||||
}
|
||||
|
||||
double _glfwPlatformGetDoubleClickInterval(_GLFWwindow* window UNUSED)
|
||||
monotonic_t _glfwPlatformGetDoubleClickInterval(_GLFWwindow* window UNUSED)
|
||||
{
|
||||
return 0.5;
|
||||
return ms_to_monotonic_t(500ll);
|
||||
}
|
||||
|
||||
void _glfwPlatformIconifyWindow(_GLFWwindow* window UNUSED)
|
||||
@@ -257,7 +262,7 @@ void _glfwPlatformWaitEvents(void)
|
||||
{
|
||||
}
|
||||
|
||||
void _glfwPlatformWaitEventsTimeout(double timeout UNUSED)
|
||||
void _glfwPlatformWaitEventsTimeout(monotonic_t timeout UNUSED)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -306,12 +311,12 @@ const char* _glfwPlatformGetClipboardString(void)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char* _glfwPlatformGetScancodeName(int scancode UNUSED)
|
||||
const char* _glfwPlatformGetNativeKeyName(int native_key UNUSED)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
int _glfwPlatformGetKeyScancode(int key UNUSED)
|
||||
int _glfwPlatformGetNativeKeyForKey(int key UNUSED)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
87
glfw/posix_time.c
vendored
87
glfw/posix_time.c
vendored
@@ -1,87 +0,0 @@
|
||||
//========================================================================
|
||||
// GLFW 3.4 POSIX - www.glfw.org
|
||||
//------------------------------------------------------------------------
|
||||
// Copyright (c) 2002-2006 Marcus Geelnard
|
||||
// Copyright (c) 2006-2017 Camilla Löwy <elmindreda@glfw.org>
|
||||
//
|
||||
// This software is provided 'as-is', without any express or implied
|
||||
// warranty. In no event will the authors be held liable for any damages
|
||||
// arising from the use of this software.
|
||||
//
|
||||
// Permission is granted to anyone to use this software for any purpose,
|
||||
// including commercial applications, and to alter it and redistribute it
|
||||
// freely, subject to the following restrictions:
|
||||
//
|
||||
// 1. The origin of this software must not be misrepresented; you must not
|
||||
// claim that you wrote the original software. If you use this software
|
||||
// in a product, an acknowledgment in the product documentation would
|
||||
// be appreciated but is not required.
|
||||
//
|
||||
// 2. Altered source versions must be plainly marked as such, and must not
|
||||
// be misrepresented as being the original software.
|
||||
//
|
||||
// 3. This notice may not be removed or altered from any source
|
||||
// distribution.
|
||||
//
|
||||
//========================================================================
|
||||
// It is fine to use C99 in this file because it will not be built with VS
|
||||
//========================================================================
|
||||
|
||||
#include "internal.h"
|
||||
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
////// GLFW internal API //////
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Initialise timer
|
||||
//
|
||||
void _glfwInitTimerPOSIX(void)
|
||||
{
|
||||
#if defined(CLOCK_MONOTONIC)
|
||||
struct timespec ts;
|
||||
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0)
|
||||
{
|
||||
_glfw.timer.posix.monotonic = true;
|
||||
_glfw.timer.posix.frequency = 1000000000;
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
_glfw.timer.posix.monotonic = false;
|
||||
_glfw.timer.posix.frequency = 1000000;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
////// GLFW platform API //////
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
uint64_t _glfwPlatformGetTimerValue(void)
|
||||
{
|
||||
#if defined(CLOCK_MONOTONIC)
|
||||
if (_glfw.timer.posix.monotonic)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t) ts.tv_sec * (uint64_t) 1000000000 + (uint64_t) ts.tv_nsec;
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
struct timeval tv;
|
||||
gettimeofday(&tv, NULL);
|
||||
return (uint64_t) tv.tv_sec * (uint64_t) 1000000 + (uint64_t) tv.tv_usec;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t _glfwPlatformGetTimerFrequency(void)
|
||||
{
|
||||
return _glfw.timer.posix.frequency;
|
||||
}
|
||||
|
||||
44
glfw/posix_time.h
vendored
44
glfw/posix_time.h
vendored
@@ -1,44 +0,0 @@
|
||||
//========================================================================
|
||||
// GLFW 3.4 POSIX - www.glfw.org
|
||||
//------------------------------------------------------------------------
|
||||
// Copyright (c) 2002-2006 Marcus Geelnard
|
||||
// Copyright (c) 2006-2017 Camilla Löwy <elmindreda@glfw.org>
|
||||
//
|
||||
// This software is provided 'as-is', without any express or implied
|
||||
// warranty. In no event will the authors be held liable for any damages
|
||||
// arising from the use of this software.
|
||||
//
|
||||
// Permission is granted to anyone to use this software for any purpose,
|
||||
// including commercial applications, and to alter it and redistribute it
|
||||
// freely, subject to the following restrictions:
|
||||
//
|
||||
// 1. The origin of this software must not be misrepresented; you must not
|
||||
// claim that you wrote the original software. If you use this software
|
||||
// in a product, an acknowledgment in the product documentation would
|
||||
// be appreciated but is not required.
|
||||
//
|
||||
// 2. Altered source versions must be plainly marked as such, and must not
|
||||
// be misrepresented as being the original software.
|
||||
//
|
||||
// 3. This notice may not be removed or altered from any source
|
||||
// distribution.
|
||||
//
|
||||
//========================================================================
|
||||
|
||||
#define _GLFW_PLATFORM_LIBRARY_TIMER_STATE _GLFWtimerPOSIX posix
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
|
||||
// POSIX-specific global timer data
|
||||
//
|
||||
typedef struct _GLFWtimerPOSIX
|
||||
{
|
||||
bool monotonic;
|
||||
uint64_t frequency;
|
||||
|
||||
} _GLFWtimerPOSIX;
|
||||
|
||||
|
||||
void _glfwInitTimerPOSIX(void);
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"cocoa_joystick.m",
|
||||
"cocoa_monitor.m",
|
||||
"cocoa_window.m",
|
||||
"cocoa_time.c",
|
||||
"posix_thread.c",
|
||||
"nsgl_context.m",
|
||||
"egl_context.c",
|
||||
@@ -31,6 +30,7 @@
|
||||
"input.c",
|
||||
"monitor.c",
|
||||
"vulkan.c",
|
||||
"monotonic.c",
|
||||
"window.c"
|
||||
]
|
||||
},
|
||||
@@ -38,7 +38,6 @@
|
||||
"headers": [
|
||||
"null_platform.h",
|
||||
"null_joystick.h",
|
||||
"posix_time.h",
|
||||
"posix_thread.h",
|
||||
"osmesa_context.h"
|
||||
],
|
||||
@@ -47,7 +46,6 @@
|
||||
"null_monitor.c",
|
||||
"null_window.c",
|
||||
"null_joystick.c",
|
||||
"posix_time.c",
|
||||
"posix_thread.c",
|
||||
"osmesa_context.c"
|
||||
]
|
||||
@@ -55,7 +53,6 @@
|
||||
"wayland": {
|
||||
"headers": [
|
||||
"wl_platform.h",
|
||||
"posix_time.h",
|
||||
"posix_thread.h",
|
||||
"xkb_glfw.h",
|
||||
"dbus_glfw.h",
|
||||
@@ -81,7 +78,6 @@
|
||||
"wl_init.c",
|
||||
"wl_monitor.c",
|
||||
"wl_window.c",
|
||||
"posix_time.c",
|
||||
"posix_thread.c",
|
||||
"xkb_glfw.c",
|
||||
"dbus_glfw.c",
|
||||
@@ -125,7 +121,6 @@
|
||||
"dbus_glfw.h",
|
||||
"ibus_glfw.h",
|
||||
"backend_utils.h",
|
||||
"posix_time.h",
|
||||
"posix_thread.h",
|
||||
"glx_context.h",
|
||||
"egl_context.h",
|
||||
@@ -142,7 +137,6 @@
|
||||
"xkb_glfw.c",
|
||||
"dbus_glfw.c",
|
||||
"ibus_glfw.c",
|
||||
"posix_time.c",
|
||||
"posix_thread.c",
|
||||
"glx_context.c",
|
||||
"egl_context.c",
|
||||
|
||||
4
glfw/vulkan.c
vendored
4
glfw/vulkan.c
vendored
@@ -57,6 +57,8 @@ bool _glfwInitVulkan(int mode)
|
||||
_glfw.vk.handle = _glfw_dlopen("vulkan-1.dll");
|
||||
#elif defined(_GLFW_COCOA)
|
||||
_glfw.vk.handle = _glfw_dlopen("libvulkan.1.dylib");
|
||||
if (!_glfw.vk.handle)
|
||||
_glfw.vk.handle = _glfwLoadLocalVulkanLoaderNS();
|
||||
#else
|
||||
_glfw.vk.handle = _glfw_dlopen("libvulkan.so.1");
|
||||
#endif
|
||||
@@ -130,6 +132,8 @@ bool _glfwInitVulkan(int mode)
|
||||
#elif defined(_GLFW_COCOA)
|
||||
else if (strcmp(ep[i].extensionName, "VK_MVK_macos_surface") == 0)
|
||||
_glfw.vk.MVK_macos_surface = true;
|
||||
else if (strcmp(ep[i].extensionName, "VK_EXT_metal_surface") == 0)
|
||||
_glfw.vk.EXT_metal_surface = true;
|
||||
#elif defined(_GLFW_X11)
|
||||
else if (strcmp(ep[i].extensionName, "VK_KHR_xlib_surface") == 0)
|
||||
_glfw.vk.KHR_xlib_surface = true;
|
||||
|
||||
27
glfw/window.c
vendored
27
glfw/window.c
vendored
@@ -29,6 +29,7 @@
|
||||
//========================================================================
|
||||
|
||||
#include "internal.h"
|
||||
#include "../kitty/monotonic.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
@@ -56,8 +57,10 @@ void _glfwInputWindowFocus(_GLFWwindow* window, bool focused)
|
||||
{
|
||||
if (window->keys[key] == GLFW_PRESS)
|
||||
{
|
||||
const int scancode = _glfwPlatformGetKeyScancode(key);
|
||||
_glfwInputKeyboard(window, key, scancode, GLFW_RELEASE, 0, "", 0);
|
||||
const int native_key = _glfwPlatformGetNativeKeyForKey(key);
|
||||
GLFWkeyevent ev;
|
||||
_glfwInitializeKeyEvent(&ev, key, native_key, GLFW_RELEASE, 0);
|
||||
_glfwInputKeyboard(window, &ev);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +254,8 @@ GLFWAPI GLFWwindow* glfwCreateWindow(int width, int height,
|
||||
window->maxheight = GLFW_DONT_CARE;
|
||||
window->numer = GLFW_DONT_CARE;
|
||||
window->denom = GLFW_DONT_CARE;
|
||||
window->widthincr = GLFW_DONT_CARE;
|
||||
window->heightincr = GLFW_DONT_CARE;
|
||||
|
||||
// Open the actual window and create its context
|
||||
if (!_glfwPlatformCreateWindow(window, &wndconfig, &ctxconfig, &fbconfig))
|
||||
@@ -687,6 +692,20 @@ GLFWAPI void glfwSetWindowAspectRatio(GLFWwindow* handle, int numer, int denom)
|
||||
_glfwPlatformSetWindowAspectRatio(window, numer, denom);
|
||||
}
|
||||
|
||||
GLFWAPI void glfwSetWindowSizeIncrements(GLFWwindow* handle, int widthincr, int heightincr)
|
||||
{
|
||||
_GLFWwindow* window = (_GLFWwindow*) handle;
|
||||
assert(window != NULL);
|
||||
assert(widthincr >= 0);
|
||||
assert(heightincr >= 0);
|
||||
|
||||
_GLFW_REQUIRE_INIT();
|
||||
|
||||
window->widthincr = widthincr;
|
||||
window->heightincr = heightincr;
|
||||
_glfwPlatformSetWindowSizeIncrements(window, window->widthincr, window->heightincr);
|
||||
}
|
||||
|
||||
GLFWAPI void glfwGetFramebufferSize(GLFWwindow* handle, int* width, int* height)
|
||||
{
|
||||
_GLFWwindow* window = (_GLFWwindow*) handle;
|
||||
@@ -736,12 +755,12 @@ GLFWAPI void glfwGetWindowContentScale(GLFWwindow* handle,
|
||||
_glfwPlatformGetWindowContentScale(window, xscale, yscale);
|
||||
}
|
||||
|
||||
GLFWAPI double glfwGetDoubleClickInterval(GLFWwindow* handle)
|
||||
GLFWAPI monotonic_t glfwGetDoubleClickInterval(GLFWwindow* handle)
|
||||
{
|
||||
_GLFWwindow* window = (_GLFWwindow*) handle;
|
||||
assert(window != NULL);
|
||||
|
||||
_GLFW_REQUIRE_INIT_OR_RETURN(0.5f);
|
||||
_GLFW_REQUIRE_INIT_OR_RETURN(ms_to_monotonic_t(500ll));
|
||||
return _glfwPlatformGetDoubleClickInterval(window);
|
||||
}
|
||||
|
||||
|
||||
62
glfw/wl_init.c
vendored
62
glfw/wl_init.c
vendored
@@ -29,6 +29,7 @@
|
||||
#define _GNU_SOURCE
|
||||
#include "internal.h"
|
||||
#include "backend_utils.h"
|
||||
#include "../kitty/monotonic.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
@@ -136,6 +137,7 @@ static void pointerHandleLeave(void* data UNUSED,
|
||||
_glfw.wl.pointerSerial = serial;
|
||||
_glfw.wl.pointerFocus = NULL;
|
||||
_glfwInputCursorEnter(window, false);
|
||||
_glfw.wl.cursorPreviousShape = GLFW_INVALID_CURSOR;
|
||||
}
|
||||
|
||||
static void setCursor(GLFWCursorShape shape)
|
||||
@@ -164,6 +166,7 @@ static void setCursor(GLFWCursorShape shape)
|
||||
wl_surface_damage(surface, 0, 0,
|
||||
image->width, image->height);
|
||||
wl_surface_commit(surface);
|
||||
_glfw.wl.cursorPreviousShape = shape;
|
||||
}
|
||||
|
||||
static void pointerHandleMotion(void* data UNUSED,
|
||||
@@ -174,43 +177,46 @@ static void pointerHandleMotion(void* data UNUSED,
|
||||
{
|
||||
_GLFWwindow* window = _glfw.wl.pointerFocus;
|
||||
GLFWCursorShape cursorShape = GLFW_ARROW_CURSOR;
|
||||
double x, y;
|
||||
|
||||
if (!window)
|
||||
return;
|
||||
|
||||
if (window->cursorMode == GLFW_CURSOR_DISABLED)
|
||||
return;
|
||||
window->wl.cursorPosX = wl_fixed_to_double(sx);
|
||||
window->wl.cursorPosY = wl_fixed_to_double(sy);
|
||||
x = wl_fixed_to_double(sx);
|
||||
y = wl_fixed_to_double(sy);
|
||||
|
||||
switch (window->wl.decorations.focus)
|
||||
{
|
||||
case mainWindow:
|
||||
_glfwInputCursorPos(window,
|
||||
window->wl.cursorPosX, window->wl.cursorPosY);
|
||||
window->wl.cursorPosX = x;
|
||||
window->wl.cursorPosY = y;
|
||||
_glfwInputCursorPos(window, x, y);
|
||||
_glfw.wl.cursorPreviousShape = GLFW_INVALID_CURSOR;
|
||||
return;
|
||||
case topDecoration:
|
||||
if (window->wl.cursorPosY < _GLFW_DECORATION_WIDTH)
|
||||
if (y < _GLFW_DECORATION_WIDTH)
|
||||
cursorShape = GLFW_VRESIZE_CURSOR;
|
||||
else
|
||||
cursorShape = GLFW_ARROW_CURSOR;
|
||||
break;
|
||||
case leftDecoration:
|
||||
if (window->wl.cursorPosY < _GLFW_DECORATION_WIDTH)
|
||||
if (y < _GLFW_DECORATION_WIDTH)
|
||||
cursorShape = GLFW_NW_RESIZE_CURSOR;
|
||||
else
|
||||
cursorShape = GLFW_HRESIZE_CURSOR;
|
||||
break;
|
||||
case rightDecoration:
|
||||
if (window->wl.cursorPosY < _GLFW_DECORATION_WIDTH)
|
||||
if (y < _GLFW_DECORATION_WIDTH)
|
||||
cursorShape = GLFW_NE_RESIZE_CURSOR;
|
||||
else
|
||||
cursorShape = GLFW_HRESIZE_CURSOR;
|
||||
break;
|
||||
case bottomDecoration:
|
||||
if (window->wl.cursorPosX < _GLFW_DECORATION_WIDTH)
|
||||
if (x < _GLFW_DECORATION_WIDTH)
|
||||
cursorShape = GLFW_SW_RESIZE_CURSOR;
|
||||
else if (window->wl.cursorPosX > window->wl.width + _GLFW_DECORATION_WIDTH)
|
||||
else if (x > window->wl.width + _GLFW_DECORATION_WIDTH)
|
||||
cursorShape = GLFW_SE_RESIZE_CURSOR;
|
||||
else
|
||||
cursorShape = GLFW_VRESIZE_CURSOR;
|
||||
@@ -218,7 +224,8 @@ static void pointerHandleMotion(void* data UNUSED,
|
||||
default:
|
||||
assert(0);
|
||||
}
|
||||
setCursor(cursorShape);
|
||||
if (_glfw.wl.cursorPreviousShape != cursorShape)
|
||||
setCursor(cursorShape);
|
||||
}
|
||||
|
||||
static void pointerHandleButton(void* data UNUSED,
|
||||
@@ -284,8 +291,8 @@ static void pointerHandleButton(void* data UNUSED,
|
||||
{
|
||||
xdg_toplevel_show_window_menu(window->wl.xdg.toplevel,
|
||||
_glfw.wl.seat, serial,
|
||||
window->wl.cursorPosX,
|
||||
window->wl.cursorPosY);
|
||||
(int32_t)window->wl.cursorPosX,
|
||||
(int32_t)window->wl.cursorPosY);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -327,7 +334,7 @@ static void pointerHandleAxis(void* data UNUSED,
|
||||
else if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL)
|
||||
y = wl_fixed_to_double(value) * -1;
|
||||
|
||||
_glfwInputScroll(window, x, y, 1);
|
||||
_glfwInputScroll(window, x, y, 1, _glfw.wl.xkb.states.modifiers);
|
||||
}
|
||||
|
||||
static const struct wl_pointer_listener pointerListener = {
|
||||
@@ -367,7 +374,7 @@ static void keyboardHandleEnter(void* data UNUSED,
|
||||
struct wl_keyboard* keyboard UNUSED,
|
||||
uint32_t serial UNUSED,
|
||||
struct wl_surface* surface,
|
||||
struct wl_array* keys UNUSED)
|
||||
struct wl_array* keys)
|
||||
{
|
||||
// Happens in the case we just destroyed the surface.
|
||||
if (!surface)
|
||||
@@ -383,6 +390,15 @@ static void keyboardHandleEnter(void* data UNUSED,
|
||||
|
||||
_glfw.wl.keyboardFocus = window;
|
||||
_glfwInputWindowFocus(window, true);
|
||||
uint32_t* key;
|
||||
if (keys && _glfw.wl.keyRepeatInfo.key) {
|
||||
wl_array_for_each(key, keys) {
|
||||
if (*key == _glfw.wl.keyRepeatInfo.key) {
|
||||
toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void keyboardHandleLeave(void* data UNUSED,
|
||||
@@ -397,13 +413,14 @@ static void keyboardHandleLeave(void* data UNUSED,
|
||||
|
||||
_glfw.wl.keyboardFocus = NULL;
|
||||
_glfwInputWindowFocus(window, false);
|
||||
toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, 0);
|
||||
}
|
||||
|
||||
static void
|
||||
dispatchPendingKeyRepeats(id_type timer_id UNUSED, void *data UNUSED) {
|
||||
if (_glfw.wl.keyRepeatInfo.keyboardFocus != _glfw.wl.keyboardFocus || _glfw.wl.keyboardRepeatRate == 0) return;
|
||||
glfw_xkb_handle_key_event(_glfw.wl.keyRepeatInfo.keyboardFocus, &_glfw.wl.xkb, _glfw.wl.keyRepeatInfo.key, GLFW_REPEAT);
|
||||
changeTimerInterval(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, (1.0 / (double)_glfw.wl.keyboardRepeatRate));
|
||||
changeTimerInterval(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, (s_to_monotonic_t(1ll) / (monotonic_t)_glfw.wl.keyboardRepeatRate));
|
||||
toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, 1);
|
||||
}
|
||||
|
||||
@@ -421,6 +438,7 @@ static void keyboardHandleKey(void* data UNUSED,
|
||||
int action = state == WL_KEYBOARD_KEY_STATE_PRESSED ? GLFW_PRESS : GLFW_RELEASE;
|
||||
glfw_xkb_handle_key_event(window, &_glfw.wl.xkb, key, action);
|
||||
bool repeatable = false;
|
||||
_glfw.wl.keyRepeatInfo.key = 0;
|
||||
|
||||
if (action == GLFW_PRESS && _glfw.wl.keyboardRepeatRate > 0 && glfw_xkb_should_repeat(&_glfw.wl.xkb, key))
|
||||
{
|
||||
@@ -429,7 +447,7 @@ static void keyboardHandleKey(void* data UNUSED,
|
||||
_glfw.wl.keyRepeatInfo.keyboardFocus = window;
|
||||
}
|
||||
if (repeatable) {
|
||||
changeTimerInterval(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, (double)(_glfw.wl.keyboardRepeatDelay) / 1000.0);
|
||||
changeTimerInterval(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, _glfw.wl.keyboardRepeatDelay);
|
||||
}
|
||||
toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, repeatable ? 1 : 0);
|
||||
}
|
||||
@@ -454,7 +472,7 @@ static void keyboardHandleRepeatInfo(void* data UNUSED,
|
||||
return;
|
||||
|
||||
_glfw.wl.keyboardRepeatRate = rate;
|
||||
_glfw.wl.keyboardRepeatDelay = delay;
|
||||
_glfw.wl.keyboardRepeatDelay = ms_to_monotonic_t(delay);
|
||||
}
|
||||
|
||||
static const struct wl_keyboard_listener keyboardListener = {
|
||||
@@ -607,8 +625,7 @@ static void registryHandleGlobal(void* data UNUSED,
|
||||
_glfwSetupWaylandDataDevice();
|
||||
}
|
||||
}
|
||||
else if (strcmp(interface, "zwp_primary_selection_device_manager_v1") == 0 ||
|
||||
strcmp(interface, "gtk_primary_selection_device_manager") == 0)
|
||||
else if (strcmp(interface, "zwp_primary_selection_device_manager_v1") == 0)
|
||||
{
|
||||
_glfw.wl.primarySelectionDeviceManager =
|
||||
wl_registry_bind(registry, name,
|
||||
@@ -728,8 +745,8 @@ int _glfwPlatformInit(void)
|
||||
"Wayland: Failed to initialize event loop data");
|
||||
}
|
||||
glfw_dbus_init(&_glfw.wl.dbus, &_glfw.wl.eventLoopData);
|
||||
_glfw.wl.keyRepeatInfo.keyRepeatTimer = addTimer(&_glfw.wl.eventLoopData, "wayland-key-repeat", 0.5, 0, true, dispatchPendingKeyRepeats, NULL, NULL);
|
||||
_glfw.wl.cursorAnimationTimer = addTimer(&_glfw.wl.eventLoopData, "wayland-cursor-animation", 0.5, 0, true, animateCursorImage, NULL, NULL);
|
||||
_glfw.wl.keyRepeatInfo.keyRepeatTimer = addTimer(&_glfw.wl.eventLoopData, "wayland-key-repeat", ms_to_monotonic_t(500ll), 0, true, dispatchPendingKeyRepeats, NULL, NULL);
|
||||
_glfw.wl.cursorAnimationTimer = addTimer(&_glfw.wl.eventLoopData, "wayland-cursor-animation", ms_to_monotonic_t(500ll), 0, true, animateCursorImage, NULL, NULL);
|
||||
|
||||
_glfw.wl.registry = wl_display_get_registry(_glfw.wl.display);
|
||||
wl_registry_add_listener(_glfw.wl.registry, ®istryListener, NULL);
|
||||
@@ -749,7 +766,6 @@ int _glfwPlatformInit(void)
|
||||
}
|
||||
#endif
|
||||
|
||||
_glfwInitTimerPOSIX();
|
||||
if (!_glfw.wl.wmBase)
|
||||
{
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||
@@ -836,7 +852,7 @@ void _glfwPlatformTerminate(void)
|
||||
wl_data_source_destroy(_glfw.wl.dataSourceForClipboard);
|
||||
for (size_t doi=0; doi < arraysz(_glfw.wl.dataOffers); doi++) {
|
||||
if (_glfw.wl.dataOffers[doi].id) {
|
||||
wl_data_offer_destroy(_glfw.wl.dataOffers[doi].id);
|
||||
destroy_data_offer(&_glfw.wl.dataOffers[doi]);
|
||||
}
|
||||
}
|
||||
if (_glfw.wl.dataDevice)
|
||||
|
||||
29
glfw/wl_platform.h
vendored
29
glfw/wl_platform.h
vendored
@@ -43,7 +43,6 @@ typedef VkResult (APIENTRY *PFN_vkCreateWaylandSurfaceKHR)(VkInstance,const VkWa
|
||||
typedef VkBool32 (APIENTRY *PFN_vkGetPhysicalDeviceWaylandPresentationSupportKHR)(VkPhysicalDevice,uint32_t,struct wl_display*);
|
||||
|
||||
#include "posix_thread.h"
|
||||
#include "posix_time.h"
|
||||
#ifdef __linux__
|
||||
#include "linux_joystick.h"
|
||||
#else
|
||||
@@ -156,6 +155,7 @@ typedef struct _GLFWwindowWayland
|
||||
// We need to track the monitors the window spans on to calculate the
|
||||
// optimal scaling factor.
|
||||
int scale;
|
||||
bool initial_scale_notified;
|
||||
_GLFWmonitor** monitors;
|
||||
int monitorsCount;
|
||||
int monitorsSize;
|
||||
@@ -195,28 +195,19 @@ typedef enum _GLFWWaylandOfferType
|
||||
|
||||
typedef struct _GLFWWaylandDataOffer
|
||||
{
|
||||
struct wl_data_offer *id;
|
||||
const char *mime;
|
||||
void *id;
|
||||
_GLFWWaylandOfferType offer_type;
|
||||
size_t idx;
|
||||
int is_self_offer;
|
||||
int has_uri_list;
|
||||
bool is_self_offer;
|
||||
bool is_primary;
|
||||
const char *plain_text_mime, *mime_for_drop;
|
||||
uint32_t source_actions;
|
||||
uint32_t dnd_action;
|
||||
struct wl_surface *surface;
|
||||
const char **mimes;
|
||||
size_t mimes_capacity, mimes_count;
|
||||
} _GLFWWaylandDataOffer;
|
||||
|
||||
typedef struct _GLFWWaylandPrimaryOffer
|
||||
{
|
||||
struct zwp_primary_selection_offer_v1 *id;
|
||||
const char *mime;
|
||||
_GLFWWaylandOfferType offer_type;
|
||||
size_t idx;
|
||||
int is_self_offer;
|
||||
int has_uri_list;
|
||||
struct wl_surface *surface;
|
||||
} _GLFWWaylandPrimaryOffer;
|
||||
|
||||
// Wayland-specific global data
|
||||
//
|
||||
typedef struct _GLFWlibraryWayland
|
||||
@@ -247,10 +238,11 @@ typedef struct _GLFWlibraryWayland
|
||||
|
||||
struct wl_cursor_theme* cursorTheme;
|
||||
struct wl_surface* cursorSurface;
|
||||
GLFWCursorShape cursorPreviousShape;
|
||||
uint32_t pointerSerial;
|
||||
|
||||
int32_t keyboardRepeatRate;
|
||||
int32_t keyboardRepeatDelay;
|
||||
monotonic_t keyboardRepeatDelay;
|
||||
struct {
|
||||
uint32_t key;
|
||||
id_type keyRepeatTimer;
|
||||
@@ -286,8 +278,6 @@ typedef struct _GLFWlibraryWayland
|
||||
size_t dataOffersCounter;
|
||||
_GLFWWaylandDataOffer dataOffers[8];
|
||||
char* primarySelectionString;
|
||||
size_t primarySelectionOffersCounter;
|
||||
_GLFWWaylandPrimaryOffer primarySelectionOffers[8];
|
||||
} _GLFWlibraryWayland;
|
||||
|
||||
// Wayland-specific per-monitor data
|
||||
@@ -321,3 +311,4 @@ void _glfwSetupWaylandDataDevice(void);
|
||||
void _glfwSetupWaylandPrimarySelectionDevice(void);
|
||||
void animateCursorImage(id_type timer_id, void *data);
|
||||
struct wl_cursor* _glfwLoadCursor(GLFWCursorShape);
|
||||
void destroy_data_offer(_GLFWWaylandDataOffer*);
|
||||
|
||||
296
glfw/wl_window.c
vendored
296
glfw/wl_window.c
vendored
@@ -32,6 +32,7 @@
|
||||
#include "backend_utils.h"
|
||||
#include "memfd.h"
|
||||
#include "linux_notify.h"
|
||||
#include "../kitty/monotonic.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -41,7 +42,7 @@
|
||||
#include <sys/mman.h>
|
||||
|
||||
|
||||
#define URI_LIST_MIME "text/uri-list"
|
||||
|
||||
|
||||
|
||||
static bool checkScaleChange(_GLFWwindow* window)
|
||||
@@ -54,13 +55,18 @@ static bool checkScaleChange(_GLFWwindow* window)
|
||||
if (_glfw.wl.compositorVersion < 3)
|
||||
return false;
|
||||
|
||||
// Get the scale factor from the highest scale monitor.
|
||||
// Get the scale factor from the highest scale monitor that this window is on
|
||||
for (i = 0; i < window->wl.monitorsCount; ++i)
|
||||
{
|
||||
monitorScale = window->wl.monitors[i]->wl.scale;
|
||||
if (scale < monitorScale)
|
||||
scale = monitorScale;
|
||||
}
|
||||
if (window->wl.monitorsCount < 1 && _glfw.monitorCount > 0) {
|
||||
// The window has not yet been assigned to any monitors, use the primary monitor
|
||||
_GLFWmonitor *m = _glfw.monitors[0];
|
||||
if (m && m->wl.scale > scale) scale = m->wl.scale;
|
||||
}
|
||||
|
||||
// Only change the framebuffer size if the scale changed.
|
||||
if (scale != window->wl.scale)
|
||||
@@ -69,6 +75,10 @@ static bool checkScaleChange(_GLFWwindow* window)
|
||||
wl_surface_set_buffer_scale(window->wl.surface, scale);
|
||||
return true;
|
||||
}
|
||||
if (window->wl.monitorsCount > 0 && !window->wl.initial_scale_notified) {
|
||||
window->wl.initial_scale_notified = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -241,8 +251,8 @@ static struct wl_buffer* createShmBuffer(const GLFWimage* image)
|
||||
if (fd < 0)
|
||||
{
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||
"Wayland: Creating a buffer file for %d B failed: %%m",
|
||||
length);
|
||||
"Wayland: Creating a buffer file for %d B failed: %s",
|
||||
length, strerror(errno));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -250,7 +260,7 @@ static struct wl_buffer* createShmBuffer(const GLFWimage* image)
|
||||
if (data == MAP_FAILED)
|
||||
{
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||
"Wayland: mmap failed: %%m");
|
||||
"Wayland: mmap failed: %s", strerror(errno));
|
||||
close(fd);
|
||||
return NULL;
|
||||
}
|
||||
@@ -545,9 +555,9 @@ static void xdgToplevelHandleConfigure(void* data,
|
||||
aspectRatio = (float)width / (float)height;
|
||||
targetRatio = (float)window->numer / (float)window->denom;
|
||||
if (aspectRatio < targetRatio)
|
||||
height = width / targetRatio;
|
||||
height = (int32_t)((float)width / targetRatio);
|
||||
else if (aspectRatio > targetRatio)
|
||||
width = height * targetRatio;
|
||||
width = (int32_t)((float)height * targetRatio);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -684,7 +694,7 @@ setCursorImage(_GLFWcursorWayland* cursorWayland)
|
||||
image = cursorWayland->cursor->images[cursorWayland->currentImage];
|
||||
buffer = wl_cursor_image_get_buffer(image);
|
||||
if (image->delay) {
|
||||
changeTimerInterval(&_glfw.wl.eventLoopData, _glfw.wl.cursorAnimationTimer, ((double)image->delay) / 1000.0);
|
||||
changeTimerInterval(&_glfw.wl.eventLoopData, _glfw.wl.cursorAnimationTimer, ms_to_monotonic_t(image->delay));
|
||||
toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.cursorAnimationTimer, 1);
|
||||
} else {
|
||||
toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.cursorAnimationTimer, 0);
|
||||
@@ -745,24 +755,23 @@ abortOnFatalError(int last_error) {
|
||||
}
|
||||
|
||||
static void
|
||||
handleEvents(double timeout)
|
||||
wayland_read_events(int poll_result, int events, void *data UNUSED) {
|
||||
EVDBG("wayland_read_events poll_result: %d events: %d", poll_result, events);
|
||||
if (poll_result > 0 && events) wl_display_read_events(_glfw.wl.display);
|
||||
else wl_display_cancel_read(_glfw.wl.display);
|
||||
}
|
||||
|
||||
static void
|
||||
handleEvents(monotonic_t timeout)
|
||||
{
|
||||
struct wl_display* display = _glfw.wl.display;
|
||||
errno = 0;
|
||||
EVDBG("starting handleEvents(%.2f)", timeout);
|
||||
EVDBG("starting handleEvents(%.2f)", monotonic_t_to_s_double(timeout));
|
||||
|
||||
while (wl_display_prepare_read(display) != 0) {
|
||||
while(1) {
|
||||
errno = 0;
|
||||
int num_dispatched = wl_display_dispatch_pending(display);
|
||||
if (num_dispatched < 0) {
|
||||
if (errno == EAGAIN) continue;
|
||||
int last_error = wl_display_get_error(display);
|
||||
if (last_error) abortOnFatalError(last_error);
|
||||
wl_display_cancel_read(display);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
if (wl_display_dispatch_pending(display) == -1) {
|
||||
abortOnFatalError(errno);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -772,23 +781,22 @@ handleEvents(double timeout)
|
||||
errno = 0;
|
||||
if (wl_display_flush(display) < 0 && errno != EAGAIN)
|
||||
{
|
||||
abortOnFatalError(errno);
|
||||
wl_display_cancel_read(display);
|
||||
abortOnFatalError(errno);
|
||||
return;
|
||||
}
|
||||
|
||||
bool display_read_ok = pollForEvents(&_glfw.wl.eventLoopData, timeout);
|
||||
// we pass in wayland_read_events to ensure that the above wl_display_prepare_read call
|
||||
// is followed by either wl_display_cancel_read or wl_display_read_events
|
||||
// before any events/timers are dispatched. This allows other wayland functions
|
||||
// to be called in the event/timer handlers without causing a deadlock
|
||||
bool display_read_ok = pollForEvents(&_glfw.wl.eventLoopData, timeout, wayland_read_events);
|
||||
EVDBG("display_read_ok: %d", display_read_ok);
|
||||
if (display_read_ok) {
|
||||
wl_display_read_events(display);
|
||||
int num = wl_display_dispatch_pending(display);
|
||||
(void)num;
|
||||
EVDBG("dispatched %d Wayland events", num);
|
||||
}
|
||||
else
|
||||
{
|
||||
wl_display_cancel_read(display);
|
||||
}
|
||||
glfw_ibus_dispatch(&_glfw.wl.xkb.ibus);
|
||||
glfw_dbus_session_bus_dispatch();
|
||||
EVDBG("other dispatch done");
|
||||
@@ -950,8 +958,13 @@ void _glfwPlatformSetWindowTitle(_GLFWwindow* window, const char* title)
|
||||
if (window->wl.title)
|
||||
free(window->wl.title);
|
||||
window->wl.title = _glfw_strdup(title);
|
||||
// Wayland cannot handle requests larger than ~8200 bytes. Sending
|
||||
// one causes an abort(). Since titles this large are meaningless anyway
|
||||
// ensure they do not happen. One should really truncate ensuring valid UTF-8
|
||||
// but I cant be bothered.
|
||||
if (title && strnlen(title, 2048) >= 2048) window->wl.title[2048] = 0;
|
||||
if (window->wl.xdg.toplevel)
|
||||
xdg_toplevel_set_title(window->wl.xdg.toplevel, title);
|
||||
xdg_toplevel_set_title(window->wl.xdg.toplevel, window->wl.title);
|
||||
}
|
||||
|
||||
void _glfwPlatformSetWindowIcon(_GLFWwindow* window UNUSED,
|
||||
@@ -1018,6 +1031,13 @@ void _glfwPlatformSetWindowAspectRatio(_GLFWwindow* window UNUSED,
|
||||
// The actual limits are checked in the xdg_toplevel::configure handler.
|
||||
}
|
||||
|
||||
void _glfwPlatformSetWindowSizeIncrements(_GLFWwindow* window UNUSED,
|
||||
int widthincr UNUSED, int heightincr UNUSED)
|
||||
{
|
||||
// TODO: find out how to trigger a resize.
|
||||
// The actual limits are checked in the xdg_toplevel::configure handler.
|
||||
}
|
||||
|
||||
void _glfwPlatformGetFramebufferSize(_GLFWwindow* window,
|
||||
int* width, int* height)
|
||||
{
|
||||
@@ -1052,9 +1072,9 @@ void _glfwPlatformGetWindowContentScale(_GLFWwindow* window,
|
||||
*yscale = (float) window->wl.scale;
|
||||
}
|
||||
|
||||
double _glfwPlatformGetDoubleClickInterval(_GLFWwindow* window UNUSED)
|
||||
monotonic_t _glfwPlatformGetDoubleClickInterval(_GLFWwindow* window UNUSED)
|
||||
{
|
||||
return 0.5;
|
||||
return ms_to_monotonic_t(500ll);
|
||||
}
|
||||
|
||||
void _glfwPlatformIconifyWindow(_GLFWwindow* window)
|
||||
@@ -1227,11 +1247,11 @@ void _glfwPlatformPollEvents(void)
|
||||
|
||||
void _glfwPlatformWaitEvents(void)
|
||||
{
|
||||
double timeout = wl_display_dispatch_pending(_glfw.wl.display) > 0 ? 0 : -1;
|
||||
monotonic_t timeout = wl_display_dispatch_pending(_glfw.wl.display) > 0 ? 0 : -1;
|
||||
handleEvents(timeout);
|
||||
}
|
||||
|
||||
void _glfwPlatformWaitEventsTimeout(double timeout)
|
||||
void _glfwPlatformWaitEventsTimeout(monotonic_t timeout)
|
||||
{
|
||||
if (wl_display_dispatch_pending(_glfw.wl.display) > 0) timeout = 0;
|
||||
handleEvents(timeout);
|
||||
@@ -1268,12 +1288,12 @@ void _glfwPlatformSetCursorMode(_GLFWwindow* window, int mode UNUSED)
|
||||
_glfwPlatformSetCursor(window, window->wl.currentCursor);
|
||||
}
|
||||
|
||||
const char* _glfwPlatformGetScancodeName(int scancode)
|
||||
const char* _glfwPlatformGetNativeKeyName(int native_key)
|
||||
{
|
||||
return glfw_xkb_keysym_name(scancode);
|
||||
return glfw_xkb_keysym_name(native_key);
|
||||
}
|
||||
|
||||
int _glfwPlatformGetKeyScancode(int key)
|
||||
int _glfwPlatformGetNativeKeyForKey(int key)
|
||||
{
|
||||
return glfw_xkb_sym_for_key(key);
|
||||
}
|
||||
@@ -1462,8 +1482,8 @@ static void send_text(char *text, int fd)
|
||||
{
|
||||
if (text) {
|
||||
size_t len = strlen(text), pos = 0;
|
||||
double start = glfwGetTime();
|
||||
while (pos < len && glfwGetTime() - start < 2.0) {
|
||||
monotonic_t start = glfwGetTime();
|
||||
while (pos < len && glfwGetTime() - start < s_to_monotonic_t(2ll)) {
|
||||
ssize_t ret = write(fd, text + pos, len - pos);
|
||||
if (ret < 0) {
|
||||
if (errno == EAGAIN || errno == EINTR) continue;
|
||||
@@ -1489,14 +1509,15 @@ static void _glfwSendPrimarySelectionText(void *data UNUSED, struct zwp_primary_
|
||||
send_text(_glfw.wl.primarySelectionString, fd);
|
||||
}
|
||||
|
||||
static char* read_offer_string(int data_pipe) {
|
||||
static char* read_offer_string(int data_pipe, size_t *data_sz) {
|
||||
wl_display_flush(_glfw.wl.display);
|
||||
size_t sz = 0, capacity = 0;
|
||||
size_t capacity = 0;
|
||||
char *buf = NULL;
|
||||
*data_sz = 0;
|
||||
struct pollfd fds;
|
||||
fds.fd = data_pipe;
|
||||
fds.events = POLLIN;
|
||||
double start = glfwGetTime();
|
||||
monotonic_t start = glfwGetTime();
|
||||
#define bail(...) { \
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR, __VA_ARGS__); \
|
||||
free(buf); buf = NULL; \
|
||||
@@ -1504,7 +1525,7 @@ static char* read_offer_string(int data_pipe) {
|
||||
return NULL; \
|
||||
}
|
||||
|
||||
while (glfwGetTime() - start < 2) {
|
||||
while (glfwGetTime() - start < s_to_monotonic_t(2ll)) {
|
||||
int ret = poll(&fds, 1, 2000);
|
||||
if (ret == -1) {
|
||||
if (errno == EINTR) continue;
|
||||
@@ -1513,20 +1534,20 @@ static char* read_offer_string(int data_pipe) {
|
||||
if (!ret) {
|
||||
bail("Wayland: Failed to read clipboard data from pipe (timed out)");
|
||||
}
|
||||
if (capacity <= sz || capacity - sz <= 64) {
|
||||
if (capacity <= *data_sz || capacity - *data_sz <= 64) {
|
||||
capacity += 4096;
|
||||
buf = realloc(buf, capacity);
|
||||
if (!buf) {
|
||||
bail("Wayland: Failed to allocate memory to read clipboard data");
|
||||
}
|
||||
}
|
||||
ret = read(data_pipe, buf + sz, capacity - sz - 1);
|
||||
ret = read(data_pipe, buf + *data_sz, capacity - *data_sz - 1);
|
||||
if (ret == -1) {
|
||||
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) continue;
|
||||
bail("Wayland: Failed to read clipboard data from pipe with error: %s", strerror(errno));
|
||||
}
|
||||
if (ret == 0) { close(data_pipe); buf[sz] = 0; return buf; }
|
||||
sz += ret;
|
||||
if (ret == 0) { close(data_pipe); buf[*data_sz] = 0; return buf; }
|
||||
*data_sz += ret;
|
||||
start = glfwGetTime();
|
||||
}
|
||||
bail("Wayland: Failed to read clipboard data from pipe (timed out)");
|
||||
@@ -1539,15 +1560,16 @@ static char* read_primary_selection_offer(struct zwp_primary_selection_offer_v1
|
||||
if (pipe2(pipefd, O_CLOEXEC) != 0) return NULL;
|
||||
zwp_primary_selection_offer_v1_receive(primary_selection_offer, mime, pipefd[1]);
|
||||
close(pipefd[1]);
|
||||
return read_offer_string(pipefd[0]);
|
||||
size_t sz = 0;
|
||||
return read_offer_string(pipefd[0], &sz);
|
||||
}
|
||||
|
||||
static char* read_data_offer(struct wl_data_offer *data_offer, const char *mime) {
|
||||
static char* read_data_offer(struct wl_data_offer *data_offer, const char *mime, size_t *sz) {
|
||||
int pipefd[2];
|
||||
if (pipe2(pipefd, O_CLOEXEC) != 0) return NULL;
|
||||
wl_data_offer_receive(data_offer, mime, pipefd[1]);
|
||||
close(pipefd[1]);
|
||||
return read_offer_string(pipefd[0]);
|
||||
return read_offer_string(pipefd[0], sz);
|
||||
}
|
||||
|
||||
static void data_source_canceled(void *data UNUSED, struct wl_data_source *wl_data_source) {
|
||||
@@ -1576,20 +1598,23 @@ static const struct zwp_primary_selection_source_v1_listener primary_selection_s
|
||||
.cancelled = primary_selection_source_canceled,
|
||||
};
|
||||
|
||||
void
|
||||
destroy_data_offer(_GLFWWaylandDataOffer *offer) {
|
||||
if (offer->id) {
|
||||
if (offer->is_primary) zwp_primary_selection_offer_v1_destroy(offer->id);
|
||||
else wl_data_offer_destroy(offer->id);
|
||||
}
|
||||
if (offer->mimes) {
|
||||
for (size_t i = 0; i < offer->mimes_count; i++) free((char*)offer->mimes[i]);
|
||||
free(offer->mimes);
|
||||
}
|
||||
memset(offer, 0, sizeof(_GLFWWaylandDataOffer));
|
||||
}
|
||||
|
||||
static void prune_unclaimed_data_offers(void) {
|
||||
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
|
||||
if (_glfw.wl.dataOffers[i].id && !_glfw.wl.dataOffers[i].offer_type) {
|
||||
wl_data_offer_destroy(_glfw.wl.dataOffers[i].id);
|
||||
memset(_glfw.wl.dataOffers + i, 0, sizeof(_glfw.wl.dataOffers[0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void prune_unclaimed_primary_selection_offers(void) {
|
||||
for (size_t i = 0; i < arraysz(_glfw.wl.primarySelectionOffers); i++) {
|
||||
if (_glfw.wl.primarySelectionOffers[i].id && !_glfw.wl.dataOffers[i].offer_type) {
|
||||
zwp_primary_selection_offer_v1_destroy(_glfw.wl.primarySelectionOffers[i].id);
|
||||
memset(_glfw.wl.primarySelectionOffers + i, 0, sizeof(_glfw.wl.primarySelectionOffers[0]));
|
||||
destroy_data_offer(&_glfw.wl.dataOffers[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1608,25 +1633,32 @@ static void mark_selection_offer(void *data UNUSED, struct wl_data_device *data_
|
||||
|
||||
static void mark_primary_selection_offer(void *data UNUSED, struct zwp_primary_selection_device_v1* primary_selection_device UNUSED,
|
||||
struct zwp_primary_selection_offer_v1 *primary_selection_offer) {
|
||||
for (size_t i = 0; i < arraysz(_glfw.wl.primarySelectionOffers); i++) {
|
||||
if (_glfw.wl.primarySelectionOffers[i].id == primary_selection_offer) {
|
||||
_glfw.wl.primarySelectionOffers[i].offer_type = PRIMARY_SELECTION;
|
||||
} else if (_glfw.wl.primarySelectionOffers[i].offer_type == PRIMARY_SELECTION) {
|
||||
_glfw.wl.primarySelectionOffers[i].offer_type = EXPIRED; // previous selection offer
|
||||
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
|
||||
if (_glfw.wl.dataOffers[i].id == primary_selection_offer) {
|
||||
_glfw.wl.dataOffers[i].offer_type = PRIMARY_SELECTION;
|
||||
} else if (_glfw.wl.dataOffers[i].offer_type == PRIMARY_SELECTION) {
|
||||
_glfw.wl.dataOffers[i].offer_type = EXPIRED; // previous selection offer
|
||||
}
|
||||
}
|
||||
prune_unclaimed_primary_selection_offers();
|
||||
prune_unclaimed_data_offers();
|
||||
}
|
||||
|
||||
static void set_offer_mimetype(struct _GLFWWaylandDataOffer* offer, const char* mime) {
|
||||
if (strcmp(mime, "text/plain;charset=utf-8") == 0)
|
||||
offer->mime = "text/plain;charset=utf-8";
|
||||
else if (!offer->mime && strcmp(mime, "text/plain") == 0)
|
||||
offer->mime = "text/plain";
|
||||
else if (strcmp(mime, clipboard_mime()) == 0)
|
||||
offer->is_self_offer = 1;
|
||||
else if (strcmp(mime, URI_LIST_MIME) == 0)
|
||||
offer->has_uri_list = 1;
|
||||
static void
|
||||
set_offer_mimetype(_GLFWWaylandDataOffer* offer, const char* mime) {
|
||||
if (strcmp(mime, "text/plain;charset=utf-8") == 0) {
|
||||
offer->plain_text_mime = "text/plain;charset=utf-8";
|
||||
} else if (!offer->plain_text_mime && strcmp(mime, "text/plain")) {
|
||||
offer->plain_text_mime = "text/plain";
|
||||
}
|
||||
if (strcmp(mime, clipboard_mime()) == 0) {
|
||||
offer->is_self_offer = true;
|
||||
}
|
||||
if (!offer->mimes || offer->mimes_count >= offer->mimes_capacity - 1) {
|
||||
offer->mimes = realloc(offer->mimes, sizeof(char*) * (offer->mimes_capacity + 64));
|
||||
if (offer->mimes) offer->mimes_capacity += 64;
|
||||
else return;
|
||||
}
|
||||
offer->mimes[offer->mimes_count++] = _glfw_strdup(mime);
|
||||
}
|
||||
|
||||
static void handle_offer_mimetype(void *data UNUSED, struct wl_data_offer* id, const char *mime) {
|
||||
@@ -1639,9 +1671,9 @@ static void handle_offer_mimetype(void *data UNUSED, struct wl_data_offer* id, c
|
||||
}
|
||||
|
||||
static void handle_primary_selection_offer_mimetype(void *data UNUSED, struct zwp_primary_selection_offer_v1* id, const char *mime) {
|
||||
for (size_t i = 0; i < arraysz(_glfw.wl.primarySelectionOffers); i++) {
|
||||
if (_glfw.wl.primarySelectionOffers[i].id == id) {
|
||||
set_offer_mimetype((struct _GLFWWaylandDataOffer*)&_glfw.wl.primarySelectionOffers[i], mime);
|
||||
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
|
||||
if (_glfw.wl.dataOffers[i].id == id) {
|
||||
set_offer_mimetype((_GLFWWaylandDataOffer*)&_glfw.wl.dataOffers[i], mime);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1676,7 +1708,8 @@ static const struct zwp_primary_selection_offer_v1_listener primary_selection_of
|
||||
.offer = handle_primary_selection_offer_mimetype,
|
||||
};
|
||||
|
||||
static void handle_data_offer(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED, struct wl_data_offer *id) {
|
||||
static size_t
|
||||
handle_data_offer_generic(void *id, bool is_primary) {
|
||||
size_t smallest_idx = SIZE_MAX, pos = 0;
|
||||
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
|
||||
if (_glfw.wl.dataOffers[i].idx && _glfw.wl.dataOffers[i].idx < smallest_idx) {
|
||||
@@ -1684,47 +1717,48 @@ static void handle_data_offer(void *data UNUSED, struct wl_data_device *wl_data_
|
||||
pos = i;
|
||||
}
|
||||
if (_glfw.wl.dataOffers[i].id == NULL) {
|
||||
_glfw.wl.dataOffers[i].id = id;
|
||||
_glfw.wl.dataOffers[i].idx = ++_glfw.wl.dataOffersCounter;
|
||||
pos = i;
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
if (_glfw.wl.dataOffers[pos].id) wl_data_offer_destroy(_glfw.wl.dataOffers[pos].id);
|
||||
memset(_glfw.wl.dataOffers + pos, 0, sizeof(_glfw.wl.dataOffers[0]));
|
||||
_glfw.wl.dataOffers[pos].id = id;
|
||||
_glfw.wl.dataOffers[pos].idx = ++_glfw.wl.dataOffersCounter;
|
||||
if (_glfw.wl.dataOffers[pos].id) destroy_data_offer(&_glfw.wl.dataOffers[pos]);
|
||||
end:
|
||||
_glfw.wl.dataOffers[pos].id = id;
|
||||
_glfw.wl.dataOffers[pos].is_primary = is_primary;
|
||||
_glfw.wl.dataOffers[pos].idx = ++_glfw.wl.dataOffersCounter;
|
||||
return pos;
|
||||
}
|
||||
|
||||
static void handle_data_offer(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED, struct wl_data_offer *id) {
|
||||
handle_data_offer_generic(id, false);
|
||||
wl_data_offer_add_listener(id, &data_offer_listener, NULL);
|
||||
}
|
||||
|
||||
static void handle_primary_selection_offer(void *data UNUSED, struct zwp_primary_selection_device_v1 *zwp_primary_selection_device_v1 UNUSED, struct zwp_primary_selection_offer_v1 *id) {
|
||||
size_t smallest_idx = SIZE_MAX, pos = 0;
|
||||
for (size_t i = 0; i < arraysz(_glfw.wl.primarySelectionOffers); i++) {
|
||||
if (_glfw.wl.primarySelectionOffers[i].idx && _glfw.wl.primarySelectionOffers[i].idx < smallest_idx) {
|
||||
smallest_idx = _glfw.wl.primarySelectionOffers[i].idx;
|
||||
pos = i;
|
||||
}
|
||||
if (_glfw.wl.primarySelectionOffers[i].id == NULL) {
|
||||
_glfw.wl.primarySelectionOffers[i].id = id;
|
||||
_glfw.wl.primarySelectionOffers[i].idx = ++_glfw.wl.primarySelectionOffersCounter;
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
if (_glfw.wl.primarySelectionOffers[pos].id) zwp_primary_selection_offer_v1_destroy(_glfw.wl.primarySelectionOffers[pos].id);
|
||||
memset(_glfw.wl.primarySelectionOffers + pos, 0, sizeof(_glfw.wl.primarySelectionOffers[0]));
|
||||
_glfw.wl.primarySelectionOffers[pos].id = id;
|
||||
_glfw.wl.primarySelectionOffers[pos].idx = ++_glfw.wl.primarySelectionOffersCounter;
|
||||
end:
|
||||
handle_data_offer_generic(id, true);
|
||||
zwp_primary_selection_offer_v1_add_listener(id, &primary_selection_offer_listener, NULL);
|
||||
}
|
||||
|
||||
static void drag_enter(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED, uint32_t serial, struct wl_surface *surface, wl_fixed_t x UNUSED, wl_fixed_t y UNUSED, struct wl_data_offer *id) {
|
||||
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
|
||||
if (_glfw.wl.dataOffers[i].id == id) {
|
||||
_glfw.wl.dataOffers[i].offer_type = DRAG_AND_DROP;
|
||||
_glfw.wl.dataOffers[i].surface = surface;
|
||||
const char *mime = _glfw.wl.dataOffers[i].has_uri_list ? URI_LIST_MIME : NULL;
|
||||
wl_data_offer_accept(id, serial, mime);
|
||||
_GLFWWaylandDataOffer *d = _glfw.wl.dataOffers + i;
|
||||
if (d->id == id) {
|
||||
d->offer_type = DRAG_AND_DROP;
|
||||
d->surface = surface;
|
||||
_GLFWwindow* window = _glfw.windowListHead;
|
||||
int format_priority = 0;
|
||||
while (window)
|
||||
{
|
||||
if (window->wl.surface == surface) {
|
||||
for (size_t x = 0; x < d->mimes_count; x++) {
|
||||
int prio = _glfwInputDrop(window, d->mimes[x], NULL, 0);
|
||||
if (prio > format_priority) d->mime_for_drop = d->mimes[x];
|
||||
}
|
||||
break;
|
||||
}
|
||||
window = window->next;
|
||||
}
|
||||
wl_data_offer_accept(id, serial, d->mime_for_drop);
|
||||
} else if (_glfw.wl.dataOffers[i].offer_type == DRAG_AND_DROP) {
|
||||
_glfw.wl.dataOffers[i].offer_type = EXPIRED; // previous drag offer
|
||||
}
|
||||
@@ -1735,39 +1769,34 @@ static void drag_enter(void *data UNUSED, struct wl_data_device *wl_data_device
|
||||
static void drag_leave(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED) {
|
||||
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
|
||||
if (_glfw.wl.dataOffers[i].offer_type == DRAG_AND_DROP) {
|
||||
wl_data_offer_destroy(_glfw.wl.dataOffers[i].id);
|
||||
memset(_glfw.wl.dataOffers + i, 0, sizeof(_glfw.wl.dataOffers[0]));
|
||||
destroy_data_offer(&_glfw.wl.dataOffers[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void drop(void *data, struct wl_data_device *wl_data_device UNUSED) {
|
||||
static void drop(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED) {
|
||||
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
|
||||
if (_glfw.wl.dataOffers[i].offer_type == DRAG_AND_DROP) {
|
||||
char *uri_list = read_data_offer(_glfw.wl.dataOffers[i].id, URI_LIST_MIME);
|
||||
if (uri_list) {
|
||||
wl_data_offer_finish(_glfw.wl.dataOffers[i].id);
|
||||
int count;
|
||||
char** paths = parseUriList(data, &count);
|
||||
if (_glfw.wl.dataOffers[i].offer_type == DRAG_AND_DROP && _glfw.wl.dataOffers[i].mime_for_drop) {
|
||||
size_t sz = 0;
|
||||
char *data = read_data_offer(_glfw.wl.dataOffers[i].id, _glfw.wl.dataOffers[i].mime_for_drop, &sz);
|
||||
if (data) {
|
||||
// We dont do finish as this requires version 3 for wl_data_device_manager
|
||||
// which then requires more work with calling set_actions for drag and drop to function
|
||||
// wl_data_offer_finish(_glfw.wl.dataOffers[i].id);
|
||||
|
||||
_GLFWwindow* window = _glfw.windowListHead;
|
||||
while (window)
|
||||
{
|
||||
if (window->wl.surface == _glfw.wl.dataOffers[i].surface) {
|
||||
_glfwInputDrop(window, count, (const char**) paths);
|
||||
_glfwInputDrop(window, _glfw.wl.dataOffers[i].mime_for_drop, data, sz);
|
||||
break;
|
||||
}
|
||||
window = window->next;
|
||||
}
|
||||
|
||||
|
||||
for (int k = 0; k < count; k++)
|
||||
free(paths[k]);
|
||||
free(paths);
|
||||
free(uri_list);
|
||||
free(data);
|
||||
}
|
||||
wl_data_offer_destroy(_glfw.wl.dataOffers[i].id);
|
||||
memset(_glfw.wl.dataOffers + i, 0, sizeof(_glfw.wl.dataOffers[0]));
|
||||
destroy_data_offer(&_glfw.wl.dataOffers[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1872,10 +1901,12 @@ void _glfwPlatformSetClipboardString(const char* string)
|
||||
const char* _glfwPlatformGetClipboardString(void)
|
||||
{
|
||||
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
|
||||
if (_glfw.wl.dataOffers[i].id && _glfw.wl.dataOffers[i].mime && _glfw.wl.dataOffers[i].offer_type == CLIPBOARD) {
|
||||
if (_glfw.wl.dataOffers[i].is_self_offer) return _glfw.wl.clipboardString;
|
||||
_GLFWWaylandDataOffer *d = _glfw.wl.dataOffers + i;
|
||||
if (d->id && d->offer_type == CLIPBOARD && d->plain_text_mime) {
|
||||
if (d->is_self_offer) return _glfw.wl.clipboardString;
|
||||
free(_glfw.wl.pasteString);
|
||||
_glfw.wl.pasteString = read_data_offer(_glfw.wl.dataOffers[i].id, _glfw.wl.dataOffers[i].mime);
|
||||
size_t sz = 0;
|
||||
_glfw.wl.pasteString = read_data_offer(d->id, d->plain_text_mime, &sz);
|
||||
return _glfw.wl.pasteString;
|
||||
}
|
||||
}
|
||||
@@ -1922,13 +1953,14 @@ const char* _glfwPlatformGetPrimarySelectionString(void)
|
||||
{
|
||||
if (_glfw.wl.dataSourceForPrimarySelection != NULL) return _glfw.wl.primarySelectionString;
|
||||
|
||||
for (size_t i = 0; i < arraysz(_glfw.wl.primarySelectionOffers); i++) {
|
||||
if (_glfw.wl.primarySelectionOffers[i].id && _glfw.wl.primarySelectionOffers[i].mime && _glfw.wl.primarySelectionOffers[i].offer_type == PRIMARY_SELECTION) {
|
||||
if (_glfw.wl.primarySelectionOffers[i].is_self_offer) {
|
||||
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
|
||||
_GLFWWaylandDataOffer *d = _glfw.wl.dataOffers + i;
|
||||
if (d->id && d->is_primary && d->offer_type == PRIMARY_SELECTION && d->plain_text_mime) {
|
||||
if (d->is_self_offer) {
|
||||
return _glfw.wl.primarySelectionString;
|
||||
}
|
||||
free(_glfw.wl.pasteString);
|
||||
_glfw.wl.pasteString = read_primary_selection_offer(_glfw.wl.primarySelectionOffers[i].id, _glfw.wl.primarySelectionOffers[i].mime);
|
||||
_glfw.wl.pasteString = read_primary_selection_offer(d->id, d->plain_text_mime);
|
||||
return _glfw.wl.pasteString;
|
||||
}
|
||||
}
|
||||
@@ -2031,7 +2063,7 @@ GLFWAPI struct wl_surface* glfwGetWaylandWindow(GLFWwindow* handle)
|
||||
return window->wl.surface;
|
||||
}
|
||||
|
||||
GLFWAPI int glfwGetXKBScancode(const char* keyName, bool caseSensitive) {
|
||||
GLFWAPI int glfwGetNativeKeyForName(const char* keyName, bool caseSensitive) {
|
||||
return glfw_xkb_keysym_from_name(keyName, caseSensitive);
|
||||
}
|
||||
|
||||
|
||||
19
glfw/x11_init.c
vendored
19
glfw/x11_init.c
vendored
@@ -384,7 +384,6 @@ static bool initExtensions(void)
|
||||
_glfw.x11.XdndFinished = XInternAtom(_glfw.x11.display, "XdndFinished", False);
|
||||
_glfw.x11.XdndSelection = XInternAtom(_glfw.x11.display, "XdndSelection", False);
|
||||
_glfw.x11.XdndTypeList = XInternAtom(_glfw.x11.display, "XdndTypeList", False);
|
||||
_glfw.x11.text_uri_list = XInternAtom(_glfw.x11.display, "text/uri-list", False);
|
||||
|
||||
// ICCCM, EWMH and Motif window property atoms
|
||||
// These can be set safely even without WM support
|
||||
@@ -433,12 +432,10 @@ static bool initExtensions(void)
|
||||
//
|
||||
void _glfwGetSystemContentScaleX11(float* xscale, float* yscale, bool bypass_cache)
|
||||
{
|
||||
// NOTE: Default to the display-wide DPI as we don't currently have a policy
|
||||
// for which monitor a window is considered to be on
|
||||
float xdpi = DisplayWidth(_glfw.x11.display, _glfw.x11.screen) *
|
||||
25.4f / DisplayWidthMM(_glfw.x11.display, _glfw.x11.screen);
|
||||
float ydpi = DisplayHeight(_glfw.x11.display, _glfw.x11.screen) *
|
||||
25.4f / DisplayHeightMM(_glfw.x11.display, _glfw.x11.screen);
|
||||
// Start by assuming the default X11 DPI
|
||||
// NOTE: Some desktop environments (KDE) may remove the Xft.dpi field when it
|
||||
// would be set to 96, so assume that is the case if we cannot find it
|
||||
float xdpi = 96.f, ydpi = 96.f;
|
||||
|
||||
// NOTE: Basing the scale on Xft.dpi where available should provide the most
|
||||
// consistent user experience (matches Qt, Gtk, etc), although not
|
||||
@@ -468,7 +465,7 @@ void _glfwGetSystemContentScaleX11(float* xscale, float* yscale, bool bypass_cac
|
||||
if (XrmGetResource(db, "Xft.dpi", "Xft.Dpi", &type, &value))
|
||||
{
|
||||
if (type && strcmp(type, "String") == 0)
|
||||
xdpi = ydpi = atof(value.addr);
|
||||
xdpi = ydpi = (float)atof(value.addr);
|
||||
}
|
||||
|
||||
XrmDestroyDatabase(db);
|
||||
@@ -505,8 +502,10 @@ static Window createHelperWindow(void)
|
||||
|
||||
// X error handler
|
||||
//
|
||||
static int errorHandler(Display *display UNUSED, XErrorEvent* event)
|
||||
static int errorHandler(Display *display, XErrorEvent* event)
|
||||
{
|
||||
if (_glfw.x11.display != display)
|
||||
return 0;
|
||||
_glfw.x11.errorCode = event->error_code;
|
||||
return 0;
|
||||
}
|
||||
@@ -637,8 +636,6 @@ int _glfwPlatformInit(void)
|
||||
}
|
||||
#endif
|
||||
|
||||
_glfwInitTimerPOSIX();
|
||||
|
||||
_glfwPollMonitorsX11();
|
||||
return true;
|
||||
}
|
||||
|
||||
10
glfw/x11_monitor.c
vendored
10
glfw/x11_monitor.c
vendored
@@ -166,6 +166,16 @@ void _glfwPollMonitorsX11(void)
|
||||
heightMM = oi->mm_height;
|
||||
}
|
||||
|
||||
if (widthMM <= 0 || heightMM <= 0)
|
||||
{
|
||||
// HACK: If RandR does not provide a physical size, assume the
|
||||
// X11 default 96 DPI and calcuate from the CRTC viewport
|
||||
// NOTE: These members are affected by rotation, unlike the mode
|
||||
// info and output info members
|
||||
widthMM = (int) (ci->width * 25.4f / 96.f);
|
||||
heightMM = (int) (ci->height * 25.4f / 96.f);
|
||||
}
|
||||
|
||||
_GLFWmonitor* monitor = _glfwAllocMonitor(oi->name, widthMM, heightMM);
|
||||
monitor->x11.output = sr->outputs[i];
|
||||
monitor->x11.crtc = oi->crtc;
|
||||
|
||||
6
glfw/x11_platform.h
vendored
6
glfw/x11_platform.h
vendored
@@ -152,7 +152,6 @@ typedef VkResult (APIENTRY *PFN_vkCreateXcbSurfaceKHR)(VkInstance,const VkXcbSur
|
||||
typedef VkBool32 (APIENTRY *PFN_vkGetPhysicalDeviceXcbPresentationSupportKHR)(VkPhysicalDevice,uint32_t,xcb_connection_t*,xcb_visualid_t);
|
||||
|
||||
#include "posix_thread.h"
|
||||
#include "posix_time.h"
|
||||
#include "glx_context.h"
|
||||
#include "egl_context.h"
|
||||
#include "osmesa_context.h"
|
||||
@@ -181,6 +180,7 @@ typedef struct _GLFWwindowX11
|
||||
{
|
||||
Colormap colormap;
|
||||
Window handle;
|
||||
Window parent;
|
||||
|
||||
bool iconified;
|
||||
bool maximized;
|
||||
@@ -266,7 +266,6 @@ typedef struct _GLFWlibraryX11
|
||||
Atom XdndFinished;
|
||||
Atom XdndSelection;
|
||||
Atom XdndTypeList;
|
||||
Atom text_uri_list;
|
||||
|
||||
// Selection (clipboard) atoms
|
||||
Atom TARGETS;
|
||||
@@ -327,7 +326,8 @@ typedef struct _GLFWlibraryX11
|
||||
struct {
|
||||
int version;
|
||||
Window source;
|
||||
Atom format;
|
||||
char format[128];
|
||||
int format_priority;
|
||||
} xdnd;
|
||||
|
||||
struct {
|
||||
|
||||
412
glfw/x11_window.c
vendored
412
glfw/x11_window.c
vendored
@@ -31,6 +31,7 @@
|
||||
#include "internal.h"
|
||||
#include "backend_utils.h"
|
||||
#include "linux_notify.h"
|
||||
#include "../kitty/monotonic.h"
|
||||
|
||||
#include <X11/cursorfont.h>
|
||||
#include <X11/Xmd.h>
|
||||
@@ -51,6 +52,10 @@
|
||||
#define Button6 6
|
||||
#define Button7 7
|
||||
|
||||
// Motif WM hints flags
|
||||
#define MWM_HINTS_DECORATIONS 2
|
||||
#define MWM_DECOR_ALL 1
|
||||
|
||||
#define _GLFW_XDND_VERSION 5
|
||||
|
||||
|
||||
@@ -61,9 +66,9 @@
|
||||
static unsigned _glfwDispatchX11Events(void);
|
||||
|
||||
static void
|
||||
handleEvents(double timeout) {
|
||||
EVDBG("starting handleEvents(%.2f)", timeout);
|
||||
int display_read_ok = pollForEvents(&_glfw.x11.eventLoopData, timeout);
|
||||
handleEvents(monotonic_t timeout) {
|
||||
EVDBG("starting handleEvents(%.2f)", monotonic_t_to_s_double(timeout));
|
||||
int display_read_ok = pollForEvents(&_glfw.x11.eventLoopData, timeout, NULL);
|
||||
EVDBG("display_read_ok: %d", display_read_ok);
|
||||
if (display_read_ok) {
|
||||
unsigned dispatched = _glfwDispatchX11Events();
|
||||
@@ -77,9 +82,9 @@ handleEvents(double timeout) {
|
||||
}
|
||||
|
||||
static bool
|
||||
waitForX11Event(double timeout) {
|
||||
waitForX11Event(monotonic_t timeout) {
|
||||
// returns true if there is X11 data waiting to be read, does not run watches and timers
|
||||
double end_time = glfwGetTime() + timeout;
|
||||
monotonic_t end_time = glfwGetTime() + timeout;
|
||||
while(true) {
|
||||
if (timeout >= 0) {
|
||||
const int result = pollWithTimeout(_glfw.x11.eventLoopData.fds, 1, timeout);
|
||||
@@ -109,7 +114,7 @@ static bool waitForVisibilityNotify(_GLFWwindow* window)
|
||||
VisibilityNotify,
|
||||
&dummy))
|
||||
{
|
||||
if (!waitForX11Event(0.1))
|
||||
if (!waitForX11Event(ms_to_monotonic_t(100ll)))
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -250,6 +255,14 @@ static void updateNormalHints(_GLFWwindow* window, int width, int height)
|
||||
hints->min_aspect.x = hints->max_aspect.x = window->numer;
|
||||
hints->min_aspect.y = hints->max_aspect.y = window->denom;
|
||||
}
|
||||
|
||||
if (window->widthincr != GLFW_DONT_CARE &&
|
||||
window->heightincr != GLFW_DONT_CARE)
|
||||
{
|
||||
hints->flags |= PResizeInc;
|
||||
hints->width_inc = window->widthincr;
|
||||
hints->height_inc = window->heightincr;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -508,8 +521,8 @@ static bool createNativeWindow(_GLFWwindow* window,
|
||||
|
||||
if (wndconfig->scaleToMonitor)
|
||||
{
|
||||
width *= _glfw.x11.contentScaleX;
|
||||
height *= _glfw.x11.contentScaleY;
|
||||
width *= (int)_glfw.x11.contentScaleX;
|
||||
height *= (int)_glfw.x11.contentScaleY;
|
||||
}
|
||||
|
||||
// Create a colormap based on the visual used by the current context
|
||||
@@ -520,46 +533,41 @@ static bool createNativeWindow(_GLFWwindow* window,
|
||||
|
||||
window->x11.transparent = _glfwIsVisualTransparentX11(visual);
|
||||
|
||||
// Create the actual window
|
||||
XSetWindowAttributes wa = { 0 };
|
||||
wa.colormap = window->x11.colormap;
|
||||
wa.event_mask = StructureNotifyMask | KeyPressMask | KeyReleaseMask |
|
||||
PointerMotionMask | ButtonPressMask | ButtonReleaseMask |
|
||||
ExposureMask | FocusChangeMask | VisibilityChangeMask |
|
||||
EnterWindowMask | LeaveWindowMask | PropertyChangeMask;
|
||||
|
||||
_glfwGrabErrorHandlerX11();
|
||||
|
||||
window->x11.parent = _glfw.x11.root;
|
||||
window->x11.handle = XCreateWindow(_glfw.x11.display,
|
||||
_glfw.x11.root,
|
||||
0, 0, // Position
|
||||
width, height,
|
||||
0, // Border width
|
||||
depth, // Color depth
|
||||
InputOutput,
|
||||
visual,
|
||||
CWBorderPixel | CWColormap | CWEventMask,
|
||||
&wa);
|
||||
|
||||
_glfwReleaseErrorHandlerX11();
|
||||
|
||||
if (!window->x11.handle)
|
||||
{
|
||||
XSetWindowAttributes wa;
|
||||
const unsigned long wamask = CWBorderPixel | CWColormap | CWEventMask;
|
||||
|
||||
wa.colormap = window->x11.colormap;
|
||||
wa.border_pixel = 0;
|
||||
wa.event_mask = StructureNotifyMask | KeyPressMask | KeyReleaseMask |
|
||||
PointerMotionMask | ButtonPressMask | ButtonReleaseMask |
|
||||
ExposureMask | FocusChangeMask | VisibilityChangeMask |
|
||||
EnterWindowMask | LeaveWindowMask | PropertyChangeMask;
|
||||
|
||||
_glfwGrabErrorHandlerX11();
|
||||
|
||||
window->x11.handle = XCreateWindow(_glfw.x11.display,
|
||||
_glfw.x11.root,
|
||||
0, 0,
|
||||
width, height,
|
||||
0, // Border width
|
||||
depth, // Color depth
|
||||
InputOutput,
|
||||
visual,
|
||||
wamask,
|
||||
&wa);
|
||||
|
||||
_glfwReleaseErrorHandlerX11();
|
||||
|
||||
if (!window->x11.handle)
|
||||
{
|
||||
_glfwInputErrorX11(GLFW_PLATFORM_ERROR,
|
||||
"X11: Failed to create window");
|
||||
return false;
|
||||
}
|
||||
|
||||
XSaveContext(_glfw.x11.display,
|
||||
window->x11.handle,
|
||||
_glfw.x11.context,
|
||||
(XPointer) window);
|
||||
_glfwInputErrorX11(GLFW_PLATFORM_ERROR,
|
||||
"X11: Failed to create window");
|
||||
return false;
|
||||
}
|
||||
|
||||
XSaveContext(_glfw.x11.display,
|
||||
window->x11.handle,
|
||||
_glfw.x11.context,
|
||||
(XPointer) window);
|
||||
|
||||
if (!wndconfig->decorated)
|
||||
_glfwPlatformSetWindowDecorated(window, false);
|
||||
|
||||
@@ -589,7 +597,7 @@ static bool createNativeWindow(_GLFWwindow* window,
|
||||
{
|
||||
XChangeProperty(_glfw.x11.display, window->x11.handle,
|
||||
_glfw.x11.NET_WM_STATE, XA_ATOM, 32,
|
||||
PropModeReplace, (unsigned char*) &states, count);
|
||||
PropModeReplace, (unsigned char*) states, count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -880,7 +888,7 @@ static const char* getSelectionString(Atom selection)
|
||||
Atom actualType;
|
||||
int actualFormat;
|
||||
unsigned long itemCount, bytesAfter;
|
||||
double start = glfwGetTime();
|
||||
monotonic_t start = glfwGetTime();
|
||||
XEvent notification, dummy;
|
||||
|
||||
XConvertSelection(_glfw.x11.display,
|
||||
@@ -895,10 +903,10 @@ static const char* getSelectionString(Atom selection)
|
||||
SelectionNotify,
|
||||
¬ification))
|
||||
{
|
||||
double time = glfwGetTime();
|
||||
if (time - start > 2)
|
||||
monotonic_t time = glfwGetTime();
|
||||
if (time - start > s_to_monotonic_t(2ll))
|
||||
return "";
|
||||
waitForX11Event(2.0 - (time - start));
|
||||
waitForX11Event(s_to_monotonic_t(2ll) - (time - start));
|
||||
}
|
||||
|
||||
if (notification.xselection.property == None)
|
||||
@@ -935,10 +943,10 @@ static const char* getSelectionString(Atom selection)
|
||||
isSelPropNewValueNotify,
|
||||
(XPointer) ¬ification))
|
||||
{
|
||||
double time = glfwGetTime();
|
||||
if (time - start > 2)
|
||||
monotonic_t time = glfwGetTime();
|
||||
if (time - start > s_to_monotonic_t(2ll))
|
||||
return "";
|
||||
waitForX11Event(2.0 - (time - start));
|
||||
waitForX11Event(s_to_monotonic_t(2ll) - (time - start));
|
||||
}
|
||||
|
||||
XFree(data);
|
||||
@@ -1067,6 +1075,21 @@ static void onConfigChange(void)
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
get_atom_names(Atom *atoms, int count, char **atom_names) {
|
||||
_glfwGrabErrorHandlerX11();
|
||||
XGetAtomNames(_glfw.x11.display, atoms, count, atom_names);
|
||||
_glfwReleaseErrorHandlerX11();
|
||||
if (_glfw.x11.errorCode != Success) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
_glfwGrabErrorHandlerX11();
|
||||
atom_names[i] = XGetAtomName(_glfw.x11.display, atoms[i]);
|
||||
_glfwReleaseErrorHandlerX11();
|
||||
if (_glfw.x11.errorCode != Success) atom_names[i] = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process the specified X event
|
||||
//
|
||||
static void processEvent(XEvent *event)
|
||||
@@ -1177,6 +1200,12 @@ static void processEvent(XEvent *event)
|
||||
|
||||
switch (event->type)
|
||||
{
|
||||
case ReparentNotify:
|
||||
{
|
||||
window->x11.parent = event->xreparent.parent;
|
||||
return;
|
||||
}
|
||||
|
||||
case KeyPress:
|
||||
{
|
||||
UPDATE_KEYMAP_IF_NEEDED;
|
||||
@@ -1236,13 +1265,13 @@ static void processEvent(XEvent *event)
|
||||
|
||||
// Modern X provides scroll events as mouse button presses
|
||||
else if (event->xbutton.button == Button4)
|
||||
_glfwInputScroll(window, 0.0, 1.0, 0);
|
||||
_glfwInputScroll(window, 0.0, 1.0, 0, mods);
|
||||
else if (event->xbutton.button == Button5)
|
||||
_glfwInputScroll(window, 0.0, -1.0, 0);
|
||||
_glfwInputScroll(window, 0.0, -1.0, 0, mods);
|
||||
else if (event->xbutton.button == Button6)
|
||||
_glfwInputScroll(window, 1.0, 0.0, 0);
|
||||
_glfwInputScroll(window, 1.0, 0.0, 0, mods);
|
||||
else if (event->xbutton.button == Button7)
|
||||
_glfwInputScroll(window, -1.0, 0.0, 0);
|
||||
_glfwInputScroll(window, -1.0, 0.0, 0, mods);
|
||||
|
||||
else
|
||||
{
|
||||
@@ -1370,20 +1399,36 @@ static void processEvent(XEvent *event)
|
||||
window->x11.height = event->xconfigure.height;
|
||||
}
|
||||
|
||||
if (event->xconfigure.x != window->x11.xpos ||
|
||||
event->xconfigure.y != window->x11.ypos)
|
||||
{
|
||||
if (event->xany.send_event)
|
||||
{
|
||||
_glfwInputWindowPos(window,
|
||||
event->xconfigure.x,
|
||||
event->xconfigure.y);
|
||||
int xpos = event->xconfigure.x;
|
||||
int ypos = event->xconfigure.y;
|
||||
|
||||
window->x11.xpos = event->xconfigure.x;
|
||||
window->x11.ypos = event->xconfigure.y;
|
||||
// NOTE: ConfigureNotify events from the server are in local
|
||||
// coordinates, so if we are reparented we need to translate
|
||||
// the position into root (screen) coordinates
|
||||
if (!event->xany.send_event && window->x11.parent != _glfw.x11.root)
|
||||
{
|
||||
Window dummy;
|
||||
_glfwGrabErrorHandlerX11();
|
||||
XTranslateCoordinates(_glfw.x11.display,
|
||||
window->x11.parent,
|
||||
_glfw.x11.root,
|
||||
xpos, ypos,
|
||||
&xpos, &ypos,
|
||||
&dummy);
|
||||
_glfwReleaseErrorHandlerX11();
|
||||
if (_glfw.x11.errorCode != Success) {
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR, "X11: Failed to translate ConfigureNotiy co-ords for reparented window");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (xpos != window->x11.xpos || ypos != window->x11.ypos)
|
||||
{
|
||||
_glfwInputWindowPos(window, xpos, ypos);
|
||||
window->x11.xpos = xpos;
|
||||
window->x11.ypos = ypos;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1430,7 +1475,8 @@ static void processEvent(XEvent *event)
|
||||
|
||||
_glfw.x11.xdnd.source = event->xclient.data.l[0];
|
||||
_glfw.x11.xdnd.version = event->xclient.data.l[1] >> 24;
|
||||
_glfw.x11.xdnd.format = None;
|
||||
memset(_glfw.x11.xdnd.format, 0, sizeof(_glfw.x11.xdnd.format));
|
||||
_glfw.x11.xdnd.format_priority = 0;
|
||||
|
||||
if (_glfw.x11.xdnd.version > _GLFW_XDND_VERSION)
|
||||
return;
|
||||
@@ -1447,14 +1493,22 @@ static void processEvent(XEvent *event)
|
||||
count = 3;
|
||||
formats = (Atom*) event->xclient.data.l + 2;
|
||||
}
|
||||
char **atom_names = calloc(count, sizeof(char**));
|
||||
if (atom_names) {
|
||||
get_atom_names(formats, count, atom_names);
|
||||
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
if (formats[i] == _glfw.x11.text_uri_list)
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
_glfw.x11.xdnd.format = _glfw.x11.text_uri_list;
|
||||
break;
|
||||
if (atom_names[i]) {
|
||||
int prio = _glfwInputDrop(window, atom_names[i], NULL, 0);
|
||||
if (prio > _glfw.x11.xdnd.format_priority) {
|
||||
_glfw.x11.xdnd.format_priority = prio;
|
||||
strncpy(_glfw.x11.xdnd.format, atom_names[i], arraysz(_glfw.x11.xdnd.format) - 1);
|
||||
}
|
||||
XFree(atom_names[i]);
|
||||
}
|
||||
}
|
||||
free(atom_names);
|
||||
}
|
||||
|
||||
if (list && formats)
|
||||
@@ -1468,7 +1522,7 @@ static void processEvent(XEvent *event)
|
||||
if (_glfw.x11.xdnd.version > _GLFW_XDND_VERSION)
|
||||
return;
|
||||
|
||||
if (_glfw.x11.xdnd.format)
|
||||
if (_glfw.x11.xdnd.format_priority > 0)
|
||||
{
|
||||
if (_glfw.x11.xdnd.version >= 1)
|
||||
time = event->xclient.data.l[2];
|
||||
@@ -1476,7 +1530,7 @@ static void processEvent(XEvent *event)
|
||||
// Request the chosen format from the source window
|
||||
XConvertSelection(_glfw.x11.display,
|
||||
_glfw.x11.XdndSelection,
|
||||
_glfw.x11.xdnd.format,
|
||||
XInternAtom(_glfw.x11.display, _glfw.x11.xdnd.format, 0),
|
||||
_glfw.x11.XdndSelection,
|
||||
window->x11.handle,
|
||||
time);
|
||||
@@ -1502,17 +1556,21 @@ static void processEvent(XEvent *event)
|
||||
const int xabs = (event->xclient.data.l[2] >> 16) & 0xffff;
|
||||
const int yabs = (event->xclient.data.l[2]) & 0xffff;
|
||||
Window dummy;
|
||||
int xpos, ypos;
|
||||
int xpos = 0, ypos = 0;
|
||||
|
||||
if (_glfw.x11.xdnd.version > _GLFW_XDND_VERSION)
|
||||
return;
|
||||
|
||||
_glfwGrabErrorHandlerX11();
|
||||
XTranslateCoordinates(_glfw.x11.display,
|
||||
_glfw.x11.root,
|
||||
window->x11.handle,
|
||||
xabs, yabs,
|
||||
&xpos, &ypos,
|
||||
&dummy);
|
||||
_glfwReleaseErrorHandlerX11();
|
||||
if (_glfw.x11.errorCode != Success)
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR, "X11: Failed to get DND event position");
|
||||
|
||||
_glfwInputCursorPos(window, xpos, ypos);
|
||||
|
||||
@@ -1524,7 +1582,7 @@ static void processEvent(XEvent *event)
|
||||
reply.xclient.data.l[2] = 0; // Specify an empty rectangle
|
||||
reply.xclient.data.l[3] = 0;
|
||||
|
||||
if (_glfw.x11.xdnd.format)
|
||||
if (_glfw.x11.xdnd.format_priority > 0)
|
||||
{
|
||||
// Reply that we are ready to copy the dragged data
|
||||
reply.xclient.data.l[1] = 1; // Accept with no rectangle
|
||||
@@ -1554,14 +1612,7 @@ static void processEvent(XEvent *event)
|
||||
|
||||
if (result)
|
||||
{
|
||||
int i, count;
|
||||
char** paths = parseUriList(data, &count);
|
||||
|
||||
_glfwInputDrop(window, count, (const char**) paths);
|
||||
|
||||
for (i = 0; i < count; i++)
|
||||
free(paths[i]);
|
||||
free(paths);
|
||||
_glfwInputDrop(window, _glfw.x11.xdnd.format, data, result);
|
||||
}
|
||||
|
||||
if (data)
|
||||
@@ -1947,10 +1998,14 @@ void _glfwPlatformSetWindowIcon(_GLFWwindow* window,
|
||||
void _glfwPlatformGetWindowPos(_GLFWwindow* window, int* xpos, int* ypos)
|
||||
{
|
||||
Window dummy;
|
||||
int x, y;
|
||||
int x = 0, y = 0;
|
||||
|
||||
_glfwGrabErrorHandlerX11();
|
||||
XTranslateCoordinates(_glfw.x11.display, window->x11.handle, _glfw.x11.root,
|
||||
0, 0, &x, &y, &dummy);
|
||||
_glfwReleaseErrorHandlerX11();
|
||||
if (_glfw.x11.errorCode != Success)
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR, "X11: Failed to get window position");
|
||||
|
||||
if (xpos)
|
||||
*xpos = x;
|
||||
@@ -2029,6 +2084,14 @@ void _glfwPlatformSetWindowAspectRatio(_GLFWwindow* window, int numer UNUSED, in
|
||||
XFlush(_glfw.x11.display);
|
||||
}
|
||||
|
||||
void _glfwPlatformSetWindowSizeIncrements(_GLFWwindow* window, int widthincr UNUSED, int heightincr UNUSED)
|
||||
{
|
||||
int width, height;
|
||||
_glfwPlatformGetWindowSize(window, &width, &height);
|
||||
updateNormalHints(window, width, height);
|
||||
XFlush(_glfw.x11.display);
|
||||
}
|
||||
|
||||
void _glfwPlatformGetFramebufferSize(_GLFWwindow* window, int* width, int* height)
|
||||
{
|
||||
_glfwPlatformGetWindowSize(window, width, height);
|
||||
@@ -2066,7 +2129,7 @@ void _glfwPlatformGetWindowFrameSize(_GLFWwindow* window,
|
||||
isFrameExtentsEvent,
|
||||
(XPointer) window))
|
||||
{
|
||||
if (!waitForX11Event(0.5))
|
||||
if (!waitForX11Event(ms_to_monotonic_t(500ll)))
|
||||
{
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||
"X11: The window manager has a broken _NET_REQUEST_FRAME_EXTENTS implementation; please report this issue");
|
||||
@@ -2103,9 +2166,9 @@ void _glfwPlatformGetWindowContentScale(_GLFWwindow* window UNUSED,
|
||||
*yscale = _glfw.x11.contentScaleY;
|
||||
}
|
||||
|
||||
double _glfwPlatformGetDoubleClickInterval(_GLFWwindow* window UNUSED)
|
||||
monotonic_t _glfwPlatformGetDoubleClickInterval(_GLFWwindow* window UNUSED)
|
||||
{
|
||||
return 0.5;
|
||||
return ms_to_monotonic_t(500ll);
|
||||
}
|
||||
|
||||
void _glfwPlatformIconifyWindow(_GLFWwindow* window)
|
||||
@@ -2141,18 +2204,67 @@ void _glfwPlatformRestoreWindow(_GLFWwindow* window)
|
||||
|
||||
void _glfwPlatformMaximizeWindow(_GLFWwindow* window)
|
||||
{
|
||||
if (_glfw.x11.NET_WM_STATE &&
|
||||
_glfw.x11.NET_WM_STATE_MAXIMIZED_VERT &&
|
||||
_glfw.x11.NET_WM_STATE_MAXIMIZED_HORZ)
|
||||
if (!_glfw.x11.NET_WM_STATE ||
|
||||
!_glfw.x11.NET_WM_STATE_MAXIMIZED_VERT ||
|
||||
!_glfw.x11.NET_WM_STATE_MAXIMIZED_HORZ)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_glfwPlatformWindowVisible(window))
|
||||
{
|
||||
sendEventToWM(window,
|
||||
_glfw.x11.NET_WM_STATE,
|
||||
_NET_WM_STATE_ADD,
|
||||
_glfw.x11.NET_WM_STATE_MAXIMIZED_VERT,
|
||||
_glfw.x11.NET_WM_STATE_MAXIMIZED_HORZ,
|
||||
1, 0);
|
||||
XFlush(_glfw.x11.display);
|
||||
_glfw.x11.NET_WM_STATE,
|
||||
_NET_WM_STATE_ADD,
|
||||
_glfw.x11.NET_WM_STATE_MAXIMIZED_VERT,
|
||||
_glfw.x11.NET_WM_STATE_MAXIMIZED_HORZ,
|
||||
1, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
Atom* states = NULL;
|
||||
unsigned long count =
|
||||
_glfwGetWindowPropertyX11(window->x11.handle,
|
||||
_glfw.x11.NET_WM_STATE,
|
||||
XA_ATOM,
|
||||
(unsigned char**) &states);
|
||||
|
||||
// NOTE: We don't check for failure as this property may not exist yet
|
||||
// and that's fine (and we'll create it implicitly with append)
|
||||
|
||||
Atom missing[2] =
|
||||
{
|
||||
_glfw.x11.NET_WM_STATE_MAXIMIZED_VERT,
|
||||
_glfw.x11.NET_WM_STATE_MAXIMIZED_HORZ
|
||||
};
|
||||
unsigned long missingCount = 2;
|
||||
|
||||
for (unsigned long i = 0; i < count; i++)
|
||||
{
|
||||
for (unsigned long j = 0; j < missingCount; j++)
|
||||
{
|
||||
if (states[i] == missing[j])
|
||||
{
|
||||
missing[j] = missing[missingCount - 1];
|
||||
missingCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (states)
|
||||
XFree(states);
|
||||
|
||||
if (!missingCount)
|
||||
return;
|
||||
|
||||
XChangeProperty(_glfw.x11.display, window->x11.handle,
|
||||
_glfw.x11.NET_WM_STATE, XA_ATOM, 32,
|
||||
PropModeAppend,
|
||||
(unsigned char*) missing,
|
||||
missingCount);
|
||||
}
|
||||
|
||||
XFlush(_glfw.x11.display);
|
||||
}
|
||||
|
||||
void _glfwPlatformShowWindow(_GLFWwindow* window)
|
||||
@@ -2172,6 +2284,9 @@ void _glfwPlatformHideWindow(_GLFWwindow* window)
|
||||
|
||||
void _glfwPlatformRequestWindowAttention(_GLFWwindow* window)
|
||||
{
|
||||
if (!_glfw.x11.NET_WM_STATE || !_glfw.x11.NET_WM_STATE_DEMANDS_ATTENTION)
|
||||
return;
|
||||
|
||||
sendEventToWM(window,
|
||||
_glfw.x11.NET_WM_STATE,
|
||||
_NET_WM_STATE_ADD,
|
||||
@@ -2188,7 +2303,7 @@ void _glfwPlatformFocusWindow(_GLFWwindow* window)
|
||||
{
|
||||
if (_glfw.x11.NET_ACTIVE_WINDOW)
|
||||
sendEventToWM(window, _glfw.x11.NET_ACTIVE_WINDOW, 1, 0, 0, 0, 0);
|
||||
else
|
||||
else if (_glfwPlatformWindowVisible(window))
|
||||
{
|
||||
XRaiseWindow(_glfw.x11.display, window->x11.handle);
|
||||
XSetInputFocus(_glfw.x11.display, window->x11.handle,
|
||||
@@ -2350,33 +2465,24 @@ void _glfwPlatformSetWindowResizable(_GLFWwindow* window, bool enabled UNUSED)
|
||||
|
||||
void _glfwPlatformSetWindowDecorated(_GLFWwindow* window, bool enabled)
|
||||
{
|
||||
if (enabled)
|
||||
struct
|
||||
{
|
||||
XDeleteProperty(_glfw.x11.display,
|
||||
window->x11.handle,
|
||||
_glfw.x11.MOTIF_WM_HINTS);
|
||||
}
|
||||
else
|
||||
{
|
||||
struct
|
||||
{
|
||||
unsigned long flags;
|
||||
unsigned long functions;
|
||||
unsigned long decorations;
|
||||
long input_mode;
|
||||
unsigned long status;
|
||||
} hints;
|
||||
unsigned long flags;
|
||||
unsigned long functions;
|
||||
unsigned long decorations;
|
||||
long input_mode;
|
||||
unsigned long status;
|
||||
} hints = {0};
|
||||
|
||||
hints.flags = 2; // Set decorations
|
||||
hints.decorations = 0; // No decorations
|
||||
hints.flags = MWM_HINTS_DECORATIONS;
|
||||
hints.decorations = enabled ? MWM_DECOR_ALL : 0;
|
||||
|
||||
XChangeProperty(_glfw.x11.display, window->x11.handle,
|
||||
_glfw.x11.MOTIF_WM_HINTS,
|
||||
_glfw.x11.MOTIF_WM_HINTS, 32,
|
||||
PropModeReplace,
|
||||
(unsigned char*) &hints,
|
||||
sizeof(hints) / sizeof(long));
|
||||
}
|
||||
XChangeProperty(_glfw.x11.display, window->x11.handle,
|
||||
_glfw.x11.MOTIF_WM_HINTS,
|
||||
_glfw.x11.MOTIF_WM_HINTS, 32,
|
||||
PropModeReplace,
|
||||
(unsigned char*) &hints,
|
||||
sizeof(hints) / sizeof(long));
|
||||
}
|
||||
|
||||
void _glfwPlatformSetWindowFloating(_GLFWwindow* window, bool enabled)
|
||||
@@ -2386,7 +2492,7 @@ void _glfwPlatformSetWindowFloating(_GLFWwindow* window, bool enabled)
|
||||
|
||||
if (_glfwPlatformWindowVisible(window))
|
||||
{
|
||||
const Atom action = enabled ? _NET_WM_STATE_ADD : _NET_WM_STATE_REMOVE;
|
||||
const long action = enabled ? _NET_WM_STATE_ADD : _NET_WM_STATE_REMOVE;
|
||||
sendEventToWM(window,
|
||||
_glfw.x11.NET_WM_STATE,
|
||||
action,
|
||||
@@ -2395,15 +2501,16 @@ void _glfwPlatformSetWindowFloating(_GLFWwindow* window, bool enabled)
|
||||
}
|
||||
else
|
||||
{
|
||||
Atom* states;
|
||||
Atom* states = NULL;
|
||||
unsigned long i, count;
|
||||
|
||||
count = _glfwGetWindowPropertyX11(window->x11.handle,
|
||||
_glfw.x11.NET_WM_STATE,
|
||||
XA_ATOM,
|
||||
(unsigned char**) &states);
|
||||
if (!states)
|
||||
return;
|
||||
|
||||
// NOTE: We don't check for failure as this property may not exist yet
|
||||
// and that's fine (and we'll create it implicitly with append)
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
@@ -2413,32 +2520,36 @@ void _glfwPlatformSetWindowFloating(_GLFWwindow* window, bool enabled)
|
||||
break;
|
||||
}
|
||||
|
||||
if (i == count)
|
||||
{
|
||||
XChangeProperty(_glfw.x11.display, window->x11.handle,
|
||||
_glfw.x11.NET_WM_STATE, XA_ATOM, 32,
|
||||
PropModeAppend,
|
||||
(unsigned char*) &_glfw.x11.NET_WM_STATE_ABOVE,
|
||||
1);
|
||||
}
|
||||
if (i < count)
|
||||
return;
|
||||
|
||||
XChangeProperty(_glfw.x11.display, window->x11.handle,
|
||||
_glfw.x11.NET_WM_STATE, XA_ATOM, 32,
|
||||
PropModeAppend,
|
||||
(unsigned char*) &_glfw.x11.NET_WM_STATE_ABOVE,
|
||||
1);
|
||||
}
|
||||
else
|
||||
else if (states)
|
||||
{
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
if (states[i] == _glfw.x11.NET_WM_STATE_ABOVE)
|
||||
{
|
||||
states[i] = states[count - 1];
|
||||
count--;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (i == count)
|
||||
return;
|
||||
|
||||
states[i] = states[count - 1];
|
||||
count--;
|
||||
|
||||
XChangeProperty(_glfw.x11.display, window->x11.handle,
|
||||
_glfw.x11.NET_WM_STATE, XA_ATOM, 32,
|
||||
PropModeReplace, (unsigned char*) &states, count);
|
||||
PropModeReplace, (unsigned char*) states, count);
|
||||
}
|
||||
|
||||
XFree(states);
|
||||
if (states)
|
||||
XFree(states);
|
||||
}
|
||||
|
||||
XFlush(_glfw.x11.display);
|
||||
@@ -2527,11 +2638,11 @@ void _glfwPlatformPollEvents(void)
|
||||
|
||||
void _glfwPlatformWaitEvents(void)
|
||||
{
|
||||
double timeout = _glfwDispatchX11Events() ? 0 : -1;
|
||||
monotonic_t timeout = _glfwDispatchX11Events() ? 0 : -1;
|
||||
handleEvents(timeout);
|
||||
}
|
||||
|
||||
void _glfwPlatformWaitEventsTimeout(double timeout)
|
||||
void _glfwPlatformWaitEventsTimeout(monotonic_t timeout)
|
||||
{
|
||||
if (_glfwDispatchX11Events()) timeout = 0;
|
||||
handleEvents(timeout);
|
||||
@@ -2585,13 +2696,13 @@ void _glfwPlatformSetCursorMode(_GLFWwindow* window, int mode)
|
||||
XFlush(_glfw.x11.display);
|
||||
}
|
||||
|
||||
const char* _glfwPlatformGetScancodeName(int scancode)
|
||||
const char* _glfwPlatformGetNativeKeyName(int native_key)
|
||||
{
|
||||
|
||||
return glfw_xkb_keysym_name(scancode);
|
||||
return glfw_xkb_keysym_name(native_key);
|
||||
}
|
||||
|
||||
int _glfwPlatformGetKeyScancode(int key)
|
||||
int _glfwPlatformGetNativeKeyForKey(int key)
|
||||
{
|
||||
return glfw_xkb_sym_for_key(key);
|
||||
}
|
||||
@@ -2612,7 +2723,7 @@ int _glfwPlatformCreateStandardCursor(_GLFWcursor* cursor, GLFWCursorShape shape
|
||||
int native = 0;
|
||||
#define C(name, val) case name: native = val; break;
|
||||
switch(shape) {
|
||||
C(GLFW_ARROW_CURSOR, XC_arrow);
|
||||
C(GLFW_ARROW_CURSOR, XC_left_ptr);
|
||||
C(GLFW_IBEAM_CURSOR, XC_xterm);
|
||||
C(GLFW_CROSSHAIR_CURSOR, XC_crosshair);
|
||||
C(GLFW_HAND_CURSOR, XC_hand2);
|
||||
@@ -2655,8 +2766,9 @@ void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor UNUSED)
|
||||
|
||||
void _glfwPlatformSetClipboardString(const char* string)
|
||||
{
|
||||
char* copy = _glfw_strdup(string);
|
||||
free(_glfw.x11.clipboardString);
|
||||
_glfw.x11.clipboardString = _glfw_strdup(string);
|
||||
_glfw.x11.clipboardString = copy;
|
||||
|
||||
XSetSelectionOwner(_glfw.x11.display,
|
||||
_glfw.x11.CLIPBOARD,
|
||||
@@ -2870,7 +2982,7 @@ GLFWAPI Window glfwGetX11Window(GLFWwindow* handle)
|
||||
return window->x11.handle;
|
||||
}
|
||||
|
||||
GLFWAPI int glfwGetXKBScancode(const char* keyName, bool caseSensitive) {
|
||||
GLFWAPI int glfwGetNativeKeyForName(const char* keyName, bool caseSensitive) {
|
||||
return glfw_xkb_keysym_from_name(keyName, caseSensitive);
|
||||
}
|
||||
|
||||
|
||||
123
glfw/xkb_glfw.c
vendored
123
glfw/xkb_glfw.c
vendored
@@ -61,6 +61,7 @@ START_ALLOW_CASE_RANGE
|
||||
S(bracketleft, LEFT_BRACKET); \
|
||||
S(backslash, BACKSLASH); \
|
||||
S(bracketright, RIGHT_BRACKET); \
|
||||
S(asciicircum, CIRCUMFLEX); \
|
||||
S(underscore, UNDERSCORE); \
|
||||
S(grave, GRAVE_ACCENT); \
|
||||
R(a, z, A, Z); \
|
||||
@@ -437,24 +438,22 @@ glfw_xkb_update_modifiers(_GLFWXKBData *xkb, xkb_mod_mask_t depressed, xkb_mod_m
|
||||
}
|
||||
|
||||
bool
|
||||
glfw_xkb_should_repeat(_GLFWXKBData *xkb, xkb_keycode_t scancode) {
|
||||
glfw_xkb_should_repeat(_GLFWXKBData *xkb, xkb_keycode_t keycode) {
|
||||
#ifdef _GLFW_WAYLAND
|
||||
scancode += 8;
|
||||
keycode += 8;
|
||||
#endif
|
||||
return xkb_keymap_key_repeats(xkb->keymap, scancode);
|
||||
return xkb_keymap_key_repeats(xkb->keymap, keycode);
|
||||
}
|
||||
|
||||
|
||||
static KeyEvent key_event = {0};
|
||||
|
||||
static inline xkb_keysym_t
|
||||
compose_symbol(struct xkb_compose_state *composeState, xkb_keysym_t sym, int *compose_completed) {
|
||||
compose_symbol(struct xkb_compose_state *composeState, xkb_keysym_t sym, int *compose_completed, char *key_text, int n) {
|
||||
*compose_completed = 0;
|
||||
if (sym == XKB_KEY_NoSymbol || !composeState) return sym;
|
||||
if (xkb_compose_state_feed(composeState, sym) != XKB_COMPOSE_FEED_ACCEPTED) return sym;
|
||||
switch (xkb_compose_state_get_status(composeState)) {
|
||||
case XKB_COMPOSE_COMPOSED:
|
||||
xkb_compose_state_get_utf8(composeState, key_event.text, sizeof(key_event.text));
|
||||
xkb_compose_state_get_utf8(composeState, key_text, n);
|
||||
*compose_completed = 1;
|
||||
return xkb_compose_state_get_one_sym(composeState);
|
||||
case XKB_COMPOSE_COMPOSING:
|
||||
@@ -534,11 +533,14 @@ glfw_xkb_update_ime_state(_GLFWwindow *w, _GLFWXKBData *xkb, int which, int a, i
|
||||
}
|
||||
|
||||
void
|
||||
glfw_xkb_key_from_ime(KeyEvent *ev, bool handled_by_ime, bool failed) {
|
||||
glfw_xkb_key_from_ime(_GLFWIBUSKeyEvent *ev, bool handled_by_ime, bool failed) {
|
||||
_GLFWwindow *window = _glfwWindowForId(ev->window_id);
|
||||
if (failed && window && window->callbacks.keyboard) {
|
||||
// notify application to remove any existing pre-edit text
|
||||
window->callbacks.keyboard((GLFWwindow*) window, GLFW_KEY_UNKNOWN, 0, GLFW_PRESS, 0, "", 1);
|
||||
GLFWkeyevent fake_ev;
|
||||
_glfwInitializeKeyEvent(&fake_ev, GLFW_KEY_UNKNOWN, 0, GLFW_PRESS, 0);
|
||||
fake_ev.ime_state = 1;
|
||||
window->callbacks.keyboard((GLFWwindow*) window, &fake_ev);
|
||||
}
|
||||
static xkb_keycode_t last_handled_press_keycode = 0;
|
||||
// We filter out release events that correspond to the last press event
|
||||
@@ -547,53 +549,58 @@ glfw_xkb_key_from_ime(KeyEvent *ev, bool handled_by_ime, bool failed) {
|
||||
// you'd need to implement a ring buffer to store pending key presses.
|
||||
xkb_keycode_t prev_handled_press = last_handled_press_keycode;
|
||||
last_handled_press_keycode = 0;
|
||||
bool is_release = ev->action == GLFW_RELEASE;
|
||||
debug("From IBUS: scancode: 0x%x name: %s is_release: %d\n", ev->keycode, glfw_xkb_keysym_name(ev->keysym), is_release);
|
||||
if (window && !handled_by_ime && !(is_release && ev->keycode == prev_handled_press)) {
|
||||
bool is_release = ev->glfw_ev.action == GLFW_RELEASE;
|
||||
debug("From IBUS: native_key: 0x%x name: %s is_release: %d\n", ev->glfw_ev.native_key, glfw_xkb_keysym_name(ev->glfw_ev.key), is_release);
|
||||
if (window && !handled_by_ime && !(is_release && ev->glfw_ev.native_key == (int) prev_handled_press)) {
|
||||
debug("↳ to application: glfw_keycode: 0x%x (%s) keysym: 0x%x (%s) action: %s %s text: %s\n",
|
||||
ev->glfw_keycode, _glfwGetKeyName(ev->glfw_keycode), ev->keysym, glfw_xkb_keysym_name(ev->keysym),
|
||||
(ev->action == GLFW_RELEASE ? "RELEASE" : (ev->action == GLFW_PRESS ? "PRESS" : "REPEAT")),
|
||||
format_mods(ev->glfw_modifiers), ev->text
|
||||
ev->glfw_ev.native_key, _glfwGetKeyName(ev->glfw_ev.native_key), ev->glfw_ev.key, glfw_xkb_keysym_name(ev->glfw_ev.key),
|
||||
(ev->glfw_ev.action == GLFW_RELEASE ? "RELEASE" : (ev->glfw_ev.action == GLFW_PRESS ? "PRESS" : "REPEAT")),
|
||||
format_mods(ev->glfw_ev.mods), ev->glfw_ev.text
|
||||
);
|
||||
_glfwInputKeyboard(window, ev->glfw_keycode, ev->keysym, ev->action, ev->glfw_modifiers, ev->text, 0);
|
||||
|
||||
ev->glfw_ev.ime_state = 0;
|
||||
_glfwInputKeyboard(window, &ev->glfw_ev);
|
||||
} else debug("↳ discarded\n");
|
||||
if (!is_release && handled_by_ime) last_handled_press_keycode = ev->keycode;
|
||||
if (!is_release && handled_by_ime)
|
||||
last_handled_press_keycode = ev->glfw_ev.native_key;
|
||||
}
|
||||
|
||||
void
|
||||
glfw_xkb_handle_key_event(_GLFWwindow *window, _GLFWXKBData *xkb, xkb_keycode_t scancode, int action) {
|
||||
glfw_xkb_handle_key_event(_GLFWwindow *window, _GLFWXKBData *xkb, xkb_keycode_t xkb_keycode, int action) {
|
||||
static char key_text[64] = {0};
|
||||
const xkb_keysym_t *syms, *clean_syms, *default_syms;
|
||||
xkb_keysym_t glfw_sym;
|
||||
xkb_keycode_t code_for_sym = scancode;
|
||||
key_event.ibus_keycode = scancode;
|
||||
xkb_keysym_t xkb_sym;
|
||||
xkb_keycode_t code_for_sym = xkb_keycode, ibus_keycode = xkb_keycode;
|
||||
GLFWkeyevent glfw_ev;
|
||||
_glfwInitializeKeyEvent(&glfw_ev, GLFW_KEY_UNKNOWN, 0, GLFW_PRESS, 0); // init with default values
|
||||
#ifdef _GLFW_WAYLAND
|
||||
code_for_sym += 8;
|
||||
#else
|
||||
key_event.ibus_keycode -= 8;
|
||||
ibus_keycode -= 8;
|
||||
#endif
|
||||
debug("%s scancode: 0x%x ", action == GLFW_RELEASE ? "Release" : "Press", scancode);
|
||||
debug("%s xkb_keycode: 0x%x ", action == GLFW_RELEASE ? "Release" : "Press", xkb_keycode);
|
||||
XKBStateGroup *sg = &xkb->states;
|
||||
int num_syms = xkb_state_key_get_syms(sg->state, code_for_sym, &syms);
|
||||
int num_clean_syms = xkb_state_key_get_syms(sg->clean_state, code_for_sym, &clean_syms);
|
||||
key_event.text[0] = 0;
|
||||
key_text[0] = 0;
|
||||
// According to the documentation of xkb_compose_state_feed it does not
|
||||
// support multi-sym events, so we ignore them
|
||||
if (num_syms != 1 || num_clean_syms != 1) {
|
||||
debug("num_syms: %d num_clean_syms: %d ignoring event\n", num_syms, num_clean_syms);
|
||||
return;
|
||||
}
|
||||
glfw_sym = clean_syms[0];
|
||||
xkb_sym = clean_syms[0];
|
||||
debug("clean_sym: %s ", glfw_xkb_keysym_name(clean_syms[0]));
|
||||
if (action == GLFW_PRESS || action == GLFW_REPEAT) {
|
||||
const char *text_type = "composed_text";
|
||||
int compose_completed;
|
||||
glfw_sym = compose_symbol(sg->composeState, syms[0], &compose_completed);
|
||||
if (glfw_sym == XKB_KEY_NoSymbol && !compose_completed) {
|
||||
xkb_sym = compose_symbol(sg->composeState, syms[0], &compose_completed, key_text, sizeof(key_text));
|
||||
if (xkb_sym == XKB_KEY_NoSymbol && !compose_completed) {
|
||||
debug("compose not complete, ignoring.\n");
|
||||
return;
|
||||
}
|
||||
debug("composed_sym: %s ", glfw_xkb_keysym_name(glfw_sym));
|
||||
if (glfw_sym == syms[0]) { // composed sym is the same as non-composed sym
|
||||
debug("composed_sym: %s ", glfw_xkb_keysym_name(xkb_sym));
|
||||
if (xkb_sym == syms[0]) { // composed sym is the same as non-composed sym
|
||||
// Only use the clean_sym if no mods other than the mods we report
|
||||
// are active (for example if ISO_Shift_Level_* mods are active
|
||||
// they are not reported by GLFW so the key should be the shifted
|
||||
@@ -601,37 +608,61 @@ glfw_xkb_handle_key_event(_GLFWwindow *window, _GLFWXKBData *xkb, xkb_keycode_t
|
||||
xkb_mod_mask_t consumed_unknown_mods = xkb_state_key_get_consumed_mods(sg->state, code_for_sym) & sg->activeUnknownModifiers;
|
||||
if (sg->activeUnknownModifiers) debug("%s", format_xkb_mods(xkb, "active_unknown_mods", sg->activeUnknownModifiers));
|
||||
if (consumed_unknown_mods) { debug("%s", format_xkb_mods(xkb, "consumed_unknown_mods", consumed_unknown_mods)); }
|
||||
else glfw_sym = clean_syms[0];
|
||||
else xkb_sym = clean_syms[0];
|
||||
// xkb returns text even if alt and/or super are pressed
|
||||
if ( ((GLFW_MOD_CONTROL | GLFW_MOD_ALT | GLFW_MOD_SUPER) & sg->modifiers) == 0) xkb_state_key_get_utf8(sg->state, code_for_sym, key_event.text, sizeof(key_event.text));
|
||||
if ( ((GLFW_MOD_CONTROL | GLFW_MOD_ALT | GLFW_MOD_SUPER) & sg->modifiers) == 0) {
|
||||
xkb_state_key_get_utf8(sg->state, code_for_sym, key_text, sizeof(key_text));
|
||||
}
|
||||
text_type = "text";
|
||||
}
|
||||
if ((1 <= key_event.text[0] && key_event.text[0] <= 31) || key_event.text[0] == 127) key_event.text[0] = 0; // don't send text for ascii control codes
|
||||
if (key_event.text[0]) { debug("%s: %s ", text_type, key_event.text); }
|
||||
if ((1 <= key_text[0] && key_text[0] <= 31) || key_text[0] == 127) {
|
||||
key_text[0] = 0; // don't send text for ascii control codes
|
||||
}
|
||||
if (key_text[0]) { debug("%s: %s ", text_type, key_text); }
|
||||
}
|
||||
int glfw_keycode = glfw_key_for_sym(glfw_sym);
|
||||
if (xkb_sym == XKB_KEY_ISO_First_Group || xkb_sym == XKB_KEY_ISO_Last_Group || xkb_sym == XKB_KEY_ISO_Next_Group || xkb_sym == XKB_KEY_ISO_Prev_Group || xkb_sym == XKB_KEY_Mode_switch) {
|
||||
return;
|
||||
}
|
||||
int glfw_sym = glfw_key_for_sym(xkb_sym);
|
||||
bool is_fallback = false;
|
||||
if (glfw_keycode == GLFW_KEY_UNKNOWN && !key_event.text[0]) {
|
||||
if (glfw_sym == GLFW_KEY_UNKNOWN && !key_text[0]) {
|
||||
int num_default_syms = xkb_state_key_get_syms(sg->default_state, code_for_sym, &default_syms);
|
||||
if (num_default_syms > 0) {
|
||||
glfw_sym = default_syms[0];
|
||||
glfw_keycode = glfw_key_for_sym(glfw_sym);
|
||||
xkb_sym = default_syms[0];
|
||||
glfw_sym = glfw_key_for_sym(xkb_sym);
|
||||
is_fallback = true;
|
||||
}
|
||||
}
|
||||
debug(
|
||||
"%s%s: %d (%s) xkb_key: %d (%s)\n",
|
||||
format_mods(sg->modifiers),
|
||||
is_fallback ? "glfw_fallback_key" : "glfw_key", glfw_keycode, _glfwGetKeyName(glfw_keycode),
|
||||
glfw_sym, glfw_xkb_keysym_name(glfw_sym)
|
||||
is_fallback ? "glfw_fallback_key" : "glfw_key", glfw_sym, _glfwGetKeyName(glfw_sym),
|
||||
xkb_sym, glfw_xkb_keysym_name(xkb_sym)
|
||||
);
|
||||
key_event.action = action; key_event.glfw_modifiers = sg->modifiers;
|
||||
key_event.keycode = scancode; key_event.keysym = glfw_sym;
|
||||
key_event.window_id = window->id; key_event.glfw_keycode = glfw_keycode;
|
||||
key_event.ibus_sym = syms[0];
|
||||
if (ibus_process_key(&key_event, &xkb->ibus)) {
|
||||
debug("↳ to IBUS: keycode: 0x%x keysym: 0x%x (%s) %s\n", key_event.ibus_keycode, key_event.ibus_sym, glfw_xkb_keysym_name(key_event.ibus_sym), format_mods(key_event.glfw_modifiers));
|
||||
|
||||
// NOTE: On linux, the reported native key identifier is the XKB keysym value.
|
||||
// Do not confuse `native_key` with `xkb_keycode` (the native keycode reported for the
|
||||
// glfw event VS the X internal code for a key).
|
||||
//
|
||||
// We use the XKB keysym instead of the X keycode to be able to go back-and-forth between
|
||||
// the GLFW keysym and the XKB keysym when needed, which is not possible using the X keycode,
|
||||
// because of the lost information when resolving the keycode to the keysym, like consumed
|
||||
// mods.
|
||||
glfw_ev.native_key = xkb_sym;
|
||||
|
||||
glfw_ev.action = action;
|
||||
glfw_ev.key = glfw_sym;
|
||||
glfw_ev.mods = sg->modifiers;
|
||||
glfw_ev.text = key_text;
|
||||
|
||||
_GLFWIBUSKeyEvent ibus_ev;
|
||||
ibus_ev.glfw_ev = glfw_ev;
|
||||
ibus_ev.ibus_keycode = ibus_keycode;
|
||||
ibus_ev.window_id = window->id;
|
||||
ibus_ev.ibus_keysym = syms[0];
|
||||
if (ibus_process_key(&ibus_ev, &xkb->ibus)) {
|
||||
debug("↳ to IBUS: keycode: 0x%x keysym: 0x%x (%s) %s\n", ibus_ev.ibus_keycode, ibus_ev.ibus_keysym, glfw_xkb_keysym_name(ibus_ev.ibus_keysym), format_mods(ibus_ev.glfw_ev.mods));
|
||||
} else {
|
||||
_glfwInputKeyboard(window, glfw_keycode, glfw_sym, action, sg->modifiers, key_event.text, 0);
|
||||
_glfwInputKeyboard(window, &glfw_ev);
|
||||
}
|
||||
}
|
||||
|
||||
6
glfw/xkb_glfw.h
vendored
6
glfw/xkb_glfw.h
vendored
@@ -87,10 +87,10 @@ void glfw_xkb_release(_GLFWXKBData *xkb);
|
||||
bool glfw_xkb_create_context(_GLFWXKBData *xkb);
|
||||
bool glfw_xkb_compile_keymap(_GLFWXKBData *xkb, const char *map_str);
|
||||
void glfw_xkb_update_modifiers(_GLFWXKBData *xkb, xkb_mod_mask_t depressed, xkb_mod_mask_t latched, xkb_mod_mask_t locked, xkb_layout_index_t base_group, xkb_layout_index_t latched_group, xkb_layout_index_t locked_group);
|
||||
bool glfw_xkb_should_repeat(_GLFWXKBData *xkb, xkb_keycode_t scancode);
|
||||
bool glfw_xkb_should_repeat(_GLFWXKBData *xkb, xkb_keycode_t keycode);
|
||||
const char* glfw_xkb_keysym_name(xkb_keysym_t sym);
|
||||
xkb_keysym_t glfw_xkb_sym_for_key(int key);
|
||||
void glfw_xkb_handle_key_event(_GLFWwindow *window, _GLFWXKBData *xkb, xkb_keycode_t scancode, int action);
|
||||
void glfw_xkb_handle_key_event(_GLFWwindow *window, _GLFWXKBData *xkb, xkb_keycode_t keycode, int action);
|
||||
int glfw_xkb_keysym_from_name(const char *name, bool case_sensitive);
|
||||
void glfw_xkb_update_ime_state(_GLFWwindow *w, _GLFWXKBData *xkb, int which, int a, int b, int c, int d);
|
||||
void glfw_xkb_key_from_ime(KeyEvent *ev, bool handled_by_ime, bool failed);
|
||||
void glfw_xkb_key_from_ime(_GLFWIBUSKeyEvent *ev, bool handled_by_ime, bool failed);
|
||||
|
||||
@@ -4,27 +4,34 @@
|
||||
|
||||
import os
|
||||
from contextlib import suppress
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
|
||||
from kitty.cli import parse_args
|
||||
from kitty.cli_stub import AskCLIOptions
|
||||
from kitty.constants import cache_dir
|
||||
from kitty.typing import BossType
|
||||
|
||||
from ..tui.handler import result_handler
|
||||
from ..tui.operations import alternate_screen, styled
|
||||
|
||||
readline = None
|
||||
if TYPE_CHECKING:
|
||||
import readline
|
||||
else:
|
||||
readline = None
|
||||
|
||||
|
||||
def get_history_items():
|
||||
def get_history_items() -> List[str]:
|
||||
return list(map(readline.get_history_item, range(1, readline.get_current_history_length() + 1)))
|
||||
|
||||
|
||||
def sort_key(item):
|
||||
def sort_key(item: str) -> Tuple[int, str]:
|
||||
return len(item), item.lower()
|
||||
|
||||
|
||||
class HistoryCompleter:
|
||||
|
||||
def __init__(self, name=None):
|
||||
self.matches = []
|
||||
def __init__(self, name: Optional[str] = None):
|
||||
self.matches: List[str] = []
|
||||
self.history_path = None
|
||||
if name:
|
||||
ddir = os.path.join(cache_dir(), 'ask')
|
||||
@@ -32,7 +39,7 @@ class HistoryCompleter:
|
||||
os.makedirs(ddir)
|
||||
self.history_path = os.path.join(ddir, name)
|
||||
|
||||
def complete(self, text, state):
|
||||
def complete(self, text: str, state: int) -> Optional[str]:
|
||||
response = None
|
||||
if state == 0:
|
||||
history_values = get_history_items()
|
||||
@@ -47,19 +54,19 @@ class HistoryCompleter:
|
||||
response = None
|
||||
return response
|
||||
|
||||
def __enter__(self):
|
||||
def __enter__(self) -> 'HistoryCompleter':
|
||||
if self.history_path:
|
||||
with suppress(Exception):
|
||||
readline.read_history_file(self.history_path)
|
||||
readline.set_completer(self.complete)
|
||||
return self
|
||||
|
||||
def __exit__(self, *a):
|
||||
def __exit__(self, *a: object) -> None:
|
||||
if self.history_path:
|
||||
readline.write_history_file(self.history_path)
|
||||
|
||||
|
||||
def option_text():
|
||||
def option_text() -> str:
|
||||
return '''\
|
||||
--type -t
|
||||
choices=line
|
||||
@@ -78,7 +85,18 @@ be used for completions and via the browse history readline bindings.
|
||||
'''
|
||||
|
||||
|
||||
def main(args):
|
||||
try:
|
||||
from typing import TypedDict
|
||||
except ImportError:
|
||||
TypedDict = dict
|
||||
|
||||
|
||||
class Response(TypedDict):
|
||||
items: List[str]
|
||||
response: Optional[str]
|
||||
|
||||
|
||||
def main(args: List[str]) -> Response:
|
||||
# For some reason importing readline in a key handler in the main kitty process
|
||||
# causes a crash of the python interpreter, probably because of some global
|
||||
# lock
|
||||
@@ -88,7 +106,7 @@ def main(args):
|
||||
from kitty.shell import init_readline
|
||||
msg = 'Ask the user for input'
|
||||
try:
|
||||
args, items = parse_args(args[1:], option_text, '', msg, 'kitty ask')
|
||||
cli_opts, items = parse_args(args[1:], option_text, '', msg, 'kitty ask', result_class=AskCLIOptions)
|
||||
except SystemExit as e:
|
||||
if e.code != 0:
|
||||
print(e.args[0])
|
||||
@@ -96,20 +114,21 @@ def main(args):
|
||||
raise SystemExit(e.code)
|
||||
|
||||
init_readline(readline)
|
||||
ans = {'items': items}
|
||||
response = None
|
||||
|
||||
with alternate_screen(), HistoryCompleter(args.name):
|
||||
if args.message:
|
||||
print(styled(args.message, bold=True))
|
||||
with alternate_screen(), HistoryCompleter(cli_opts.name):
|
||||
if cli_opts.message:
|
||||
print(styled(cli_opts.message, bold=True))
|
||||
|
||||
prompt = '> '
|
||||
with suppress(KeyboardInterrupt, EOFError):
|
||||
ans['response'] = input(prompt)
|
||||
return ans
|
||||
response = input(prompt)
|
||||
return {'items': items, 'response': response}
|
||||
|
||||
|
||||
def handle_result(args, data, target_window_id, boss):
|
||||
if 'response' in data:
|
||||
@result_handler()
|
||||
def handle_result(args: List[str], data: Response, target_window_id: int, boss: BossType) -> None:
|
||||
if data['response'] is not None:
|
||||
func, *args = data['items']
|
||||
getattr(boss, func)(data['response'], *args)
|
||||
|
||||
|
||||
@@ -3,39 +3,40 @@
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import sys
|
||||
from typing import Iterable, List, Union
|
||||
|
||||
from kitty.key_encoding import KeyEvent
|
||||
|
||||
from . import subseq_matcher
|
||||
from ..tui.handler import Handler
|
||||
from ..tui.loop import Loop
|
||||
|
||||
from . import subseq_matcher
|
||||
|
||||
|
||||
def match(
|
||||
input_data,
|
||||
query,
|
||||
threads=0,
|
||||
positions=False,
|
||||
level1='/',
|
||||
level2='-_0123456789',
|
||||
level3='.',
|
||||
limit=0,
|
||||
mark_before='',
|
||||
mark_after='',
|
||||
delimiter='\n'
|
||||
):
|
||||
input_data: Union[str, bytes, Iterable[Union[str, bytes]]],
|
||||
query: str,
|
||||
threads: int = 0,
|
||||
positions: bool = False,
|
||||
level1: str = '/',
|
||||
level2: str = '-_0123456789',
|
||||
level3: str = '.',
|
||||
limit: int = 0,
|
||||
mark_before: str = '',
|
||||
mark_after: str = '',
|
||||
delimiter: str = '\n'
|
||||
) -> List[str]:
|
||||
if isinstance(input_data, str):
|
||||
input_data = input_data.encode('utf-8')
|
||||
if isinstance(input_data, bytes):
|
||||
input_data = input_data.split(delimiter.encode('utf-8'))
|
||||
idata = [x.encode('utf-8') for x in input_data.split(delimiter)]
|
||||
elif isinstance(input_data, bytes):
|
||||
idata = input_data.split(delimiter.encode('utf-8'))
|
||||
else:
|
||||
input_data = [x.encode('utf-8') if isinstance(x, str) else x for x in input_data]
|
||||
idata = [x.encode('utf-8') if isinstance(x, str) else x for x in input_data]
|
||||
query = query.lower()
|
||||
level1 = level1.lower()
|
||||
level2 = level2.lower()
|
||||
level3 = level3.lower()
|
||||
data = subseq_matcher.match(
|
||||
input_data, (level1, level2, level3), query,
|
||||
idata, (level1, level2, level3), query,
|
||||
positions, limit, threads,
|
||||
mark_before, mark_after, delimiter)
|
||||
if data is None:
|
||||
@@ -45,23 +46,23 @@ def match(
|
||||
|
||||
class ChooseHandler(Handler):
|
||||
|
||||
def initialize(self):
|
||||
def initialize(self) -> None:
|
||||
pass
|
||||
|
||||
def on_text(self, text, in_bracketed_paste=False):
|
||||
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
|
||||
pass
|
||||
|
||||
def on_key(self, key_event):
|
||||
def on_key(self, key_event: KeyEvent) -> None:
|
||||
pass
|
||||
|
||||
def on_interrupt(self):
|
||||
def on_interrupt(self) -> None:
|
||||
self.quit_loop(1)
|
||||
|
||||
def on_eot(self):
|
||||
def on_eot(self) -> None:
|
||||
self.quit_loop(1)
|
||||
|
||||
|
||||
def main(args):
|
||||
def main(args: List[str]) -> None:
|
||||
loop = Loop()
|
||||
handler = ChooseHandler()
|
||||
loop.loop(handler)
|
||||
|
||||
9
kittens/choose/subseq_matcher.pyi
Normal file
9
kittens/choose/subseq_matcher.pyi
Normal file
@@ -0,0 +1,9 @@
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
|
||||
def match(
|
||||
lines: List[bytes], levels: Tuple[str, str, str], needle: str,
|
||||
output_positions: bool, limit: int, num_threads: int, mark_before: str,
|
||||
mark_after: str, delimiter: str
|
||||
) -> Optional[str]:
|
||||
pass
|
||||
@@ -4,8 +4,10 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import List, NoReturn, Optional
|
||||
|
||||
from kitty.cli import parse_args
|
||||
from kitty.cli_stub import ClipboardCLIOptions
|
||||
|
||||
from ..tui.handler import Handler
|
||||
from ..tui.loop import Loop
|
||||
@@ -13,12 +15,12 @@ from ..tui.loop import Loop
|
||||
|
||||
class Clipboard(Handler):
|
||||
|
||||
def __init__(self, data_to_send, args):
|
||||
def __init__(self, data_to_send: Optional[bytes], args: ClipboardCLIOptions):
|
||||
self.args = args
|
||||
self.clipboard_contents = None
|
||||
self.clipboard_contents: Optional[str] = None
|
||||
self.data_to_send = data_to_send
|
||||
|
||||
def initialize(self):
|
||||
def initialize(self) -> None:
|
||||
if self.data_to_send is not None:
|
||||
self.cmd.write_to_clipboard(self.data_to_send, self.args.use_primary)
|
||||
if not self.args.get_clipboard:
|
||||
@@ -32,17 +34,17 @@ class Clipboard(Handler):
|
||||
return
|
||||
self.cmd.request_from_clipboard(self.args.use_primary)
|
||||
|
||||
def on_clipboard_response(self, text, from_primary=False):
|
||||
def on_clipboard_response(self, text: str, from_primary: bool = False) -> None:
|
||||
self.clipboard_contents = text
|
||||
self.quit_loop(0)
|
||||
|
||||
def on_capability_response(self, name, val):
|
||||
def on_capability_response(self, name: str, val: str) -> None:
|
||||
self.quit_loop(0)
|
||||
|
||||
def on_interrupt(self):
|
||||
def on_interrupt(self) -> None:
|
||||
self.quit_loop(1)
|
||||
|
||||
def on_eot(self):
|
||||
def on_eot(self) -> None:
|
||||
self.quit_loop(1)
|
||||
|
||||
|
||||
@@ -80,16 +82,16 @@ To set the clipboard text, pipe in the new text on stdin. Use the
|
||||
usage = ''
|
||||
|
||||
|
||||
def main(args):
|
||||
args, items = parse_args(args[1:], OPTIONS, usage, help_text, 'kitty +kitten clipboard')
|
||||
def main(args: List[str]) -> NoReturn:
|
||||
cli_opts, items = parse_args(args[1:], OPTIONS, usage, help_text, 'kitty +kitten clipboard', result_class=ClipboardCLIOptions)
|
||||
if items:
|
||||
raise SystemExit('Unrecognized extra command line arguments')
|
||||
data = None
|
||||
data: Optional[bytes] = None
|
||||
if not sys.stdin.isatty():
|
||||
data = sys.stdin.buffer.read()
|
||||
sys.stdin = open(os.ctermid(), 'r')
|
||||
loop = Loop()
|
||||
handler = Clipboard(data, args)
|
||||
handler = Clipboard(data, cli_opts)
|
||||
loop.loop(handler)
|
||||
if loop.return_code == 0 and handler.clipboard_contents:
|
||||
sys.stdout.write(handler.clipboard_contents)
|
||||
@@ -100,6 +102,7 @@ def main(args):
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
elif __name__ == '__doc__':
|
||||
sys.cli_docs['usage'] = usage
|
||||
sys.cli_docs['options'] = OPTIONS
|
||||
sys.cli_docs['help_text'] = help_text
|
||||
cd = sys.cli_docs # type: ignore
|
||||
cd['usage'] = usage
|
||||
cd['options'] = OPTIONS
|
||||
cd['help_text'] = help_text
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
class GlobalData:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.title = ''
|
||||
self.cmd = ''
|
||||
|
||||
|
||||
global_data = GlobalData
|
||||
|
||||
@@ -4,86 +4,94 @@
|
||||
|
||||
import os
|
||||
import re
|
||||
from contextlib import suppress
|
||||
from functools import lru_cache
|
||||
from hashlib import md5
|
||||
from mimetypes import guess_type
|
||||
from contextlib import suppress
|
||||
from typing import TYPE_CHECKING, Dict, List, Set, Optional, Iterator, Tuple, Union
|
||||
|
||||
path_name_map = {}
|
||||
if TYPE_CHECKING:
|
||||
from .highlight import DiffHighlight # noqa
|
||||
|
||||
|
||||
path_name_map: Dict[str, str] = {}
|
||||
|
||||
|
||||
class Segment:
|
||||
|
||||
__slots__ = ('start', 'end', 'start_code', 'end_code')
|
||||
|
||||
def __init__(self, start, start_code):
|
||||
def __init__(self, start: int, start_code: str):
|
||||
self.start = start
|
||||
self.start_code = start_code
|
||||
self.end: Optional[int] = None
|
||||
self.end_code: Optional[str] = None
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return 'Segment(start={!r}, start_code={!r}, end={!r}, end_code={!r})'.format(
|
||||
self.start, self.start_code, getattr(self, 'end', None), getattr(self, 'end_code', None)
|
||||
)
|
||||
self.start, self.start_code, self.end, self.end_code)
|
||||
|
||||
|
||||
class Collection:
|
||||
|
||||
def __init__(self):
|
||||
self.changes = {}
|
||||
self.renames = {}
|
||||
self.adds = set()
|
||||
self.removes = set()
|
||||
self.all_paths = []
|
||||
self.type_map = {}
|
||||
def __init__(self) -> None:
|
||||
self.changes: Dict[str, str] = {}
|
||||
self.renames: Dict[str, str] = {}
|
||||
self.adds: Set[str] = set()
|
||||
self.removes: Set[str] = set()
|
||||
self.all_paths: List[str] = []
|
||||
self.type_map: Dict[str, str] = {}
|
||||
self.added_count = self.removed_count = 0
|
||||
|
||||
def add_change(self, left_path, right_path):
|
||||
def add_change(self, left_path: str, right_path: str) -> None:
|
||||
self.changes[left_path] = right_path
|
||||
self.all_paths.append(left_path)
|
||||
self.type_map[left_path] = 'diff'
|
||||
|
||||
def add_rename(self, left_path, right_path):
|
||||
def add_rename(self, left_path: str, right_path: str) -> None:
|
||||
self.renames[left_path] = right_path
|
||||
self.all_paths.append(left_path)
|
||||
self.type_map[left_path] = 'rename'
|
||||
|
||||
def add_add(self, right_path):
|
||||
def add_add(self, right_path: str) -> None:
|
||||
self.adds.add(right_path)
|
||||
self.all_paths.append(right_path)
|
||||
self.type_map[right_path] = 'add'
|
||||
if isinstance(data_for_path(right_path), str):
|
||||
self.added_count += len(lines_for_path(right_path))
|
||||
|
||||
def add_removal(self, left_path):
|
||||
def add_removal(self, left_path: str) -> None:
|
||||
self.removes.add(left_path)
|
||||
self.all_paths.append(left_path)
|
||||
self.type_map[left_path] = 'removal'
|
||||
if isinstance(data_for_path(left_path), str):
|
||||
self.removed_count += len(lines_for_path(left_path))
|
||||
|
||||
def finalize(self):
|
||||
def finalize(self) -> None:
|
||||
self.all_paths.sort(key=path_name_map.get)
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> Iterator[Tuple[str, str, Optional[str]]]:
|
||||
for path in self.all_paths:
|
||||
typ = self.type_map[path]
|
||||
if typ == 'diff':
|
||||
data = self.changes[path]
|
||||
data: Optional[str] = self.changes[path]
|
||||
elif typ == 'rename':
|
||||
data = self.renames[path]
|
||||
else:
|
||||
data = None
|
||||
yield path, typ, data
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
return len(self.all_paths)
|
||||
|
||||
|
||||
def collect_files(collection, left, right):
|
||||
left_names, right_names = set(), set()
|
||||
left_path_map, right_path_map = {}, {}
|
||||
def collect_files(collection: Collection, left: str, right: str) -> None:
|
||||
left_names: Set[str] = set()
|
||||
right_names: Set[str] = set()
|
||||
left_path_map: Dict[str, str] = {}
|
||||
right_path_map: Dict[str, str] = {}
|
||||
|
||||
def walk(base, names, pmap):
|
||||
def walk(base: str, names: Set[str], pmap: Dict[str, str]) -> None:
|
||||
for dirpath, dirnames, filenames in os.walk(base):
|
||||
for filename in filenames:
|
||||
path = os.path.abspath(os.path.join(dirpath, filename))
|
||||
@@ -91,7 +99,8 @@ def collect_files(collection, left, right):
|
||||
names.add(name)
|
||||
pmap[name] = path
|
||||
|
||||
walk(left, left_names, left_path_map), walk(right, right_names, right_path_map)
|
||||
walk(left, left_names, left_path_map)
|
||||
walk(right, right_names, right_path_map)
|
||||
common_names = left_names & right_names
|
||||
changed_names = {n for n in common_names if data_for_path(left_path_map[n]) != data_for_path(right_path_map[n])}
|
||||
for n in changed_names:
|
||||
@@ -117,50 +126,56 @@ def collect_files(collection, left, right):
|
||||
sanitize_pat = re.compile('[\x00-\x09\x0b-\x1f\x7f\x80-\x9f]')
|
||||
|
||||
|
||||
def sanitize(text):
|
||||
def sanitize(text: str) -> str:
|
||||
ntext = text.replace('\r\n', '⏎\n')
|
||||
return sanitize_pat.sub('░', ntext)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def mime_type_for_path(path):
|
||||
def mime_type_for_path(path: str) -> str:
|
||||
return guess_type(path)[0] or 'application/octet-stream'
|
||||
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def raw_data_for_path(path):
|
||||
def raw_data_for_path(path: str) -> bytes:
|
||||
with open(path, 'rb') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def is_image(path):
|
||||
return mime_type_for_path(path).startswith('image/')
|
||||
def is_image(path: Optional[str]) -> bool:
|
||||
return mime_type_for_path(path).startswith('image/') if path else False
|
||||
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def data_for_path(path):
|
||||
ans = raw_data_for_path(path)
|
||||
def data_for_path(path: str) -> Union[str, bytes]:
|
||||
raw_bytes = raw_data_for_path(path)
|
||||
if not is_image(path) and not os.path.samefile(path, os.devnull):
|
||||
with suppress(UnicodeDecodeError):
|
||||
ans = ans.decode('utf-8')
|
||||
return ans
|
||||
return raw_bytes.decode('utf-8')
|
||||
return raw_bytes
|
||||
|
||||
|
||||
class LinesForPath:
|
||||
|
||||
replace_tab_by = ' ' * 4
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def __call__(self, path: str) -> Tuple[str, ...]:
|
||||
data = data_for_path(path)
|
||||
assert isinstance(data, str)
|
||||
data = data.replace('\t', self.replace_tab_by)
|
||||
return tuple(sanitize(data).splitlines())
|
||||
|
||||
|
||||
lines_for_path = LinesForPath()
|
||||
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def lines_for_path(path):
|
||||
data = data_for_path(path).replace('\t', lines_for_path.replace_tab_by)
|
||||
return tuple(sanitize(data).splitlines())
|
||||
|
||||
|
||||
lines_for_path.replace_tab_by = ' ' * 4
|
||||
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def hash_for_path(path):
|
||||
def hash_for_path(path: str) -> bytes:
|
||||
return md5(raw_data_for_path(path)).digest()
|
||||
|
||||
|
||||
def create_collection(left, right):
|
||||
def create_collection(left: str, right: str) -> Collection:
|
||||
collection = Collection()
|
||||
if os.path.isdir(left):
|
||||
collect_files(collection, left, right)
|
||||
@@ -173,13 +188,13 @@ def create_collection(left, right):
|
||||
return collection
|
||||
|
||||
|
||||
highlight_data = {}
|
||||
highlight_data: Dict[str, 'DiffHighlight'] = {}
|
||||
|
||||
|
||||
def set_highlight_data(data):
|
||||
def set_highlight_data(data: Dict[str, 'DiffHighlight']) -> None:
|
||||
global highlight_data
|
||||
highlight_data = data
|
||||
|
||||
|
||||
def highlights_for_path(path):
|
||||
def highlights_for_path(path: str) -> 'DiffHighlight':
|
||||
return highlight_data.get(path, [])
|
||||
|
||||
@@ -3,27 +3,30 @@
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import os
|
||||
from typing import Any, Dict, Iterable, Optional, Tuple, Type, Union
|
||||
|
||||
from kitty.conf.utils import (
|
||||
init_config as _init_config, key_func, load_config as _load_config, merge_dicts,
|
||||
parse_config_base, parse_kittens_key, resolve_config
|
||||
)
|
||||
from kitty.conf.definition import config_lines
|
||||
from kitty.conf.utils import (
|
||||
init_config as _init_config, key_func, load_config as _load_config,
|
||||
merge_dicts, parse_config_base, parse_kittens_key, resolve_config
|
||||
)
|
||||
from kitty.constants import config_dir
|
||||
from kitty.options_stub import DiffOptions
|
||||
from kitty.cli_stub import DiffCLIOptions
|
||||
from kitty.rgb import color_as_sgr
|
||||
|
||||
from .config_data import type_map, all_options
|
||||
from .config_data import all_options, type_convert
|
||||
|
||||
defaults = None
|
||||
defaults: Optional[DiffOptions] = None
|
||||
|
||||
formats = {
|
||||
formats: Dict[str, str] = {
|
||||
'title': '',
|
||||
'margin': '',
|
||||
'text': '',
|
||||
}
|
||||
|
||||
|
||||
def set_formats(opts):
|
||||
def set_formats(opts: DiffOptions) -> None:
|
||||
formats['text'] = '48' + color_as_sgr(opts.background)
|
||||
formats['title'] = '38' + color_as_sgr(opts.title_fg) + ';48' + color_as_sgr(opts.title_bg) + ';1'
|
||||
formats['margin'] = '38' + color_as_sgr(opts.margin_fg) + ';48' + color_as_sgr(opts.margin_bg)
|
||||
@@ -32,6 +35,7 @@ def set_formats(opts):
|
||||
formats['added'] = '48' + color_as_sgr(opts.added_bg)
|
||||
formats['removed'] = '48' + color_as_sgr(opts.removed_bg)
|
||||
formats['filler'] = '48' + color_as_sgr(opts.filler_bg)
|
||||
formats['margin_filler'] = '48' + color_as_sgr(opts.margin_filler_bg or opts.filler_bg)
|
||||
formats['hunk_margin'] = '38' + color_as_sgr(opts.margin_fg) + ';48' + color_as_sgr(opts.hunk_margin_bg)
|
||||
formats['hunk'] = '38' + color_as_sgr(opts.margin_fg) + ';48' + color_as_sgr(opts.hunk_bg)
|
||||
formats['removed_highlight'] = '48' + color_as_sgr(opts.highlight_removed_bg)
|
||||
@@ -42,7 +46,7 @@ func_with_args, args_funcs = key_func()
|
||||
|
||||
|
||||
@func_with_args('scroll_by')
|
||||
def parse_scroll_by(func, rest):
|
||||
def parse_scroll_by(func: str, rest: str) -> Tuple[str, int]:
|
||||
try:
|
||||
return func, int(rest)
|
||||
except Exception:
|
||||
@@ -50,7 +54,7 @@ def parse_scroll_by(func, rest):
|
||||
|
||||
|
||||
@func_with_args('scroll_to')
|
||||
def parse_scroll_to(func, rest):
|
||||
def parse_scroll_to(func: str, rest: str) -> Tuple[str, str]:
|
||||
rest = rest.lower()
|
||||
if rest not in {'start', 'end', 'next-change', 'prev-change', 'next-page', 'prev-page', 'next-match', 'prev-match'}:
|
||||
rest = 'start'
|
||||
@@ -58,7 +62,7 @@ def parse_scroll_to(func, rest):
|
||||
|
||||
|
||||
@func_with_args('change_context')
|
||||
def parse_change_context(func, rest):
|
||||
def parse_change_context(func: str, rest: str) -> Tuple[str, Union[int, str]]:
|
||||
rest = rest.lower()
|
||||
if rest in {'all', 'default'}:
|
||||
return func, rest
|
||||
@@ -70,26 +74,29 @@ def parse_change_context(func, rest):
|
||||
|
||||
|
||||
@func_with_args('start_search')
|
||||
def parse_start_search(func, rest):
|
||||
rest = rest.lower().split()
|
||||
is_regex = rest and rest[0] == 'regex'
|
||||
is_backward = len(rest) > 1 and rest[1] == 'backward'
|
||||
def parse_start_search(func: str, rest: str) -> Tuple[str, Tuple[bool, bool]]:
|
||||
rest_ = rest.lower().split()
|
||||
is_regex = bool(rest_ and rest_[0] == 'regex')
|
||||
is_backward = bool(len(rest_) > 1 and rest_[1] == 'backward')
|
||||
return func, (is_regex, is_backward)
|
||||
|
||||
|
||||
def special_handling(key, val, ans):
|
||||
def special_handling(key: str, val: str, ans: Dict) -> bool:
|
||||
if key == 'map':
|
||||
action, *key_def = parse_kittens_key(val, args_funcs)
|
||||
ans['key_definitions'][tuple(key_def)] = action
|
||||
return True
|
||||
x = parse_kittens_key(val, args_funcs)
|
||||
if x is not None:
|
||||
action, key_def = x
|
||||
ans['key_definitions'][key_def] = action
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def parse_config(lines, check_keys=True):
|
||||
ans = {'key_definitions': {}}
|
||||
def parse_config(lines: Iterable[str], check_keys: bool = True) -> Dict[str, Any]:
|
||||
ans: Dict[str, Any] = {'key_definitions': {}}
|
||||
parse_config_base(
|
||||
lines,
|
||||
defaults,
|
||||
type_map,
|
||||
type_convert,
|
||||
special_handling,
|
||||
ans,
|
||||
check_keys=check_keys
|
||||
@@ -97,7 +104,7 @@ def parse_config(lines, check_keys=True):
|
||||
return ans
|
||||
|
||||
|
||||
def merge_configs(defaults, vals):
|
||||
def merge_configs(defaults: Dict, vals: Dict) -> Dict:
|
||||
ans = {}
|
||||
for k, v in defaults.items():
|
||||
if isinstance(v, dict):
|
||||
@@ -108,14 +115,16 @@ def merge_configs(defaults, vals):
|
||||
return ans
|
||||
|
||||
|
||||
def parse_defaults(lines, check_keys=False):
|
||||
def parse_defaults(lines: Iterable[str], check_keys: bool = False) -> Dict[str, Any]:
|
||||
return parse_config(lines, check_keys)
|
||||
|
||||
|
||||
Options, defaults = _init_config(config_lines(all_options), parse_defaults)
|
||||
x = _init_config(config_lines(all_options), parse_defaults)
|
||||
Options: Type[DiffOptions] = x[0]
|
||||
defaults = x[1]
|
||||
|
||||
|
||||
def load_config(*paths, overrides=None):
|
||||
def load_config(*paths: str, overrides: Optional[Iterable[str]] = None) -> DiffOptions:
|
||||
return _load_config(Options, defaults, parse_config, merge_configs, *paths, overrides=overrides)
|
||||
|
||||
|
||||
@@ -123,7 +132,7 @@ SYSTEM_CONF = '/etc/xdg/kitty/diff.conf'
|
||||
defconf = os.path.join(config_dir, 'diff.conf')
|
||||
|
||||
|
||||
def init_config(args):
|
||||
def init_config(args: DiffCLIOptions) -> DiffOptions:
|
||||
config = tuple(resolve_config(SYSTEM_CONF, defconf, args.config))
|
||||
overrides = (a.replace('=', ' ', 1) for a in args.override or ())
|
||||
opts = load_config(*config, overrides=overrides)
|
||||
|
||||
@@ -4,17 +4,18 @@
|
||||
|
||||
|
||||
# Utils {{{
|
||||
from gettext import gettext as _
|
||||
from functools import partial
|
||||
from gettext import gettext as _
|
||||
from typing import Any, Dict, Sequence, Union
|
||||
|
||||
from kitty.conf.definition import option_func
|
||||
from kitty.conf.definition import Option, Shortcut, option_func
|
||||
from kitty.conf.utils import (
|
||||
positive_int, python_string, to_color
|
||||
positive_int, python_string, to_color, to_color_or_none
|
||||
)
|
||||
|
||||
# }}}
|
||||
|
||||
all_options = {}
|
||||
all_options: Dict[str, Union[Option, Sequence[Shortcut]]] = {}
|
||||
o, k, g, all_groups = option_func(all_options, {
|
||||
'colors': [_('Colors')],
|
||||
'diff': [_('Diffing'), ],
|
||||
@@ -25,7 +26,7 @@ o, k, g, all_groups = option_func(all_options, {
|
||||
g('diff')
|
||||
|
||||
|
||||
def syntax_aliases(raw):
|
||||
def syntax_aliases(raw: str) -> Dict[str, str]:
|
||||
ans = {}
|
||||
for x in raw.split():
|
||||
a, b = x.partition(':')[::2]
|
||||
@@ -34,7 +35,7 @@ def syntax_aliases(raw):
|
||||
return ans
|
||||
|
||||
|
||||
o('syntax_aliases', 'pyj:py recipe:py', option_type=syntax_aliases, long_text=_('''
|
||||
o('syntax_aliases', 'pyj:py pyi:py recipe:py', option_type=syntax_aliases, long_text=_('''
|
||||
File extension aliases for syntax highlight
|
||||
For example, to syntax highlight :file:`file.xyz` as
|
||||
:file:`file.abc` use a setting of :code:`xyz:abc`
|
||||
@@ -79,6 +80,8 @@ c('highlight_added_bg', '#acf2bd')
|
||||
c('added_margin_bg', '#cdffd8')
|
||||
|
||||
c('filler_bg', '#fafbfc', long_text=_('Filler (empty) line background'))
|
||||
c('margin_filler_bg', 'none', option_type=to_color_or_none, long_text=_(
|
||||
'Filler (empty) line background in margins, defaults to the filler background'))
|
||||
|
||||
c('hunk_margin_bg', '#dbedff', long_text=_('Hunk header colors'))
|
||||
c('hunk_bg', '#f1f8ff')
|
||||
@@ -121,4 +124,9 @@ k('prev_match', '<', 'scroll_to prev-match', _('Scroll to previous search match'
|
||||
k('search_forward_simple', 'f', 'start_search substring forward', _('Search forward (no regex)'))
|
||||
k('search_backward_simple', 'b', 'start_search substring backward', _('Search backward (no regex)'))
|
||||
|
||||
type_map = {o.name: o.option_type for o in all_options.values() if hasattr(o, 'option_type')}
|
||||
|
||||
def type_convert(name: str, val: Any) -> Any:
|
||||
o = all_options.get(name)
|
||||
if isinstance(o, Option):
|
||||
val = o.option_type(val)
|
||||
return val
|
||||
|
||||
14
kittens/diff/diff_speedup.pyi
Normal file
14
kittens/diff/diff_speedup.pyi
Normal file
@@ -0,0 +1,14 @@
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from .collect import Segment
|
||||
|
||||
|
||||
def split_with_highlights(
|
||||
line: str, truncate_points: List[int], fg_highlights: List,
|
||||
bg_highlight: Optional[Segment]
|
||||
) -> List:
|
||||
pass
|
||||
|
||||
|
||||
def changed_center(left_prefix: str, right_postfix: str) -> Tuple[int, int]:
|
||||
pass
|
||||
@@ -5,15 +5,16 @@
|
||||
import concurrent
|
||||
import os
|
||||
import re
|
||||
from typing import IO, Dict, Iterable, List, Optional, Tuple, Union, cast
|
||||
|
||||
from pygments import highlight
|
||||
from pygments.formatter import Formatter
|
||||
from pygments.lexers import get_lexer_for_filename
|
||||
from pygments.util import ClassNotFound
|
||||
from pygments import highlight # type: ignore
|
||||
from pygments.formatter import Formatter # type: ignore
|
||||
from pygments.lexers import get_lexer_for_filename # type: ignore
|
||||
from pygments.util import ClassNotFound # type: ignore
|
||||
|
||||
from kitty.rgb import color_as_sgr, parse_sharp
|
||||
|
||||
from .collect import Segment, data_for_path, lines_for_path
|
||||
from .collect import Collection, Segment, data_for_path, lines_for_path
|
||||
|
||||
|
||||
class StyleNotFound(Exception):
|
||||
@@ -22,7 +23,7 @@ class StyleNotFound(Exception):
|
||||
|
||||
class DiffFormatter(Formatter):
|
||||
|
||||
def __init__(self, style='default'):
|
||||
def __init__(self, style: str = 'default') -> None:
|
||||
try:
|
||||
Formatter.__init__(self, style=style)
|
||||
initialized = True
|
||||
@@ -31,30 +32,34 @@ class DiffFormatter(Formatter):
|
||||
if not initialized:
|
||||
raise StyleNotFound('pygments style "{}" not found'.format(style))
|
||||
|
||||
self.styles = {}
|
||||
for token, style in self.style:
|
||||
self.styles: Dict[str, Tuple[str, str]] = {}
|
||||
for token, token_style in self.style:
|
||||
start = []
|
||||
end = []
|
||||
fstart = fend = ''
|
||||
# a style item is a tuple in the following form:
|
||||
# colors are readily specified in hex: 'RRGGBB'
|
||||
if style['color']:
|
||||
start.append('38' + color_as_sgr(parse_sharp(style['color'])))
|
||||
end.append('39')
|
||||
if style['bold']:
|
||||
col = token_style['color']
|
||||
if col:
|
||||
pc = parse_sharp(col)
|
||||
if pc is not None:
|
||||
start.append('38' + color_as_sgr(pc))
|
||||
end.append('39')
|
||||
if token_style['bold']:
|
||||
start.append('1')
|
||||
end.append('22')
|
||||
if style['italic']:
|
||||
if token_style['italic']:
|
||||
start.append('3')
|
||||
end.append('23')
|
||||
if style['underline']:
|
||||
if token_style['underline']:
|
||||
start.append('4')
|
||||
end.append('24')
|
||||
if start:
|
||||
start = '\033[{}m'.format(';'.join(start))
|
||||
end = '\033[{}m'.format(';'.join(end))
|
||||
self.styles[token] = start or '', end or ''
|
||||
fstart = '\033[{}m'.format(';'.join(start))
|
||||
fend = '\033[{}m'.format(';'.join(end))
|
||||
self.styles[token] = fstart, fend
|
||||
|
||||
def format(self, tokensource, outfile):
|
||||
def format(self, tokensource: Iterable[Tuple[str, str]], outfile: IO[str]) -> None:
|
||||
for ttype, value in tokensource:
|
||||
not_found = True
|
||||
if value.rstrip('\n'):
|
||||
@@ -76,15 +81,15 @@ class DiffFormatter(Formatter):
|
||||
outfile.write(value)
|
||||
|
||||
|
||||
formatter = None
|
||||
formatter: Optional[DiffFormatter] = None
|
||||
|
||||
|
||||
def initialize_highlighter(style='default'):
|
||||
def initialize_highlighter(style: str = 'default') -> None:
|
||||
global formatter
|
||||
formatter = DiffFormatter(style)
|
||||
|
||||
|
||||
def highlight_data(code, filename, aliases=None):
|
||||
def highlight_data(code: str, filename: str, aliases: Optional[Dict[str, str]] = None) -> Optional[str]:
|
||||
if aliases:
|
||||
base, ext = os.path.splitext(filename)
|
||||
alias = aliases.get(ext[1:])
|
||||
@@ -93,17 +98,16 @@ def highlight_data(code, filename, aliases=None):
|
||||
try:
|
||||
lexer = get_lexer_for_filename(filename, stripnl=False)
|
||||
except ClassNotFound:
|
||||
pass
|
||||
else:
|
||||
return highlight(code, lexer, formatter)
|
||||
return None
|
||||
return cast(str, highlight(code, lexer, formatter))
|
||||
|
||||
|
||||
split_pat = re.compile(r'(\033\[.*?m)')
|
||||
|
||||
|
||||
def highlight_line(line):
|
||||
ans = []
|
||||
current = None
|
||||
def highlight_line(line: str) -> List[Segment]:
|
||||
ans: List[Segment] = []
|
||||
current: Optional[Segment] = None
|
||||
pos = 0
|
||||
for x in split_pat.split(line):
|
||||
if x.startswith('\033'):
|
||||
@@ -119,8 +123,11 @@ def highlight_line(line):
|
||||
return ans
|
||||
|
||||
|
||||
def highlight_for_diff(path, aliases):
|
||||
ans = []
|
||||
DiffHighlight = List[List[Segment]]
|
||||
|
||||
|
||||
def highlight_for_diff(path: str, aliases: Dict[str, str]) -> DiffHighlight:
|
||||
ans: DiffHighlight = []
|
||||
lines = lines_for_path(path)
|
||||
hd = highlight_data('\n'.join(lines), path, aliases)
|
||||
if hd is not None:
|
||||
@@ -129,11 +136,10 @@ def highlight_for_diff(path, aliases):
|
||||
return ans
|
||||
|
||||
|
||||
def highlight_collection(collection, aliases=None):
|
||||
def highlight_collection(collection: Collection, aliases: Optional[Dict[str, str]] = None) -> Union[str, Dict[str, DiffHighlight]]:
|
||||
jobs = {}
|
||||
ans = {}
|
||||
ans: Dict[str, DiffHighlight] = {}
|
||||
with concurrent.futures.ProcessPoolExecutor(max_workers=os.cpu_count()) as executor:
|
||||
highlight_collection.processes = executor._processes
|
||||
for path, item_type, other_path in collection:
|
||||
if item_type != 'rename':
|
||||
for p in (path, other_path):
|
||||
@@ -151,13 +157,14 @@ def highlight_collection(collection, aliases=None):
|
||||
return ans
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> None:
|
||||
from .config import defaults
|
||||
# kitty +runpy "from kittens.diff.highlight import main; main()" file
|
||||
import sys
|
||||
initialize_highlighter()
|
||||
with open(sys.argv[-1]) as f:
|
||||
highlighted = highlight_data(f.read(), f.name, defaults.syntax_aliases)
|
||||
if highlighted is None:
|
||||
raise SystemExit('Unknown filetype: {}'.format(sys.argv[-1]))
|
||||
print(highlighted)
|
||||
if defaults is not None:
|
||||
with open(sys.argv[-1]) as f:
|
||||
highlighted = highlight_data(f.read(), f.name, defaults.syntax_aliases)
|
||||
if highlighted is None:
|
||||
raise SystemExit('Unknown filetype: {}'.format(sys.argv[-1]))
|
||||
print(highlighted)
|
||||
|
||||
@@ -2,50 +2,68 @@
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import atexit
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import warnings
|
||||
from collections import defaultdict
|
||||
from contextlib import suppress
|
||||
from functools import partial
|
||||
from gettext import gettext as _
|
||||
from contextlib import suppress
|
||||
from typing import DefaultDict, Dict, Iterable, List, Optional, Tuple, Union
|
||||
|
||||
from kitty.cli import CONFIG_HELP, parse_args
|
||||
from kitty.cli_stub import DiffCLIOptions
|
||||
from kitty.conf.utils import KittensKeyAction
|
||||
from kitty.constants import appname
|
||||
from kitty.fast_data_types import wcswidth
|
||||
from kitty.key_encoding import ESCAPE, RELEASE, enter_key
|
||||
from kitty.key_encoding import RELEASE, KeyEvent, enter_key, key_defs as K
|
||||
from kitty.options_stub import DiffOptions
|
||||
from kitty.utils import ScreenSize
|
||||
|
||||
from ..tui.handler import Handler
|
||||
from ..tui.images import ImageManager
|
||||
from ..tui.line_edit import LineEdit
|
||||
from ..tui.loop import Loop
|
||||
from ..tui.operations import styled
|
||||
from . import global_data
|
||||
from .collect import (
|
||||
create_collection, data_for_path, lines_for_path, sanitize,
|
||||
Collection, create_collection, data_for_path, lines_for_path, sanitize,
|
||||
set_highlight_data
|
||||
)
|
||||
from .config import init_config
|
||||
from .patch import Differ, set_diff_command, worker_processes
|
||||
from .render import ImageSupportWarning, LineRef, render_diff
|
||||
from .patch import Differ, Patch, set_diff_command, worker_processes
|
||||
from .render import (
|
||||
ImagePlacement, ImageSupportWarning, Line, LineRef, Reference, render_diff
|
||||
)
|
||||
from .search import BadRegex, Search
|
||||
from ..tui.handler import Handler
|
||||
from ..tui.images import ImageManager, Placement
|
||||
from ..tui.line_edit import LineEdit
|
||||
from ..tui.loop import Loop
|
||||
from ..tui.operations import styled
|
||||
|
||||
try:
|
||||
from .highlight import initialize_highlighter, highlight_collection
|
||||
from .highlight import initialize_highlighter, highlight_collection, DiffHighlight
|
||||
has_highlighter = True
|
||||
DiffHighlight
|
||||
except ImportError:
|
||||
initialize_highlighter = highlight_collection = None
|
||||
has_highlighter = False
|
||||
|
||||
def highlight_collection(collection: 'Collection', aliases: Optional[Dict[str, str]] = None) -> Union[str, Dict[str, 'DiffHighlight']]:
|
||||
return ''
|
||||
|
||||
|
||||
INITIALIZING, COLLECTED, DIFFED, COMMAND, MESSAGE = range(5)
|
||||
ESCAPE = K['ESCAPE']
|
||||
|
||||
|
||||
def generate_diff(collection, context):
|
||||
def generate_diff(collection: Collection, context: int) -> Union[str, Dict[str, Patch]]:
|
||||
d = Differ()
|
||||
|
||||
for path, item_type, changed_path in collection:
|
||||
if item_type == 'diff':
|
||||
is_binary = isinstance(data_for_path(path), bytes) or isinstance(data_for_path(changed_path), bytes)
|
||||
if not is_binary:
|
||||
assert changed_path is not None
|
||||
d.add_diff(path, changed_path)
|
||||
|
||||
return d(context)
|
||||
@@ -55,35 +73,35 @@ class DiffHandler(Handler):
|
||||
|
||||
image_manager_class = ImageManager
|
||||
|
||||
def __init__(self, args, opts, left, right):
|
||||
def __init__(self, args: DiffCLIOptions, opts: DiffOptions, left: str, right: str) -> None:
|
||||
self.state = INITIALIZING
|
||||
self.message = ''
|
||||
self.current_search_is_regex = True
|
||||
self.current_search = None
|
||||
self.current_search: Optional[Search] = None
|
||||
self.line_edit = LineEdit()
|
||||
self.opts = opts
|
||||
self.left, self.right = left, right
|
||||
self.report_traceback_on_exit = None
|
||||
self.report_traceback_on_exit: Union[str, Dict[str, Patch], None] = None
|
||||
self.args = args
|
||||
self.scroll_pos = self.max_scroll_pos = 0
|
||||
self.current_context_count = self.original_context_count = self.args.context
|
||||
if self.current_context_count < 0:
|
||||
self.current_context_count = self.original_context_count = self.opts.num_context_lines
|
||||
self.highlighting_done = False
|
||||
self.restore_position = None
|
||||
self.restore_position: Optional[Reference] = None
|
||||
for key_def, action in self.opts.key_definitions.items():
|
||||
self.add_shortcut(action, *key_def)
|
||||
|
||||
def perform_action(self, action):
|
||||
def perform_action(self, action: KittensKeyAction) -> None:
|
||||
func, args = action
|
||||
if func == 'quit':
|
||||
self.quit_loop(0)
|
||||
return
|
||||
if self.state <= DIFFED:
|
||||
if func == 'scroll_by':
|
||||
return self.scroll_lines(*args)
|
||||
return self.scroll_lines(int(args[0]))
|
||||
if func == 'scroll_to':
|
||||
where = args[0]
|
||||
where = str(args[0])
|
||||
if 'change' in where:
|
||||
return self.scroll_to_next_change(backwards='prev' in where)
|
||||
if 'match' in where:
|
||||
@@ -95,7 +113,7 @@ class DiffHandler(Handler):
|
||||
return self.scroll_lines(amt)
|
||||
if func == 'change_context':
|
||||
new_ctx = self.current_context_count
|
||||
to = args[0]
|
||||
to = int(args[0])
|
||||
if to == 'all':
|
||||
new_ctx = 100000
|
||||
elif to == 'default':
|
||||
@@ -104,25 +122,25 @@ class DiffHandler(Handler):
|
||||
new_ctx += to
|
||||
return self.change_context_count(new_ctx)
|
||||
if func == 'start_search':
|
||||
self.start_search(*args)
|
||||
self.start_search(bool(args[0]), bool(args[1]))
|
||||
return
|
||||
|
||||
def create_collection(self):
|
||||
def create_collection(self) -> None:
|
||||
|
||||
def collect_done(collection):
|
||||
def collect_done(collection: Collection) -> None:
|
||||
self.collection = collection
|
||||
self.state = COLLECTED
|
||||
self.generate_diff()
|
||||
|
||||
def collect(left, right):
|
||||
def collect(left: str, right: str) -> None:
|
||||
collection = create_collection(left, right)
|
||||
self.asyncio_loop.call_soon_threadsafe(collect_done, collection)
|
||||
|
||||
self.asyncio_loop.run_in_executor(None, collect, self.left, self.right)
|
||||
|
||||
def generate_diff(self):
|
||||
def generate_diff(self) -> None:
|
||||
|
||||
def diff_done(diff_map):
|
||||
def diff_done(diff_map: Union[str, Dict[str, Patch]]) -> None:
|
||||
if isinstance(diff_map, str):
|
||||
self.report_traceback_on_exit = diff_map
|
||||
self.quit_loop(1)
|
||||
@@ -136,7 +154,7 @@ class DiffHandler(Handler):
|
||||
self.current_position = self.restore_position
|
||||
self.restore_position = None
|
||||
self.draw_screen()
|
||||
if initialize_highlighter is not None and not self.highlighting_done:
|
||||
if has_highlighter and not self.highlighting_done:
|
||||
from .highlight import StyleNotFound
|
||||
self.highlighting_done = True
|
||||
try:
|
||||
@@ -147,15 +165,15 @@ class DiffHandler(Handler):
|
||||
return
|
||||
self.syntax_highlight()
|
||||
|
||||
def diff(collection, current_context_count):
|
||||
def diff(collection: Collection, current_context_count: int) -> None:
|
||||
diff_map = generate_diff(collection, current_context_count)
|
||||
self.asyncio_loop.call_soon_threadsafe(diff_done, diff_map)
|
||||
|
||||
self.asyncio_loop.run_in_executor(None, diff, self.collection, self.current_context_count)
|
||||
|
||||
def syntax_highlight(self):
|
||||
def syntax_highlight(self) -> None:
|
||||
|
||||
def highlighting_done(hdata):
|
||||
def highlighting_done(hdata: Union[str, Dict[str, 'DiffHighlight']]) -> None:
|
||||
if isinstance(hdata, str):
|
||||
self.report_traceback_on_exit = hdata
|
||||
self.quit_loop(1)
|
||||
@@ -164,23 +182,23 @@ class DiffHandler(Handler):
|
||||
self.render_diff()
|
||||
self.draw_screen()
|
||||
|
||||
def highlight(*a):
|
||||
result = highlight_collection(*a)
|
||||
def highlight(collection: Collection, aliases: Optional[Dict[str, str]] = None) -> None:
|
||||
result = highlight_collection(collection, aliases)
|
||||
self.asyncio_loop.call_soon_threadsafe(highlighting_done, result)
|
||||
|
||||
self.asyncio_loop.run_in_executor(None, highlight, self.collection, self.opts.syntax_aliases)
|
||||
|
||||
def calculate_statistics(self):
|
||||
def calculate_statistics(self) -> None:
|
||||
self.added_count = self.collection.added_count
|
||||
self.removed_count = self.collection.removed_count
|
||||
for patch in self.diff_map.values():
|
||||
self.added_count += patch.added_count
|
||||
self.removed_count += patch.removed_count
|
||||
|
||||
def render_diff(self):
|
||||
self.diff_lines = tuple(render_diff(self.collection, self.diff_map, self.args, self.screen_size.cols, self.image_manager))
|
||||
def render_diff(self) -> None:
|
||||
self.diff_lines: Tuple[Line, ...] = tuple(render_diff(self.collection, self.diff_map, self.args, self.screen_size.cols, self.image_manager))
|
||||
self.margin_size = render_diff.margin_size
|
||||
self.ref_path_map = defaultdict(list)
|
||||
self.ref_path_map: DefaultDict[str, List[Tuple[int, Reference]]] = defaultdict(list)
|
||||
for i, l in enumerate(self.diff_lines):
|
||||
self.ref_path_map[l.ref.path].append((i, l.ref))
|
||||
self.max_scroll_pos = len(self.diff_lines) - self.num_lines
|
||||
@@ -188,11 +206,11 @@ class DiffHandler(Handler):
|
||||
self.current_search(self.diff_lines, self.margin_size, self.screen_size.cols)
|
||||
|
||||
@property
|
||||
def current_position(self):
|
||||
def current_position(self) -> Reference:
|
||||
return self.diff_lines[min(len(self.diff_lines) - 1, self.scroll_pos)].ref
|
||||
|
||||
@current_position.setter
|
||||
def current_position(self, ref):
|
||||
def current_position(self, ref: Reference) -> None:
|
||||
num = None
|
||||
if isinstance(ref.extra, LineRef):
|
||||
sln = ref.extra.src_line_number
|
||||
@@ -212,10 +230,10 @@ class DiffHandler(Handler):
|
||||
self.scroll_pos = max(0, min(num, self.max_scroll_pos))
|
||||
|
||||
@property
|
||||
def num_lines(self):
|
||||
def num_lines(self) -> int:
|
||||
return self.screen_size.rows - 1
|
||||
|
||||
def scroll_to_next_change(self, backwards=False):
|
||||
def scroll_to_next_change(self, backwards: bool = False) -> None:
|
||||
if backwards:
|
||||
r = range(self.scroll_pos - 1, -1, -1)
|
||||
else:
|
||||
@@ -227,7 +245,7 @@ class DiffHandler(Handler):
|
||||
return
|
||||
self.cmd.bell()
|
||||
|
||||
def scroll_to_next_match(self, backwards=False, include_current=False):
|
||||
def scroll_to_next_match(self, backwards: bool = False, include_current: bool = False) -> None:
|
||||
if self.current_search is not None:
|
||||
offset = 0 if include_current else 1
|
||||
if backwards:
|
||||
@@ -240,10 +258,10 @@ class DiffHandler(Handler):
|
||||
return
|
||||
self.cmd.bell()
|
||||
|
||||
def set_scrolling_region(self):
|
||||
def set_scrolling_region(self) -> None:
|
||||
self.cmd.set_scrolling_region(self.screen_size, 0, self.num_lines - 2)
|
||||
|
||||
def scroll_lines(self, amt=1):
|
||||
def scroll_lines(self, amt: int = 1) -> None:
|
||||
new_pos = max(0, min(self.scroll_pos + amt, self.max_scroll_pos))
|
||||
if new_pos == self.scroll_pos:
|
||||
self.cmd.bell()
|
||||
@@ -263,30 +281,30 @@ class DiffHandler(Handler):
|
||||
self.draw_lines(amt, self.num_lines - amt)
|
||||
self.draw_status_line()
|
||||
|
||||
def init_terminal_state(self):
|
||||
def init_terminal_state(self) -> None:
|
||||
self.cmd.set_line_wrapping(False)
|
||||
self.cmd.set_window_title(main.title)
|
||||
self.cmd.set_window_title(global_data.title)
|
||||
self.cmd.set_default_colors(
|
||||
fg=self.opts.foreground, bg=self.opts.background,
|
||||
cursor=self.opts.foreground, select_fg=self.opts.select_fg,
|
||||
select_bg=self.opts.select_bg)
|
||||
self.cmd.set_cursor_shape('bar')
|
||||
|
||||
def finalize(self):
|
||||
def finalize(self) -> None:
|
||||
self.cmd.set_default_colors()
|
||||
self.cmd.set_cursor_visible(True)
|
||||
self.cmd.set_scrolling_region()
|
||||
|
||||
def initialize(self):
|
||||
def initialize(self) -> None:
|
||||
self.init_terminal_state()
|
||||
self.set_scrolling_region()
|
||||
self.draw_screen()
|
||||
self.create_collection()
|
||||
|
||||
def enforce_cursor_state(self):
|
||||
def enforce_cursor_state(self) -> None:
|
||||
self.cmd.set_cursor_visible(self.state == COMMAND)
|
||||
|
||||
def draw_lines(self, num, offset=0):
|
||||
def draw_lines(self, num: int, offset: int = 0) -> None:
|
||||
offset += self.scroll_pos
|
||||
image_involved = False
|
||||
limit = len(self.diff_lines)
|
||||
@@ -307,7 +325,43 @@ class DiffHandler(Handler):
|
||||
if image_involved:
|
||||
self.place_images()
|
||||
|
||||
def place_images(self):
|
||||
def update_image_placement_for_resend(self, image_id: int, pl: Placement) -> bool:
|
||||
offset = self.scroll_pos
|
||||
limit = len(self.diff_lines)
|
||||
in_image = False
|
||||
|
||||
def adjust(row: int, candidate: ImagePlacement, is_left: bool) -> bool:
|
||||
if candidate.image.image_id == image_id:
|
||||
q = self.xpos_for_image(row, candidate, is_left)
|
||||
if q is not None:
|
||||
pl['x'] = q[0]
|
||||
pl['y'] = row
|
||||
return True
|
||||
return False
|
||||
|
||||
for row in range(self.num_lines):
|
||||
lpos = offset + row
|
||||
if lpos >= limit:
|
||||
break
|
||||
line = self.diff_lines[lpos]
|
||||
if in_image:
|
||||
if line.image_data is None:
|
||||
in_image = False
|
||||
continue
|
||||
if line.image_data is not None:
|
||||
left_placement, right_placement = line.image_data
|
||||
if left_placement is not None:
|
||||
if adjust(row, left_placement, True):
|
||||
return True
|
||||
in_image = True
|
||||
if right_placement is not None:
|
||||
if adjust(row, right_placement, False):
|
||||
return True
|
||||
in_image = True
|
||||
return False
|
||||
|
||||
def place_images(self) -> None:
|
||||
self.image_manager.update_image_placement_for_resend = self.update_image_placement_for_resend
|
||||
self.cmd.clear_images_on_screen()
|
||||
offset = self.scroll_pos
|
||||
limit = len(self.diff_lines)
|
||||
@@ -330,19 +384,26 @@ class DiffHandler(Handler):
|
||||
self.place_image(row, right_placement, False)
|
||||
in_image = True
|
||||
|
||||
def place_image(self, row, placement, is_left):
|
||||
def xpos_for_image(self, row: int, placement: ImagePlacement, is_left: bool) -> Optional[Tuple[int, float]]:
|
||||
xpos = (0 if is_left else (self.screen_size.cols // 2)) + placement.image.margin_size
|
||||
image_height_in_rows = placement.image.rows
|
||||
topmost_visible_row = placement.row
|
||||
num_visible_rows = image_height_in_rows - topmost_visible_row
|
||||
visible_frac = min(num_visible_rows / image_height_in_rows, 1)
|
||||
if visible_frac > 0:
|
||||
if visible_frac <= 0:
|
||||
return None
|
||||
return xpos, visible_frac
|
||||
|
||||
def place_image(self, row: int, placement: ImagePlacement, is_left: bool) -> None:
|
||||
q = self.xpos_for_image(row, placement, is_left)
|
||||
if q is not None:
|
||||
xpos, visible_frac = q
|
||||
height = int(visible_frac * placement.image.height)
|
||||
top = placement.image.height - height
|
||||
self.image_manager.show_image(placement.image.image_id, xpos, row, src_rect=(
|
||||
0, top, placement.image.width, height))
|
||||
|
||||
def draw_screen(self):
|
||||
def draw_screen(self) -> None:
|
||||
self.enforce_cursor_state()
|
||||
if self.state < DIFFED:
|
||||
self.cmd.clear_screen()
|
||||
@@ -353,7 +414,7 @@ class DiffHandler(Handler):
|
||||
self.draw_lines(self.num_lines)
|
||||
self.draw_status_line()
|
||||
|
||||
def draw_status_line(self):
|
||||
def draw_status_line(self) -> None:
|
||||
if self.state < DIFFED:
|
||||
return
|
||||
self.enforce_cursor_state()
|
||||
@@ -380,7 +441,7 @@ class DiffHandler(Handler):
|
||||
text = '{}{}{}'.format(prefix, ' ' * filler, suffix)
|
||||
self.write(text)
|
||||
|
||||
def change_context_count(self, new_ctx):
|
||||
def change_context_count(self, new_ctx: int) -> None:
|
||||
new_ctx = max(0, new_ctx)
|
||||
if new_ctx != self.current_context_count:
|
||||
self.current_context_count = new_ctx
|
||||
@@ -389,7 +450,7 @@ class DiffHandler(Handler):
|
||||
self.restore_position = self.current_position
|
||||
self.draw_screen()
|
||||
|
||||
def start_search(self, is_regex, is_backward):
|
||||
def start_search(self, is_regex: bool, is_backward: bool) -> None:
|
||||
if self.state != DIFFED:
|
||||
self.cmd.bell()
|
||||
return
|
||||
@@ -399,7 +460,7 @@ class DiffHandler(Handler):
|
||||
self.current_search_is_regex = is_regex
|
||||
self.draw_status_line()
|
||||
|
||||
def do_search(self):
|
||||
def do_search(self) -> None:
|
||||
self.current_search = None
|
||||
query = self.line_edit.current_input
|
||||
if len(query) < 2:
|
||||
@@ -418,7 +479,7 @@ class DiffHandler(Handler):
|
||||
self.message = sanitize(_('No matches found'))
|
||||
self.cmd.bell()
|
||||
|
||||
def on_text(self, text, in_bracketed_paste=False):
|
||||
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
|
||||
if self.state is COMMAND:
|
||||
self.line_edit.on_text(text, in_bracketed_paste)
|
||||
self.draw_status_line()
|
||||
@@ -431,7 +492,7 @@ class DiffHandler(Handler):
|
||||
if action is not None:
|
||||
return self.perform_action(action)
|
||||
|
||||
def on_key(self, key_event):
|
||||
def on_key(self, key_event: KeyEvent) -> None:
|
||||
if self.state is MESSAGE:
|
||||
if key_event.type is not RELEASE:
|
||||
self.state = DIFFED
|
||||
@@ -464,7 +525,7 @@ class DiffHandler(Handler):
|
||||
if action is not None:
|
||||
return self.perform_action(action)
|
||||
|
||||
def on_resize(self, screen_size):
|
||||
def on_resize(self, screen_size: ScreenSize) -> None:
|
||||
self.screen_size = screen_size
|
||||
self.set_scrolling_region()
|
||||
if self.state > COLLECTED:
|
||||
@@ -472,10 +533,10 @@ class DiffHandler(Handler):
|
||||
self.render_diff()
|
||||
self.draw_screen()
|
||||
|
||||
def on_interrupt(self):
|
||||
def on_interrupt(self) -> None:
|
||||
self.quit_loop(1)
|
||||
|
||||
def on_eot(self):
|
||||
def on_eot(self) -> None:
|
||||
self.quit_loop(1)
|
||||
|
||||
|
||||
@@ -500,40 +561,60 @@ Syntax: :italic:`name=value`. For example: :italic:`-o background=gray`
|
||||
'''.format, config_help=CONFIG_HELP.format(conf_name='diff', appname=appname))
|
||||
|
||||
|
||||
def showwarning(message, category, filename, lineno, file=None, line=None):
|
||||
if category is ImageSupportWarning:
|
||||
showwarning.warnings.append(message)
|
||||
class ShowWarning:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.warnings: List[str] = []
|
||||
|
||||
def __call__(self, message: str, category: object, filename: str, lineno: int, file: object = None, line: object = None) -> None:
|
||||
if category is ImageSupportWarning:
|
||||
showwarning.warnings.append(message)
|
||||
|
||||
|
||||
showwarning.warnings = []
|
||||
help_text = 'Show a side-by-side diff of the specified files/directories'
|
||||
showwarning = ShowWarning()
|
||||
help_text = 'Show a side-by-side diff of the specified files/directories. You can also use ssh:hostname:remote-file-path to diff remote files.'
|
||||
usage = 'file_or_directory_left file_or_directory_right'
|
||||
|
||||
|
||||
def terminate_processes(processes):
|
||||
def terminate_processes(processes: Iterable[int]) -> None:
|
||||
for pid in processes:
|
||||
with suppress(Exception):
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
|
||||
|
||||
def main(args):
|
||||
def get_remote_file(path: str) -> str:
|
||||
if path.startswith('ssh:'):
|
||||
parts = path.split(':', 2)
|
||||
if len(parts) == 3:
|
||||
hostname, rpath = parts[1:]
|
||||
with tempfile.NamedTemporaryFile(suffix='-' + os.path.basename(rpath), prefix='remote:', delete=False) as tf:
|
||||
atexit.register(os.remove, tf.name)
|
||||
p = subprocess.Popen(['ssh', hostname, 'cat', rpath], stdout=tf)
|
||||
if p.wait() != 0:
|
||||
raise SystemExit(p.returncode)
|
||||
return tf.name
|
||||
return path
|
||||
|
||||
|
||||
def main(args: List[str]) -> None:
|
||||
warnings.showwarning = showwarning
|
||||
args, items = parse_args(args[1:], OPTIONS, usage, help_text, 'kitty +kitten diff')
|
||||
cli_opts, items = parse_args(args[1:], OPTIONS, usage, help_text, 'kitty +kitten diff', result_class=DiffCLIOptions)
|
||||
if len(items) != 2:
|
||||
raise SystemExit('You must specify exactly two files/directories to compare')
|
||||
left, right = items
|
||||
main.title = _('{} vs. {}').format(left, right)
|
||||
global_data.title = _('{} vs. {}').format(left, right)
|
||||
if os.path.isdir(left) != os.path.isdir(right):
|
||||
raise SystemExit('The items to be diffed should both be either directories or files. Comparing a directory to a file is not valid.')
|
||||
opts = init_config(args)
|
||||
opts = init_config(cli_opts)
|
||||
set_diff_command(opts.diff_cmd)
|
||||
lines_for_path.replace_tab_by = opts.replace_tab_by
|
||||
left, right = map(get_remote_file, (left, right))
|
||||
for f in left, right:
|
||||
if not os.path.exists(f):
|
||||
raise SystemExit('{} does not exist'.format(f))
|
||||
|
||||
loop = Loop()
|
||||
handler = DiffHandler(args, opts, left, right)
|
||||
handler = DiffHandler(cli_opts, opts, left, right)
|
||||
loop.loop(handler)
|
||||
for message in showwarning.warnings:
|
||||
from kitty.utils import safe_print
|
||||
@@ -551,9 +632,10 @@ def main(args):
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
elif __name__ == '__doc__':
|
||||
sys.cli_docs['usage'] = usage
|
||||
sys.cli_docs['options'] = OPTIONS
|
||||
sys.cli_docs['help_text'] = help_text
|
||||
cd = sys.cli_docs # type: ignore
|
||||
cd['usage'] = usage
|
||||
cd['options'] = OPTIONS
|
||||
cd['help_text'] = help_text
|
||||
elif __name__ == '__conf__':
|
||||
from .config import all_options
|
||||
sys.all_options = all_options
|
||||
from .config_data import all_options
|
||||
sys.all_options = all_options # type: ignore
|
||||
|
||||
@@ -7,40 +7,44 @@ import os
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import Dict, Iterator, List, Optional, Sequence, Tuple, Union
|
||||
|
||||
from . import global_data
|
||||
from .collect import lines_for_path
|
||||
from .diff_speedup import changed_center
|
||||
|
||||
left_lines = right_lines = None
|
||||
left_lines: Tuple[str, ...] = ()
|
||||
right_lines: Tuple[str, ...] = ()
|
||||
GIT_DIFF = 'git diff --no-color --no-ext-diff --exit-code -U_CONTEXT_ --no-index --'
|
||||
DIFF_DIFF = 'diff -p -U _CONTEXT_ --'
|
||||
worker_processes = []
|
||||
worker_processes: List[int] = []
|
||||
|
||||
|
||||
def find_differ():
|
||||
def find_differ() -> Optional[str]:
|
||||
if shutil.which('git') and subprocess.Popen(['git', '--help'], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL).wait() == 0:
|
||||
return GIT_DIFF
|
||||
if shutil.which('diff'):
|
||||
return DIFF_DIFF
|
||||
|
||||
|
||||
def set_diff_command(opt):
|
||||
def set_diff_command(opt: str) -> None:
|
||||
if opt == 'auto':
|
||||
cmd = find_differ()
|
||||
if cmd is None:
|
||||
raise SystemExit('Failed to find either the git or diff programs on your system')
|
||||
else:
|
||||
cmd = opt
|
||||
set_diff_command.cmd = cmd
|
||||
global_data.cmd = cmd
|
||||
|
||||
|
||||
def run_diff(file1, file2, context=3):
|
||||
def run_diff(file1: str, file2: str, context: int = 3) -> Tuple[bool, Union[int, bool], str]:
|
||||
# returns: ok, is_different, patch
|
||||
cmd = shlex.split(set_diff_command.cmd.replace('_CONTEXT_', str(context)))
|
||||
cmd = shlex.split(global_data.cmd.replace('_CONTEXT_', str(context)))
|
||||
# we resolve symlinks because git diff does not follow symlinks, while diff
|
||||
# does. We want consistent behavior, also for integration with git difftool
|
||||
# we always want symlinks to be followed.
|
||||
path1, path2 = map(os.path.realpath, (file1, file2))
|
||||
path1 = os.path.realpath(file1)
|
||||
path2 = os.path.realpath(file2)
|
||||
p = subprocess.Popen(
|
||||
cmd + [path1, path2],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL)
|
||||
@@ -57,49 +61,49 @@ class Chunk:
|
||||
|
||||
__slots__ = ('is_context', 'left_start', 'right_start', 'left_count', 'right_count', 'centers')
|
||||
|
||||
def __init__(self, left_start, right_start, is_context=False):
|
||||
def __init__(self, left_start: int, right_start: int, is_context: bool = False) -> None:
|
||||
self.is_context = is_context
|
||||
self.left_start = left_start
|
||||
self.right_start = right_start
|
||||
self.left_count = self.right_count = 0
|
||||
self.centers = None
|
||||
self.centers: Optional[Tuple[Tuple[int, int], ...]] = None
|
||||
|
||||
def add_line(self):
|
||||
def add_line(self) -> None:
|
||||
self.right_count += 1
|
||||
|
||||
def remove_line(self):
|
||||
def remove_line(self) -> None:
|
||||
self.left_count += 1
|
||||
|
||||
def context_line(self):
|
||||
def context_line(self) -> None:
|
||||
self.left_count += 1
|
||||
self.right_count += 1
|
||||
|
||||
def finalize(self):
|
||||
def finalize(self) -> None:
|
||||
if not self.is_context and self.left_count == self.right_count:
|
||||
self.centers = tuple(
|
||||
changed_center(left_lines[self.left_start + i], right_lines[self.right_start + i])
|
||||
for i in range(self.left_count)
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return 'Chunk(is_context={}, left_start={}, left_count={}, right_start={}, right_count={})'.format(
|
||||
self.is_context, self.left_start, self.left_count, self.right_start, self.right_count)
|
||||
|
||||
|
||||
class Hunk:
|
||||
|
||||
def __init__(self, title, left, right):
|
||||
def __init__(self, title: str, left: Tuple[int, int], right: Tuple[int, int]) -> None:
|
||||
self.left_start, self.left_count = left
|
||||
self.right_start, self.right_count = right
|
||||
self.left_start -= 1 # 0-index
|
||||
self.right_start -= 1 # 0-index
|
||||
self.title = title
|
||||
self.added_count = self.removed_count = 0
|
||||
self.chunks = []
|
||||
self.current_chunk = None
|
||||
self.chunks: List[Chunk] = []
|
||||
self.current_chunk: Optional[Chunk] = None
|
||||
self.largest_line_number = max(self.left_start + self.left_count, self.right_start + self.right_count)
|
||||
|
||||
def new_chunk(self, is_context=False):
|
||||
def new_chunk(self, is_context: bool = False) -> Chunk:
|
||||
if self.chunks:
|
||||
c = self.chunks[-1]
|
||||
left_start = c.left_start + c.left_count
|
||||
@@ -109,36 +113,40 @@ class Hunk:
|
||||
right_start = self.right_start
|
||||
return Chunk(left_start, right_start, is_context)
|
||||
|
||||
def ensure_diff_chunk(self):
|
||||
def ensure_diff_chunk(self) -> None:
|
||||
if self.current_chunk is None:
|
||||
self.current_chunk = self.new_chunk(is_context=False)
|
||||
elif self.current_chunk.is_context:
|
||||
self.chunks.append(self.current_chunk)
|
||||
self.current_chunk = self.new_chunk(is_context=False)
|
||||
|
||||
def ensure_context_chunk(self):
|
||||
def ensure_context_chunk(self) -> None:
|
||||
if self.current_chunk is None:
|
||||
self.current_chunk = self.new_chunk(is_context=True)
|
||||
elif not self.current_chunk.is_context:
|
||||
self.chunks.append(self.current_chunk)
|
||||
self.current_chunk = self.new_chunk(is_context=True)
|
||||
|
||||
def add_line(self):
|
||||
def add_line(self) -> None:
|
||||
self.ensure_diff_chunk()
|
||||
self.current_chunk.add_line()
|
||||
if self.current_chunk is not None:
|
||||
self.current_chunk.add_line()
|
||||
self.added_count += 1
|
||||
|
||||
def remove_line(self):
|
||||
def remove_line(self) -> None:
|
||||
self.ensure_diff_chunk()
|
||||
self.current_chunk.remove_line()
|
||||
if self.current_chunk is not None:
|
||||
self.current_chunk.remove_line()
|
||||
self.removed_count += 1
|
||||
|
||||
def context_line(self):
|
||||
def context_line(self) -> None:
|
||||
self.ensure_context_chunk()
|
||||
self.current_chunk.context_line()
|
||||
if self.current_chunk is not None:
|
||||
self.current_chunk.context_line()
|
||||
|
||||
def finalize(self):
|
||||
self.chunks.append(self.current_chunk)
|
||||
def finalize(self) -> None:
|
||||
if self.current_chunk is not None:
|
||||
self.chunks.append(self.current_chunk)
|
||||
del self.current_chunk
|
||||
# Sanity check
|
||||
c = self.chunks[-1]
|
||||
@@ -150,15 +158,15 @@ class Hunk:
|
||||
c.finalize()
|
||||
|
||||
|
||||
def parse_range(x):
|
||||
def parse_range(x: str) -> Tuple[int, int]:
|
||||
parts = x[1:].split(',', 1)
|
||||
start = abs(int(parts[0]))
|
||||
count = 1 if len(parts) < 2 else int(parts[1])
|
||||
return start, count
|
||||
|
||||
|
||||
def parse_hunk_header(line):
|
||||
parts = tuple(filter(None, line.split('@@', 2)))
|
||||
def parse_hunk_header(line: str) -> Hunk:
|
||||
parts: Tuple[str, ...] = tuple(filter(None, line.split('@@', 2)))
|
||||
linespec = parts[0].strip()
|
||||
title = ''
|
||||
if len(parts) == 2:
|
||||
@@ -169,20 +177,20 @@ def parse_hunk_header(line):
|
||||
|
||||
class Patch:
|
||||
|
||||
def __init__(self, all_hunks):
|
||||
def __init__(self, all_hunks: Sequence[Hunk]):
|
||||
self.all_hunks = all_hunks
|
||||
self.largest_line_number = self.all_hunks[-1].largest_line_number if self.all_hunks else 0
|
||||
self.added_count = sum(h.added_count for h in all_hunks)
|
||||
self.removed_count = sum(h.removed_count for h in all_hunks)
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> Iterator[Hunk]:
|
||||
return iter(self.all_hunks)
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
return len(self.all_hunks)
|
||||
|
||||
|
||||
def parse_patch(raw):
|
||||
def parse_patch(raw: str) -> Patch:
|
||||
all_hunks = []
|
||||
current_hunk = None
|
||||
for line in raw.splitlines():
|
||||
@@ -208,22 +216,23 @@ def parse_patch(raw):
|
||||
|
||||
class Differ:
|
||||
|
||||
diff_executor = None
|
||||
diff_executor: Optional[concurrent.futures.ThreadPoolExecutor] = None
|
||||
|
||||
def __init__(self):
|
||||
self.jmap = {}
|
||||
self.jobs = []
|
||||
def __init__(self) -> None:
|
||||
self.jmap: Dict[str, str] = {}
|
||||
self.jobs: List[str] = []
|
||||
if Differ.diff_executor is None:
|
||||
Differ.diff_executor = self.diff_executor = concurrent.futures.ThreadPoolExecutor(max_workers=os.cpu_count())
|
||||
|
||||
def add_diff(self, file1, file2):
|
||||
def add_diff(self, file1: str, file2: str) -> None:
|
||||
self.jmap[file1] = file2
|
||||
self.jobs.append(file1)
|
||||
|
||||
def __call__(self, context=3):
|
||||
def __call__(self, context: int = 3) -> Union[str, Dict[str, Patch]]:
|
||||
global left_lines, right_lines
|
||||
ans = {}
|
||||
executor = Differ.diff_executor
|
||||
ans: Dict[str, Patch] = {}
|
||||
executor = self.diff_executor
|
||||
assert executor is not None
|
||||
jobs = {executor.submit(run_diff, key, self.jmap[key], context): key for key in self.jobs}
|
||||
for future in concurrent.futures.as_completed(jobs):
|
||||
key = jobs[future]
|
||||
|
||||
@@ -3,40 +3,44 @@
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import warnings
|
||||
from functools import lru_cache
|
||||
from gettext import gettext as _
|
||||
from itertools import repeat, zip_longest
|
||||
from math import ceil
|
||||
from typing import Callable, Dict, Generator, Iterable, List, Optional, Tuple
|
||||
|
||||
from kitty.fast_data_types import truncate_point_for_length, wcswidth
|
||||
from kitty.cli_stub import DiffCLIOptions
|
||||
from kitty.utils import ScreenSize
|
||||
|
||||
from ..tui.images import can_display_images
|
||||
from .collect import (
|
||||
Segment, data_for_path, highlights_for_path, is_image, lines_for_path,
|
||||
path_name_map, sanitize
|
||||
Collection, Segment, data_for_path, highlights_for_path, is_image,
|
||||
lines_for_path, path_name_map, sanitize
|
||||
)
|
||||
from .config import formats
|
||||
from .diff_speedup import split_with_highlights as _split_with_highlights
|
||||
from .patch import Chunk, Hunk, Patch
|
||||
from ..tui.images import ImageManager, can_display_images
|
||||
|
||||
|
||||
class ImageSupportWarning(Warning):
|
||||
pass
|
||||
|
||||
|
||||
def images_supported():
|
||||
ans = getattr(images_supported, 'ans', None)
|
||||
if ans is None:
|
||||
images_supported.ans = ans = can_display_images()
|
||||
if not ans:
|
||||
warnings.warn('ImageMagick not found images cannot be displayed', ImageSupportWarning)
|
||||
@lru_cache(maxsize=2)
|
||||
def images_supported() -> bool:
|
||||
ans = can_display_images()
|
||||
if not ans:
|
||||
warnings.warn('ImageMagick not found images cannot be displayed', ImageSupportWarning)
|
||||
return ans
|
||||
|
||||
|
||||
class Ref:
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
def __setattr__(self, name: str, value: object) -> None:
|
||||
raise AttributeError("can't set attribute")
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return '{}({})'.format(self.__class__.__name__, ', '.join(
|
||||
'{}={}'.format(n, getattr(self, n)) for n in self.__slots__ if n != '_hash'))
|
||||
|
||||
@@ -44,8 +48,10 @@ class Ref:
|
||||
class LineRef(Ref):
|
||||
|
||||
__slots__ = ('src_line_number', 'wrapped_line_idx')
|
||||
src_line_number: int
|
||||
wrapped_line_idx: int
|
||||
|
||||
def __init__(self, sln, wli=0):
|
||||
def __init__(self, sln: int, wli: int = 0) -> None:
|
||||
object.__setattr__(self, 'src_line_number', sln)
|
||||
object.__setattr__(self, 'wrapped_line_idx', wli)
|
||||
|
||||
@@ -53,8 +59,10 @@ class LineRef(Ref):
|
||||
class Reference(Ref):
|
||||
|
||||
__slots__ = ('path', 'extra')
|
||||
path: str
|
||||
extra: Optional[LineRef]
|
||||
|
||||
def __init__(self, path, extra=None):
|
||||
def __init__(self, path: str, extra: Optional[LineRef] = None) -> None:
|
||||
object.__setattr__(self, 'path', path)
|
||||
object.__setattr__(self, 'extra', extra)
|
||||
|
||||
@@ -63,35 +71,41 @@ class Line:
|
||||
|
||||
__slots__ = ('text', 'ref', 'is_change_start', 'image_data')
|
||||
|
||||
def __init__(self, text, ref, change_start=False, image_data=None):
|
||||
def __init__(
|
||||
self,
|
||||
text: str,
|
||||
ref: Reference,
|
||||
change_start: bool = False,
|
||||
image_data: Optional[Tuple[Optional['ImagePlacement'], Optional['ImagePlacement']]] = None
|
||||
) -> None:
|
||||
self.text = text
|
||||
self.ref = ref
|
||||
self.is_change_start = change_start
|
||||
self.image_data = image_data
|
||||
|
||||
|
||||
def yield_lines_from(iterator, reference, is_change_start=True):
|
||||
def yield_lines_from(iterator: Iterable[str], reference: Reference, is_change_start: bool = True) -> Generator[Line, None, None]:
|
||||
for text in iterator:
|
||||
yield Line(text, reference, is_change_start)
|
||||
is_change_start = False
|
||||
|
||||
|
||||
def human_readable(size, sep=' '):
|
||||
def human_readable(size: int, sep: str = ' ') -> str:
|
||||
""" Convert a size in bytes into a human readable form """
|
||||
divisor, suffix = 1, "B"
|
||||
for i, candidate in enumerate(('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB')):
|
||||
if size < (1 << ((i + 1) * 10)):
|
||||
divisor, suffix = (1 << (i * 10)), candidate
|
||||
break
|
||||
size = str(float(size)/divisor)
|
||||
if size.find(".") > -1:
|
||||
size = size[:size.find(".")+2]
|
||||
if size.endswith('.0'):
|
||||
size = size[:-2]
|
||||
return size + sep + suffix
|
||||
s = str(float(size)/divisor)
|
||||
if s.find(".") > -1:
|
||||
s = s[:s.find(".")+2]
|
||||
if s.endswith('.0'):
|
||||
s = s[:-2]
|
||||
return s + sep + suffix
|
||||
|
||||
|
||||
def fit_in(text, count):
|
||||
def fit_in(text: str, count: int) -> str:
|
||||
p = truncate_point_for_length(text, count)
|
||||
if p >= len(text):
|
||||
return text
|
||||
@@ -100,19 +114,19 @@ def fit_in(text, count):
|
||||
return text[:p] + '…'
|
||||
|
||||
|
||||
def fill_in(text, sz):
|
||||
def fill_in(text: str, sz: int) -> str:
|
||||
w = wcswidth(text)
|
||||
if w < sz:
|
||||
text += ' ' * (sz - w)
|
||||
return text
|
||||
|
||||
|
||||
def place_in(text, sz):
|
||||
def place_in(text: str, sz: int) -> str:
|
||||
return fill_in(fit_in(text, sz), sz)
|
||||
|
||||
|
||||
def format_func(which):
|
||||
def formatted(text):
|
||||
def format_func(which: str) -> Callable[[str], str]:
|
||||
def formatted(text: str) -> str:
|
||||
fmt = formats[which]
|
||||
return '\x1b[' + fmt + 'm' + text + '\x1b[0m'
|
||||
formatted.__name__ = which + '_format'
|
||||
@@ -127,44 +141,47 @@ removed_format = format_func('removed')
|
||||
removed_margin_format = format_func('removed_margin')
|
||||
added_margin_format = format_func('added_margin')
|
||||
filler_format = format_func('filler')
|
||||
margin_filler_format = format_func('margin_filler')
|
||||
hunk_margin_format = format_func('hunk_margin')
|
||||
hunk_format = format_func('hunk')
|
||||
highlight_map = {'remove': ('removed_highlight', 'removed'), 'add': ('added_highlight', 'added')}
|
||||
|
||||
|
||||
def highlight_boundaries(ltype):
|
||||
def highlight_boundaries(ltype: str) -> Tuple[str, str]:
|
||||
s, e = highlight_map[ltype]
|
||||
start = '\x1b[' + formats[s] + 'm'
|
||||
stop = '\x1b[' + formats[e] + 'm'
|
||||
return start, stop
|
||||
|
||||
|
||||
def title_lines(left_path, right_path, args, columns, margin_size):
|
||||
def title_lines(left_path: Optional[str], right_path: Optional[str], args: DiffCLIOptions, columns: int, margin_size: int) -> Generator[str, None, None]:
|
||||
m = ' ' * margin_size
|
||||
left_name, right_name = map(path_name_map.get, (left_path, right_path))
|
||||
left_name = path_name_map.get(left_path) if left_path else None
|
||||
right_name = path_name_map.get(right_path) if right_path else None
|
||||
if right_name and right_name != left_name:
|
||||
n1 = fit_in(m + sanitize(left_name), columns // 2 - margin_size)
|
||||
n1 = fit_in(m + sanitize(left_name or ''), columns // 2 - margin_size)
|
||||
n1 = place_in(n1, columns // 2)
|
||||
n2 = fit_in(m + sanitize(right_name), columns // 2 - margin_size)
|
||||
n2 = place_in(n2, columns // 2)
|
||||
name = n1 + n2
|
||||
else:
|
||||
name = place_in(m + sanitize(left_name), columns)
|
||||
name = place_in(m + sanitize(left_name or ''), columns)
|
||||
yield title_format(place_in(name, columns))
|
||||
yield title_format('━' * columns)
|
||||
|
||||
|
||||
def binary_lines(path, other_path, columns, margin_size):
|
||||
def binary_lines(path: Optional[str], other_path: Optional[str], columns: int, margin_size: int) -> Generator[str, None, None]:
|
||||
template = _('Binary file: {}')
|
||||
available_cols = columns // 2 - margin_size
|
||||
|
||||
def fl(path, fmt):
|
||||
def fl(path: str, fmt: Callable[[str], str]) -> str:
|
||||
text = template.format(human_readable(len(data_for_path(path))))
|
||||
text = place_in(text, available_cols)
|
||||
return margin_format(' ' * margin_size) + fmt(text)
|
||||
|
||||
if path is None:
|
||||
filler = render_diff_line('', '', 'filler', margin_size, available_cols)
|
||||
assert other_path is not None
|
||||
yield filler + fl(other_path, added_format)
|
||||
elif other_path is None:
|
||||
filler = render_diff_line('', '', 'filler', margin_size, available_cols)
|
||||
@@ -173,7 +190,7 @@ def binary_lines(path, other_path, columns, margin_size):
|
||||
yield fl(path, removed_format) + fl(other_path, added_format)
|
||||
|
||||
|
||||
def split_to_size(line, width):
|
||||
def split_to_size(line: str, width: int) -> Generator[str, None, None]:
|
||||
if not line:
|
||||
yield line
|
||||
while line:
|
||||
@@ -182,7 +199,7 @@ def split_to_size(line, width):
|
||||
line = line[p:]
|
||||
|
||||
|
||||
def truncate_points(line, width):
|
||||
def truncate_points(line: str, width: int) -> Generator[int, None, None]:
|
||||
pos = 0
|
||||
sz = len(line)
|
||||
while True:
|
||||
@@ -193,18 +210,18 @@ def truncate_points(line, width):
|
||||
break
|
||||
|
||||
|
||||
def split_with_highlights(line, width, highlights, bg_highlight=None):
|
||||
def split_with_highlights(line: str, width: int, highlights: List, bg_highlight: Optional[Segment] = None) -> List:
|
||||
truncate_pts = list(truncate_points(line, width))
|
||||
return _split_with_highlights(line, truncate_pts, highlights, bg_highlight)
|
||||
|
||||
|
||||
margin_bg_map = {'filler': filler_format, 'remove': removed_margin_format, 'add': added_margin_format, 'context': margin_format}
|
||||
margin_bg_map = {'filler': margin_filler_format, 'remove': removed_margin_format, 'add': added_margin_format, 'context': margin_format}
|
||||
text_bg_map = {'filler': filler_format, 'remove': removed_format, 'add': added_format, 'context': text_format}
|
||||
|
||||
|
||||
class DiffData:
|
||||
|
||||
def __init__(self, left_path, right_path, available_cols, margin_size):
|
||||
def __init__(self, left_path: str, right_path: str, available_cols: int, margin_size: int):
|
||||
self.left_path, self.right_path = left_path, right_path
|
||||
self.available_cols = available_cols
|
||||
self.margin_size = margin_size
|
||||
@@ -215,24 +232,28 @@ class DiffData:
|
||||
self.left_hdata = highlights_for_path(left_path)
|
||||
self.right_hdata = highlights_for_path(right_path)
|
||||
|
||||
def left_highlights_for_line(self, line_num):
|
||||
def left_highlights_for_line(self, line_num: int) -> List[Segment]:
|
||||
if line_num < len(self.left_hdata):
|
||||
return self.left_hdata[line_num]
|
||||
return []
|
||||
|
||||
def right_highlights_for_line(self, line_num):
|
||||
def right_highlights_for_line(self, line_num: int) -> List[Segment]:
|
||||
if line_num < len(self.right_hdata):
|
||||
return self.right_hdata[line_num]
|
||||
return []
|
||||
|
||||
|
||||
def render_diff_line(number, text, ltype, margin_size, available_cols):
|
||||
margin = margin_bg_map[ltype](place_in(number, margin_size))
|
||||
def render_diff_line(number: Optional[str], text: str, ltype: str, margin_size: int, available_cols: int) -> str:
|
||||
margin = margin_bg_map[ltype](place_in(number or '', margin_size))
|
||||
content = text_bg_map[ltype](fill_in(text or '', available_cols))
|
||||
return margin + content
|
||||
|
||||
|
||||
def render_diff_pair(left_line_number, left, left_is_change, right_line_number, right, right_is_change, is_first, margin_size, available_cols):
|
||||
def render_diff_pair(
|
||||
left_line_number: Optional[str], left: str, left_is_change: bool,
|
||||
right_line_number: Optional[str], right: str, right_is_change: bool,
|
||||
is_first: bool, margin_size: int, available_cols: int
|
||||
) -> str:
|
||||
ltype = 'filler' if left_line_number is None else ('remove' if left_is_change else 'context')
|
||||
rtype = 'filler' if right_line_number is None else ('add' if right_is_change else 'context')
|
||||
return (
|
||||
@@ -241,14 +262,22 @@ def render_diff_pair(left_line_number, left, left_is_change, right_line_number,
|
||||
)
|
||||
|
||||
|
||||
def hunk_title(hunk_num, hunk, margin_size, available_cols):
|
||||
def hunk_title(hunk_num: int, hunk: Hunk, margin_size: int, available_cols: int) -> str:
|
||||
m = hunk_margin_format(' ' * margin_size)
|
||||
t = '@@ -{},{} +{},{} @@ {}'.format(hunk.left_start + 1, hunk.left_count, hunk.right_start + 1, hunk.right_count, hunk.title)
|
||||
return m + hunk_format(place_in(t, available_cols))
|
||||
|
||||
|
||||
def render_half_line(line_number, line, highlights, ltype, margin_size, available_cols, changed_center=None):
|
||||
bg_highlight = None
|
||||
def render_half_line(
|
||||
line_number: int,
|
||||
line: str,
|
||||
highlights: List,
|
||||
ltype: str,
|
||||
margin_size: int,
|
||||
available_cols: int,
|
||||
changed_center: Optional[Tuple[int, int]] = None
|
||||
) -> Generator[str, None, None]:
|
||||
bg_highlight: Optional[Segment] = None
|
||||
if changed_center is not None and changed_center[0]:
|
||||
prefix_count, suffix_count = changed_center
|
||||
line_sz = len(line)
|
||||
@@ -259,40 +288,41 @@ def render_half_line(line_number, line, highlights, ltype, margin_size, availabl
|
||||
seg.end_code = stop
|
||||
bg_highlight = seg
|
||||
if highlights or bg_highlight:
|
||||
lines = split_with_highlights(line, available_cols, highlights, bg_highlight)
|
||||
lines: Iterable[str] = split_with_highlights(line, available_cols, highlights, bg_highlight)
|
||||
else:
|
||||
lines = split_to_size(line, available_cols)
|
||||
line_number = str(line_number + 1)
|
||||
lnum = str(line_number + 1)
|
||||
for line in lines:
|
||||
yield render_diff_line(line_number, line, ltype, margin_size, available_cols)
|
||||
line_number = ''
|
||||
yield render_diff_line(lnum, line, ltype, margin_size, available_cols)
|
||||
lnum = ''
|
||||
|
||||
|
||||
def lines_for_chunk(data, hunk_num, chunk, chunk_num):
|
||||
def lines_for_chunk(data: DiffData, hunk_num: int, chunk: Chunk, chunk_num: int) -> Generator[Line, None, None]:
|
||||
if chunk.is_context:
|
||||
for i in range(chunk.left_count):
|
||||
left_line_number = line_ref = chunk.left_start + i
|
||||
right_line_number = chunk.right_start + i
|
||||
highlights = data.left_highlights_for_line(left_line_number)
|
||||
if highlights:
|
||||
lines = split_with_highlights(data.left_lines[left_line_number], data.available_cols, highlights)
|
||||
lines: Iterable[str] = split_with_highlights(data.left_lines[left_line_number], data.available_cols, highlights)
|
||||
else:
|
||||
lines = split_to_size(data.left_lines[left_line_number], data.available_cols)
|
||||
left_line_number = str(left_line_number + 1)
|
||||
right_line_number = str(right_line_number + 1)
|
||||
left_line_number_s = str(left_line_number + 1)
|
||||
right_line_number_s = str(right_line_number + 1)
|
||||
for wli, text in enumerate(lines):
|
||||
line = render_diff_line(left_line_number, text, 'context', data.margin_size, data.available_cols)
|
||||
if right_line_number == left_line_number:
|
||||
line = render_diff_line(left_line_number_s, text, 'context', data.margin_size, data.available_cols)
|
||||
if right_line_number_s == left_line_number_s:
|
||||
r = line
|
||||
else:
|
||||
r = render_diff_line(right_line_number, text, 'context', data.margin_size, data.available_cols)
|
||||
r = render_diff_line(right_line_number_s, text, 'context', data.margin_size, data.available_cols)
|
||||
ref = Reference(data.left_path, LineRef(line_ref, wli))
|
||||
yield Line(line + r, ref)
|
||||
left_line_number = right_line_number = ''
|
||||
left_line_number_s = right_line_number_s = ''
|
||||
else:
|
||||
common = min(chunk.left_count, chunk.right_count)
|
||||
for i in range(max(chunk.left_count, chunk.right_count)):
|
||||
ll, rl = [], []
|
||||
ll: List[str] = []
|
||||
rl: List[str] = []
|
||||
if i < chunk.left_count:
|
||||
rln = ref_ln = chunk.left_start + i
|
||||
ll.extend(render_half_line(
|
||||
@@ -327,7 +357,7 @@ def lines_for_chunk(data, hunk_num, chunk, chunk_num):
|
||||
yield Line(left_line + right_line, ref, i == 0 and wli == 0)
|
||||
|
||||
|
||||
def lines_for_diff(left_path, right_path, hunks, args, columns, margin_size):
|
||||
def lines_for_diff(left_path: str, right_path: str, hunks: Iterable[Hunk], args: DiffCLIOptions, columns: int, margin_size: int) -> Generator[Line, None, None]:
|
||||
available_cols = columns // 2 - margin_size
|
||||
data = DiffData(left_path, right_path, available_cols, margin_size)
|
||||
|
||||
@@ -337,7 +367,7 @@ def lines_for_diff(left_path, right_path, hunks, args, columns, margin_size):
|
||||
yield from lines_for_chunk(data, hunk_num, chunk, cnum)
|
||||
|
||||
|
||||
def all_lines(path, args, columns, margin_size, is_add=True):
|
||||
def all_lines(path: str, args: DiffCLIOptions, columns: int, margin_size: int, is_add: bool = True) -> Generator[Line, None, None]:
|
||||
available_cols = columns // 2 - margin_size
|
||||
ltype = 'add' if is_add else 'remove'
|
||||
lines = lines_for_path(path)
|
||||
@@ -345,7 +375,7 @@ def all_lines(path, args, columns, margin_size, is_add=True):
|
||||
msg_written = False
|
||||
hdata = highlights_for_path(path)
|
||||
|
||||
def highlights(num):
|
||||
def highlights(num: int) -> List[Segment]:
|
||||
return hdata[num] if num < len(hdata) else []
|
||||
|
||||
for line_number, line in enumerate(lines):
|
||||
@@ -362,7 +392,7 @@ def all_lines(path, args, columns, margin_size, is_add=True):
|
||||
yield Line(text, ref, line_number == 0 and i == 0)
|
||||
|
||||
|
||||
def rename_lines(path, other_path, args, columns, margin_size):
|
||||
def rename_lines(path: str, other_path: str, args: DiffCLIOptions, columns: int, margin_size: int) -> Generator[str, None, None]:
|
||||
m = ' ' * margin_size
|
||||
for line in split_to_size(_('The file {0} was renamed to {1}').format(
|
||||
sanitize(path_name_map[path]), sanitize(path_name_map[other_path])), columns - margin_size):
|
||||
@@ -371,7 +401,7 @@ def rename_lines(path, other_path, args, columns, margin_size):
|
||||
|
||||
class Image:
|
||||
|
||||
def __init__(self, image_id, width, height, margin_size, screen_size):
|
||||
def __init__(self, image_id: int, width: int, height: int, margin_size: int, screen_size: ScreenSize) -> None:
|
||||
self.image_id = image_id
|
||||
self.width, self.height = width, height
|
||||
self.rows = int(ceil(self.height / screen_size.cell_height))
|
||||
@@ -381,18 +411,23 @@ class Image:
|
||||
|
||||
class ImagePlacement:
|
||||
|
||||
def __init__(self, image, row):
|
||||
def __init__(self, image: Image, row: int) -> None:
|
||||
self.image = image
|
||||
self.row = row
|
||||
|
||||
|
||||
def render_image(path, is_left, available_cols, margin_size, image_manager):
|
||||
def render_image(
|
||||
path: str,
|
||||
is_left: bool,
|
||||
available_cols: int, margin_size: int,
|
||||
image_manager: ImageManager
|
||||
) -> Generator[Tuple[str, Reference, Optional[ImagePlacement]], None, None]:
|
||||
lnum = 0
|
||||
margin_fmt = removed_margin_format if is_left else added_margin_format
|
||||
m = margin_fmt(' ' * margin_size)
|
||||
fmt = removed_format if is_left else added_format
|
||||
|
||||
def yield_split(text):
|
||||
def yield_split(text: str) -> Generator[Tuple[str, Reference, Optional[ImagePlacement]], None, None]:
|
||||
nonlocal lnum
|
||||
for i, line in enumerate(split_to_size(text, available_cols)):
|
||||
yield m + fmt(place_in(line, available_cols)), Reference(path, LineRef(lnum, i)), None
|
||||
@@ -414,9 +449,16 @@ def render_image(path, is_left, available_cols, margin_size, image_manager):
|
||||
lnum += 1
|
||||
|
||||
|
||||
def image_lines(left_path, right_path, columns, margin_size, image_manager):
|
||||
def image_lines(
|
||||
left_path: Optional[str],
|
||||
right_path: Optional[str],
|
||||
columns: int,
|
||||
margin_size: int,
|
||||
image_manager: ImageManager
|
||||
) -> Generator[Line, None, None]:
|
||||
available_cols = columns // 2 - margin_size
|
||||
left_lines, right_lines = iter(()), iter(())
|
||||
left_lines: Iterable[Tuple[str, Reference, Optional[ImagePlacement]]] = iter(())
|
||||
right_lines: Iterable[Tuple[str, Reference, Optional[ImagePlacement]]] = iter(())
|
||||
if left_path is not None:
|
||||
left_lines = render_image(left_path, True, available_cols, margin_size, image_manager)
|
||||
if right_path is not None:
|
||||
@@ -439,52 +481,68 @@ def image_lines(left_path, right_path, columns, margin_size, image_manager):
|
||||
is_change_start = False
|
||||
|
||||
|
||||
def render_diff(collection, diff_map, args, columns, image_manager):
|
||||
largest_line_number = 0
|
||||
for path, item_type, other_path in collection:
|
||||
if item_type == 'diff':
|
||||
patch = diff_map.get(path)
|
||||
if patch is not None:
|
||||
largest_line_number = max(largest_line_number, patch.largest_line_number)
|
||||
class RenderDiff:
|
||||
|
||||
margin_size = render_diff.margin_size = max(3, len(str(largest_line_number)) + 1)
|
||||
last_item_num = len(collection) - 1
|
||||
margin_size: int = 0
|
||||
|
||||
for i, (path, item_type, other_path) in enumerate(collection):
|
||||
item_ref = Reference(path)
|
||||
is_binary = isinstance(data_for_path(path), bytes)
|
||||
if not is_binary and item_type == 'diff' and isinstance(data_for_path(other_path), bytes):
|
||||
is_binary = True
|
||||
is_img = is_binary and (is_image(path) or is_image(other_path)) and images_supported()
|
||||
yield from yield_lines_from(title_lines(path, other_path, args, columns, margin_size), item_ref, False)
|
||||
if item_type == 'diff':
|
||||
if is_binary:
|
||||
if is_img:
|
||||
ans = image_lines(path, other_path, columns, margin_size, image_manager)
|
||||
def __call__(
|
||||
self,
|
||||
collection: Collection,
|
||||
diff_map: Dict[str, Patch],
|
||||
args: DiffCLIOptions,
|
||||
columns: int,
|
||||
image_manager: ImageManager
|
||||
) -> Generator[Line, None, None]:
|
||||
largest_line_number = 0
|
||||
for path, item_type, other_path in collection:
|
||||
if item_type == 'diff':
|
||||
patch = diff_map.get(path)
|
||||
if patch is not None:
|
||||
largest_line_number = max(largest_line_number, patch.largest_line_number)
|
||||
|
||||
margin_size = self.margin_size = max(3, len(str(largest_line_number)) + 1)
|
||||
last_item_num = len(collection) - 1
|
||||
|
||||
for i, (path, item_type, other_path) in enumerate(collection):
|
||||
item_ref = Reference(path)
|
||||
is_binary = isinstance(data_for_path(path), bytes)
|
||||
if not is_binary and item_type == 'diff' and isinstance(data_for_path(other_path), bytes):
|
||||
is_binary = True
|
||||
is_img = is_binary and (is_image(path) or is_image(other_path)) and images_supported()
|
||||
yield from yield_lines_from(title_lines(path, other_path, args, columns, margin_size), item_ref, False)
|
||||
if item_type == 'diff':
|
||||
if is_binary:
|
||||
if is_img:
|
||||
ans = image_lines(path, other_path, columns, margin_size, image_manager)
|
||||
else:
|
||||
ans = yield_lines_from(binary_lines(path, other_path, columns, margin_size), item_ref)
|
||||
else:
|
||||
ans = yield_lines_from(binary_lines(path, other_path, columns, margin_size), item_ref)
|
||||
else:
|
||||
ans = lines_for_diff(path, other_path, diff_map[path], args, columns, margin_size)
|
||||
elif item_type == 'add':
|
||||
if is_binary:
|
||||
if is_img:
|
||||
ans = image_lines(None, path, columns, margin_size, image_manager)
|
||||
assert other_path is not None
|
||||
ans = lines_for_diff(path, other_path, diff_map[path], args, columns, margin_size)
|
||||
elif item_type == 'add':
|
||||
if is_binary:
|
||||
if is_img:
|
||||
ans = image_lines(None, path, columns, margin_size, image_manager)
|
||||
else:
|
||||
ans = yield_lines_from(binary_lines(None, path, columns, margin_size), item_ref)
|
||||
else:
|
||||
ans = yield_lines_from(binary_lines(None, path, columns, margin_size), item_ref)
|
||||
else:
|
||||
ans = all_lines(path, args, columns, margin_size, is_add=True)
|
||||
elif item_type == 'removal':
|
||||
if is_binary:
|
||||
if is_img:
|
||||
ans = image_lines(path, None, columns, margin_size, image_manager)
|
||||
ans = all_lines(path, args, columns, margin_size, is_add=True)
|
||||
elif item_type == 'removal':
|
||||
if is_binary:
|
||||
if is_img:
|
||||
ans = image_lines(path, None, columns, margin_size, image_manager)
|
||||
else:
|
||||
ans = yield_lines_from(binary_lines(path, None, columns, margin_size), item_ref)
|
||||
else:
|
||||
ans = yield_lines_from(binary_lines(path, None, columns, margin_size), item_ref)
|
||||
ans = all_lines(path, args, columns, margin_size, is_add=False)
|
||||
elif item_type == 'rename':
|
||||
assert other_path is not None
|
||||
ans = yield_lines_from(rename_lines(path, other_path, args, columns, margin_size), item_ref)
|
||||
else:
|
||||
ans = all_lines(path, args, columns, margin_size, is_add=False)
|
||||
elif item_type == 'rename':
|
||||
ans = yield_lines_from(rename_lines(path, other_path, args, columns, margin_size), item_ref)
|
||||
else:
|
||||
raise ValueError('Unsupported item type: {}'.format(item_type))
|
||||
yield from ans
|
||||
if i < last_item_num:
|
||||
yield Line('', item_ref)
|
||||
raise ValueError('Unsupported item type: {}'.format(item_type))
|
||||
yield from ans
|
||||
if i < last_item_num:
|
||||
yield Line('', item_ref)
|
||||
|
||||
|
||||
render_diff = RenderDiff()
|
||||
|
||||
@@ -3,11 +3,17 @@
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Tuple
|
||||
|
||||
from kitty.fast_data_types import wcswidth
|
||||
from kitty.options_stub import DiffOptions
|
||||
|
||||
from ..tui.operations import styled
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .render import Line
|
||||
Line
|
||||
|
||||
|
||||
class BadRegex(ValueError):
|
||||
pass
|
||||
@@ -15,8 +21,8 @@ class BadRegex(ValueError):
|
||||
|
||||
class Search:
|
||||
|
||||
def __init__(self, opts, query, is_regex, is_backward):
|
||||
self.matches = {}
|
||||
def __init__(self, opts: DiffOptions, query: str, is_regex: bool, is_backward: bool):
|
||||
self.matches: Dict[int, List[Tuple[int, str]]] = {}
|
||||
self.count = 0
|
||||
self.style = styled('|', fg=opts.search_fg, bg=opts.search_bg).split('|', 1)[0]
|
||||
if not is_regex:
|
||||
@@ -26,7 +32,7 @@ class Search:
|
||||
except Exception:
|
||||
raise BadRegex('Not a valid regex: {}'.format(query))
|
||||
|
||||
def __call__(self, diff_lines, margin_size, cols):
|
||||
def __call__(self, diff_lines: Iterable['Line'], margin_size: int, cols: int) -> bool:
|
||||
self.matches = {}
|
||||
self.count = 0
|
||||
half_width = cols // 2
|
||||
@@ -38,7 +44,7 @@ class Search:
|
||||
left, right = text[margin_size:half_width + 1], text[right_offset:]
|
||||
matches = []
|
||||
|
||||
def add(which, offset):
|
||||
def add(which: str, offset: int) -> None:
|
||||
for m in find(which):
|
||||
before = which[:m.start()]
|
||||
matches.append((wcswidth(before) + offset, m.group()))
|
||||
@@ -50,13 +56,13 @@ class Search:
|
||||
self.matches[i] = matches
|
||||
return bool(self.matches)
|
||||
|
||||
def __contains__(self, i):
|
||||
def __contains__(self, i: int) -> bool:
|
||||
return i in self.matches
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
return self.count
|
||||
|
||||
def highlight_line(self, write, line_num):
|
||||
def highlight_line(self, write: Callable[[str], None], line_num: int) -> bool:
|
||||
highlights = self.matches.get(line_num)
|
||||
if not highlights:
|
||||
return False
|
||||
|
||||
@@ -9,32 +9,52 @@ import sys
|
||||
from functools import lru_cache
|
||||
from gettext import gettext as _
|
||||
from itertools import repeat
|
||||
from typing import (
|
||||
Any, Callable, Dict, Generator, Iterable, List, Optional, Pattern,
|
||||
Sequence, Set, Tuple, Type, cast
|
||||
)
|
||||
|
||||
from kitty.cli import parse_args
|
||||
from kitty.cli_stub import HintsCLIOptions
|
||||
from kitty.fast_data_types import set_clipboard_string
|
||||
from kitty.key_encoding import ESCAPE, backspace_key, enter_key
|
||||
from kitty.utils import screen_size_function
|
||||
from kitty.key_encoding import (
|
||||
KeyEvent, backspace_key, enter_key, key_defs as K
|
||||
)
|
||||
from kitty.typing import BossType, KittyCommonOpts
|
||||
from kitty.utils import ScreenSize, screen_size_function, set_primary_selection
|
||||
|
||||
from ..tui.handler import Handler
|
||||
from ..tui.handler import Handler, result_handler
|
||||
from ..tui.loop import Loop
|
||||
from ..tui.operations import faint, styled
|
||||
|
||||
URL_PREFIXES = 'http https file ftp'.split()
|
||||
|
||||
@lru_cache()
|
||||
def kitty_common_opts() -> KittyCommonOpts:
|
||||
import json
|
||||
v = os.environ.get('KITTY_COMMON_OPTS')
|
||||
if v:
|
||||
return cast(KittyCommonOpts, json.loads(v))
|
||||
from kitty.config import common_opts_as_dict
|
||||
return common_opts_as_dict()
|
||||
|
||||
|
||||
DEFAULT_HINT_ALPHABET = string.digits + string.ascii_lowercase
|
||||
screen_size = screen_size_function()
|
||||
DEFAULT_REGEX = r'(?m)^\s*(.+)\s*$'
|
||||
ESCAPE = K['ESCAPE']
|
||||
|
||||
|
||||
class Mark:
|
||||
|
||||
__slots__ = ('index', 'start', 'end', 'text')
|
||||
__slots__ = ('index', 'start', 'end', 'text', 'groupdict')
|
||||
|
||||
def __init__(self, index, start, end, text):
|
||||
def __init__(self, index: int, start: int, end: int, text: str, groupdict: Any):
|
||||
self.index, self.start, self.end = index, start, end
|
||||
self.text = text
|
||||
self.groupdict = groupdict
|
||||
|
||||
|
||||
@lru_cache(maxsize=2048)
|
||||
def encode_hint(num, alphabet):
|
||||
def encode_hint(num: int, alphabet: str) -> str:
|
||||
res = ''
|
||||
d = len(alphabet)
|
||||
while not res or num > 0:
|
||||
@@ -43,7 +63,7 @@ def encode_hint(num, alphabet):
|
||||
return res
|
||||
|
||||
|
||||
def decode_hint(x, alphabet=DEFAULT_HINT_ALPHABET):
|
||||
def decode_hint(x: str, alphabet: str = DEFAULT_HINT_ALPHABET) -> int:
|
||||
base = len(alphabet)
|
||||
index_map = {c: i for i, c in enumerate(alphabet)}
|
||||
i = 0
|
||||
@@ -52,7 +72,7 @@ def decode_hint(x, alphabet=DEFAULT_HINT_ALPHABET):
|
||||
return i
|
||||
|
||||
|
||||
def highlight_mark(m, text, current_input, alphabet):
|
||||
def highlight_mark(m: Mark, text: str, current_input: str, alphabet: str) -> str:
|
||||
hint = encode_hint(m.index, alphabet)
|
||||
if current_input and not hint.startswith(current_input):
|
||||
return faint(text)
|
||||
@@ -68,7 +88,7 @@ def highlight_mark(m, text, current_input, alphabet):
|
||||
)
|
||||
|
||||
|
||||
def render(text, current_input, all_marks, ignore_mark_indices, alphabet):
|
||||
def render(text: str, current_input: str, all_marks: Sequence[Mark], ignore_mark_indices: Set[int], alphabet: str) -> str:
|
||||
for mark in reversed(all_marks):
|
||||
if mark.index in ignore_mark_indices:
|
||||
continue
|
||||
@@ -82,39 +102,47 @@ def render(text, current_input, all_marks, ignore_mark_indices, alphabet):
|
||||
|
||||
class Hints(Handler):
|
||||
|
||||
def __init__(self, text, all_marks, index_map, args):
|
||||
def __init__(self, text: str, all_marks: Sequence[Mark], index_map: Dict[int, Mark], args: HintsCLIOptions):
|
||||
self.text, self.index_map = text, index_map
|
||||
self.alphabet = args.alphabet or DEFAULT_HINT_ALPHABET
|
||||
self.all_marks = all_marks
|
||||
self.ignore_mark_indices = set()
|
||||
self.ignore_mark_indices: Set[int] = set()
|
||||
self.args = args
|
||||
self.window_title = _('Choose URL') if args.type == 'url' else _('Choose text')
|
||||
self.multiple = args.multiple
|
||||
self.match_suffix = self.get_match_suffix(args)
|
||||
self.chosen = []
|
||||
self.chosen: List[Mark] = []
|
||||
self.reset()
|
||||
|
||||
def get_match_suffix(self, args):
|
||||
@property
|
||||
def text_matches(self) -> List[str]:
|
||||
return [m.text + self.match_suffix for m in self.chosen]
|
||||
|
||||
@property
|
||||
def groupdicts(self) -> List[Any]:
|
||||
return [m.groupdict for m in self.chosen]
|
||||
|
||||
def get_match_suffix(self, args: HintsCLIOptions) -> str:
|
||||
if args.add_trailing_space == 'always':
|
||||
return ' '
|
||||
if args.add_trailing_space == 'never':
|
||||
return ''
|
||||
return ' ' if args.multiple else ''
|
||||
|
||||
def reset(self):
|
||||
def reset(self) -> None:
|
||||
self.current_input = ''
|
||||
self.current_text = None
|
||||
self.current_text: Optional[str] = None
|
||||
|
||||
def init_terminal_state(self):
|
||||
def init_terminal_state(self) -> None:
|
||||
self.cmd.set_cursor_visible(False)
|
||||
self.cmd.set_window_title(self.window_title)
|
||||
self.cmd.set_line_wrapping(False)
|
||||
|
||||
def initialize(self):
|
||||
def initialize(self) -> None:
|
||||
self.init_terminal_state()
|
||||
self.draw_screen()
|
||||
|
||||
def on_text(self, text, in_bracketed_paste):
|
||||
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
|
||||
changed = False
|
||||
for c in text:
|
||||
if c in self.alphabet:
|
||||
@@ -126,7 +154,7 @@ class Hints(Handler):
|
||||
if encode_hint(idx, self.alphabet).startswith(self.current_input)
|
||||
]
|
||||
if len(matches) == 1:
|
||||
self.chosen.append(matches[0].text + self.match_suffix)
|
||||
self.chosen.append(matches[0])
|
||||
if self.multiple:
|
||||
self.ignore_mark_indices.add(matches[0].index)
|
||||
self.reset()
|
||||
@@ -136,7 +164,7 @@ class Hints(Handler):
|
||||
self.current_text = None
|
||||
self.draw_screen()
|
||||
|
||||
def on_key(self, key_event):
|
||||
def on_key(self, key_event: KeyEvent) -> None:
|
||||
if key_event is backspace_key:
|
||||
self.current_input = self.current_input[:-1]
|
||||
self.current_text = None
|
||||
@@ -144,7 +172,7 @@ class Hints(Handler):
|
||||
elif key_event is enter_key and self.current_input:
|
||||
try:
|
||||
idx = decode_hint(self.current_input, self.alphabet)
|
||||
self.chosen.append(self.index_map[idx].text + self.match_suffix)
|
||||
self.chosen.append(self.index_map[idx])
|
||||
self.ignore_mark_indices.add(idx)
|
||||
except Exception:
|
||||
self.current_input = ''
|
||||
@@ -159,43 +187,45 @@ class Hints(Handler):
|
||||
elif key_event.key is ESCAPE:
|
||||
self.quit_loop(0 if self.multiple else 1)
|
||||
|
||||
def on_interrupt(self):
|
||||
def on_interrupt(self) -> None:
|
||||
self.quit_loop(1)
|
||||
|
||||
def on_eot(self):
|
||||
def on_eot(self) -> None:
|
||||
self.quit_loop(1)
|
||||
|
||||
def on_resize(self, new_size):
|
||||
def on_resize(self, new_size: ScreenSize) -> None:
|
||||
self.draw_screen()
|
||||
|
||||
def draw_screen(self):
|
||||
def draw_screen(self) -> None:
|
||||
if self.current_text is None:
|
||||
self.current_text = render(self.text, self.current_input, self.all_marks, self.ignore_mark_indices, self.alphabet)
|
||||
self.cmd.clear_screen()
|
||||
self.write(self.current_text)
|
||||
|
||||
|
||||
def regex_finditer(pat, minimum_match_length, text):
|
||||
def regex_finditer(pat: Pattern, minimum_match_length: int, text: str) -> Generator[Tuple[int, int, Dict], None, None]:
|
||||
has_named_groups = bool(pat.groupindex)
|
||||
for m in pat.finditer(text):
|
||||
s, e = m.span(pat.groups)
|
||||
s, e = m.span(0 if has_named_groups else pat.groups)
|
||||
while e > s + 1 and text[e-1] == '\0':
|
||||
e -= 1
|
||||
if e - s >= minimum_match_length:
|
||||
yield s, e
|
||||
yield s, e, m.groupdict()
|
||||
|
||||
|
||||
closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>', '*': '*', '"': '"', "'": "'"}
|
||||
opening_brackets = ''.join(closing_bracket_map)
|
||||
postprocessor_map = {}
|
||||
PostprocessorFunc = Callable[[str, int, int], Tuple[int, int]]
|
||||
postprocessor_map: Dict[str, PostprocessorFunc] = {}
|
||||
|
||||
|
||||
def postprocessor(func):
|
||||
def postprocessor(func: PostprocessorFunc) -> PostprocessorFunc:
|
||||
postprocessor_map[func.__name__] = func
|
||||
return func
|
||||
|
||||
|
||||
@postprocessor
|
||||
def url(text, s, e):
|
||||
def url(text: str, s: int, e: int) -> Tuple[int, int]:
|
||||
if s > 4 and text[s - 5:s] == 'link:': # asciidoc URLs
|
||||
url = text[s:e]
|
||||
idx = url.rfind('[')
|
||||
@@ -217,7 +247,7 @@ def url(text, s, e):
|
||||
|
||||
|
||||
@postprocessor
|
||||
def brackets(text, s, e):
|
||||
def brackets(text: str, s: int, e: int) -> Tuple[int, int]:
|
||||
# Remove matching brackets
|
||||
if s < e <= len(text):
|
||||
before = text[s]
|
||||
@@ -228,7 +258,7 @@ def brackets(text, s, e):
|
||||
|
||||
|
||||
@postprocessor
|
||||
def quotes(text, s, e):
|
||||
def quotes(text: str, s: int, e: int) -> Tuple[int, int]:
|
||||
# Remove matching quotes
|
||||
if s < e <= len(text):
|
||||
before = text[s]
|
||||
@@ -238,36 +268,42 @@ def quotes(text, s, e):
|
||||
return s, e
|
||||
|
||||
|
||||
def mark(pattern, post_processors, text, args):
|
||||
def mark(pattern: str, post_processors: Iterable[PostprocessorFunc], text: str, args: HintsCLIOptions) -> Generator[Mark, None, None]:
|
||||
pat = re.compile(pattern)
|
||||
for idx, (s, e) in enumerate(regex_finditer(pat, args.minimum_match_length, text)):
|
||||
for idx, (s, e, groupdict) in enumerate(regex_finditer(pat, args.minimum_match_length, text)):
|
||||
for func in post_processors:
|
||||
s, e = func(text, s, e)
|
||||
mark_text = text[s:e].replace('\n', '').replace('\0', '')
|
||||
yield Mark(idx, s, e, mark_text)
|
||||
yield Mark(idx, s, e, mark_text, groupdict)
|
||||
|
||||
|
||||
def run_loop(args, text, all_marks, index_map):
|
||||
def run_loop(args: HintsCLIOptions, text: str, all_marks: Sequence[Mark], index_map: Dict[int, Mark], extra_cli_args: Sequence[str] = ()) -> Dict[str, Any]:
|
||||
loop = Loop()
|
||||
handler = Hints(text, all_marks, index_map, args)
|
||||
loop.loop(handler)
|
||||
if handler.chosen and loop.return_code == 0:
|
||||
return {'match': handler.chosen, 'programs': args.program,
|
||||
'multiple_joiner': args.multiple_joiner,
|
||||
'type': args.type}
|
||||
return {
|
||||
'match': handler.text_matches, 'programs': args.program,
|
||||
'multiple_joiner': args.multiple_joiner, 'customize_processing': args.customize_processing,
|
||||
'type': args.type, 'groupdicts': handler.groupdicts, 'extra_cli_args': extra_cli_args, 'linenum_action': args.linenum_action
|
||||
}
|
||||
raise SystemExit(loop.return_code)
|
||||
|
||||
|
||||
def escape(chars):
|
||||
def escape(chars: str) -> str:
|
||||
return chars.replace('\\', '\\\\').replace('-', r'\-').replace(']', r'\]')
|
||||
|
||||
|
||||
def functions_for(args):
|
||||
def functions_for(args: HintsCLIOptions) -> Tuple[str, List[PostprocessorFunc]]:
|
||||
post_processors = []
|
||||
if args.type == 'url':
|
||||
if args.url_prefixes == 'default':
|
||||
url_prefixes = kitty_common_opts()['url_prefixes']
|
||||
else:
|
||||
url_prefixes = tuple(args.url_prefixes.split(','))
|
||||
from .url_regex import url_delimiters
|
||||
pattern = '(?:{})://[^{}]{{3,}}'.format(
|
||||
'|'.join(args.url_prefixes.split(',')), url_delimiters
|
||||
'|'.join(url_prefixes), url_delimiters
|
||||
)
|
||||
post_processors.append(url)
|
||||
elif args.type == 'path':
|
||||
@@ -280,8 +316,7 @@ def functions_for(args):
|
||||
elif args.type == 'word':
|
||||
chars = args.word_characters
|
||||
if chars is None:
|
||||
import json
|
||||
chars = json.loads(os.environ['KITTY_COMMON_OPTS'])['select_by_word_characters']
|
||||
chars = kitty_common_opts()['select_by_word_characters']
|
||||
pattern = r'(?u)[{}\w]{{{},}}'.format(escape(chars), args.minimum_match_length)
|
||||
post_processors.extend((brackets, quotes))
|
||||
else:
|
||||
@@ -289,8 +324,8 @@ def functions_for(args):
|
||||
return pattern, post_processors
|
||||
|
||||
|
||||
def convert_text(text, cols):
|
||||
lines = []
|
||||
def convert_text(text: str, cols: int) -> str:
|
||||
lines: List[str] = []
|
||||
empty_line = '\0' * cols
|
||||
for full_line in text.split('\n'):
|
||||
if full_line:
|
||||
@@ -303,29 +338,61 @@ def convert_text(text, cols):
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def parse_input(text):
|
||||
def parse_input(text: str) -> str:
|
||||
try:
|
||||
cols = int(os.environ['OVERLAID_WINDOW_COLS'])
|
||||
except KeyError:
|
||||
cols = screen_size().cols
|
||||
cols = screen_size_function()().cols
|
||||
return convert_text(text, cols)
|
||||
|
||||
|
||||
def run(args, text):
|
||||
def linenum_marks(text: str, args: HintsCLIOptions, Mark: Type[Mark], extra_cli_args: Sequence[str], *a: Any) -> Generator[Mark, None, None]:
|
||||
regex = args.regex
|
||||
if regex == DEFAULT_REGEX:
|
||||
regex = r'(?P<path>(?:\S*/\S+?)|(?:\S+[.][a-zA-Z0-9]{2,7})):(?P<line>\d+)'
|
||||
yield from mark(regex, [brackets, quotes], text, args)
|
||||
|
||||
|
||||
def load_custom_processor(customize_processing: str) -> Any:
|
||||
if customize_processing.startswith('::import::'):
|
||||
import importlib
|
||||
m = importlib.import_module(customize_processing[len('::import::'):])
|
||||
return {k: getattr(m, k) for k in dir(m)}
|
||||
if customize_processing == '::linenum::':
|
||||
return {'mark': linenum_marks, 'handle_result': linenum_handle_result}
|
||||
from kitty.constants import resolve_custom_file
|
||||
custom_path = resolve_custom_file(customize_processing)
|
||||
import runpy
|
||||
return runpy.run_path(custom_path, run_name='__main__')
|
||||
|
||||
|
||||
def run(args: HintsCLIOptions, text: str, extra_cli_args: Sequence[str] = ()) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
pattern, post_processors = functions_for(args)
|
||||
text = parse_input(text)
|
||||
all_marks = tuple(mark(pattern, post_processors, text, args))
|
||||
pattern, post_processors = functions_for(args)
|
||||
if args.type == 'linenum':
|
||||
args.customize_processing = '::linenum::'
|
||||
if args.customize_processing:
|
||||
m = load_custom_processor(args.customize_processing)
|
||||
if 'mark' in m:
|
||||
all_marks = tuple(m['mark'](text, args, Mark, extra_cli_args))
|
||||
else:
|
||||
all_marks = tuple(mark(pattern, post_processors, text, args))
|
||||
else:
|
||||
all_marks = tuple(mark(pattern, post_processors, text, args))
|
||||
if not all_marks:
|
||||
input(_('No {} found, press Enter to quit.').format(
|
||||
'URLs' if args.type == 'url' else 'matches'
|
||||
))
|
||||
return
|
||||
return None
|
||||
|
||||
largest_index = all_marks[-1].index
|
||||
offset = max(0, args.hints_offset)
|
||||
for m in all_marks:
|
||||
m.index = largest_index - m.index + offset
|
||||
if args.ascending:
|
||||
m.index += offset
|
||||
else:
|
||||
m.index = largest_index - m.index + offset
|
||||
index_map = {m.index: m for m in all_marks}
|
||||
except Exception:
|
||||
import traceback
|
||||
@@ -333,7 +400,7 @@ def run(args, text):
|
||||
input('Press Enter to quit.')
|
||||
raise SystemExit(1)
|
||||
|
||||
return run_loop(args, text, all_marks, index_map)
|
||||
return run_loop(args, text, all_marks, index_map, extra_cli_args)
|
||||
|
||||
|
||||
# CLI {{{
|
||||
@@ -342,30 +409,52 @@ OPTIONS = r'''
|
||||
type=list
|
||||
What program to use to open matched text. Defaults to the default open program
|
||||
for the operating system. Use a value of :file:`-` to paste the match into the
|
||||
terminal window instead. A value of :file:`@` will copy the match to the clipboard.
|
||||
A value of :file:`default` will run the default open program. Can be specified
|
||||
multiple times to run multiple programs.
|
||||
terminal window instead. A value of :file:`@` will copy the match to the
|
||||
clipboard. A value of :file:`*` will copy the match to the primary selection
|
||||
(on systems that support primary selections). A value of :file:`default` will
|
||||
run the default open program. Can be specified multiple times to run multiple
|
||||
programs.
|
||||
|
||||
|
||||
--type
|
||||
default=url
|
||||
choices=url,regex,path,line,hash,word
|
||||
The type of text to search for.
|
||||
choices=url,regex,path,line,hash,word,linenum
|
||||
The type of text to search for. A value of :code:`linenum` looks for error messages
|
||||
using the pattern specified with :option:`--regex`, which must have the named groups,
|
||||
path and line. If not specified, will look for :code:`path:line`.
|
||||
The :option:`--linenum-action` option controls what to do with the selected error message.
|
||||
|
||||
|
||||
--regex
|
||||
default=(?m)^\s*(.+)\s*$
|
||||
default={default_regex}
|
||||
The regular expression to use when :option:`kitty +kitten hints --type`=regex.
|
||||
The regular expression is in python syntax. If you specify a group in
|
||||
The regular expression is in python syntax. If you specify a numbered group in
|
||||
the regular expression only the group will be matched. This allow you to match
|
||||
text ignoring a prefix/suffix, as needed. The default expression matches lines.
|
||||
To match text over multiple lines you should prefix the regular expression with
|
||||
:code:`(?ms)`, which turns on MULTILINE and DOTALL modes for the regex engine.
|
||||
If you specify named groups and a :option:`kitty +kitten hints --program` then
|
||||
the program will be passed arguments corresponding to each named group of
|
||||
the form key=value.
|
||||
|
||||
|
||||
--linenum-action
|
||||
default=self
|
||||
type=choice
|
||||
choices=self,window,tab,os_window,background
|
||||
The action to perform on the matched errors. The actual action is whatever
|
||||
arguments are provided to the kitten, for example:
|
||||
:code:`kitty + kitten hints --type=linenum vim +{line} {path}`
|
||||
will open the matched path at the matched line number in vim. This option
|
||||
controls where the action is executed: :code:`self` means the current window,
|
||||
:code:`window` a new kitty window, :code:`tab` a new tab, :code:`os_window`
|
||||
a new OS window and :code:`background` run in the background.
|
||||
|
||||
|
||||
--url-prefixes
|
||||
default={0}
|
||||
Comma separated list of recognized URL prefixes.
|
||||
default=default
|
||||
Comma separated list of recognized URL prefixes. Defaults, to
|
||||
the list of prefixes defined in kitty.conf.
|
||||
|
||||
|
||||
--word-characters
|
||||
@@ -418,50 +507,105 @@ unless you specify the hints offset as zero the first match will be highlighted
|
||||
the second character you specify.
|
||||
|
||||
|
||||
'''.format(','.join(sorted(URL_PREFIXES))).format
|
||||
--ascending
|
||||
type=bool-set
|
||||
Have the hints increase from top to bottom instead of decreasing from top to bottom.
|
||||
|
||||
|
||||
--customize-processing
|
||||
Name of a python file in the kitty config directory which will be imported to provide
|
||||
custom implementations for pattern finding and performing actions
|
||||
on selected matches. See https://sw.kovidgoyal.net/kitty/kittens/hints.html
|
||||
for details. You can also specify absolute paths to load the script from elsewhere.
|
||||
|
||||
|
||||
'''.format(
|
||||
default_regex=DEFAULT_REGEX,
|
||||
line='{{line}}', path='{{path}}'
|
||||
).format
|
||||
help_text = 'Select text from the screen using the keyboard. Defaults to searching for URLs.'
|
||||
usage = ''
|
||||
|
||||
|
||||
def parse_hints_args(args):
|
||||
return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten hints')
|
||||
def parse_hints_args(args: List[str]) -> Tuple[HintsCLIOptions, List[str]]:
|
||||
return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten hints', result_class=HintsCLIOptions)
|
||||
|
||||
|
||||
def main(args):
|
||||
def main(args: List[str]) -> Optional[Dict[str, Any]]:
|
||||
text = ''
|
||||
if sys.stdin.isatty():
|
||||
if '--help' not in args and '-h' not in args:
|
||||
print('You must pass the text to be hinted on STDIN', file=sys.stderr)
|
||||
input(_('Press Enter to quit'))
|
||||
return
|
||||
return None
|
||||
else:
|
||||
text = sys.stdin.buffer.read().decode('utf-8')
|
||||
sys.stdin = open(os.ctermid())
|
||||
try:
|
||||
args, items = parse_hints_args(args[1:])
|
||||
opts, items = parse_hints_args(args[1:])
|
||||
except SystemExit as e:
|
||||
if e.code != 0:
|
||||
print(e.args[0], file=sys.stderr)
|
||||
input(_('Press Enter to quit'))
|
||||
return
|
||||
if items:
|
||||
return None
|
||||
if items and not (opts.customize_processing or opts.type == 'linenum'):
|
||||
print('Extra command line arguments present: {}'.format(' '.join(items)), file=sys.stderr)
|
||||
input(_('Press Enter to quit'))
|
||||
return run(args, text)
|
||||
return run(opts, text, items)
|
||||
|
||||
|
||||
def handle_result(args, data, target_window_id, boss):
|
||||
def linenum_handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: BossType, extra_cli_args: Sequence[str], *a: Any) -> None:
|
||||
for m, g in zip(data['match'], data['groupdicts']):
|
||||
if m:
|
||||
path, line = g['path'], g['line']
|
||||
path = path.split(':')[-1]
|
||||
line = int(line)
|
||||
break
|
||||
else:
|
||||
return
|
||||
|
||||
cmd = [x.format(path=path, line=line) for x in extra_cli_args or ('vim', '+{line}', '{path}')]
|
||||
w = boss.window_id_map.get(target_window_id)
|
||||
action = data['linenum_action']
|
||||
|
||||
if action == 'self':
|
||||
if w is not None:
|
||||
import shlex
|
||||
text = ' '.join(shlex.quote(arg) for arg in cmd)
|
||||
w.paste_bytes(text + '\r')
|
||||
elif action == 'background':
|
||||
import subprocess
|
||||
subprocess.Popen(cmd)
|
||||
else:
|
||||
getattr(boss, {
|
||||
'window': 'new_window_with_cwd', 'tab': 'new_tab_with_cwd', 'os_window': 'new_os_window_with_cwd'
|
||||
}[action])(*cmd)
|
||||
|
||||
|
||||
@result_handler(type_of_input='screen')
|
||||
def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: BossType) -> None:
|
||||
if data['customize_processing']:
|
||||
m = load_custom_processor(data['customize_processing'])
|
||||
if 'handle_result' in m:
|
||||
m['handle_result'](args, data, target_window_id, boss, data['extra_cli_args'])
|
||||
return None
|
||||
|
||||
programs = data['programs'] or ('default',)
|
||||
matches = tuple(filter(None, data['match']))
|
||||
matches: List[str] = []
|
||||
groupdicts = []
|
||||
for m, g in zip(data['match'], data['groupdicts']):
|
||||
if m:
|
||||
matches.append(m)
|
||||
groupdicts.append(g)
|
||||
joiner = data['multiple_joiner']
|
||||
try:
|
||||
is_int = int(joiner)
|
||||
is_int: Optional[int] = int(joiner)
|
||||
except Exception:
|
||||
is_int = None
|
||||
text_type = data['type']
|
||||
|
||||
@lru_cache()
|
||||
def joined_text():
|
||||
def joined_text() -> str:
|
||||
if is_int is not None:
|
||||
try:
|
||||
return matches[is_int]
|
||||
@@ -483,26 +627,30 @@ def handle_result(args, data, target_window_id, boss):
|
||||
w.paste(joined_text())
|
||||
elif program == '@':
|
||||
set_clipboard_string(joined_text())
|
||||
elif program == '*':
|
||||
set_primary_selection(joined_text())
|
||||
else:
|
||||
cwd = None
|
||||
w = boss.window_id_map.get(target_window_id)
|
||||
if w is not None:
|
||||
cwd = w.cwd_of_child
|
||||
program = None if program == 'default' else program
|
||||
for m in matches:
|
||||
for m, groupdict in zip(matches, groupdicts):
|
||||
if groupdict:
|
||||
m = []
|
||||
for k, v in groupdict.items():
|
||||
m.append('{}={}'.format(k, v or ''))
|
||||
boss.open_url(m, program, cwd=cwd)
|
||||
|
||||
|
||||
handle_result.type_of_input = 'screen'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Run with kitty +kitten hints
|
||||
ans = main(sys.argv)
|
||||
if ans:
|
||||
print(ans)
|
||||
elif __name__ == '__doc__':
|
||||
sys.cli_docs['usage'] = usage
|
||||
sys.cli_docs['options'] = OPTIONS
|
||||
sys.cli_docs['help_text'] = help_text
|
||||
cd = sys.cli_docs # type: ignore
|
||||
cd['usage'] = usage
|
||||
cd['options'] = OPTIONS
|
||||
cd['help_text'] = help_text
|
||||
# }}}
|
||||
|
||||
@@ -9,20 +9,27 @@ import signal
|
||||
import sys
|
||||
import zlib
|
||||
from base64 import standard_b64encode
|
||||
from collections import namedtuple
|
||||
from functools import lru_cache
|
||||
from math import ceil
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import (
|
||||
Dict, Generator, List, NamedTuple, Optional, Pattern, Tuple, Union
|
||||
)
|
||||
|
||||
from kitty.cli import parse_args
|
||||
from kitty.cli_stub import IcatCLIOptions
|
||||
from kitty.constants import appname
|
||||
from kitty.utils import TTYIO, fit_image, screen_size_function
|
||||
from kitty.typing import GRT_f, GRT_t
|
||||
from kitty.utils import (
|
||||
TTYIO, ScreenSize, ScreenSizeGetter, fit_image, screen_size_function
|
||||
)
|
||||
|
||||
from ..tui.images import (
|
||||
ConvertFailed, NoImageMagick, OpenFailed, convert, fsenc, identify
|
||||
ConvertFailed, GraphicsCommand, NoImageMagick, OpenFailed, convert, fsenc,
|
||||
identify
|
||||
)
|
||||
from ..tui.operations import clear_images_on_screen, serialize_gr_command
|
||||
from ..tui.operations import clear_images_on_screen
|
||||
|
||||
screen_size = None
|
||||
OPTIONS = '''\
|
||||
--align
|
||||
type=choices
|
||||
@@ -94,23 +101,43 @@ but you can turn it off or on explicitly, if needed.
|
||||
--silent
|
||||
type=bool-set
|
||||
Do not print out anything to stdout during operation.
|
||||
|
||||
|
||||
--z-index -z
|
||||
default=0
|
||||
Z-index of the image. When negative, text will be displayed on top of the image. Use
|
||||
a double minus for values under the threshold for drawing images under cell background
|
||||
colors. For example, --1 evaluates as -1,073,741,825.
|
||||
'''
|
||||
|
||||
|
||||
def options_spec():
|
||||
if not hasattr(options_spec, 'ans'):
|
||||
options_spec.ans = OPTIONS.format(
|
||||
appname='{}-icat'.format(appname),
|
||||
)
|
||||
return options_spec.ans
|
||||
screen_size: Optional[ScreenSizeGetter] = None
|
||||
can_transfer_with_files = False
|
||||
|
||||
|
||||
def write_gr_cmd(cmd, payload=None):
|
||||
sys.stdout.buffer.write(serialize_gr_command(cmd, payload))
|
||||
def get_screen_size_function() -> ScreenSizeGetter:
|
||||
global screen_size
|
||||
if screen_size is None:
|
||||
screen_size = screen_size_function()
|
||||
return screen_size
|
||||
|
||||
|
||||
def get_screen_size() -> ScreenSize:
|
||||
screen_size = get_screen_size_function()
|
||||
return screen_size()
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def options_spec() -> str:
|
||||
return OPTIONS.format(appname='{}-icat'.format(appname))
|
||||
|
||||
|
||||
def write_gr_cmd(cmd: GraphicsCommand, payload: Optional[bytes] = None) -> None:
|
||||
sys.stdout.buffer.write(cmd.serialize(payload or b''))
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def calculate_in_cell_x_offset(width, cell_width, align):
|
||||
def calculate_in_cell_x_offset(width: int, cell_width: int, align: str) -> int:
|
||||
if align == 'left':
|
||||
return 0
|
||||
extra_pixels = width % cell_width
|
||||
@@ -121,17 +148,17 @@ def calculate_in_cell_x_offset(width, cell_width, align):
|
||||
return (cell_width - extra_pixels) // 2
|
||||
|
||||
|
||||
def set_cursor(cmd, width, height, align):
|
||||
ss = screen_size()
|
||||
def set_cursor(cmd: GraphicsCommand, width: int, height: int, align: str) -> None:
|
||||
ss = get_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))
|
||||
cmd['c'], cmd['r'] = ss.cols, num_of_rows_needed
|
||||
cmd.c, cmd.r = ss.cols, num_of_rows_needed
|
||||
else:
|
||||
cmd['X'] = calculate_in_cell_x_offset(width, cw, align)
|
||||
cmd.X = calculate_in_cell_x_offset(width, cw, align)
|
||||
extra_cells = 0
|
||||
if align == 'center':
|
||||
extra_cells = (ss.cols - num_of_cells_needed) // 2
|
||||
@@ -141,12 +168,12 @@ def set_cursor(cmd, width, height, align):
|
||||
sys.stdout.buffer.write(b' ' * extra_cells)
|
||||
|
||||
|
||||
def set_cursor_for_place(place, cmd, width, height, align):
|
||||
def set_cursor_for_place(place: 'Place', cmd: GraphicsCommand, width: int, height: int, align: str) -> None:
|
||||
x = place.left + 1
|
||||
ss = screen_size()
|
||||
ss = get_screen_size()
|
||||
cw = int(ss.width / ss.cols)
|
||||
num_of_cells_needed = int(ceil(width / cw))
|
||||
cmd['X'] = calculate_in_cell_x_offset(width, cw, align)
|
||||
cmd.X = calculate_in_cell_x_offset(width, cw, align)
|
||||
extra_cells = 0
|
||||
if align == 'center':
|
||||
extra_cells = (place.width - num_of_cells_needed) // 2
|
||||
@@ -155,27 +182,38 @@ def set_cursor_for_place(place, cmd, width, height, align):
|
||||
sys.stdout.buffer.write('\033[{};{}H'.format(place.top + 1, x + extra_cells).encode('ascii'))
|
||||
|
||||
|
||||
def write_chunked(cmd, data):
|
||||
if cmd['f'] != 100:
|
||||
def write_chunked(cmd: GraphicsCommand, data: bytes) -> None:
|
||||
if cmd.f != 100:
|
||||
data = zlib.compress(data)
|
||||
cmd['o'] = 'z'
|
||||
cmd.o = 'z'
|
||||
data = standard_b64encode(data)
|
||||
while data:
|
||||
chunk, data = data[:4096], data[4096:]
|
||||
m = 1 if data else 0
|
||||
cmd['m'] = m
|
||||
cmd.m = 1 if data else 0
|
||||
write_gr_cmd(cmd, chunk)
|
||||
cmd.clear()
|
||||
|
||||
|
||||
def show(outfile, width, height, fmt, transmit_mode='t', align='center', place=None):
|
||||
cmd = {'a': 'T', 'f': fmt, 's': width, 'v': height}
|
||||
def show(
|
||||
outfile: str,
|
||||
width: int, height: int, zindex: int,
|
||||
fmt: 'GRT_f',
|
||||
transmit_mode: 'GRT_t' = 't',
|
||||
align: str = 'center',
|
||||
place: Optional['Place'] = None
|
||||
) -> None:
|
||||
cmd = GraphicsCommand()
|
||||
cmd.a = 'T'
|
||||
cmd.f = fmt
|
||||
cmd.s = width
|
||||
cmd.v = height
|
||||
cmd.z = zindex
|
||||
if place:
|
||||
set_cursor_for_place(place, cmd, width, height, align)
|
||||
else:
|
||||
set_cursor(cmd, width, height, align)
|
||||
if detect_support.has_files:
|
||||
cmd['t'] = transmit_mode
|
||||
if can_transfer_with_files:
|
||||
cmd.t = transmit_mode
|
||||
write_gr_cmd(cmd, standard_b64encode(os.path.abspath(outfile).encode(fsenc)))
|
||||
else:
|
||||
with open(outfile, 'rb') as f:
|
||||
@@ -183,35 +221,49 @@ def show(outfile, width, height, fmt, transmit_mode='t', align='center', place=N
|
||||
if transmit_mode == 't':
|
||||
os.unlink(outfile)
|
||||
if fmt == 100:
|
||||
cmd['S'] = len(data)
|
||||
cmd.S = len(data)
|
||||
write_chunked(cmd, data)
|
||||
|
||||
|
||||
def process(path, args, is_tempfile):
|
||||
def parse_z_index(val: str) -> int:
|
||||
origin = 0
|
||||
if val.startswith('--'):
|
||||
val = val[1:]
|
||||
origin = -1073741824
|
||||
return origin + int(val)
|
||||
|
||||
|
||||
class ParsedOpts:
|
||||
|
||||
place: Optional['Place'] = None
|
||||
z_index: int = 0
|
||||
|
||||
|
||||
def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfile: bool) -> bool:
|
||||
m = identify(path)
|
||||
ss = screen_size()
|
||||
available_width = args.place.width * (ss.width / ss.cols) if args.place else ss.width
|
||||
available_height = args.place.height * (ss.height / ss.rows) if args.place else 10 * m.height
|
||||
ss = get_screen_size()
|
||||
available_width = parsed_opts.place.width * (ss.width // ss.cols) if parsed_opts.place else ss.width
|
||||
available_height = parsed_opts.place.height * (ss.height // ss.rows) if parsed_opts.place else 10 * m.height
|
||||
needs_scaling = m.width > available_width or m.height > available_height
|
||||
needs_scaling = needs_scaling or args.scale_up
|
||||
file_removed = False
|
||||
if m.fmt == 'png' and not needs_scaling:
|
||||
outfile = path
|
||||
transmit_mode = 't' if is_tempfile else 'f'
|
||||
fmt = 100
|
||||
transmit_mode: 'GRT_t' = 't' if is_tempfile else 'f'
|
||||
fmt: 'GRT_f' = 100
|
||||
width, height = m.width, m.height
|
||||
file_removed = transmit_mode == 't'
|
||||
else:
|
||||
fmt = 24 if m.mode == 'rgb' else 32
|
||||
transmit_mode = 't'
|
||||
outfile, width, height = convert(path, m, available_width, available_height, args.scale_up)
|
||||
show(outfile, width, height, fmt, transmit_mode, align=args.align, place=args.place)
|
||||
show(outfile, width, height, parsed_opts.z_index, fmt, transmit_mode, align=args.align, place=parsed_opts.place)
|
||||
if not args.place:
|
||||
print() # ensure cursor is on a new line
|
||||
return file_removed
|
||||
|
||||
|
||||
def scan(d):
|
||||
def scan(d: str) -> Generator[Tuple[str, str], None, None]:
|
||||
for dirpath, dirnames, filenames in os.walk(d):
|
||||
for f in filenames:
|
||||
mt = mimetypes.guess_type(f)[0]
|
||||
@@ -219,23 +271,24 @@ def scan(d):
|
||||
yield os.path.join(dirpath, f), mt
|
||||
|
||||
|
||||
def detect_support(wait_for=10, silent=False):
|
||||
def detect_support(wait_for: float = 10, silent: bool = False) -> bool:
|
||||
global can_transfer_with_files
|
||||
if not silent:
|
||||
print('Checking for graphics ({}s max. wait)...'.format(wait_for), end='\r')
|
||||
sys.stdout.flush()
|
||||
try:
|
||||
received = b''
|
||||
responses = {}
|
||||
responses: Dict[int, bool] = {}
|
||||
|
||||
def parse_responses():
|
||||
def parse_responses() -> None:
|
||||
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'
|
||||
iid_ = int(iid.decode('ascii'))
|
||||
if iid_ not in responses:
|
||||
responses[iid_] = m.group(2) == b'OK'
|
||||
|
||||
def more_needed(data):
|
||||
def more_needed(data: bytes) -> bool:
|
||||
nonlocal received
|
||||
received += data
|
||||
parse_responses()
|
||||
@@ -243,23 +296,36 @@ def detect_support(wait_for=10, silent=False):
|
||||
|
||||
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)))
|
||||
gc = GraphicsCommand()
|
||||
gc.a = 'q'
|
||||
gc.s = gc.v = gc.i = 1
|
||||
write_gr_cmd(gc, standard_b64encode(b'abcd'))
|
||||
gc.t = 'f'
|
||||
gc.i = 2
|
||||
write_gr_cmd(gc, standard_b64encode(f.name.encode(fsenc)))
|
||||
with TTYIO() as io:
|
||||
io.recv(more_needed, timeout=float(wait_for))
|
||||
io.recv(more_needed, timeout=wait_for)
|
||||
finally:
|
||||
if not silent:
|
||||
sys.stdout.buffer.write(b'\033[J'), sys.stdout.flush()
|
||||
detect_support.has_files = bool(responses.get(2))
|
||||
can_transfer_with_files = bool(responses.get(2))
|
||||
return responses.get(1, False)
|
||||
|
||||
|
||||
def parse_place(raw):
|
||||
class Place(NamedTuple):
|
||||
width: int
|
||||
height: int
|
||||
left: int
|
||||
top: int
|
||||
|
||||
|
||||
def parse_place(raw: str) -> Optional[Place]:
|
||||
if raw:
|
||||
area, pos = raw.split('@', 1)
|
||||
w, h = map(int, area.split('x'))
|
||||
l, t = map(int, pos.split('x'))
|
||||
return namedtuple('Place', 'width height left top')(w, h, l, t)
|
||||
return Place(w, h, l, t)
|
||||
return None
|
||||
|
||||
|
||||
help_text = (
|
||||
@@ -273,7 +339,13 @@ help_text = (
|
||||
usage = 'image-file-or-url-or-directory ...'
|
||||
|
||||
|
||||
def process_single_item(item, args, url_pat=None, maybe_dir=True):
|
||||
def process_single_item(
|
||||
item: Union[bytes, str],
|
||||
args: IcatCLIOptions,
|
||||
parsed_opts: ParsedOpts,
|
||||
url_pat: Optional[Pattern] = None,
|
||||
maybe_dir: bool = True
|
||||
) -> None:
|
||||
is_tempfile = False
|
||||
file_removed = False
|
||||
try:
|
||||
@@ -291,34 +363,35 @@ def process_single_item(item, args, url_pat=None, maybe_dir=True):
|
||||
raise SystemExit('Failed to download image at URL: {} with error: {}'.format(item, e))
|
||||
item = tf.name
|
||||
is_tempfile = True
|
||||
file_removed = process(item, args, is_tempfile)
|
||||
file_removed = process(item, args, parsed_opts, is_tempfile)
|
||||
elif item.lower().startswith('file://'):
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import url2pathname
|
||||
item = urlparse(item)
|
||||
pitem = urlparse(item)
|
||||
if os.sep == '\\':
|
||||
item = item.netloc + item.path
|
||||
item = pitem.netloc + pitem.path
|
||||
else:
|
||||
item = item.path
|
||||
item = pitem.path
|
||||
item = url2pathname(item)
|
||||
file_removed = process(item, args, is_tempfile)
|
||||
file_removed = process(item, args, parsed_opts, is_tempfile)
|
||||
else:
|
||||
if maybe_dir and os.path.isdir(item):
|
||||
for (x, mt) in scan(item):
|
||||
process_single_item(x, args, url_pat=None, maybe_dir=False)
|
||||
process_single_item(x, args, parsed_opts, url_pat=None, maybe_dir=False)
|
||||
else:
|
||||
file_removed = process(item, args, is_tempfile)
|
||||
file_removed = process(item, args, parsed_opts, is_tempfile)
|
||||
finally:
|
||||
if is_tempfile and not file_removed:
|
||||
os.remove(item)
|
||||
|
||||
|
||||
def main(args=sys.argv):
|
||||
global screen_size
|
||||
args, items = parse_args(args[1:], options_spec, usage, help_text, '{} +kitten icat'.format(appname))
|
||||
def main(args: List[str] = sys.argv) -> None:
|
||||
global can_transfer_with_files
|
||||
cli_opts, items_ = parse_args(args[1:], options_spec, usage, help_text, '{} +kitten icat'.format(appname), result_class=IcatCLIOptions)
|
||||
items: List[Union[str, bytes]] = list(items_)
|
||||
|
||||
if args.print_window_size:
|
||||
screen_size_function.ans = None
|
||||
if cli_opts.print_window_size:
|
||||
screen_size_function.cache_clear()
|
||||
with open(os.ctermid()) as tty:
|
||||
ss = screen_size_function(tty)()
|
||||
print('{}x{}'.format(ss.width, ss.height), end='')
|
||||
@@ -327,58 +400,65 @@ def main(args=sys.argv):
|
||||
if not sys.stdout.isatty():
|
||||
sys.stdout = open(os.ctermid(), 'w')
|
||||
stdin_data = None
|
||||
if args.stdin == 'yes' or (not sys.stdin.isatty() and args.stdin == 'detect'):
|
||||
if cli_opts.stdin == 'yes' or (not sys.stdin.isatty() and cli_opts.stdin == 'detect'):
|
||||
stdin_data = sys.stdin.buffer.read()
|
||||
if stdin_data:
|
||||
items.insert(0, stdin_data)
|
||||
sys.stdin.close()
|
||||
sys.stdin = open(os.ctermid(), 'r')
|
||||
|
||||
screen_size = screen_size_function()
|
||||
screen_size = get_screen_size_function()
|
||||
signal.signal(signal.SIGWINCH, lambda signum, frame: setattr(screen_size, 'changed', True))
|
||||
if screen_size().width == 0:
|
||||
if args.detect_support:
|
||||
if cli_opts.detect_support:
|
||||
raise SystemExit(1)
|
||||
raise SystemExit(
|
||||
'Terminal does not support reporting screen sizes via the TIOCGWINSZ ioctl'
|
||||
)
|
||||
try:
|
||||
args.place = parse_place(args.place)
|
||||
except Exception:
|
||||
raise SystemExit('Not a valid place specification: {}'.format(args.place))
|
||||
parsed_opts = ParsedOpts()
|
||||
if cli_opts.place:
|
||||
try:
|
||||
parsed_opts.place = parse_place(cli_opts.place)
|
||||
except Exception:
|
||||
raise SystemExit('Not a valid place specification: {}'.format(cli_opts.place))
|
||||
|
||||
if args.detect_support:
|
||||
if not detect_support(wait_for=args.detection_timeout, silent=True):
|
||||
try:
|
||||
parsed_opts.z_index = parse_z_index(cli_opts.z_index)
|
||||
except Exception:
|
||||
raise SystemExit('Not a valid z-index specification: {}'.format(cli_opts.z_index))
|
||||
|
||||
if cli_opts.detect_support:
|
||||
if not detect_support(wait_for=cli_opts.detection_timeout, silent=True):
|
||||
raise SystemExit(1)
|
||||
print('file' if detect_support.has_files else 'stream', end='', file=sys.stderr)
|
||||
print('file' if can_transfer_with_files else 'stream', end='', file=sys.stderr)
|
||||
return
|
||||
if args.transfer_mode == 'detect':
|
||||
if not detect_support(wait_for=args.detection_timeout, silent=args.silent):
|
||||
if cli_opts.transfer_mode == 'detect':
|
||||
if not detect_support(wait_for=cli_opts.detection_timeout, silent=cli_opts.silent):
|
||||
raise SystemExit('This terminal emulator does not support the graphics protocol, use a terminal emulator such as kitty that does support it')
|
||||
else:
|
||||
detect_support.has_files = args.transfer_mode == 'file'
|
||||
can_transfer_with_files = cli_opts.transfer_mode == 'file'
|
||||
errors = []
|
||||
if args.clear:
|
||||
sys.stdout.buffer.write(clear_images_on_screen(delete_data=True))
|
||||
if cli_opts.clear:
|
||||
sys.stdout.write(clear_images_on_screen(delete_data=True))
|
||||
if not items:
|
||||
return
|
||||
if not items:
|
||||
raise SystemExit('You must specify at least one file to cat')
|
||||
if args.place:
|
||||
if parsed_opts.place:
|
||||
if len(items) > 1 or (isinstance(items[0], str) and os.path.isdir(items[0])):
|
||||
raise SystemExit(f'The --place option can only be used with a single image, not {items}')
|
||||
sys.stdout.buffer.write(b'\0337') # save cursor
|
||||
url_pat = re.compile(r'(?:https?|ftp)://', flags=re.I)
|
||||
for item in items:
|
||||
try:
|
||||
process_single_item(item, args, url_pat)
|
||||
process_single_item(item, cli_opts, parsed_opts, url_pat)
|
||||
except NoImageMagick as e:
|
||||
raise SystemExit(str(e))
|
||||
except ConvertFailed as e:
|
||||
raise SystemExit(str(e))
|
||||
except OpenFailed as e:
|
||||
errors.append(e)
|
||||
if args.place:
|
||||
if parsed_opts.place:
|
||||
sys.stdout.buffer.write(b'\0338') # restore cursor
|
||||
if not errors:
|
||||
return
|
||||
@@ -390,6 +470,7 @@ def main(args=sys.argv):
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
elif __name__ == '__doc__':
|
||||
sys.cli_docs['usage'] = usage
|
||||
sys.cli_docs['options'] = options_spec
|
||||
sys.cli_docs['help_text'] = help_text
|
||||
cd = sys.cli_docs # type: ignore
|
||||
cd['usage'] = usage
|
||||
cd['options'] = options_spec
|
||||
cd['help_text'] = help_text
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import sys
|
||||
from typing import List
|
||||
|
||||
from kitty.key_encoding import (
|
||||
ALT, CTRL, PRESS, RELEASE, REPEAT, SHIFT, SUPER, encode_key_event
|
||||
ALT, CTRL, PRESS, RELEASE, REPEAT, SHIFT, SUPER, KeyEvent,
|
||||
encode_key_event
|
||||
)
|
||||
|
||||
from ..tui.handler import Handler
|
||||
@@ -14,41 +16,41 @@ from ..tui.loop import Loop
|
||||
|
||||
class KeysHandler(Handler):
|
||||
|
||||
def initialize(self):
|
||||
def initialize(self) -> None:
|
||||
self.cmd.set_window_title('Kitty extended keyboard protocol demo')
|
||||
self.cmd.set_cursor_visible(False)
|
||||
self.print('Press any keys - Ctrl+C or Ctrl+D will terminate')
|
||||
|
||||
def on_text(self, text, in_bracketed_paste=False):
|
||||
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
|
||||
self.print('Text input: ' + text)
|
||||
|
||||
def on_key(self, key_event):
|
||||
def on_key(self, key_event: KeyEvent) -> None:
|
||||
etype = {
|
||||
PRESS: 'PRESS',
|
||||
REPEAT: 'REPEAT',
|
||||
RELEASE: 'RELEASE'
|
||||
}[key_event.type]
|
||||
mods = []
|
||||
lmods = []
|
||||
for m, name in {
|
||||
SHIFT: 'Shift',
|
||||
ALT: 'Alt',
|
||||
CTRL: 'Ctrl',
|
||||
SUPER: 'Super'}.items():
|
||||
if key_event.mods & m:
|
||||
mods.append(name)
|
||||
mods = '+'.join(mods)
|
||||
lmods.append(name)
|
||||
mods = '+'.join(lmods)
|
||||
if mods:
|
||||
mods += '+'
|
||||
self.print('Key {}: {}{} [{}]'.format(etype, mods, key_event.key, encode_key_event(key_event)))
|
||||
|
||||
def on_interrupt(self):
|
||||
def on_interrupt(self) -> None:
|
||||
self.quit_loop(0)
|
||||
|
||||
def on_eot(self):
|
||||
def on_eot(self) -> None:
|
||||
self.quit_loop(0)
|
||||
|
||||
|
||||
def main(args):
|
||||
def main(args: List[str]) -> None:
|
||||
loop = Loop()
|
||||
handler = KeysHandler()
|
||||
loop.loop(handler)
|
||||
|
||||
@@ -6,8 +6,11 @@ import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Callable, Dict, List, Tuple
|
||||
|
||||
from kitty.cli import parse_args
|
||||
from kitty.cli_stub import PanelCLIOptions
|
||||
from kitty.options_stub import Options
|
||||
from kitty.constants import is_macos
|
||||
|
||||
OPTIONS = r'''
|
||||
@@ -42,19 +45,19 @@ Syntax: :italic:`name=value`. For example: :option:`kitty +kitten panel -o` font
|
||||
'''.format
|
||||
|
||||
|
||||
args = None
|
||||
args = PanelCLIOptions()
|
||||
help_text = 'Use a command line program to draw a GPU accelerated panel on your X11 desktop'
|
||||
usage = 'program-to-run'
|
||||
|
||||
|
||||
def parse_panel_args(args):
|
||||
return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten panel')
|
||||
def parse_panel_args(args: List[str]) -> Tuple[PanelCLIOptions, List[str]]:
|
||||
return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten panel', result_class=PanelCLIOptions)
|
||||
|
||||
|
||||
def call_xprop(*cmd, silent=False):
|
||||
cmd = ['xprop'] + list(cmd)
|
||||
def call_xprop(*cmd: str, silent: bool = False) -> None:
|
||||
cmd_ = ['xprop'] + list(cmd)
|
||||
try:
|
||||
cp = subprocess.run(cmd, stdout=subprocess.DEVNULL if silent else None)
|
||||
cp = subprocess.run(cmd_, stdout=subprocess.DEVNULL if silent else None)
|
||||
except FileNotFoundError:
|
||||
raise SystemExit('You must have the xprop program installed')
|
||||
if cp.returncode != 0:
|
||||
@@ -62,11 +65,11 @@ def call_xprop(*cmd, silent=False):
|
||||
|
||||
|
||||
def create_strut(
|
||||
win_id,
|
||||
left=0, right=0, top=0, bottom=0, left_start_y=0, left_end_y=0,
|
||||
right_start_y=0, right_end_y=0, top_start_x=0, top_end_x=0,
|
||||
bottom_start_x=0, bottom_end_x=0
|
||||
):
|
||||
win_id: int,
|
||||
left: int = 0, right: int = 0, top: int = 0, bottom: int = 0, left_start_y: int = 0, left_end_y: int = 0,
|
||||
right_start_y: int = 0, right_end_y: int = 0, top_start_x: int = 0, top_end_x: int = 0,
|
||||
bottom_start_x: int = 0, bottom_end_x: int = 0
|
||||
) -> None:
|
||||
call_xprop(
|
||||
'-id',
|
||||
str(int(win_id)), '-format', '_NET_WM_STRUT_PARTIAL', '32cccccccccccc',
|
||||
@@ -77,50 +80,54 @@ def create_strut(
|
||||
)
|
||||
|
||||
|
||||
def create_top_strut(win_id, width, height):
|
||||
def create_top_strut(win_id: int, width: int, height: int) -> None:
|
||||
create_strut(win_id, top=height, top_end_x=width)
|
||||
|
||||
|
||||
def create_bottom_strut(win_id, width, height):
|
||||
def create_bottom_strut(win_id: int, width: int, height: int) -> None:
|
||||
create_strut(win_id, bottom=height, bottom_end_x=width)
|
||||
|
||||
|
||||
def create_left_strut(win_id, width, height):
|
||||
def create_left_strut(win_id: int, width: int, height: int) -> None:
|
||||
create_strut(win_id, left=width, left_end_y=height)
|
||||
|
||||
|
||||
def create_right_strut(win_id, width, height):
|
||||
def create_right_strut(win_id: int, width: int, height: int) -> None:
|
||||
create_strut(win_id, right=width, right_end_y=height)
|
||||
|
||||
|
||||
def setup_x11_window(win_id):
|
||||
window_width = window_height = 0
|
||||
|
||||
|
||||
def setup_x11_window(win_id: int) -> None:
|
||||
call_xprop(
|
||||
'-id', str(win_id), '-format', '_NET_WM_WINDOW_TYPE', '32a',
|
||||
'-set', '_NET_WM_WINDOW_TYPE', '_NET_WM_WINDOW_TYPE_DOCK'
|
||||
)
|
||||
func = globals()['create_{}_strut'.format(args.edge)]
|
||||
func(win_id, initial_window_size_func.width, initial_window_size_func.height)
|
||||
func(win_id, window_width, window_height)
|
||||
|
||||
|
||||
def initial_window_size_func(opts, *a):
|
||||
def initial_window_size_func(opts: Options, cached_values: Dict) -> Callable[[int, int, float, float, float, float], Tuple[int, int]]:
|
||||
from kitty.fast_data_types import glfw_primary_monitor_size, set_smallest_allowed_resize
|
||||
|
||||
def initial_window_size(cell_width, cell_height, dpi_x, dpi_y, xscale, yscale):
|
||||
def initial_window_size(cell_width: int, cell_height: int, dpi_x: float, dpi_y: float, xscale: float, yscale: float) -> Tuple[int, int]:
|
||||
global window_width, window_height
|
||||
monitor_width, monitor_height = glfw_primary_monitor_size()
|
||||
if args.edge in {'top', 'bottom'}:
|
||||
h = initial_window_size_func.height = cell_height * args.lines + 1
|
||||
initial_window_size_func.width = monitor_width
|
||||
h = window_height = cell_height * args.lines + 1
|
||||
window_width = monitor_width
|
||||
set_smallest_allowed_resize(100, h)
|
||||
else:
|
||||
w = initial_window_size_func.width = cell_width * args.columns + 1
|
||||
initial_window_size_func.height = monitor_height
|
||||
w = window_width = cell_width * args.columns + 1
|
||||
window_height = monitor_height
|
||||
set_smallest_allowed_resize(w, 100)
|
||||
return initial_window_size_func.width, initial_window_size_func.height
|
||||
return window_width, window_height
|
||||
|
||||
return initial_window_size
|
||||
|
||||
|
||||
def main(sys_args):
|
||||
def main(sys_args: List[str]) -> None:
|
||||
global args
|
||||
if is_macos or not os.environ.get('DISPLAY'):
|
||||
raise SystemExit('Currently the panel kitten is supported only on X11 desktops')
|
||||
@@ -130,21 +137,22 @@ def main(sys_args):
|
||||
if not items:
|
||||
raise SystemExit('You must specify the program to run')
|
||||
sys.argv = ['kitty']
|
||||
if args.config:
|
||||
sys.argv.append('--config={}'.format(args.config))
|
||||
for config in args.config:
|
||||
sys.argv.append('--config={}'.format(config))
|
||||
for override in args.override:
|
||||
sys.argv.append('--override={}'.format(override))
|
||||
sys.argv.extend(items)
|
||||
from kitty.main import run_app, main
|
||||
from kitty.main import run_app, main as real_main
|
||||
run_app.cached_values_name = 'panel'
|
||||
run_app.first_window_callback = setup_x11_window
|
||||
run_app.initial_window_size_func = initial_window_size_func
|
||||
main()
|
||||
real_main()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
elif __name__ == '__doc__':
|
||||
sys.cli_docs['usage'] = usage
|
||||
sys.cli_docs['options'] = OPTIONS
|
||||
sys.cli_docs['help_text'] = help_text
|
||||
cd: dict = sys.cli_docs # type: ignore
|
||||
cd['usage'] = usage
|
||||
cd['options'] = OPTIONS
|
||||
cd['help_text'] = help_text
|
||||
|
||||
@@ -4,28 +4,36 @@
|
||||
|
||||
|
||||
import sys
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from kitty.cli import parse_args
|
||||
from kitty.cmds import cmap, parse_subcommand_cli
|
||||
from kitty.cli_stub import RCOptions, ResizeCLIOptions
|
||||
from kitty.constants import version
|
||||
from kitty.key_encoding import CTRL, ESCAPE, RELEASE, N, S, T, W
|
||||
from kitty.key_encoding import CTRL, RELEASE, KeyEvent, key_defs as K
|
||||
from kitty.rc.base import command_for_name, parse_subcommand_cli
|
||||
from kitty.remote_control import encode_send, parse_rc_args
|
||||
from kitty.utils import ScreenSize
|
||||
|
||||
from ..tui.handler import Handler
|
||||
from ..tui.loop import Loop
|
||||
from ..tui.operations import styled
|
||||
|
||||
global_opts = None
|
||||
global_opts = RCOptions()
|
||||
ESCAPE = K['ESCAPE']
|
||||
N = K['N']
|
||||
S = K['S']
|
||||
T = K['T']
|
||||
W = K['W']
|
||||
|
||||
|
||||
class Resize(Handler):
|
||||
|
||||
print_on_fail = None
|
||||
print_on_fail: Optional[str] = None
|
||||
|
||||
def __init__(self, opts):
|
||||
def __init__(self, opts: ResizeCLIOptions):
|
||||
self.opts = opts
|
||||
|
||||
def initialize(self):
|
||||
def initialize(self) -> None:
|
||||
global global_opts
|
||||
global_opts = parse_rc_args(['kitty', '@resize-window'])[0]
|
||||
self.original_size = self.screen_size
|
||||
@@ -33,8 +41,8 @@ class Resize(Handler):
|
||||
self.cmd.set_line_wrapping(False)
|
||||
self.draw_screen()
|
||||
|
||||
def do_window_resize(self, is_decrease=False, is_horizontal=True, reset=False, multiplier=1):
|
||||
resize_window = cmap['resize-window']
|
||||
def do_window_resize(self, is_decrease: bool = False, is_horizontal: bool = True, reset: bool = False, multiplier: int = 1) -> None:
|
||||
resize_window = command_for_name('resize_window')
|
||||
increment = self.opts.horizontal_increment if is_horizontal else self.opts.vertical_increment
|
||||
increment *= multiplier
|
||||
if is_decrease:
|
||||
@@ -42,11 +50,11 @@ class Resize(Handler):
|
||||
axis = 'reset' if reset else ('horizontal' if is_horizontal else 'vertical')
|
||||
cmdline = [resize_window.name, '--self', '--increment={}'.format(increment), '--axis=' + axis]
|
||||
opts, items = parse_subcommand_cli(resize_window, cmdline)
|
||||
payload = resize_window(global_opts, opts, items)
|
||||
payload = resize_window.message_to_kitty(global_opts, opts, items)
|
||||
send = {'cmd': resize_window.name, 'version': version, 'payload': payload, 'no_response': False}
|
||||
self.write(encode_send(send))
|
||||
|
||||
def on_kitty_cmd_response(self, response):
|
||||
def on_kitty_cmd_response(self, response: Dict[str, Any]) -> None:
|
||||
if not response.get('ok'):
|
||||
err = response['error']
|
||||
if response.get('tb'):
|
||||
@@ -58,14 +66,14 @@ class Resize(Handler):
|
||||
if res:
|
||||
self.cmd.bell()
|
||||
|
||||
def on_text(self, text, in_bracketed_paste=False):
|
||||
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
|
||||
text = text.upper()
|
||||
if text in 'WNTSR':
|
||||
self.do_window_resize(is_decrease=text in 'NS', is_horizontal=text in 'WN', reset=text == 'R')
|
||||
elif text == 'Q':
|
||||
self.quit_loop(0)
|
||||
|
||||
def on_key(self, key_event):
|
||||
def on_key(self, key_event: KeyEvent) -> None:
|
||||
if key_event.type is RELEASE:
|
||||
return
|
||||
if key_event.key is ESCAPE:
|
||||
@@ -73,10 +81,10 @@ class Resize(Handler):
|
||||
elif key_event.key in (W, N, T, S) and key_event.mods & CTRL:
|
||||
self.do_window_resize(is_decrease=key_event.key in (N, S), is_horizontal=key_event.key in (W, N), multiplier=2)
|
||||
|
||||
def on_resize(self, new_size):
|
||||
def on_resize(self, new_size: ScreenSize) -> None:
|
||||
self.draw_screen()
|
||||
|
||||
def draw_screen(self):
|
||||
def draw_screen(self) -> None:
|
||||
self.cmd.clear_screen()
|
||||
print = self.print
|
||||
print(styled('Resize this window', bold=True, fg='gray', fg_intense=True))
|
||||
@@ -94,7 +102,7 @@ class Resize(Handler):
|
||||
print(styled('Sizes', bold=True, fg='white', fg_intense=True))
|
||||
print('Original: {} rows {} cols'.format(self.original_size.rows, self.original_size.cols))
|
||||
print('Current: {} rows {} cols'.format(
|
||||
styled(self.screen_size.rows, fg='magenta'), styled(self.screen_size.cols, fg='magenta')))
|
||||
styled(str(self.screen_size.rows), fg='magenta'), styled(str(self.screen_size.cols), fg='magenta')))
|
||||
|
||||
|
||||
OPTIONS = r'''
|
||||
@@ -111,10 +119,10 @@ The base vertical increment.
|
||||
'''.format
|
||||
|
||||
|
||||
def main(args):
|
||||
def main(args: List[str]) -> None:
|
||||
msg = 'Resize the current window'
|
||||
try:
|
||||
args, items = parse_args(args[1:], OPTIONS, '', msg, 'resize_window')
|
||||
cli_opts, items = parse_args(args[1:], OPTIONS, '', msg, 'resize_window', result_class=ResizeCLIOptions)
|
||||
except SystemExit as e:
|
||||
if e.code != 0:
|
||||
print(e.args[0], file=sys.stderr)
|
||||
@@ -122,7 +130,7 @@ def main(args):
|
||||
return
|
||||
|
||||
loop = Loop()
|
||||
handler = Resize(args)
|
||||
handler = Resize(cli_opts)
|
||||
loop.loop(handler)
|
||||
if handler.print_on_fail:
|
||||
print(handler.print_on_fail, file=sys.stderr)
|
||||
|
||||
@@ -6,17 +6,17 @@
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
from functools import partial
|
||||
from functools import lru_cache, partial
|
||||
from typing import Any, Dict, FrozenSet, List
|
||||
|
||||
aliases = {'url_hints': 'hints'}
|
||||
|
||||
|
||||
def resolved_kitten(k):
|
||||
def resolved_kitten(k: str) -> str:
|
||||
return aliases.get(k, k).replace('-', '_')
|
||||
|
||||
|
||||
def path_to_custom_kitten(config_dir, kitten):
|
||||
def path_to_custom_kitten(config_dir: str, kitten: str) -> str:
|
||||
path = os.path.expanduser(kitten)
|
||||
if not os.path.isabs(path):
|
||||
path = os.path.join(config_dir, path)
|
||||
@@ -24,7 +24,7 @@ def path_to_custom_kitten(config_dir, kitten):
|
||||
return path
|
||||
|
||||
|
||||
def import_kitten_main_module(config_dir, kitten):
|
||||
def import_kitten_main_module(config_dir: str, kitten: str) -> Dict[str, Any]:
|
||||
if kitten.endswith('.py'):
|
||||
path_modified = False
|
||||
path = path_to_custom_kitten(config_dir, kitten)
|
||||
@@ -43,26 +43,26 @@ def import_kitten_main_module(config_dir, kitten):
|
||||
|
||||
kitten = resolved_kitten(kitten)
|
||||
m = importlib.import_module('kittens.{}.main'.format(kitten))
|
||||
return {'start': m.main, 'end': getattr(m, 'handle_result', lambda *a, **k: None)}
|
||||
return {'start': getattr(m, 'main'), 'end': getattr(m, 'handle_result', lambda *a, **k: None)}
|
||||
|
||||
|
||||
def create_kitten_handler(kitten, orig_args):
|
||||
def create_kitten_handler(kitten: str, orig_args: List[str]) -> Any:
|
||||
from kitty.constants import config_dir
|
||||
kitten = resolved_kitten(kitten)
|
||||
m = import_kitten_main_module(config_dir, kitten)
|
||||
ans = partial(m['end'], [kitten] + orig_args)
|
||||
ans.type_of_input = getattr(m['end'], 'type_of_input', None)
|
||||
ans.no_ui = getattr(m['end'], 'no_ui', False)
|
||||
setattr(ans, 'type_of_input', getattr(m['end'], 'type_of_input', None))
|
||||
setattr(ans, 'no_ui', getattr(m['end'], 'no_ui', False))
|
||||
return ans
|
||||
|
||||
|
||||
def set_debug(kitten):
|
||||
def set_debug(kitten: str) -> None:
|
||||
from kittens.tui.loop import debug
|
||||
import builtins
|
||||
builtins.debug = debug
|
||||
setattr(builtins, 'debug', debug)
|
||||
|
||||
|
||||
def launch(args):
|
||||
def launch(args: List[str]) -> None:
|
||||
config_dir, kitten = args[:2]
|
||||
kitten = resolved_kitten(kitten)
|
||||
del args[:2]
|
||||
@@ -84,7 +84,7 @@ def launch(args):
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def deserialize(output):
|
||||
def deserialize(output: str) -> Any:
|
||||
import json
|
||||
if output.startswith('OK: '):
|
||||
try:
|
||||
@@ -94,14 +94,16 @@ def deserialize(output):
|
||||
raise ValueError('Failed to parse kitten output: {!r}'.format(output))
|
||||
|
||||
|
||||
def run_kitten(kitten, run_name='__main__'):
|
||||
def run_kitten(kitten: str, run_name: str = '__main__') -> None:
|
||||
import runpy
|
||||
original_kitten_name = kitten
|
||||
kitten = resolved_kitten(kitten)
|
||||
set_debug(kitten)
|
||||
with suppress(ImportError):
|
||||
try:
|
||||
runpy.run_module('kittens.{}.main'.format(kitten), run_name=run_name)
|
||||
return
|
||||
except ImportError:
|
||||
pass
|
||||
# Look for a custom kitten
|
||||
if not kitten.endswith('.py'):
|
||||
kitten += '.py'
|
||||
@@ -116,21 +118,19 @@ def run_kitten(kitten, run_name='__main__'):
|
||||
m['main'](sys.argv)
|
||||
|
||||
|
||||
def all_kitten_names():
|
||||
ans = getattr(all_kitten_names, 'ans', None)
|
||||
if ans is None:
|
||||
n = []
|
||||
import glob
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
for x in glob.glob(os.path.join(base, '*', '__init__.py')):
|
||||
q = os.path.basename(os.path.dirname(x))
|
||||
if q != 'tui':
|
||||
n.append(q)
|
||||
all_kitten_names.ans = ans = frozenset(n)
|
||||
return ans
|
||||
@lru_cache(maxsize=2)
|
||||
def all_kitten_names() -> FrozenSet[str]:
|
||||
n = []
|
||||
import glob
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
for x in glob.glob(os.path.join(base, '*', '__init__.py')):
|
||||
q = os.path.basename(os.path.dirname(x))
|
||||
if q != 'tui':
|
||||
n.append(q)
|
||||
return frozenset(n)
|
||||
|
||||
|
||||
def list_kittens():
|
||||
def list_kittens() -> None:
|
||||
print('You must specify the name of a kitten to run')
|
||||
print('Choose from:')
|
||||
print()
|
||||
@@ -138,24 +138,24 @@ def list_kittens():
|
||||
print(kitten)
|
||||
|
||||
|
||||
def get_kitten_cli_docs(kitten):
|
||||
sys.cli_docs = {}
|
||||
def get_kitten_cli_docs(kitten: str) -> Any:
|
||||
setattr(sys, 'cli_docs', {})
|
||||
run_kitten(kitten, run_name='__doc__')
|
||||
ans = sys.cli_docs
|
||||
del sys.cli_docs
|
||||
ans = getattr(sys, 'cli_docs')
|
||||
delattr(sys, 'cli_docs')
|
||||
if 'help_text' in ans and 'usage' in ans and 'options' in ans:
|
||||
return ans
|
||||
|
||||
|
||||
def get_kitten_conf_docs(kitten):
|
||||
sys.all_options = None
|
||||
def get_kitten_conf_docs(kitten: str) -> Any:
|
||||
setattr(sys, 'all_options', None)
|
||||
run_kitten(kitten, run_name='__conf__')
|
||||
ans = sys.all_options
|
||||
del sys.all_options
|
||||
ans = getattr(sys, 'all_options')
|
||||
delattr(sys, 'all_options')
|
||||
return ans
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> None:
|
||||
try:
|
||||
args = sys.argv[1:]
|
||||
launch(args)
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
from typing import List
|
||||
|
||||
from kitty.cli import parse_args
|
||||
from kitty.cli_stub import ErrorCLIOptions
|
||||
|
||||
from ..tui.operations import styled
|
||||
|
||||
@@ -16,23 +19,22 @@ The title for the error message.
|
||||
'''.format
|
||||
|
||||
|
||||
def real_main(args):
|
||||
def real_main(args: List[str]) -> None:
|
||||
msg = 'Show an error message'
|
||||
args, items = parse_args(args[1:], OPTIONS, '', msg, 'hints')
|
||||
cli_opts, items = parse_args(args[1:], OPTIONS, '', msg, 'hints', result_class=ErrorCLIOptions)
|
||||
error_message = sys.stdin.buffer.read().decode('utf-8')
|
||||
sys.stdin = open(os.ctermid())
|
||||
print(styled(args.title, fg_intense=True, fg='red', bold=True))
|
||||
print(styled(cli_opts.title, fg_intense=True, fg='red', bold=True))
|
||||
print()
|
||||
print(error_message)
|
||||
print()
|
||||
input('Press Enter to close.')
|
||||
|
||||
|
||||
def main(args):
|
||||
def main(args: List[str]) -> None:
|
||||
try:
|
||||
real_main(args)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
with suppress(KeyboardInterrupt):
|
||||
real_main(args)
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -7,12 +7,13 @@ import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import List, NoReturn, Set, Tuple
|
||||
|
||||
SHELL_SCRIPT = '''\
|
||||
#!/bin/sh
|
||||
# macOS ships with an ancient version of tic that cannot read from stdin, so we
|
||||
# create a temp file for it
|
||||
tmp=$(mktemp /tmp/terminfo.XXXXXX)
|
||||
tmp=$(mktemp)
|
||||
cat >$tmp << 'TERMEOF'
|
||||
TERMINFO
|
||||
TERMEOF
|
||||
@@ -42,9 +43,12 @@ exec -a "-$shell_name" "$0"
|
||||
'''
|
||||
|
||||
|
||||
def get_ssh_cli():
|
||||
other_ssh_args, boolean_ssh_args = [], []
|
||||
raw = subprocess.Popen(['ssh'], stderr=subprocess.PIPE).stderr.read().decode('utf-8')
|
||||
def get_ssh_cli() -> Tuple[Set[str], Set[str]]:
|
||||
other_ssh_args: List[str] = []
|
||||
boolean_ssh_args: List[str] = []
|
||||
stderr = subprocess.Popen(['ssh'], stderr=subprocess.PIPE).stderr
|
||||
assert stderr is not None
|
||||
raw = stderr.read().decode('utf-8')
|
||||
for m in re.finditer(r'\[(.+?)\]', raw):
|
||||
q = m.group(1)
|
||||
if len(q) < 2 or q[0] != '-':
|
||||
@@ -56,11 +60,11 @@ def get_ssh_cli():
|
||||
return set('-' + x for x in boolean_ssh_args), set('-' + x for x in other_ssh_args)
|
||||
|
||||
|
||||
def parse_ssh_args(args):
|
||||
def parse_ssh_args(args: List[str]) -> Tuple[List[str], List[str], bool]:
|
||||
boolean_ssh_args, other_ssh_args = get_ssh_cli()
|
||||
passthrough_args = {'-' + x for x in 'Nnf'}
|
||||
ssh_args = []
|
||||
server_args = []
|
||||
server_args: List[str] = []
|
||||
expecting_option_val = False
|
||||
passthrough = False
|
||||
for arg in args:
|
||||
@@ -76,6 +80,9 @@ def parse_ssh_args(args):
|
||||
if arg in boolean_ssh_args:
|
||||
ssh_args.append(arg)
|
||||
continue
|
||||
if arg.startswith('-p') and arg[2:].isdigit():
|
||||
ssh_args.append(arg)
|
||||
continue
|
||||
if arg in other_ssh_args:
|
||||
if i != len(all_args) - 1:
|
||||
raise SystemExit('Option {} cannot occur in the middle'.format(arg))
|
||||
@@ -94,7 +101,7 @@ def parse_ssh_args(args):
|
||||
return ssh_args, server_args, passthrough
|
||||
|
||||
|
||||
def quote(x):
|
||||
def quote(x: str) -> str:
|
||||
# we have to escape unbalanced quotes and other unparsable
|
||||
# args as they will break the shell script
|
||||
# But we do not want to quote things like * or 'echo hello'
|
||||
@@ -106,7 +113,7 @@ def quote(x):
|
||||
return x
|
||||
|
||||
|
||||
def main(args):
|
||||
def main(args: List[str]) -> NoReturn:
|
||||
ssh_args, server_args, passthrough = parse_ssh_args(args[1:])
|
||||
if passthrough:
|
||||
cmd = ['ssh'] + ssh_args + server_args
|
||||
@@ -114,8 +121,8 @@ def main(args):
|
||||
terminfo = subprocess.check_output(['infocmp']).decode('utf-8')
|
||||
sh_script = SHELL_SCRIPT.replace('TERMINFO', terminfo, 1)
|
||||
if len(server_args) > 1:
|
||||
command_to_execute = [quote(c) for c in server_args[1:]]
|
||||
command_to_execute = 'exec ' + ' '.join(command_to_execute)
|
||||
command_to_executeg = (quote(c) for c in server_args[1:])
|
||||
command_to_execute = 'exec ' + ' '.join(command_to_executeg)
|
||||
else:
|
||||
command_to_execute = ''
|
||||
sh_script = sh_script.replace('EXEC_CMD', command_to_execute)
|
||||
|
||||
@@ -3,27 +3,49 @@
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
from .operations import commander
|
||||
from types import TracebackType
|
||||
from typing import (
|
||||
Any, Callable, ContextManager, Dict, Optional, Sequence, Type, Union
|
||||
)
|
||||
|
||||
from kitty.typing import (
|
||||
AbstractEventLoop, BossType, Debug, ImageManagerType, KeyEventType,
|
||||
KittensKeyActionType, LoopType, MouseEvent, ScreenSize, TermManagerType
|
||||
)
|
||||
|
||||
|
||||
class Handler:
|
||||
|
||||
image_manager_class = None
|
||||
image_manager_class: Optional[Type[ImageManagerType]] = None
|
||||
|
||||
def _initialize(self, screen_size, term_manager, schedule_write, tui_loop, debug, image_manager=None):
|
||||
def _initialize(
|
||||
self,
|
||||
screen_size: ScreenSize,
|
||||
term_manager: TermManagerType,
|
||||
schedule_write: Callable[[bytes], None],
|
||||
tui_loop: LoopType,
|
||||
debug: Debug,
|
||||
image_manager: Optional[ImageManagerType] = None
|
||||
) -> None:
|
||||
from .operations import commander
|
||||
self.screen_size = screen_size
|
||||
self._term_manager = term_manager
|
||||
self._tui_loop = tui_loop
|
||||
self._schedule_write = schedule_write
|
||||
self.debug = debug
|
||||
self.cmd = commander(self)
|
||||
self.image_manager = image_manager
|
||||
self._image_manager = image_manager
|
||||
|
||||
@property
|
||||
def asyncio_loop(self):
|
||||
def image_manager(self) -> ImageManagerType:
|
||||
assert self._image_manager is not None
|
||||
return self._image_manager
|
||||
|
||||
@property
|
||||
def asyncio_loop(self) -> AbstractEventLoop:
|
||||
return self._tui_loop.asycio_loop
|
||||
|
||||
def add_shortcut(self, action, key, mods=None, is_text=False):
|
||||
def add_shortcut(self, action: KittensKeyActionType, key: str, mods: Optional[int] = None, is_text: Optional[bool] = False) -> None:
|
||||
if not hasattr(self, '_text_shortcuts'):
|
||||
self._text_shortcuts, self._key_shortcuts = {}, {}
|
||||
if is_text:
|
||||
@@ -31,70 +53,95 @@ class Handler:
|
||||
else:
|
||||
self._key_shortcuts[(key, mods or 0)] = action
|
||||
|
||||
def shortcut_action(self, key_event_or_text):
|
||||
def shortcut_action(self, key_event_or_text: Union[str, KeyEventType]) -> Optional[KittensKeyActionType]:
|
||||
if isinstance(key_event_or_text, str):
|
||||
return self._text_shortcuts.get(key_event_or_text)
|
||||
return self._key_shortcuts.get((key_event_or_text.key, key_event_or_text.mods))
|
||||
|
||||
def __enter__(self):
|
||||
if self.image_manager is not None:
|
||||
self.image_manager.__enter__()
|
||||
def __enter__(self) -> None:
|
||||
if self._image_manager is not None:
|
||||
self._image_manager.__enter__()
|
||||
self.debug.fobj = self
|
||||
self.initialize()
|
||||
|
||||
def __exit__(self, *a):
|
||||
def __exit__(self, etype: type, value: Exception, tb: TracebackType) -> None:
|
||||
del self.debug.fobj
|
||||
self.finalize()
|
||||
if self.image_manager is not None:
|
||||
self.image_manager.__exit__(*a)
|
||||
if self._image_manager is not None:
|
||||
self._image_manager.__exit__(etype, value, tb)
|
||||
|
||||
def initialize(self):
|
||||
def initialize(self) -> None:
|
||||
pass
|
||||
|
||||
def finalize(self):
|
||||
def finalize(self) -> None:
|
||||
pass
|
||||
|
||||
def on_resize(self, screen_size):
|
||||
def on_resize(self, screen_size: ScreenSize) -> None:
|
||||
self.screen_size = screen_size
|
||||
|
||||
def quit_loop(self, return_code=None):
|
||||
def quit_loop(self, return_code: Optional[int] = None) -> None:
|
||||
self._tui_loop.quit(return_code)
|
||||
|
||||
def on_term(self):
|
||||
def on_term(self) -> None:
|
||||
self._tui_loop.quit(1)
|
||||
|
||||
def on_text(self, text, in_bracketed_paste=False):
|
||||
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
|
||||
pass
|
||||
|
||||
def on_key(self, key_event):
|
||||
def on_key(self, key_event: KeyEventType) -> None:
|
||||
pass
|
||||
|
||||
def on_mouse(self, mouse_event):
|
||||
def on_mouse(self, mouse_event: 'MouseEvent') -> None:
|
||||
pass
|
||||
|
||||
def on_interrupt(self):
|
||||
def on_interrupt(self) -> None:
|
||||
pass
|
||||
|
||||
def on_eot(self):
|
||||
def on_eot(self) -> None:
|
||||
pass
|
||||
|
||||
def on_kitty_cmd_response(self, response):
|
||||
def on_kitty_cmd_response(self, response: Dict) -> None:
|
||||
pass
|
||||
|
||||
def on_clipboard_response(self, text, from_primary=False):
|
||||
def on_clipboard_response(self, text: str, from_primary: bool = False) -> None:
|
||||
pass
|
||||
|
||||
def on_capability_response(self, name, val):
|
||||
def on_capability_response(self, name: str, val: str) -> None:
|
||||
pass
|
||||
|
||||
def write(self, data):
|
||||
def write(self, data: Union[bytes, str]) -> None:
|
||||
if isinstance(data, str):
|
||||
data = data.encode('utf-8')
|
||||
self._schedule_write(data)
|
||||
|
||||
def print(self, *args, sep=' ', end='\r\n'):
|
||||
def flush(self) -> None:
|
||||
pass
|
||||
|
||||
def print(self, *args: object, sep: str = ' ', end: str = '\r\n') -> None:
|
||||
data = sep.join(map(str, args)) + end
|
||||
self.write(data)
|
||||
|
||||
def suspend(self):
|
||||
def suspend(self) -> ContextManager[TermManagerType]:
|
||||
return self._term_manager.suspend()
|
||||
|
||||
|
||||
class HandleResult:
|
||||
|
||||
type_of_input: Optional[str] = None
|
||||
no_ui: bool = False
|
||||
|
||||
def __init__(self, impl: Callable, type_of_input: Optional[str], no_ui: bool):
|
||||
self.impl = impl
|
||||
self.no_ui = no_ui
|
||||
self.type_of_input = type_of_input
|
||||
|
||||
def __call__(self, args: Sequence[str], data: Any, target_window_id: int, boss: BossType) -> Any:
|
||||
return self.impl(args, data, target_window_id, boss)
|
||||
|
||||
|
||||
def result_handler(type_of_input: Optional[str] = None, no_ui: bool = False) -> Callable[[Callable], HandleResult]:
|
||||
|
||||
def wrapper(impl: Callable) -> HandleResult:
|
||||
return HandleResult(impl, type_of_input, no_ui)
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -7,10 +7,18 @@ import os
|
||||
import sys
|
||||
from base64 import standard_b64encode
|
||||
from collections import defaultdict, deque
|
||||
from itertools import count
|
||||
from contextlib import suppress
|
||||
from itertools import count
|
||||
from typing import (
|
||||
Any, Callable, DefaultDict, Deque, Dict, List, Optional, Sequence, Tuple,
|
||||
Union
|
||||
)
|
||||
|
||||
from kitty.utils import fit_image
|
||||
from kitty.typing import (
|
||||
CompletedProcess, GRT_a, GRT_d, GRT_f, GRT_m, GRT_o, GRT_t, HandlerType,
|
||||
TypedDict
|
||||
)
|
||||
from kitty.utils import ScreenSize, fit_image
|
||||
|
||||
from .operations import cursor
|
||||
|
||||
@@ -23,14 +31,14 @@ except Exception:
|
||||
|
||||
class ImageData:
|
||||
|
||||
def __init__(self, fmt, width, height, mode):
|
||||
def __init__(self, fmt: str, width: int, height: int, mode: str):
|
||||
self.width, self.height, self.fmt, self.mode = width, height, fmt, mode
|
||||
self.transmit_fmt = str(24 if self.mode == 'rgb' else 32)
|
||||
self.transmit_fmt: GRT_f = (24 if self.mode == 'rgb' else 32)
|
||||
|
||||
|
||||
class OpenFailed(ValueError):
|
||||
|
||||
def __init__(self, path, message):
|
||||
def __init__(self, path: str, message: str):
|
||||
ValueError.__init__(
|
||||
self, 'Failed to open image: {} with error: {}'.format(path, message)
|
||||
)
|
||||
@@ -39,7 +47,7 @@ class OpenFailed(ValueError):
|
||||
|
||||
class ConvertFailed(ValueError):
|
||||
|
||||
def __init__(self, path, message):
|
||||
def __init__(self, path: str, message: str):
|
||||
ValueError.__init__(
|
||||
self, 'Failed to convert image: {} with error: {}'.format(path, message)
|
||||
)
|
||||
@@ -50,7 +58,7 @@ class NoImageMagick(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def run_imagemagick(path, cmd, keep_stdout=True):
|
||||
def run_imagemagick(path: str, cmd: Sequence[str], keep_stdout: bool = True) -> CompletedProcess:
|
||||
import subprocess
|
||||
try:
|
||||
p = subprocess.run(cmd, stdout=subprocess.PIPE if keep_stdout else subprocess.DEVNULL, stderr=subprocess.PIPE)
|
||||
@@ -61,14 +69,19 @@ def run_imagemagick(path, cmd, keep_stdout=True):
|
||||
return p
|
||||
|
||||
|
||||
def identify(path):
|
||||
def identify(path: str) -> ImageData:
|
||||
p = run_imagemagick(path, ['identify', '-format', '%m %w %h %A', '--', path])
|
||||
parts = tuple(filter(None, p.stdout.decode('utf-8').split()))
|
||||
parts: Tuple[str, ...] = 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, available_width, available_height, scale_up, tdir=None):
|
||||
def convert(
|
||||
path: str, m: ImageData,
|
||||
available_width: int, available_height: int,
|
||||
scale_up: bool,
|
||||
tdir: Optional[str] = None
|
||||
) -> Tuple[str, int, int]:
|
||||
from tempfile import NamedTemporaryFile
|
||||
width, height = m.width, m.height
|
||||
cmd = ['convert', '-background', 'none', '--', path]
|
||||
@@ -81,6 +94,7 @@ def convert(path, m, available_width, available_height, scale_up, tdir=None):
|
||||
if scaled or width > available_width or height > available_height:
|
||||
width, height = fit_image(width, height, available_width, available_height)
|
||||
cmd += ['-resize', '{}x{}!'.format(width, height)]
|
||||
cmd += ['-depth', '8']
|
||||
with NamedTemporaryFile(prefix='icat-', suffix='.' + m.mode, delete=False, dir=tdir) as outfile:
|
||||
run_imagemagick(path, cmd + [outfile.name])
|
||||
# ImageMagick sometimes generated rgba images smaller than the specified
|
||||
@@ -100,58 +114,122 @@ def convert(path, m, available_width, available_height, scale_up, tdir=None):
|
||||
return outfile.name, width, height
|
||||
|
||||
|
||||
def can_display_images():
|
||||
def can_display_images() -> bool:
|
||||
import shutil
|
||||
ans = getattr(can_display_images, 'ans', None)
|
||||
ans: Optional[bool] = getattr(can_display_images, 'ans', None)
|
||||
if ans is None:
|
||||
ans = shutil.which('convert') is not None
|
||||
can_display_images.ans = ans
|
||||
setattr(can_display_images, 'ans', ans)
|
||||
return ans
|
||||
|
||||
|
||||
ImageKey = Tuple[str, int, int]
|
||||
SentImageKey = Tuple[int, int, int]
|
||||
|
||||
|
||||
class GraphicsCommand:
|
||||
a: GRT_a = 't' # action
|
||||
f: GRT_f = 32 # image data format
|
||||
t: GRT_t = 'd' # transmission medium
|
||||
s: int = 0 # sent image width
|
||||
v: int = 0 # sent image height
|
||||
S: int = 0 # size of data to read from file
|
||||
O: int = 0 # offset of data to read from file
|
||||
i: int = 0 # image id
|
||||
o: Optional[GRT_o] = None # type of compression
|
||||
m: GRT_m = 0 # 0 or 1 whether there is more chunked data
|
||||
x: int = 0 # left edge of image area to display
|
||||
y: int = 0 # top edge of image area to display
|
||||
w: int = 0 # image width to display
|
||||
h: int = 0 # image height to display
|
||||
X: int = 0 # X-offset within cell
|
||||
Y: int = 0 # Y-offset within cell
|
||||
c: int = 0 # number of cols to display image over
|
||||
r: int = 0 # number of rows to display image over
|
||||
z: int = 0 # z-index
|
||||
d: GRT_d = 'a' # what to delete
|
||||
|
||||
def serialize(self, payload: bytes = b'') -> bytes:
|
||||
items = []
|
||||
for k in GraphicsCommand.__annotations__:
|
||||
val: Union[str, None, int] = getattr(self, k)
|
||||
defval: Union[str, None, int] = getattr(GraphicsCommand, k)
|
||||
if val != defval and val is not None:
|
||||
items.append('{}={}'.format(k, val))
|
||||
|
||||
ans: List[bytes] = []
|
||||
w = ans.append
|
||||
w(b'\033_G')
|
||||
w(','.join(items).encode('ascii'))
|
||||
if payload:
|
||||
w(b';')
|
||||
w(payload)
|
||||
w(b'\033\\')
|
||||
return b''.join(ans)
|
||||
|
||||
def clear(self) -> None:
|
||||
for k in GraphicsCommand.__annotations__:
|
||||
defval: Union[str, None, int] = getattr(GraphicsCommand, k)
|
||||
setattr(self, k, defval)
|
||||
|
||||
|
||||
class Placement(TypedDict):
|
||||
cmd: GraphicsCommand
|
||||
x: int
|
||||
y: int
|
||||
|
||||
|
||||
class ImageManager:
|
||||
|
||||
def __init__(self, handler):
|
||||
def __init__(self, handler: HandlerType):
|
||||
self.image_id_counter = count()
|
||||
self.handler = handler
|
||||
self.filesystem_ok = None
|
||||
self.image_data = {}
|
||||
self.failed_images = {}
|
||||
self.converted_images = {}
|
||||
self.sent_images = {}
|
||||
self.image_id_to_image_data = {}
|
||||
self.image_id_to_converted_data = {}
|
||||
self.transmission_status = {}
|
||||
self.placements_in_flight = defaultdict(deque)
|
||||
self.filesystem_ok: Optional[bool] = None
|
||||
self.image_data: Dict[str, ImageData] = {}
|
||||
self.failed_images: Dict[str, Exception] = {}
|
||||
self.converted_images: Dict[ImageKey, ImageKey] = {}
|
||||
self.sent_images: Dict[ImageKey, int] = {}
|
||||
self.image_id_to_image_data: Dict[int, ImageData] = {}
|
||||
self.image_id_to_converted_data: Dict[int, ImageKey] = {}
|
||||
self.transmission_status: Dict[int, Union[str, int]] = {}
|
||||
self.placements_in_flight: DefaultDict[int, Deque[Placement]] = defaultdict(deque)
|
||||
self.update_image_placement_for_resend: Optional[Callable[[int, Placement], bool]]
|
||||
|
||||
@property
|
||||
def next_image_id(self):
|
||||
def next_image_id(self) -> int:
|
||||
return next(self.image_id_counter) + 2
|
||||
|
||||
@property
|
||||
def screen_size(self):
|
||||
def screen_size(self) -> ScreenSize:
|
||||
return self.handler.screen_size
|
||||
|
||||
def __enter__(self):
|
||||
def __enter__(self) -> None:
|
||||
import tempfile
|
||||
self.tdir = tempfile.mkdtemp(prefix='kitten-images-')
|
||||
with tempfile.NamedTemporaryFile(dir=self.tdir, delete=False) as f:
|
||||
f.write(b'abcd')
|
||||
self.handler.cmd.gr_command(dict(a='q', s=1, v=1, i=1, t='f'), standard_b64encode(f.name.encode(fsenc)))
|
||||
gc = GraphicsCommand()
|
||||
gc.a = 'q'
|
||||
gc.s = gc.v = gc.i = 1
|
||||
gc.t = 'f'
|
||||
self.handler.cmd.gr_command(gc, standard_b64encode(f.name.encode(fsenc)))
|
||||
|
||||
def __exit__(self, *a):
|
||||
def __exit__(self, *a: Any) -> None:
|
||||
import shutil
|
||||
shutil.rmtree(self.tdir, ignore_errors=True)
|
||||
self.handler.cmd.clear_images_on_screen(delete_data=True)
|
||||
self.delete_all_sent_images()
|
||||
del self.handler
|
||||
|
||||
def delete_all_sent_images(self):
|
||||
def delete_all_sent_images(self) -> None:
|
||||
gc = GraphicsCommand()
|
||||
gc.a = 'd'
|
||||
for img_id in self.transmission_status:
|
||||
self.handler.cmd.gr_command({'a': 'd', 'i': img_id})
|
||||
gc.i = img_id
|
||||
self.handler.cmd.gr_command(gc)
|
||||
self.transmission_status.clear()
|
||||
|
||||
def handle_response(self, apc):
|
||||
def handle_response(self, apc: str) -> None:
|
||||
cdata, payload = apc[1:].partition(';')[::2]
|
||||
control = {}
|
||||
for x in cdata.split(','):
|
||||
@@ -178,7 +256,9 @@ class ImageManager:
|
||||
if not in_flight:
|
||||
self.placements_in_flight.pop(image_id, None)
|
||||
|
||||
def resend_image(self, image_id, pl):
|
||||
def resend_image(self, image_id: int, pl: Placement) -> None:
|
||||
if self.update_image_placement_for_resend is not None and not self.update_image_placement_for_resend(image_id, pl):
|
||||
return
|
||||
image_data = self.image_id_to_image_data[image_id]
|
||||
skey = self.image_id_to_converted_data[image_id]
|
||||
self.transmit_image(image_data, image_id, *skey)
|
||||
@@ -186,7 +266,7 @@ class ImageManager:
|
||||
self.handler.cmd.set_cursor_position(pl['x'], pl['y'])
|
||||
self.handler.cmd.gr_command(pl['cmd'])
|
||||
|
||||
def send_image(self, path, max_cols=None, max_rows=None, scale_up=False):
|
||||
def send_image(self, path: str, max_cols: Optional[int] = None, max_rows: Optional[int] = None, scale_up: bool = False) -> SentImageKey:
|
||||
path = os.path.abspath(path)
|
||||
if path in self.failed_images:
|
||||
raise self.failed_images[path]
|
||||
@@ -224,41 +304,50 @@ class ImageManager:
|
||||
self.image_id_to_image_data[image_id] = m
|
||||
return image_id, skey[1], skey[2]
|
||||
|
||||
def hide_image(self, image_id):
|
||||
self.handler.cmd.gr_command({'a': 'd', 'i': image_id})
|
||||
def hide_image(self, image_id: int) -> None:
|
||||
gc = GraphicsCommand()
|
||||
gc.a = 'd'
|
||||
gc.i = image_id
|
||||
self.handler.cmd.gr_command(gc)
|
||||
|
||||
def show_image(self, image_id, x, y, src_rect=None):
|
||||
cmd = {'a': 'p', 'i': image_id}
|
||||
def show_image(self, image_id: int, x: int, y: int, src_rect: Optional[Tuple[int, int, int, int]] = None) -> None:
|
||||
gc = GraphicsCommand()
|
||||
gc.a = 'p'
|
||||
gc.i = image_id
|
||||
if src_rect is not None:
|
||||
cmd['x'], cmd['y'], cmd['w'], cmd['h'] = map(int, src_rect)
|
||||
self.placements_in_flight[image_id].append({'cmd': cmd, 'x': x, 'y': y})
|
||||
gc.x, gc.y, gc.w, gc.h = map(int, src_rect)
|
||||
self.placements_in_flight[image_id].append({'cmd': gc, 'x': x, 'y': y})
|
||||
with cursor(self.handler.write):
|
||||
self.handler.cmd.set_cursor_position(x, y)
|
||||
self.handler.cmd.gr_command(cmd)
|
||||
self.handler.cmd.gr_command(gc)
|
||||
|
||||
def convert_image(self, path, available_width, available_height, image_data, scale_up=False):
|
||||
def convert_image(self, path: str, available_width: int, available_height: int, image_data: ImageData, scale_up: bool = False) -> ImageKey:
|
||||
rgba_path, width, height = convert(path, image_data, available_width, available_height, scale_up, tdir=self.tdir)
|
||||
return rgba_path, width, height
|
||||
|
||||
def transmit_image(self, image_data, image_id, rgba_path, width, height):
|
||||
def transmit_image(self, image_data: ImageData, image_id: int, rgba_path: str, width: int, height: int) -> int:
|
||||
self.transmission_status[image_id] = 0
|
||||
cmd = {'a': 't', 'f': image_data.transmit_fmt, 's': width, 'v': height, 'i': image_id}
|
||||
gc = GraphicsCommand()
|
||||
gc.a = 't'
|
||||
gc.f = image_data.transmit_fmt
|
||||
gc.s = width
|
||||
gc.v = height
|
||||
gc.i = image_id
|
||||
if self.filesystem_ok:
|
||||
cmd['t'] = 'f'
|
||||
gc.t = 'f'
|
||||
self.handler.cmd.gr_command(
|
||||
cmd, standard_b64encode(rgba_path.encode(fsenc)))
|
||||
gc, standard_b64encode(rgba_path.encode(fsenc)))
|
||||
else:
|
||||
import zlib
|
||||
with open(rgba_path, 'rb') as f:
|
||||
data = f.read()
|
||||
cmd['S'] = len(data)
|
||||
gc.S = len(data)
|
||||
data = zlib.compress(data)
|
||||
cmd['o'] = 'z'
|
||||
gc.o = 'z'
|
||||
data = standard_b64encode(data)
|
||||
while data:
|
||||
chunk, data = data[:4096], data[4096:]
|
||||
m = 1 if data else 0
|
||||
cmd['m'] = m
|
||||
self.handler.cmd.gr_command(cmd, chunk)
|
||||
cmd.clear()
|
||||
gc.m = 1 if data else 0
|
||||
self.handler.cmd.gr_command(gc, chunk)
|
||||
gc.clear()
|
||||
return image_id
|
||||
|
||||
@@ -2,27 +2,36 @@
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from typing import Callable, Tuple
|
||||
|
||||
from kitty.fast_data_types import truncate_point_for_length, wcswidth
|
||||
from kitty.key_encoding import RELEASE, HOME, END, BACKSPACE, DELETE, LEFT, RIGHT
|
||||
from kitty.key_encoding import RELEASE, KeyEvent, key_defs as K
|
||||
|
||||
HOME = K['HOME']
|
||||
END = K['END']
|
||||
BACKSPACE = K['BACKSPACE']
|
||||
DELETE = K['DELETE']
|
||||
LEFT = K['LEFT']
|
||||
RIGHT = K['RIGHT']
|
||||
|
||||
|
||||
class LineEdit:
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.clear()
|
||||
|
||||
def clear(self):
|
||||
def clear(self) -> None:
|
||||
self.current_input = ''
|
||||
self.cursor_pos = 0
|
||||
self.pending_bell = False
|
||||
|
||||
def split_at_cursor(self, delta=0):
|
||||
def split_at_cursor(self, delta: int = 0) -> Tuple[str, str]:
|
||||
pos = max(0, self.cursor_pos + delta)
|
||||
x = truncate_point_for_length(self.current_input, pos) if pos else 0
|
||||
before, after = self.current_input[:x], self.current_input[x:]
|
||||
return before, after
|
||||
|
||||
def write(self, write, prompt=''):
|
||||
def write(self, write: Callable[[str], None], prompt: str = '') -> None:
|
||||
if self.pending_bell:
|
||||
write('\a')
|
||||
self.pending_bell = False
|
||||
@@ -30,7 +39,7 @@ class LineEdit:
|
||||
write(self.current_input)
|
||||
write('\r\x1b[{}C'.format(self.cursor_pos + wcswidth(prompt)))
|
||||
|
||||
def add_text(self, text):
|
||||
def add_text(self, text: str) -> None:
|
||||
if self.current_input:
|
||||
x = truncate_point_for_length(self.current_input, self.cursor_pos) if self.cursor_pos else 0
|
||||
self.current_input = self.current_input[:x] + text + self.current_input[x:]
|
||||
@@ -38,10 +47,10 @@ class LineEdit:
|
||||
self.current_input = text
|
||||
self.cursor_pos += wcswidth(text)
|
||||
|
||||
def on_text(self, text, in_bracketed_paste):
|
||||
def on_text(self, text: str, in_bracketed_paste: bool) -> None:
|
||||
self.add_text(text)
|
||||
|
||||
def backspace(self, num=1):
|
||||
def backspace(self, num: int = 1) -> bool:
|
||||
before, after = self.split_at_cursor()
|
||||
nbefore = before[:-num]
|
||||
if nbefore != before:
|
||||
@@ -51,7 +60,7 @@ class LineEdit:
|
||||
self.pending_bell = True
|
||||
return False
|
||||
|
||||
def delete(self, num=1):
|
||||
def delete(self, num: int = 1) -> bool:
|
||||
before, after = self.split_at_cursor()
|
||||
nafter = after[num:]
|
||||
if nafter != after:
|
||||
@@ -61,7 +70,7 @@ class LineEdit:
|
||||
self.pending_bell = True
|
||||
return False
|
||||
|
||||
def _left(self):
|
||||
def _left(self) -> None:
|
||||
if not self.current_input:
|
||||
self.cursor_pos = 0
|
||||
return
|
||||
@@ -69,7 +78,7 @@ class LineEdit:
|
||||
before, after = self.split_at_cursor(-1)
|
||||
self.cursor_pos = wcswidth(before)
|
||||
|
||||
def _right(self):
|
||||
def _right(self) -> None:
|
||||
if not self.current_input:
|
||||
self.cursor_pos = 0
|
||||
return
|
||||
@@ -80,33 +89,37 @@ class LineEdit:
|
||||
before, after = self.split_at_cursor(1)
|
||||
self.cursor_pos += 1 + int(wcswidth(before) == self.cursor_pos)
|
||||
|
||||
def _move_loop(self, func, num):
|
||||
def _move_loop(self, func: Callable[[], None], num: int) -> bool:
|
||||
before = self.cursor_pos
|
||||
while func() and num > 0:
|
||||
changed = False
|
||||
while num > 0:
|
||||
func()
|
||||
changed = self.cursor_pos != before
|
||||
if not changed:
|
||||
break
|
||||
num -= 1
|
||||
changed = self.cursor_pos != before
|
||||
if not changed:
|
||||
self.pending_bell = True
|
||||
return changed
|
||||
|
||||
def left(self, num=1):
|
||||
def left(self, num: int = 1) -> bool:
|
||||
return self._move_loop(self._left, num)
|
||||
|
||||
def right(self, num=1):
|
||||
def right(self, num: int = 1) -> bool:
|
||||
return self._move_loop(self._right, num)
|
||||
|
||||
def home(self):
|
||||
def home(self) -> bool:
|
||||
if self.cursor_pos:
|
||||
self.cursor_pos = 0
|
||||
return True
|
||||
return False
|
||||
|
||||
def end(self):
|
||||
def end(self) -> bool:
|
||||
orig = self.cursor_pos
|
||||
self.cursor_pos = wcswidth(self.current_input) + 1
|
||||
self.cursor_pos = wcswidth(self.current_input)
|
||||
return self.cursor_pos != orig
|
||||
|
||||
def on_key(self, key_event):
|
||||
def on_key(self, key_event: KeyEvent) -> bool:
|
||||
if key_event.type is RELEASE:
|
||||
return False
|
||||
elif key_event.key is HOME:
|
||||
|
||||
@@ -10,65 +10,83 @@ import re
|
||||
import selectors
|
||||
import signal
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
from contextlib import contextmanager
|
||||
from functools import partial
|
||||
from typing import Any, Callable, Dict, Generator, List, NamedTuple, Optional
|
||||
|
||||
from kitty.constants import is_macos
|
||||
from kitty.fast_data_types import (
|
||||
close_tty, normal_tty, open_tty, parse_input_from_terminal, raw_tty
|
||||
)
|
||||
from kitty.key_encoding import (
|
||||
ALT, CTRL, PRESS, RELEASE, REPEAT, SHIFT, C, D, backspace_key,
|
||||
decode_key_event, enter_key
|
||||
ALT, CTRL, PRESS, RELEASE, REPEAT, SHIFT, backspace_key, decode_key_event,
|
||||
enter_key, key_defs as K
|
||||
)
|
||||
from kitty.utils import screen_size_function, write_all
|
||||
from kitty.typing import ImageManagerType, KeyEventType, Protocol
|
||||
from kitty.utils import ScreenSizeGetter, screen_size_function, write_all
|
||||
|
||||
from .handler import Handler
|
||||
from .operations import init_state, reset_state
|
||||
|
||||
C, D = K['C'], K['D']
|
||||
|
||||
def debug(*a, **kw):
|
||||
from base64 import standard_b64encode
|
||||
buf = io.StringIO()
|
||||
kw['file'] = buf
|
||||
print(*a, **kw)
|
||||
text = buf.getvalue()
|
||||
text = b'\x1bP@kitty-print|' + standard_b64encode(text.encode('utf-8')) + b'\x1b\\'
|
||||
fobj = getattr(debug, 'fobj', sys.stdout.buffer)
|
||||
fobj.write(text)
|
||||
if hasattr(fobj, 'flush'):
|
||||
|
||||
class BinaryWrite(Protocol):
|
||||
|
||||
def write(self, data: bytes) -> None:
|
||||
pass
|
||||
|
||||
def flush(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class Debug:
|
||||
|
||||
fobj: Optional[BinaryWrite] = None
|
||||
|
||||
def __call__(self, *a: Any, **kw: Any) -> None:
|
||||
from base64 import standard_b64encode
|
||||
buf = io.StringIO()
|
||||
kw['file'] = buf
|
||||
print(*a, **kw)
|
||||
stext = buf.getvalue()
|
||||
text = b'\x1bP@kitty-print|' + standard_b64encode(stext.encode('utf-8')) + b'\x1b\\'
|
||||
fobj = self.fobj or sys.stdout.buffer
|
||||
fobj.write(text)
|
||||
fobj.flush()
|
||||
|
||||
|
||||
debug = Debug()
|
||||
|
||||
|
||||
class TermManager:
|
||||
|
||||
def __init__(self):
|
||||
self.extra_finalize = None
|
||||
def __init__(self) -> None:
|
||||
self.extra_finalize: Optional[str] = None
|
||||
|
||||
def set_state_for_loop(self, set_raw=True):
|
||||
def set_state_for_loop(self, set_raw: bool = True) -> None:
|
||||
if set_raw:
|
||||
raw_tty(self.tty_fd, self.original_termios)
|
||||
write_all(self.tty_fd, init_state())
|
||||
|
||||
def reset_state_to_original(self):
|
||||
def reset_state_to_original(self) -> None:
|
||||
normal_tty(self.tty_fd, self.original_termios)
|
||||
if self.extra_finalize:
|
||||
write_all(self.tty_fd, self.extra_finalize)
|
||||
write_all(self.tty_fd, reset_state())
|
||||
|
||||
@contextmanager
|
||||
def suspend(self):
|
||||
def suspend(self) -> Generator['TermManager', None, None]:
|
||||
self.reset_state_to_original()
|
||||
yield self
|
||||
self.set_state_for_loop()
|
||||
|
||||
def __enter__(self):
|
||||
def __enter__(self) -> 'TermManager':
|
||||
self.tty_fd, self.original_termios = open_tty()
|
||||
self.set_state_for_loop(set_raw=False)
|
||||
return self
|
||||
|
||||
def __exit__(self, *a):
|
||||
def __exit__(self, *a: object) -> None:
|
||||
self.reset_state_to_original()
|
||||
close_tty(self.tty_fd, self.original_termios)
|
||||
del self.tty_fd, self.original_termios
|
||||
@@ -76,7 +94,6 @@ class TermManager:
|
||||
|
||||
LEFT, MIDDLE, RIGHT, FOURTH, FIFTH = 1, 2, 4, 8, 16
|
||||
DRAG = REPEAT
|
||||
MouseEvent = namedtuple('MouseEvent', 'x y type buttons mods')
|
||||
bmap = {0: LEFT, 1: MIDDLE, 2: RIGHT}
|
||||
MOTION_INDICATOR = 1 << 5
|
||||
EXTRA_BUTTON_INDICATOR = 1 << 6
|
||||
@@ -85,10 +102,18 @@ ALT_INDICATOR = 1 << 3
|
||||
CTRL_INDICATOR = 1 << 4
|
||||
|
||||
|
||||
def decode_sgr_mouse(text):
|
||||
cb, x, y = text.split(';')
|
||||
m, y = y[-1], y[:-1]
|
||||
cb, x, y = map(int, (cb, x, y))
|
||||
class MouseEvent(NamedTuple):
|
||||
x: int
|
||||
y: int
|
||||
type: int
|
||||
buttons: int
|
||||
mods: int
|
||||
|
||||
|
||||
def decode_sgr_mouse(text: str) -> MouseEvent:
|
||||
cb_, x_, y_ = text.split(';')
|
||||
m, y_ = y_[-1], y_[:-1]
|
||||
cb, x, y = map(int, (cb_, x_, y_))
|
||||
typ = RELEASE if m == 'm' else (DRAG if cb & MOTION_INDICATOR else PRESS)
|
||||
buttons = 0
|
||||
cb3 = cb & 3
|
||||
@@ -109,10 +134,10 @@ def decode_sgr_mouse(text):
|
||||
|
||||
class UnhandledException(Handler):
|
||||
|
||||
def __init__(self, tb):
|
||||
def __init__(self, tb: str) -> None:
|
||||
self.tb = tb
|
||||
|
||||
def initialize(self):
|
||||
def initialize(self) -> None:
|
||||
self.cmd.clear_screen()
|
||||
self.cmd.set_scrolling_region()
|
||||
self.cmd.set_cursor_visible(True)
|
||||
@@ -121,41 +146,49 @@ class UnhandledException(Handler):
|
||||
self.write('\r\n')
|
||||
self.write('Press the Enter key to quit')
|
||||
|
||||
def on_key(self, key_event):
|
||||
def on_key(self, key_event: KeyEventType) -> None:
|
||||
if key_event is enter_key:
|
||||
self.quit_loop(1)
|
||||
|
||||
def on_interrupt(self):
|
||||
def on_interrupt(self) -> None:
|
||||
self.quit_loop(1)
|
||||
on_eot = on_term = on_interrupt
|
||||
|
||||
|
||||
class SignalManager:
|
||||
|
||||
def __init__(self, loop, on_winch, on_interrupt, on_term):
|
||||
def __init__(
|
||||
self,
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
on_winch: Callable,
|
||||
on_interrupt: Callable,
|
||||
on_term: Callable
|
||||
) -> None:
|
||||
self.asycio_loop = loop
|
||||
self.on_winch, self.on_interrupt, self.on_term = on_winch, on_interrupt, on_term
|
||||
|
||||
def __enter__(self):
|
||||
def __enter__(self) -> None:
|
||||
tuple(map(lambda x: self.asycio_loop.add_signal_handler(*x), (
|
||||
(signal.SIGWINCH, self.on_winch),
|
||||
(signal.SIGINT, self.on_interrupt),
|
||||
(signal.SIGTERM, self.on_term)
|
||||
)))
|
||||
|
||||
def __exit__(self, *a):
|
||||
def __exit__(self, *a: Any) -> None:
|
||||
tuple(map(self.asycio_loop.remove_signal_handler, (
|
||||
signal.SIGWINCH, signal.SIGINT, signal.SIGTERM)))
|
||||
|
||||
|
||||
class Loop:
|
||||
|
||||
def __init__(self,
|
||||
sanitize_bracketed_paste='[\x03\x04\x0e\x0f\r\x07\x7f\x8d\x8e\x8f\x90\x9b\x9d\x9e\x9f]'):
|
||||
def __init__(
|
||||
self,
|
||||
sanitize_bracketed_paste: str = '[\x03\x04\x0e\x0f\r\x07\x7f\x8d\x8e\x8f\x90\x9b\x9d\x9e\x9f]'
|
||||
):
|
||||
if is_macos:
|
||||
# On macOS PTY devices are not supported by the KqueueSelector and
|
||||
# the PollSelector is broken, causes 100% CPU usage
|
||||
self.asycio_loop = asyncio.SelectorEventLoop(selectors.SelectSelector())
|
||||
self.asycio_loop: asyncio.AbstractEventLoop = asyncio.SelectorEventLoop(selectors.SelectSelector())
|
||||
asyncio.set_event_loop(self.asycio_loop)
|
||||
else:
|
||||
self.asycio_loop = asyncio.get_event_loop()
|
||||
@@ -173,14 +206,14 @@ class Loop:
|
||||
if self.sanitize_bracketed_paste:
|
||||
self.sanitize_ibp_pat = re.compile(sanitize_bracketed_paste)
|
||||
|
||||
def _read_ready(self, handler, fd):
|
||||
def _read_ready(self, handler: Handler, fd: int) -> None:
|
||||
try:
|
||||
data = os.read(fd, io.DEFAULT_BUFFER_SIZE)
|
||||
bdata = os.read(fd, io.DEFAULT_BUFFER_SIZE)
|
||||
except BlockingIOError:
|
||||
return
|
||||
if not data:
|
||||
if not bdata:
|
||||
raise EOFError('The input stream is closed')
|
||||
data = self.decoder.decode(data)
|
||||
data = self.decoder.decode(bdata)
|
||||
if self.read_buf:
|
||||
data = self.read_buf + data
|
||||
self.read_buf = data
|
||||
@@ -194,7 +227,7 @@ class Loop:
|
||||
del self.handler
|
||||
|
||||
# terminal input callbacks {{{
|
||||
def _on_text(self, text):
|
||||
def _on_text(self, text: str) -> None:
|
||||
if self.in_bracketed_paste and self.sanitize_bracketed_paste:
|
||||
text = self.sanitize_ibp_pat.sub('', text)
|
||||
|
||||
@@ -213,8 +246,7 @@ class Loop:
|
||||
elif chunk:
|
||||
self.handler.on_text(chunk, self.in_bracketed_paste)
|
||||
|
||||
def _on_dcs(self, dcs):
|
||||
debug(dcs)
|
||||
def _on_dcs(self, dcs: str) -> None:
|
||||
if dcs.startswith('@kitty-cmd'):
|
||||
import json
|
||||
self.handler.on_kitty_cmd_response(json.loads(dcs[len('@kitty-cmd'):]))
|
||||
@@ -229,7 +261,7 @@ class Loop:
|
||||
continue
|
||||
self.handler.on_capability_response(name, val)
|
||||
|
||||
def _on_csi(self, csi):
|
||||
def _on_csi(self, csi: str) -> None:
|
||||
q = csi[-1]
|
||||
if q in 'mM':
|
||||
if csi.startswith('<'):
|
||||
@@ -246,10 +278,10 @@ class Loop:
|
||||
elif csi == '201~':
|
||||
self.in_bracketed_paste = False
|
||||
|
||||
def _on_pm(self, pm):
|
||||
def _on_pm(self, pm: str) -> None:
|
||||
pass
|
||||
|
||||
def _on_osc(self, osc):
|
||||
def _on_osc(self, osc: str) -> None:
|
||||
m = re.match(r'(\d+);', osc)
|
||||
if m is not None:
|
||||
code = int(m.group(1))
|
||||
@@ -260,7 +292,7 @@ class Loop:
|
||||
from base64 import standard_b64decode
|
||||
self.handler.on_clipboard_response(standard_b64decode(rest).decode('utf-8'), from_primary)
|
||||
|
||||
def _on_apc(self, apc):
|
||||
def _on_apc(self, apc: str) -> None:
|
||||
if apc.startswith('K'):
|
||||
try:
|
||||
k = decode_key_event(apc)
|
||||
@@ -280,7 +312,7 @@ class Loop:
|
||||
self.handler.image_manager.handle_response(apc)
|
||||
# }}}
|
||||
|
||||
def _write_ready(self, handler, fd):
|
||||
def _write_ready(self, handler: Handler, fd: int) -> None:
|
||||
if len(self.write_buf) > self.iov_limit:
|
||||
self.write_buf[self.iov_limit - 1] = b''.join(self.write_buf[self.iov_limit - 1:])
|
||||
del self.write_buf[self.iov_limit:]
|
||||
@@ -292,7 +324,7 @@ class Loop:
|
||||
if not written:
|
||||
raise EOFError('The output stream is closed')
|
||||
if written >= sum(sizes):
|
||||
self.write_buf = []
|
||||
self.write_buf: List[bytes] = []
|
||||
self.asycio_loop.remove_writer(fd)
|
||||
self.waiting_for_writes = False
|
||||
else:
|
||||
@@ -308,24 +340,24 @@ class Loop:
|
||||
break
|
||||
del self.write_buf[:consumed]
|
||||
|
||||
def quit(self, return_code=None):
|
||||
def quit(self, return_code: Optional[int] = None) -> None:
|
||||
if return_code is not None:
|
||||
self.return_code = return_code
|
||||
self.asycio_loop.stop()
|
||||
|
||||
def loop_impl(self, handler, term_manager, image_manager=None):
|
||||
def loop_impl(self, handler: Handler, term_manager: TermManager, image_manager: Optional[ImageManagerType] = None) -> Optional[str]:
|
||||
self.write_buf = []
|
||||
tty_fd = term_manager.tty_fd
|
||||
tb = None
|
||||
self.waiting_for_writes = True
|
||||
|
||||
def schedule_write(data):
|
||||
def schedule_write(data: bytes) -> None:
|
||||
self.write_buf.append(data)
|
||||
if not self.waiting_for_writes:
|
||||
self.asycio_loop.add_writer(tty_fd, self._write_ready, handler, tty_fd)
|
||||
self.waiting_for_writes = True
|
||||
|
||||
def handle_exception(loop, context):
|
||||
def handle_exception(loop: asyncio.AbstractEventLoop, context: Dict[str, Any]) -> None:
|
||||
nonlocal tb
|
||||
loop.stop()
|
||||
tb = context['message']
|
||||
@@ -347,17 +379,17 @@ class Loop:
|
||||
self.asycio_loop.remove_writer(tty_fd)
|
||||
return tb
|
||||
|
||||
def loop(self, handler):
|
||||
tb = None
|
||||
def loop(self, handler: Handler) -> None:
|
||||
tb: Optional[str] = None
|
||||
|
||||
def _on_sigwinch():
|
||||
def _on_sigwinch() -> None:
|
||||
self._get_screen_size.changed = True
|
||||
handler.screen_size = self._get_screen_size()
|
||||
handler.on_resize(handler.screen_size)
|
||||
|
||||
signal_manager = SignalManager(self.asycio_loop, _on_sigwinch, handler.on_interrupt, handler.on_term)
|
||||
with TermManager() as term_manager, signal_manager:
|
||||
self._get_screen_size = screen_size_function(term_manager.tty_fd)
|
||||
self._get_screen_size: ScreenSizeGetter = screen_size_function(term_manager.tty_fd)
|
||||
image_manager = None
|
||||
if handler.image_manager_class is not None:
|
||||
image_manager = handler.image_manager_class(handler)
|
||||
@@ -372,5 +404,5 @@ class Loop:
|
||||
self.return_code = 1
|
||||
self._report_error_loop(tb, term_manager)
|
||||
|
||||
def _report_error_loop(self, tb, term_manager):
|
||||
def _report_error_loop(self, tb: str, term_manager: TermManager) -> None:
|
||||
self.loop_impl(UnhandledException(tb), term_manager)
|
||||
|
||||
@@ -5,9 +5,16 @@
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
from typing import (
|
||||
IO, Any, Callable, Dict, Generator, Optional, Tuple, TypeVar, Union
|
||||
)
|
||||
|
||||
from kitty.rgb import Color, color_as_sharp, to_color
|
||||
from kitty.typing import GraphicsCommandType, HandlerType, ScreenSize
|
||||
|
||||
from .operations_stub import CMD
|
||||
|
||||
GraphicsCommandType, ScreenSize # needed for stub generation
|
||||
S7C1T = '\033 F'
|
||||
SAVE_CURSOR = '\0337'
|
||||
RESTORE_CURSOR = '\0338'
|
||||
@@ -35,57 +42,77 @@ MODES = dict(
|
||||
EXTENDED_KEYBOARD=(2017, '?'),
|
||||
)
|
||||
|
||||
|
||||
def set_mode(which, private=True) -> str:
|
||||
num, private = MODES[which]
|
||||
return '\033[{}{}h'.format(private, num)
|
||||
F = TypeVar('F')
|
||||
all_cmds: Dict[str, Callable] = {}
|
||||
|
||||
|
||||
def reset_mode(which) -> str:
|
||||
def cmd(f: F) -> F:
|
||||
all_cmds[f.__name__] = f # type: ignore
|
||||
return f
|
||||
|
||||
|
||||
@cmd
|
||||
def set_mode(which: str, private: bool = True) -> str:
|
||||
num, private_ = MODES[which]
|
||||
return '\033[{}{}h'.format(private_, num)
|
||||
|
||||
|
||||
@cmd
|
||||
def reset_mode(which: str) -> str:
|
||||
num, private = MODES[which]
|
||||
return '\033[{}{}l'.format(private, num)
|
||||
|
||||
|
||||
@cmd
|
||||
def clear_screen() -> str:
|
||||
return '\033[H\033[2J'
|
||||
|
||||
|
||||
@cmd
|
||||
def clear_to_eol() -> str:
|
||||
return '\033[K'
|
||||
|
||||
|
||||
@cmd
|
||||
def bell() -> str:
|
||||
return '\a'
|
||||
|
||||
|
||||
@cmd
|
||||
def beep() -> str:
|
||||
return '\a'
|
||||
|
||||
|
||||
def set_window_title(value) -> str:
|
||||
return ('\033]2;' + value.replace('\033', '').replace('\x9c', '') + '\033\\')
|
||||
@cmd
|
||||
def set_window_title(value: str) -> str:
|
||||
return '\033]2;' + value.replace('\033', '').replace('\x9c', '') + '\033\\'
|
||||
|
||||
|
||||
def set_line_wrapping(yes_or_no) -> str:
|
||||
return (set_mode if yes_or_no else reset_mode)('DECAWM')
|
||||
@cmd
|
||||
def set_line_wrapping(yes_or_no: bool) -> str:
|
||||
return set_mode('DECAWM') if yes_or_no else reset_mode('DECAWM')
|
||||
|
||||
|
||||
def set_cursor_visible(yes_or_no) -> str:
|
||||
return (set_mode if yes_or_no else reset_mode)('DECTCEM')
|
||||
@cmd
|
||||
def set_cursor_visible(yes_or_no: bool) -> str:
|
||||
return set_mode('DECTCEM') if yes_or_no else reset_mode('DECTCEM')
|
||||
|
||||
|
||||
def set_cursor_position(x, y) -> str: # (0, 0) is top left
|
||||
@cmd
|
||||
def set_cursor_position(x: int, y: int) -> str: # (0, 0) is top left
|
||||
return '\033[{};{}H'.format(y + 1, x + 1)
|
||||
|
||||
|
||||
def set_cursor_shape(shape='block', blink=True) -> str:
|
||||
@cmd
|
||||
def set_cursor_shape(shape: str = 'block', blink: bool = True) -> str:
|
||||
val = {'block': 1, 'underline': 3, 'bar': 5}.get(shape, 1)
|
||||
if not blink:
|
||||
val += 1
|
||||
return '\033[{} q'.format(val)
|
||||
|
||||
|
||||
def set_scrolling_region(screen_size=None, top=None, bottom=None) -> str:
|
||||
@cmd
|
||||
def set_scrolling_region(screen_size: Optional['ScreenSize'] = None, top: Optional[int] = None, bottom: Optional[int] = None) -> str:
|
||||
if screen_size is None:
|
||||
return '\033[r'
|
||||
if top is None:
|
||||
@@ -99,7 +126,8 @@ def set_scrolling_region(screen_size=None, top=None, bottom=None) -> str:
|
||||
return '\033[{};{}r'.format(top + 1, bottom + 1)
|
||||
|
||||
|
||||
def scroll_screen(amt=1) -> str:
|
||||
@cmd
|
||||
def scroll_screen(amt: int = 1) -> str:
|
||||
return '\033[' + str(abs(amt)) + ('T' if amt < 0 else 'S')
|
||||
|
||||
|
||||
@@ -110,7 +138,10 @@ UNDERLINE_STYLES = {name: i + 1 for i, name in enumerate(
|
||||
'straight double curly'.split())}
|
||||
|
||||
|
||||
def color_code(color, intense=False, base=30):
|
||||
ColorSpec = Union[int, str, Tuple[int, int, int]]
|
||||
|
||||
|
||||
def color_code(color: ColorSpec, intense: bool = False, base: int = 30) -> str:
|
||||
if isinstance(color, str):
|
||||
e = str((base + 60 if intense else base) + STANDARD_COLORS[color])
|
||||
elif isinstance(color, int):
|
||||
@@ -120,20 +151,41 @@ def color_code(color, intense=False, base=30):
|
||||
return e
|
||||
|
||||
|
||||
def sgr(*parts) -> str:
|
||||
@cmd
|
||||
def sgr(*parts: str) -> str:
|
||||
return '\033[{}m'.format(';'.join(parts))
|
||||
|
||||
|
||||
def colored(text, color, intense=False, reset_to=None, reset_to_intense=False) -> str:
|
||||
@cmd
|
||||
def colored(
|
||||
text: str,
|
||||
color: ColorSpec,
|
||||
intense: bool = False,
|
||||
reset_to: Optional[ColorSpec] = None,
|
||||
reset_to_intense: bool = False
|
||||
) -> str:
|
||||
e = color_code(color, intense)
|
||||
return '\033[{}m{}\033[{}m'.format(e, text, 39 if reset_to is None else color_code(reset_to, reset_to_intense))
|
||||
|
||||
|
||||
def faint(text) -> str:
|
||||
@cmd
|
||||
def faint(text: str) -> str:
|
||||
return colored(text, 'black', True)
|
||||
|
||||
|
||||
def styled(text, fg=None, bg=None, fg_intense=False, bg_intense=False, italic=None, bold=None, underline=None, underline_color=None, reverse=None) -> str:
|
||||
@cmd
|
||||
def styled(
|
||||
text: str,
|
||||
fg: Optional[ColorSpec] = None,
|
||||
bg: Optional[ColorSpec] = None,
|
||||
fg_intense: bool = False,
|
||||
bg_intense: bool = False,
|
||||
italic: Optional[bool] = None,
|
||||
bold: Optional[bool] = None,
|
||||
underline: Optional[str] = None,
|
||||
underline_color: Optional[ColorSpec] = None,
|
||||
reverse: Optional[bool] = None
|
||||
) -> str:
|
||||
start, end = [], []
|
||||
if fg is not None:
|
||||
start.append(color_code(fg, fg_intense))
|
||||
@@ -151,39 +203,48 @@ def styled(text, fg=None, bg=None, fg_intense=False, bg_intense=False, italic=No
|
||||
end.append('4:0')
|
||||
if italic is not None:
|
||||
s, e = (start, end) if italic else (end, start)
|
||||
s.append('3'), e.append('23')
|
||||
s.append('3')
|
||||
e.append('23')
|
||||
if bold is not None:
|
||||
s, e = (start, end) if bold else (end, start)
|
||||
s.append('1'), e.append('22')
|
||||
s.append('1')
|
||||
e.append('22')
|
||||
if reverse is not None:
|
||||
s, e = (start, end) if reverse else (end, start)
|
||||
s.append('7'), e.append('27')
|
||||
s.append('7')
|
||||
e.append('27')
|
||||
if not start:
|
||||
return text
|
||||
return '\033[{}m{}\033[{}m'.format(';'.join(start), text, ';'.join(end))
|
||||
|
||||
|
||||
def serialize_gr_command(cmd, payload=None):
|
||||
cmd = ','.join('{}={}'.format(k, v) for k, v in cmd.items())
|
||||
ans = []
|
||||
w = ans.append
|
||||
w(b'\033_G'), w(cmd.encode('ascii'))
|
||||
if payload:
|
||||
w(b';')
|
||||
w(payload)
|
||||
w(b'\033\\')
|
||||
return b''.join(ans)
|
||||
def serialize_gr_command(cmd: Dict[str, Union[int, str]], payload: Optional[bytes] = None) -> bytes:
|
||||
from .images import GraphicsCommand
|
||||
gc = GraphicsCommand()
|
||||
for k, v in cmd.items():
|
||||
setattr(gc, k, v)
|
||||
return gc.serialize(payload or b'')
|
||||
|
||||
|
||||
def gr_command(cmd, payload=None) -> str:
|
||||
return serialize_gr_command(cmd, payload)
|
||||
@cmd
|
||||
def gr_command(cmd: Union[Dict, 'GraphicsCommandType'], payload: Optional[bytes] = None) -> str:
|
||||
if isinstance(cmd, dict):
|
||||
raw = serialize_gr_command(cmd, payload)
|
||||
else:
|
||||
raw = cmd.serialize(payload or b'')
|
||||
return raw.decode('ascii')
|
||||
|
||||
|
||||
def clear_images_on_screen(delete_data=False) -> str:
|
||||
return serialize_gr_command({'a': 'd', 'd': 'A' if delete_data else 'a'})
|
||||
@cmd
|
||||
def clear_images_on_screen(delete_data: bool = False) -> str:
|
||||
from .images import GraphicsCommand
|
||||
gc = GraphicsCommand()
|
||||
gc.a = 'd'
|
||||
gc.d = 'A' if delete_data else 'a'
|
||||
return gc.serialize().decode('ascii')
|
||||
|
||||
|
||||
def init_state(alternate_screen=True):
|
||||
def init_state(alternate_screen: bool = True) -> str:
|
||||
ans = (
|
||||
S7C1T + SAVE_CURSOR + SAVE_PRIVATE_MODE_VALUES + reset_mode('LNM') +
|
||||
reset_mode('IRM') + reset_mode('DECKM') + reset_mode('DECSCNM') +
|
||||
@@ -202,7 +263,7 @@ def init_state(alternate_screen=True):
|
||||
return ans
|
||||
|
||||
|
||||
def reset_state(normal_screen=True):
|
||||
def reset_state(normal_screen: bool = True) -> str:
|
||||
ans = ''
|
||||
if normal_screen:
|
||||
ans += reset_mode('ALTERNATE_SCREEN')
|
||||
@@ -213,29 +274,42 @@ def reset_state(normal_screen=True):
|
||||
|
||||
|
||||
@contextmanager
|
||||
def cursor(write):
|
||||
def cursor(write: Callable[[str], None]) -> Generator[None, None, None]:
|
||||
write(SAVE_CURSOR)
|
||||
yield
|
||||
write(RESTORE_CURSOR)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def alternate_screen(f=None):
|
||||
def alternate_screen(f: Optional[IO[str]] = None) -> Generator[None, None, None]:
|
||||
f = f or sys.stdout
|
||||
print(set_mode('ALTERNATE_SCREEN'), end='', file=f)
|
||||
yield
|
||||
print(reset_mode('ALTERNATE_SCREEN'), end='', file=f)
|
||||
|
||||
|
||||
def set_default_colors(fg=None, bg=None, cursor=None, select_bg=None, select_fg=None) -> str:
|
||||
@cmd
|
||||
def set_default_colors(
|
||||
fg: Optional[Union[Color, str]] = None,
|
||||
bg: Optional[Union[Color, str]] = None,
|
||||
cursor: Optional[Union[Color, str]] = None,
|
||||
select_bg: Optional[Union[Color, str]] = None,
|
||||
select_fg: Optional[Union[Color, str]] = None
|
||||
) -> str:
|
||||
ans = ''
|
||||
|
||||
def item(which, num):
|
||||
def item(which: Optional[Union[Color, str]], num: int) -> None:
|
||||
nonlocal ans
|
||||
if which is None:
|
||||
ans += '\x1b]1{}\x1b\\'.format(num)
|
||||
else:
|
||||
ans += '\x1b]{};{}\x1b\\'.format(num, color_as_sharp(which if isinstance(which, Color) else to_color(which)))
|
||||
if isinstance(which, Color):
|
||||
q = color_as_sharp(which)
|
||||
else:
|
||||
x = to_color(which)
|
||||
assert x is not None
|
||||
q = color_as_sharp(x)
|
||||
ans += '\x1b]{};{}\x1b\\'.format(num, q)
|
||||
|
||||
item(fg, 10)
|
||||
item(bg, 11)
|
||||
@@ -245,37 +319,67 @@ def set_default_colors(fg=None, bg=None, cursor=None, select_bg=None, select_fg=
|
||||
return ans
|
||||
|
||||
|
||||
def write_to_clipboard(data, use_primary=False) -> str:
|
||||
@cmd
|
||||
def write_to_clipboard(data: Union[str, bytes], use_primary: bool = False) -> str:
|
||||
if isinstance(data, str):
|
||||
data = data.encode('utf-8')
|
||||
from base64 import standard_b64encode
|
||||
fmt = 'p' if use_primary else 'c'
|
||||
|
||||
def esc(chunk):
|
||||
def esc(chunk: str) -> str:
|
||||
return '\x1b]52;{};{}\x07'.format(fmt, chunk)
|
||||
|
||||
ans = esc('!') # clear clipboard buffer
|
||||
for chunk in (data[i:i+512] for i in range(0, len(data), 512)):
|
||||
chunk = standard_b64encode(chunk).decode('ascii')
|
||||
ans += esc(chunk)
|
||||
s = standard_b64encode(chunk).decode('ascii')
|
||||
ans += esc(s)
|
||||
return ans
|
||||
|
||||
|
||||
def request_from_clipboard(use_primary=False) -> str:
|
||||
@cmd
|
||||
def request_from_clipboard(use_primary: bool = False) -> str:
|
||||
return '\x1b]52;{};?\x07'.format('p' if use_primary else 'c')
|
||||
|
||||
|
||||
all_cmds = tuple(
|
||||
(name, obj) for name, obj in globals().items()
|
||||
if hasattr(obj, '__annotations__') and obj.__annotations__.get('return') is str)
|
||||
# Boilerplate to make operations availble via Handler.cmd {{{
|
||||
|
||||
|
||||
def writer(handler, func):
|
||||
def writer(handler: HandlerType, func: Callable) -> Callable:
|
||||
@wraps(func)
|
||||
def f(self, *a, **kw):
|
||||
def f(*a: Any, **kw: Any) -> None:
|
||||
handler.write(func(*a, **kw))
|
||||
return f
|
||||
|
||||
|
||||
def commander(handler):
|
||||
ans = {name: writer(handler, obj) for name, obj in all_cmds}
|
||||
return type('CMD', (), ans)()
|
||||
def commander(handler: HandlerType) -> CMD:
|
||||
ans = CMD()
|
||||
for name, func in all_cmds.items():
|
||||
setattr(ans, name, writer(handler, func))
|
||||
return ans
|
||||
|
||||
|
||||
def func_sig(func: Callable) -> Generator[str, None, None]:
|
||||
import inspect
|
||||
import re
|
||||
s = inspect.signature(func)
|
||||
for val in s.parameters.values():
|
||||
yield re.sub(r'ForwardRef\([\'"](\w+?)[\'"]\)', r'\1', str(val).replace('NoneType', 'None'))
|
||||
|
||||
|
||||
def as_type_stub() -> str:
|
||||
ans = [
|
||||
'from typing import * # noqa',
|
||||
'from kitty.typing import GraphicsCommandType, ScreenSize',
|
||||
'from kitty.rgb import Color',
|
||||
'import kitty.rgb',
|
||||
]
|
||||
methods = []
|
||||
for name, func in all_cmds.items():
|
||||
args = ', '.join(func_sig(func))
|
||||
if args:
|
||||
args = ', ' + args
|
||||
methods.append(' def {}(self{}) -> str: pass'.format(name, args))
|
||||
ans += ['', '', 'class CMD:'] + methods
|
||||
|
||||
return '\n'.join(ans) + '\n\n\n'
|
||||
# }}}
|
||||
|
||||
14
kittens/tui/operations_stub.py
Normal file
14
kittens/tui/operations_stub.py
Normal file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
class CMD:
|
||||
pass
|
||||
|
||||
|
||||
def generate_stub() -> None:
|
||||
from kittens.tui.operations import as_type_stub
|
||||
from kitty.conf.definition import save_type_stub
|
||||
text = as_type_stub()
|
||||
save_type_stub(text, __file__)
|
||||
@@ -5,28 +5,46 @@
|
||||
import os
|
||||
import string
|
||||
import subprocess
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
from functools import lru_cache
|
||||
from gettext import gettext as _
|
||||
from contextlib import suppress
|
||||
|
||||
from kitty.config import cached_values_for
|
||||
from kitty.constants import config_dir
|
||||
from kitty.utils import get_editor
|
||||
from kitty.fast_data_types import wcswidth
|
||||
from kitty.key_encoding import (
|
||||
DOWN, ESCAPE, F1, F2, F3, F4, F12, LEFT, RELEASE, RIGHT, SHIFT, TAB, UP,
|
||||
enter_key
|
||||
from typing import (
|
||||
Any, Dict, FrozenSet, Generator, Iterable, List, Optional, Sequence, Tuple,
|
||||
Union
|
||||
)
|
||||
|
||||
from kitty.cli import parse_args
|
||||
from kitty.cli_stub import UnicodeCLIOptions
|
||||
from kitty.config import cached_values_for
|
||||
from kitty.constants import config_dir
|
||||
from kitty.fast_data_types import is_emoji_presentation_base, wcswidth
|
||||
from kitty.key_encoding import (
|
||||
CTRL, RELEASE, SHIFT, KeyEvent, enter_key, key_defs as K
|
||||
)
|
||||
from kitty.typing import BossType
|
||||
from kitty.utils import ScreenSize, get_editor
|
||||
|
||||
from ..tui.handler import Handler, result_handler
|
||||
from ..tui.line_edit import LineEdit
|
||||
from ..tui.handler import Handler
|
||||
from ..tui.loop import Loop
|
||||
from ..tui.operations import (
|
||||
clear_screen, colored, cursor, faint, set_line_wrapping,
|
||||
set_window_title, sgr, styled
|
||||
clear_screen, colored, cursor, faint, set_line_wrapping, set_window_title,
|
||||
sgr, styled
|
||||
)
|
||||
|
||||
HEX, NAME, EMOTICONS, FAVORITES = 'HEX', 'NAME', 'EMOTICONS', 'FAVORITES'
|
||||
UP = K['UP']
|
||||
DOWN = K['DOWN']
|
||||
LEFT = K['LEFT']
|
||||
RIGHT = K['RIGHT']
|
||||
TAB = K['TAB']
|
||||
ESCAPE = K['ESCAPE']
|
||||
F1 = K['F1']
|
||||
F2 = K['F2']
|
||||
F3 = K['F3']
|
||||
F4 = K['F4']
|
||||
F12 = K['F12']
|
||||
favorites_path = os.path.join(config_dir, 'unicode-input-favorites.conf')
|
||||
INDEX_CHAR = '.'
|
||||
DEFAULT_SET = tuple(map(
|
||||
@@ -36,28 +54,33 @@ DEFAULT_SET = tuple(map(
|
||||
'îïðñòóôõöøœš' 'ùúûüýÿþªºαΩ∞'
|
||||
))
|
||||
EMOTICONS_SET = tuple(range(0x1f600, 0x1f64f + 1))
|
||||
all_modes = (
|
||||
(_('Code'), 'F1', HEX),
|
||||
(_('Name'), 'F2', NAME),
|
||||
(_('Emoji'), 'F3', EMOTICONS),
|
||||
(_('Favorites'), 'F4', FAVORITES),
|
||||
)
|
||||
|
||||
|
||||
def codepoint_ok(code):
|
||||
def codepoint_ok(code: int) -> bool:
|
||||
return not (code <= 32 or code == 127 or 128 <= code <= 159 or 0xd800 <= code <= 0xdbff or 0xDC00 <= code <= 0xDFFF)
|
||||
|
||||
|
||||
@lru_cache(maxsize=256)
|
||||
def points_for_word(w):
|
||||
def points_for_word(w: str) -> FrozenSet[int]:
|
||||
from .unicode_names import codepoints_for_word
|
||||
return codepoints_for_word(w.lower())
|
||||
|
||||
|
||||
@lru_cache(maxsize=4096)
|
||||
def name(cp):
|
||||
def name(cp: Union[int, str]) -> str:
|
||||
from .unicode_names import name_for_codepoint
|
||||
if isinstance(cp, str):
|
||||
cp = ord(cp[0])
|
||||
return (name_for_codepoint(cp) or '').capitalize()
|
||||
c = ord(cp[0]) if isinstance(cp, str) else cp
|
||||
return (name_for_codepoint(c) or '').capitalize()
|
||||
|
||||
|
||||
@lru_cache(maxsize=256)
|
||||
def codepoints_matching_search(parts):
|
||||
def codepoints_matching_search(parts: Sequence[str]) -> List[int]:
|
||||
ans = []
|
||||
if parts and parts[0] and len(parts[0]) > 1:
|
||||
codepoints = points_for_word(parts[0])
|
||||
@@ -68,13 +91,13 @@ def codepoints_matching_search(parts):
|
||||
if intersection:
|
||||
codepoints = intersection
|
||||
continue
|
||||
codepoints = {c for c in codepoints if word in name(c).lower()}
|
||||
codepoints = frozenset(c for c in codepoints if word in name(c).lower())
|
||||
if codepoints:
|
||||
ans = list(sorted(codepoints))
|
||||
return ans
|
||||
|
||||
|
||||
def parse_favorites(raw):
|
||||
def parse_favorites(raw: str) -> Generator[int, None, None]:
|
||||
for line in raw.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith('#') or not line:
|
||||
@@ -92,7 +115,7 @@ def parse_favorites(raw):
|
||||
yield code
|
||||
|
||||
|
||||
def serialize_favorites(favorites):
|
||||
def serialize_favorites(favorites: Iterable[int]) -> str:
|
||||
ans = '''\
|
||||
# Favorite characters for unicode input
|
||||
# Enter the hex code for each favorite character on a new line. Blank lines are
|
||||
@@ -104,19 +127,20 @@ def serialize_favorites(favorites):
|
||||
return '\n'.join(ans)
|
||||
|
||||
|
||||
def load_favorites(refresh=False):
|
||||
ans = getattr(load_favorites, 'ans', None)
|
||||
def load_favorites(refresh: bool = False) -> List[int]:
|
||||
ans: Optional[List[int]] = getattr(load_favorites, 'ans', None)
|
||||
if ans is None or refresh:
|
||||
try:
|
||||
with open(favorites_path, 'rb') as f:
|
||||
raw = f.read().decode('utf-8')
|
||||
ans = load_favorites.ans = list(parse_favorites(raw)) or list(DEFAULT_SET)
|
||||
ans = list(parse_favorites(raw)) or list(DEFAULT_SET)
|
||||
except FileNotFoundError:
|
||||
ans = load_favorites.ans = list(DEFAULT_SET)
|
||||
ans = list(DEFAULT_SET)
|
||||
setattr(load_favorites, 'ans', ans)
|
||||
return ans
|
||||
|
||||
|
||||
def encode_hint(num, digits=string.digits + string.ascii_lowercase):
|
||||
def encode_hint(num: int, digits: str = string.digits + string.ascii_lowercase) -> str:
|
||||
res = ''
|
||||
d = len(digits)
|
||||
while not res or num > 0:
|
||||
@@ -125,49 +149,53 @@ def encode_hint(num, digits=string.digits + string.ascii_lowercase):
|
||||
return res
|
||||
|
||||
|
||||
def decode_hint(x):
|
||||
def decode_hint(x: str) -> int:
|
||||
return int(x, 36)
|
||||
|
||||
|
||||
class Table:
|
||||
|
||||
def __init__(self):
|
||||
self.layout_dirty = True
|
||||
def __init__(self, emoji_variation: str) -> None:
|
||||
self.emoji_variation = emoji_variation
|
||||
self.layout_dirty: bool = True
|
||||
self.last_rows = self.last_cols = -1
|
||||
self.codepoints = []
|
||||
self.codepoints: List[int] = []
|
||||
self.current_idx = 0
|
||||
self.text = ''
|
||||
self.num_cols = 0
|
||||
self.mode = HEX
|
||||
|
||||
@property
|
||||
def current_codepoint(self):
|
||||
def current_codepoint(self) -> Optional[int]:
|
||||
if self.codepoints:
|
||||
return self.codepoints[self.current_idx]
|
||||
|
||||
def set_codepoints(self, codepoints, mode=HEX):
|
||||
def set_codepoints(self, codepoints: List[int], mode: str = HEX) -> None:
|
||||
self.codepoints = codepoints
|
||||
self.mode = mode
|
||||
self.layout_dirty = True
|
||||
self.current_idx = 0
|
||||
|
||||
def codepoint_at_hint(self, hint):
|
||||
def codepoint_at_hint(self, hint: str) -> int:
|
||||
return self.codepoints[decode_hint(hint)]
|
||||
|
||||
def layout(self, rows, cols):
|
||||
def layout(self, rows: int, cols: int) -> Optional[str]:
|
||||
if not self.layout_dirty and self.last_cols == cols and self.last_rows == rows:
|
||||
return self.text
|
||||
self.last_cols, self.last_rows = cols, rows
|
||||
self.layout_dirty = False
|
||||
|
||||
def safe_chr(codepoint):
|
||||
return chr(codepoint).encode('utf-8', 'replace').decode('utf-8')
|
||||
def safe_chr(codepoint: int) -> str:
|
||||
ans = chr(codepoint).encode('utf-8', 'replace').decode('utf-8')
|
||||
if self.emoji_variation and is_emoji_presentation_base(codepoint):
|
||||
ans += self.emoji_variation
|
||||
return ans
|
||||
|
||||
if self.mode is NAME:
|
||||
def as_parts(i, codepoint):
|
||||
def as_parts(i: int, codepoint: int) -> Tuple[str, str, str]:
|
||||
return encode_hint(i).ljust(idx_size), safe_chr(codepoint), name(codepoint)
|
||||
|
||||
def cell(i, idx, c, desc):
|
||||
def cell(i: int, idx: str, c: str, desc: str) -> Generator[str, None, None]:
|
||||
is_current = i == self.current_idx
|
||||
text = colored(idx, 'green') + ' ' + sgr('49') + c + ' '
|
||||
w = wcswidth(c)
|
||||
@@ -184,10 +212,10 @@ class Table:
|
||||
yield styled(text, reverse=True if is_current else None)
|
||||
|
||||
else:
|
||||
def as_parts(i, codepoint):
|
||||
def as_parts(i: int, codepoint: int) -> Tuple[str, str, str]:
|
||||
return encode_hint(i).ljust(idx_size), safe_chr(codepoint), ''
|
||||
|
||||
def cell(i, idx, c, desc):
|
||||
def cell(i: int, idx: str, c: str, desc: str) -> Generator[str, None, None]:
|
||||
yield colored(idx, 'green') + ' '
|
||||
yield colored(c, 'gray', True)
|
||||
w = wcswidth(c)
|
||||
@@ -211,7 +239,7 @@ class Table:
|
||||
col_width = min(col_width, 40)
|
||||
space_for_desc = col_width - 2 - idx_size - 4
|
||||
num_cols = self.num_cols = max(cols // col_width, 1)
|
||||
buf = []
|
||||
buf: List[str] = []
|
||||
a = buf.append
|
||||
rows_left = rows
|
||||
|
||||
@@ -220,13 +248,13 @@ class Table:
|
||||
rows_left -= 1
|
||||
if rows_left == 0:
|
||||
break
|
||||
buf.append('\r\n')
|
||||
a('\r\n')
|
||||
buf.extend(cell(i, idx, c, desc))
|
||||
a(' ')
|
||||
self.text = ''.join(buf)
|
||||
return self.text
|
||||
|
||||
def move_current(self, rows=0, cols=0):
|
||||
def move_current(self, rows: int = 0, cols: int = 0) -> None:
|
||||
if len(self.codepoints) == 0:
|
||||
return
|
||||
if cols:
|
||||
@@ -239,31 +267,45 @@ class Table:
|
||||
self.layout_dirty = True
|
||||
|
||||
|
||||
def is_index(w):
|
||||
with suppress(Exception):
|
||||
def is_index(w: str) -> bool:
|
||||
try:
|
||||
int(w.lstrip(INDEX_CHAR), 16)
|
||||
return True
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class UnicodeInput(Handler):
|
||||
|
||||
def __init__(self, cached_values):
|
||||
def __init__(self, cached_values: Dict[str, Any], emoji_variation: str = 'none') -> None:
|
||||
self.cached_values = cached_values
|
||||
self.emoji_variation = ''
|
||||
if emoji_variation == 'text':
|
||||
self.emoji_variation = '\ufe0e'
|
||||
elif emoji_variation == 'graphic':
|
||||
self.emoji_variation = '\ufe0f'
|
||||
self.line_edit = LineEdit()
|
||||
self.recent = list(self.cached_values.get('recent', DEFAULT_SET))
|
||||
self.current_char = None
|
||||
self.current_char: Optional[str] = None
|
||||
self.prompt_template = '{}> '
|
||||
self.last_updated_code_point_at = None
|
||||
self.last_updated_code_point_at: Optional[Tuple[str, Union[Sequence[int], None, str]]] = None
|
||||
self.choice_line = ''
|
||||
self.mode = globals().get(cached_values.get('mode', 'HEX'), 'HEX')
|
||||
self.table = Table()
|
||||
self.table = Table(self.emoji_variation)
|
||||
self.update_prompt()
|
||||
|
||||
def update_codepoints(self):
|
||||
@property
|
||||
def resolved_current_char(self) -> Optional[str]:
|
||||
ans = self.current_char
|
||||
if ans:
|
||||
if self.emoji_variation and is_emoji_presentation_base(ord(ans[0])):
|
||||
ans += self.emoji_variation
|
||||
return ans
|
||||
|
||||
def update_codepoints(self) -> None:
|
||||
codepoints = None
|
||||
if self.mode is HEX:
|
||||
q = self.mode, None
|
||||
q: Tuple[str, Optional[Union[str, Sequence[int]]]] = (self.mode, None)
|
||||
codepoints = self.recent
|
||||
elif self.mode is EMOTICONS:
|
||||
q = self.mode, None
|
||||
@@ -282,14 +324,14 @@ class UnicodeInput(Handler):
|
||||
words = words[:index_words[0]]
|
||||
codepoints = codepoints_matching_search(tuple(words))
|
||||
if index_words:
|
||||
index_word = int(index_word.lstrip(INDEX_CHAR), 16)
|
||||
if index_word < len(codepoints):
|
||||
codepoints = [codepoints[index_word]]
|
||||
iindex_word = int(index_word.lstrip(INDEX_CHAR), 16)
|
||||
if codepoints and iindex_word < len(codepoints):
|
||||
codepoints = [codepoints[iindex_word]]
|
||||
if q != self.last_updated_code_point_at:
|
||||
self.last_updated_code_point_at = q
|
||||
self.table.set_codepoints(codepoints, self.mode)
|
||||
self.table.set_codepoints(codepoints or [], self.mode)
|
||||
|
||||
def update_current_char(self):
|
||||
def update_current_char(self) -> None:
|
||||
self.update_codepoints()
|
||||
self.current_char = None
|
||||
if self.mode is HEX:
|
||||
@@ -313,33 +355,30 @@ class UnicodeInput(Handler):
|
||||
if not codepoint_ok(code):
|
||||
self.current_char = None
|
||||
|
||||
def update_prompt(self):
|
||||
def update_prompt(self) -> None:
|
||||
self.update_current_char()
|
||||
if self.current_char is None:
|
||||
c, color = '??', 'red'
|
||||
self.choice_line = ''
|
||||
else:
|
||||
c, color = self.current_char, 'green'
|
||||
if self.emoji_variation and is_emoji_presentation_base(ord(c[0])):
|
||||
c += self.emoji_variation
|
||||
self.choice_line = _('Chosen:') + ' {} U+{} {}'.format(
|
||||
colored(c, 'green'), hex(ord(c))[2:], faint(styled(name(c) or '', italic=True)))
|
||||
colored(c, 'green'), hex(ord(c[0]))[2:], faint(styled(name(c) or '', italic=True)))
|
||||
self.prompt = self.prompt_template.format(colored(c, color))
|
||||
|
||||
def init_terminal_state(self):
|
||||
def init_terminal_state(self) -> None:
|
||||
self.write(set_line_wrapping(False))
|
||||
self.write(set_window_title(_('Unicode input')))
|
||||
|
||||
def initialize(self):
|
||||
def initialize(self) -> None:
|
||||
self.init_terminal_state()
|
||||
self.draw_screen()
|
||||
|
||||
def draw_title_bar(self):
|
||||
def draw_title_bar(self) -> None:
|
||||
entries = []
|
||||
for name, key, mode in [
|
||||
(_('Code'), 'F1', HEX),
|
||||
(_('Name'), 'F2', NAME),
|
||||
(_('Emoji'), 'F3', EMOTICONS),
|
||||
(_('Favorites'), 'F4', FAVORITES),
|
||||
]:
|
||||
for name, key, mode in all_modes:
|
||||
entry = ' {} ({}) '.format(name, key)
|
||||
if mode is self.mode:
|
||||
entry = styled(entry, reverse=False, bold=True)
|
||||
@@ -350,12 +389,12 @@ class UnicodeInput(Handler):
|
||||
text += ' ' * extra
|
||||
self.print(styled(text, reverse=True))
|
||||
|
||||
def draw_screen(self):
|
||||
def draw_screen(self) -> None:
|
||||
self.write(clear_screen())
|
||||
self.draw_title_bar()
|
||||
y = 1
|
||||
|
||||
def writeln(text=''):
|
||||
def writeln(text: str = '') -> None:
|
||||
nonlocal y
|
||||
self.print(text)
|
||||
y += 1
|
||||
@@ -377,17 +416,19 @@ class UnicodeInput(Handler):
|
||||
elif self.mode is FAVORITES:
|
||||
writeln(faint(_('Press F12 to edit the list of favorites')))
|
||||
self.table_at = y
|
||||
self.write(self.table.layout(self.screen_size.rows - self.table_at, self.screen_size.cols))
|
||||
q = self.table.layout(self.screen_size.rows - self.table_at, self.screen_size.cols)
|
||||
if q:
|
||||
self.write(q)
|
||||
|
||||
def refresh(self):
|
||||
def refresh(self) -> None:
|
||||
self.update_prompt()
|
||||
self.draw_screen()
|
||||
|
||||
def on_text(self, text, in_bracketed_paste):
|
||||
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
|
||||
self.line_edit.on_text(text, in_bracketed_paste)
|
||||
self.refresh()
|
||||
|
||||
def on_key(self, key_event):
|
||||
def on_key(self, key_event: KeyEvent) -> None:
|
||||
if self.mode is HEX and key_event.type is not RELEASE and not key_event.mods:
|
||||
try:
|
||||
val = int(self.line_edit.current_input, 16)
|
||||
@@ -409,21 +450,27 @@ class UnicodeInput(Handler):
|
||||
if self.mode is NAME and key_event.type is not RELEASE and not key_event.mods:
|
||||
if key_event.key is TAB:
|
||||
if key_event.mods == SHIFT:
|
||||
self.table.move_current(cols=-1), self.refresh()
|
||||
self.table.move_current(cols=-1)
|
||||
self.refresh()
|
||||
elif not key_event.mods:
|
||||
self.table.move_current(cols=1), self.refresh()
|
||||
self.table.move_current(cols=1)
|
||||
self.refresh()
|
||||
return
|
||||
elif key_event.key is LEFT and not key_event.mods:
|
||||
self.table.move_current(cols=-1), self.refresh()
|
||||
self.table.move_current(cols=-1)
|
||||
self.refresh()
|
||||
return
|
||||
elif key_event.key is RIGHT and not key_event.mods:
|
||||
self.table.move_current(cols=1), self.refresh()
|
||||
self.table.move_current(cols=1)
|
||||
self.refresh()
|
||||
return
|
||||
elif key_event.key is UP and not key_event.mods:
|
||||
self.table.move_current(rows=-1), self.refresh()
|
||||
self.table.move_current(rows=-1)
|
||||
self.refresh()
|
||||
return
|
||||
elif key_event.key is DOWN and not key_event.mods:
|
||||
self.table.move_current(rows=1), self.refresh()
|
||||
self.table.move_current(rows=1)
|
||||
self.refresh()
|
||||
return
|
||||
|
||||
if self.line_edit.on_key(key_event):
|
||||
@@ -431,21 +478,26 @@ class UnicodeInput(Handler):
|
||||
return
|
||||
if key_event is enter_key:
|
||||
self.quit_loop(0)
|
||||
elif key_event.type is RELEASE and not key_event.mods:
|
||||
if key_event.key is ESCAPE:
|
||||
self.quit_loop(1)
|
||||
elif key_event.key is F1:
|
||||
self.switch_mode(HEX)
|
||||
elif key_event.key is F2:
|
||||
self.switch_mode(NAME)
|
||||
elif key_event.key is F3:
|
||||
self.switch_mode(EMOTICONS)
|
||||
elif key_event.key is F4:
|
||||
self.switch_mode(FAVORITES)
|
||||
elif key_event.key is F12 and self.mode is FAVORITES:
|
||||
self.edit_favorites()
|
||||
elif key_event.type is RELEASE:
|
||||
if not key_event.mods:
|
||||
if key_event.key is ESCAPE:
|
||||
self.quit_loop(1)
|
||||
elif key_event.key is F1:
|
||||
self.switch_mode(HEX)
|
||||
elif key_event.key is F2:
|
||||
self.switch_mode(NAME)
|
||||
elif key_event.key is F3:
|
||||
self.switch_mode(EMOTICONS)
|
||||
elif key_event.key is F4:
|
||||
self.switch_mode(FAVORITES)
|
||||
elif key_event.key is F12 and self.mode is FAVORITES:
|
||||
self.edit_favorites()
|
||||
elif key_event.mods == CTRL and key_event.key is TAB:
|
||||
self.next_mode()
|
||||
elif key_event.mods == CTRL | SHIFT and key_event.key is TAB:
|
||||
self.next_mode(-1)
|
||||
|
||||
def edit_favorites(self):
|
||||
def edit_favorites(self) -> None:
|
||||
if not os.path.exists(favorites_path):
|
||||
with open(favorites_path, 'wb') as f:
|
||||
f.write(serialize_favorites(load_favorites()).encode('utf-8'))
|
||||
@@ -456,7 +508,7 @@ class UnicodeInput(Handler):
|
||||
self.init_terminal_state()
|
||||
self.refresh()
|
||||
|
||||
def switch_mode(self, mode):
|
||||
def switch_mode(self, mode: str) -> None:
|
||||
if mode is not self.mode:
|
||||
self.mode = mode
|
||||
self.cached_values['mode'] = mode
|
||||
@@ -465,42 +517,76 @@ class UnicodeInput(Handler):
|
||||
self.choice_line = ''
|
||||
self.refresh()
|
||||
|
||||
def on_interrupt(self):
|
||||
def next_mode(self, delta: int = 1) -> None:
|
||||
modes = tuple(x[-1] for x in all_modes)
|
||||
idx = (modes.index(self.mode) + delta + len(modes)) % len(modes)
|
||||
self.switch_mode(modes[idx])
|
||||
|
||||
def on_interrupt(self) -> None:
|
||||
self.quit_loop(1)
|
||||
|
||||
def on_eot(self):
|
||||
def on_eot(self) -> None:
|
||||
self.quit_loop(1)
|
||||
|
||||
def on_resize(self, new_size):
|
||||
def on_resize(self, new_size: ScreenSize) -> None:
|
||||
self.refresh()
|
||||
|
||||
|
||||
def main(args):
|
||||
help_text = 'Input a unicode character'
|
||||
usage = ''
|
||||
OPTIONS = '''
|
||||
--emoji-variation
|
||||
type=choices
|
||||
default=none
|
||||
choices=none,graphic,text
|
||||
Whether to use the textual or the graphical form for emoji. By default the
|
||||
default form specified in the unicode standard for the symbol is used.
|
||||
|
||||
|
||||
'''.format
|
||||
|
||||
|
||||
def parse_unicode_input_args(args: List[str]) -> Tuple[UnicodeCLIOptions, List[str]]:
|
||||
return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten unicode_input', result_class=UnicodeCLIOptions)
|
||||
|
||||
|
||||
def main(args: List[str]) -> Optional[str]:
|
||||
try:
|
||||
cli_opts, items = parse_unicode_input_args(args[1:])
|
||||
except SystemExit as e:
|
||||
if e.code != 0:
|
||||
print(e.args[0], file=sys.stderr)
|
||||
input(_('Press Enter to quit'))
|
||||
return None
|
||||
|
||||
loop = Loop()
|
||||
with cached_values_for('unicode-input') as cached_values:
|
||||
handler = UnicodeInput(cached_values)
|
||||
handler = UnicodeInput(cached_values, cli_opts.emoji_variation)
|
||||
loop.loop(handler)
|
||||
if handler.current_char and loop.return_code == 0:
|
||||
with suppress(Exception):
|
||||
handler.recent.remove(ord(handler.current_char))
|
||||
recent = [ord(handler.current_char)] + handler.recent
|
||||
cached_values['recent'] = recent[:len(DEFAULT_SET)]
|
||||
return handler.current_char
|
||||
return handler.resolved_current_char
|
||||
if loop.return_code != 0:
|
||||
raise SystemExit(loop.return_code)
|
||||
return None
|
||||
|
||||
|
||||
def handle_result(args, current_char, target_window_id, boss):
|
||||
@result_handler()
|
||||
def handle_result(args: List[str], current_char: str, target_window_id: int, boss: BossType) -> None:
|
||||
w = boss.window_id_map.get(target_window_id)
|
||||
if w is not None:
|
||||
w.paste(current_char)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
if '-h' in sys.argv or '--help' in sys.argv:
|
||||
print('Choose a unicode character to input into the terminal')
|
||||
raise SystemExit(0)
|
||||
ans = main(sys.argv)
|
||||
if ans:
|
||||
print(ans)
|
||||
elif __name__ == '__doc__':
|
||||
cd = sys.cli_docs # type: ignore
|
||||
cd['usage'] = usage
|
||||
cd['options'] = OPTIONS
|
||||
cd['help_text'] = help_text
|
||||
|
||||
56991
kittens/unicode_input/names.h
generated
56991
kittens/unicode_input/names.h
generated
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user