Compare commits
660 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55b53bcac6 | ||
|
|
57d044861f | ||
|
|
aab149366d | ||
|
|
896f93822d | ||
|
|
6890e265b6 | ||
|
|
48a4edc199 | ||
|
|
d63be90a7f | ||
|
|
6af020e4ef | ||
|
|
ca24e9ca67 | ||
|
|
fbfaefd72a | ||
|
|
94b3776066 | ||
|
|
15e4be9051 | ||
|
|
ae8f3de070 | ||
|
|
c8d18ffe26 | ||
|
|
4d80427908 | ||
|
|
0e69324469 | ||
|
|
cf22729dfa | ||
|
|
1402c5bbfa | ||
|
|
f0f0c8f4fe | ||
|
|
96faac95db | ||
|
|
08041415d1 | ||
|
|
647b18d345 | ||
|
|
a116e3cadd | ||
|
|
52c9b5f5b8 | ||
|
|
bbc9f588f1 | ||
|
|
13a3c6b5b2 | ||
|
|
fc15b20f24 | ||
|
|
cd8f2e5cc6 | ||
|
|
267798a277 | ||
|
|
921ac12e90 | ||
|
|
901e00cab1 | ||
|
|
d0769f3979 | ||
|
|
7e4dd8cf1e | ||
|
|
5ea0519f62 | ||
|
|
a1029418f8 | ||
|
|
98eacb2067 | ||
|
|
2cef9abbd3 | ||
|
|
0fa158c809 | ||
|
|
c54a6b398b | ||
|
|
27ea367123 | ||
|
|
42178a4570 | ||
|
|
e98ecc1c4f | ||
|
|
0d64246209 | ||
|
|
a0495219ab | ||
|
|
9d98aa3159 | ||
|
|
5c82f4beca | ||
|
|
13539bd8c6 | ||
|
|
26b8ab9adf | ||
|
|
dbb97a62bf | ||
|
|
b1fa1c9820 | ||
|
|
2dc9488909 | ||
|
|
e2a251224d | ||
|
|
2a2dca8a58 | ||
|
|
e1f3cb85c0 | ||
|
|
13cf38d6c6 | ||
|
|
5180a41e87 | ||
|
|
1c44da2b4a | ||
|
|
bcd78c3940 | ||
|
|
fe2f3d9870 | ||
|
|
dcf2152a7a | ||
|
|
58a3baaf0f | ||
|
|
b247759d30 | ||
|
|
ba8d30896b | ||
|
|
4d30ae55f3 | ||
|
|
983ab3c67d | ||
|
|
0bf228b030 | ||
|
|
29b3a92ffc | ||
|
|
23d73ec933 | ||
|
|
bc650f6230 | ||
|
|
5272318d67 | ||
|
|
5ba887ff68 | ||
|
|
67d70f8b8f | ||
|
|
0fac6e4de9 | ||
|
|
1e7a11b278 | ||
|
|
6741ac2087 | ||
|
|
7b2e29a6a8 | ||
|
|
6f147544d2 | ||
|
|
89570d3b52 | ||
|
|
dd4a65e0c9 | ||
|
|
79fba9dd9a | ||
|
|
b123c21010 | ||
|
|
9c5d1f9771 | ||
|
|
5131224b59 | ||
|
|
be89da2271 | ||
|
|
45b408a78b | ||
|
|
0e7da8643e | ||
|
|
410b1816ad | ||
|
|
bcfa2a64e4 | ||
|
|
91c00fb5ac | ||
|
|
c58d217d32 | ||
|
|
a9b6047741 | ||
|
|
6b432bee4f | ||
|
|
087f0b449f | ||
|
|
96160b52b6 | ||
|
|
d17a6cd3a3 | ||
|
|
7e1380cc0d | ||
|
|
9a99554ed3 | ||
|
|
19799bd538 | ||
|
|
902e94ceac | ||
|
|
5bb2cb06a9 | ||
|
|
1559a2e15a | ||
|
|
35f94963cd | ||
|
|
f0d497dfe0 | ||
|
|
5bdf27fd78 | ||
|
|
f05783e64d | ||
|
|
c455fea729 | ||
|
|
5bf5454dbe | ||
|
|
b60d9f73ce | ||
|
|
31fe35cd38 | ||
|
|
607bc845fc | ||
|
|
a420d9d19c | ||
|
|
4b81e4936e | ||
|
|
295743a96e | ||
|
|
d3a1848c27 | ||
|
|
9cec924f32 | ||
|
|
67c963ab89 | ||
|
|
0f7ced25c1 | ||
|
|
42948ffff0 | ||
|
|
922ce5bdea | ||
|
|
1b42486026 | ||
|
|
95f1a971b2 | ||
|
|
7968c88c67 | ||
|
|
992e90b0a3 | ||
|
|
afada84f52 | ||
|
|
b978ff8930 | ||
|
|
053cede581 | ||
|
|
03720402a7 | ||
|
|
fca0999814 | ||
|
|
78056c659c | ||
|
|
30e1b4680d | ||
|
|
89a0c04d19 | ||
|
|
1ba027c277 | ||
|
|
d8e43a3412 | ||
|
|
644e46e91d | ||
|
|
e2a1f8dde7 | ||
|
|
7d09c6b762 | ||
|
|
6604e0d015 | ||
|
|
bb2ec9cde7 | ||
|
|
89a2545855 | ||
|
|
49f8c0eae6 | ||
|
|
268d687814 | ||
|
|
db5b4da388 | ||
|
|
71f8e50460 | ||
|
|
22fbdbca40 | ||
|
|
6253ee2a74 | ||
|
|
5b28aed0b1 | ||
|
|
9eabc9ecf1 | ||
|
|
a77852466c | ||
|
|
c68b82e4d0 | ||
|
|
16a4845a72 | ||
|
|
16da031f68 | ||
|
|
43b2935e52 | ||
|
|
8c7968a4d6 | ||
|
|
fba759a47c | ||
|
|
3bcb694b1b | ||
|
|
555020cfd5 | ||
|
|
391e2b8488 | ||
|
|
c856d5c058 | ||
|
|
f5a70adf0b | ||
|
|
e330c38d4a | ||
|
|
d595084395 | ||
|
|
b32ee5ee46 | ||
|
|
098dd10789 | ||
|
|
b5fa643c5d | ||
|
|
664dd29970 | ||
|
|
b7a4281985 | ||
|
|
9e9aa52553 | ||
|
|
77fbdfbb53 | ||
|
|
214416f1e3 | ||
|
|
800437ec6b | ||
|
|
957eafbef0 | ||
|
|
7f228bcbf5 | ||
|
|
fd4a682c5b | ||
|
|
ba9afc61a4 | ||
|
|
9a70709bba | ||
|
|
c437a36794 | ||
|
|
0e0578ff49 | ||
|
|
64a202a125 | ||
|
|
740d1ca146 | ||
|
|
7b75f055ea | ||
|
|
8d6a3cff25 | ||
|
|
10fdde6647 | ||
|
|
e1c45dc23f | ||
|
|
2efb8f6dc2 | ||
|
|
3b77f3b4bc | ||
|
|
31f763282f | ||
|
|
39434fb1f6 | ||
|
|
44ccdd36d6 | ||
|
|
4cc0138a28 | ||
|
|
6ca39d9d83 | ||
|
|
26c099639c | ||
|
|
329043ba08 | ||
|
|
e289f4959f | ||
|
|
b6760a59fa | ||
|
|
cd35f92607 | ||
|
|
ea8f223298 | ||
|
|
a4c3154dab | ||
|
|
6aa3405c78 | ||
|
|
da618584af | ||
|
|
9bc31a5651 | ||
|
|
314dd97059 | ||
|
|
0b66f20934 | ||
|
|
aba634ce88 | ||
|
|
033950dae9 | ||
|
|
a59e1adee1 | ||
|
|
5350eb29c1 | ||
|
|
2bb42e67d7 | ||
|
|
8745c4cd4a | ||
|
|
3288400005 | ||
|
|
1f499a7c6c | ||
|
|
012731c3be | ||
|
|
5deed81737 | ||
|
|
6f6e23bf61 | ||
|
|
d2d0487356 | ||
|
|
30df4e5de2 | ||
|
|
3f4588c408 | ||
|
|
4e4ca366d9 | ||
|
|
040ff0cc9d | ||
|
|
b01ad4bb3b | ||
|
|
4476251b76 | ||
|
|
0f4cb1955f | ||
|
|
c161ee7b5e | ||
|
|
a5bb2548a6 | ||
|
|
2898a3ef8c | ||
|
|
a0568334b5 | ||
|
|
4005cfdff2 | ||
|
|
1d8c5e65b9 | ||
|
|
56f45e8b0f | ||
|
|
48a9584688 | ||
|
|
bfcd324993 | ||
|
|
a75235a260 | ||
|
|
e8de2def96 | ||
|
|
5ffc4c6498 | ||
|
|
d0c50248ea | ||
|
|
ca2a121696 | ||
|
|
02d06d6726 | ||
|
|
18bf76d49e | ||
|
|
6961845167 | ||
|
|
4c5f7f50f7 | ||
|
|
572e920466 | ||
|
|
b81fb3c865 | ||
|
|
814dd8a275 | ||
|
|
1619687d1d | ||
|
|
65fc61a507 | ||
|
|
d027f524ce | ||
|
|
df5e6e1563 | ||
|
|
5541e3c2ff | ||
|
|
57dfc379ae | ||
|
|
b5467b8e26 | ||
|
|
a9f1928913 | ||
|
|
ae16fed39b | ||
|
|
848a795d26 | ||
|
|
deb8c3dacd | ||
|
|
3cb572b47a | ||
|
|
df3846fbb8 | ||
|
|
d73ceb26ae | ||
|
|
da9ffc9b93 | ||
|
|
63fa0c4e94 | ||
|
|
ce91b6d9d9 | ||
|
|
c982f24faf | ||
|
|
9f865adb83 | ||
|
|
b54dd1cb48 | ||
|
|
ef621aa099 | ||
|
|
df4738f080 | ||
|
|
8d0a06325c | ||
|
|
f3329fcd34 | ||
|
|
af48547d8b | ||
|
|
f228f8368a | ||
|
|
9bb2c1a27b | ||
|
|
aa0b07a90d | ||
|
|
d4075bfa9a | ||
|
|
2b7455e353 | ||
|
|
f42d2c63a6 | ||
|
|
6be3ae9efc | ||
|
|
2fd2e07342 | ||
|
|
1e0e32e282 | ||
|
|
b2d8f24499 | ||
|
|
12c99741a0 | ||
|
|
91dbcf4df0 | ||
|
|
6ebd82a84c | ||
|
|
c07178f43a | ||
|
|
78a5957863 | ||
|
|
cd6882d21e | ||
|
|
926a2828ab | ||
|
|
daa7df96d1 | ||
|
|
84ca83ef69 | ||
|
|
ac9c85e783 | ||
|
|
331f6d4903 | ||
|
|
2e422e5ba8 | ||
|
|
20cffc728e | ||
|
|
b06cbc9465 | ||
|
|
1348327414 | ||
|
|
cd3eeb7a95 | ||
|
|
7be6b28011 | ||
|
|
ef1bee0f48 | ||
|
|
702da542dd | ||
|
|
5ba92d06cf | ||
|
|
34a7b42063 | ||
|
|
963d1b58f8 | ||
|
|
fe598bac8a | ||
|
|
45736d7f1d | ||
|
|
0fe4f5a686 | ||
|
|
e9ce5c02d0 | ||
|
|
5916d82580 | ||
|
|
2c83b9902e | ||
|
|
fe07825ad9 | ||
|
|
56e83d7d07 | ||
|
|
c7e3c92a0a | ||
|
|
1a643441f3 | ||
|
|
2aee746da9 | ||
|
|
e64b1ba67c | ||
|
|
31be4f041e | ||
|
|
1f5ebc408d | ||
|
|
b4d9a22df8 | ||
|
|
d4168e3b80 | ||
|
|
241c7af855 | ||
|
|
080fcd8fc1 | ||
|
|
1aad4c7ec3 | ||
|
|
e0e0582b53 | ||
|
|
0a6276b007 | ||
|
|
2a2256f7d9 | ||
|
|
ed81a60065 | ||
|
|
5ad55dd165 | ||
|
|
6860f7ffd6 | ||
|
|
33a3e3fb61 | ||
|
|
d4df3f67b6 | ||
|
|
f15a2f0c1f | ||
|
|
252288aff2 | ||
|
|
65a20a2407 | ||
|
|
5f180dbd9a | ||
|
|
ee4e9aa678 | ||
|
|
9f1c9529aa | ||
|
|
5bba1a0851 | ||
|
|
e9823eb40c | ||
|
|
14fc3f6ac4 | ||
|
|
74714f942a | ||
|
|
0d116e6ef0 | ||
|
|
06108d66b1 | ||
|
|
21eece4c6d | ||
|
|
4dd696e4e0 | ||
|
|
d04d4444af | ||
|
|
a487a78200 | ||
|
|
f06a72b418 | ||
|
|
3270c9a527 | ||
|
|
a7b7fb560a | ||
|
|
384eba6cd4 | ||
|
|
dc7e3ba1ec | ||
|
|
b1e0adcfdc | ||
|
|
2a5922ce92 | ||
|
|
93694925ce | ||
|
|
adfcbb004f | ||
|
|
6595898b73 | ||
|
|
81b33f1f6f | ||
|
|
a1c4ffec20 | ||
|
|
89854cca8b | ||
|
|
4fe5211ed7 | ||
|
|
795b32efd7 | ||
|
|
153064ef3a | ||
|
|
183a31c884 | ||
|
|
adf47bed89 | ||
|
|
cc297a8984 | ||
|
|
4ef17edace | ||
|
|
e56fa9d31f | ||
|
|
791d4b30fb | ||
|
|
a9a9f1ac00 | ||
|
|
fd6bc55db6 | ||
|
|
97d7329d14 | ||
|
|
609d42e2bc | ||
|
|
3463931dad | ||
|
|
f637bf2377 | ||
|
|
2271eb5e60 | ||
|
|
a5cbb552f0 | ||
|
|
e43ca5b031 | ||
|
|
cc7e668ad6 | ||
|
|
a01dbbdf04 | ||
|
|
e0ce0a67cc | ||
|
|
0d83a8866b | ||
|
|
f7a7e39a36 | ||
|
|
67cb466ea9 | ||
|
|
376688ab9f | ||
|
|
8668087d15 | ||
|
|
9b1c23987c | ||
|
|
784daa1a4b | ||
|
|
06f9b73a7b | ||
|
|
62c0272df2 | ||
|
|
7ce2b4c641 | ||
|
|
08c2de541f | ||
|
|
94551623ec | ||
|
|
cb0d23bae8 | ||
|
|
cb5157f2d1 | ||
|
|
32dcbbb5e1 | ||
|
|
4eedf0d36c | ||
|
|
be458303c5 | ||
|
|
78542ede14 | ||
|
|
df8800b7ff | ||
|
|
9c30cd8891 | ||
|
|
dda28efd66 | ||
|
|
b78183972c | ||
|
|
fc06196a3e | ||
|
|
2a1809a2d8 | ||
|
|
ac5c312267 | ||
|
|
8f3174038b | ||
|
|
2d8113e0e0 | ||
|
|
e881850bb4 | ||
|
|
4cbae1db89 | ||
|
|
97bce7be18 | ||
|
|
5d2a276172 | ||
|
|
762dfe8315 | ||
|
|
1c0d1ec8a9 | ||
|
|
e650699217 | ||
|
|
5ff2b5be1c | ||
|
|
f1841d7c81 | ||
|
|
fd6231a778 | ||
|
|
751a52153d | ||
|
|
61b733bae9 | ||
|
|
624e96df9b | ||
|
|
a3f1a44d83 | ||
|
|
ed1547938e | ||
|
|
f96a13e9e4 | ||
|
|
7b70ed96b6 | ||
|
|
9379853d47 | ||
|
|
6ccfebd9e3 | ||
|
|
45186a17ca | ||
|
|
24bb4585af | ||
|
|
feb96f1be8 | ||
|
|
b7d3d64975 | ||
|
|
6114b49630 | ||
|
|
87d95caae0 | ||
|
|
9535bc94ff | ||
|
|
0841be7803 | ||
|
|
6cec89181b | ||
|
|
cb78e17594 | ||
|
|
285c399ae5 | ||
|
|
c9b14f6f78 | ||
|
|
6a101c51d2 | ||
|
|
8998970adc | ||
|
|
991fbacb99 | ||
|
|
72f3e8cd40 | ||
|
|
cf8113ea24 | ||
|
|
5d1cf49fb6 | ||
|
|
90f3c0f9af | ||
|
|
d02598688a | ||
|
|
1991f1ccd7 | ||
|
|
8fad8972d5 | ||
|
|
fc3e78e226 | ||
|
|
15aace29a1 | ||
|
|
12a3a2d7fe | ||
|
|
7e26a340a3 | ||
|
|
f0764c24d2 | ||
|
|
d080bf3b9c | ||
|
|
51698dff07 | ||
|
|
4154926be7 | ||
|
|
806ca2b224 | ||
|
|
92bbf08343 | ||
|
|
dc84e3de67 | ||
|
|
8011801549 | ||
|
|
dfec88ed15 | ||
|
|
5d3e2c7111 | ||
|
|
af1c48e0f7 | ||
|
|
d4bc906c5c | ||
|
|
62fe467336 | ||
|
|
ef3e9d1c2e | ||
|
|
09ddbbf600 | ||
|
|
4195c239c8 | ||
|
|
a6c3c57d40 | ||
|
|
de9263a117 | ||
|
|
7b7f1ecc54 | ||
|
|
d1b028c27a | ||
|
|
055aae2382 | ||
|
|
dbb084da7a | ||
|
|
8332cd2f79 | ||
|
|
4e29c0c16b | ||
|
|
320d45a3f5 | ||
|
|
73795b5257 | ||
|
|
16e59784c6 | ||
|
|
65a7ec3f85 | ||
|
|
360ff35061 | ||
|
|
5098792723 | ||
|
|
a4883e6d41 | ||
|
|
44c9d66dd4 | ||
|
|
c7718d078c | ||
|
|
2525994db4 | ||
|
|
81e6afa9b3 | ||
|
|
d8f5ccf9e4 | ||
|
|
774489c578 | ||
|
|
39bb0ecfcf | ||
|
|
b222ab1bf6 | ||
|
|
7a31c7ff50 | ||
|
|
132f87d152 | ||
|
|
aedda4bf9b | ||
|
|
40bba70bfe | ||
|
|
8202247b27 | ||
|
|
e2117ee8a4 | ||
|
|
e3a4150fea | ||
|
|
0ce2a23af3 | ||
|
|
6b2337c9c0 | ||
|
|
54661b7fc4 | ||
|
|
a1f895b781 | ||
|
|
199c271edc | ||
|
|
9ac744ae63 | ||
|
|
93b05bcd1e | ||
|
|
546cdbefae | ||
|
|
352c07987b | ||
|
|
f6a1eb19d7 | ||
|
|
a352804c14 | ||
|
|
28ae8c4d20 | ||
|
|
1eef584382 | ||
|
|
4e1cd0590b | ||
|
|
fa670e1f5d | ||
|
|
c7fa6f620a | ||
|
|
04690c8c7c | ||
|
|
4345ea6602 | ||
|
|
d39c212336 | ||
|
|
54d2f06abe | ||
|
|
237bfc9a6e | ||
|
|
29b3d49cd5 | ||
|
|
fe8892ec54 | ||
|
|
2d8dd7a53e | ||
|
|
b493599690 | ||
|
|
d9371dc265 | ||
|
|
ef2a12fbdf | ||
|
|
7215c6d6be | ||
|
|
bd9e1f58fe | ||
|
|
79dec269e1 | ||
|
|
d6ffbf4f3c | ||
|
|
32588939ae | ||
|
|
a02e07bfe8 | ||
|
|
03df0c3cca | ||
|
|
1aa50b73a1 | ||
|
|
c910f483bf | ||
|
|
14f4a8d28e | ||
|
|
ee931a17b0 | ||
|
|
0a2ba3fb66 | ||
|
|
512db660a1 | ||
|
|
b0666c9180 | ||
|
|
57d3d09679 | ||
|
|
3a17f54bca | ||
|
|
4c2800b294 | ||
|
|
8358377ec3 | ||
|
|
3ab12c616c | ||
|
|
ef4e784121 | ||
|
|
1ead20a5b9 | ||
|
|
31dfee8a95 | ||
|
|
d6492264c7 | ||
|
|
7281b9dbb1 | ||
|
|
4b63ee036e | ||
|
|
fb8eb69c20 | ||
|
|
4b1fa2609d | ||
|
|
a8b756f040 | ||
|
|
a94a0f3026 | ||
|
|
71c902eaf8 | ||
|
|
6e7af8ce2e | ||
|
|
6c80cd040c | ||
|
|
4ed413eaa7 | ||
|
|
86357d2c38 | ||
|
|
c0d4d01b88 | ||
|
|
b3fcb53625 | ||
|
|
4f29dea8ef | ||
|
|
4f8ef05058 | ||
|
|
7fc1735a21 | ||
|
|
1e49038747 | ||
|
|
295fbe42ad | ||
|
|
60d33f2847 | ||
|
|
cd369f633a | ||
|
|
c3df6d2fe3 | ||
|
|
72dfa74b3e | ||
|
|
1411269187 | ||
|
|
185d959f25 | ||
|
|
1652d41992 | ||
|
|
297dea881d | ||
|
|
01df828353 | ||
|
|
617115d447 | ||
|
|
31f9978599 | ||
|
|
dc068c16e8 | ||
|
|
89626a462f | ||
|
|
a043588b4a | ||
|
|
2e08ee16c2 | ||
|
|
2cc506476b | ||
|
|
0a462ad706 | ||
|
|
81c1bfe1c5 | ||
|
|
8ce80e39d9 | ||
|
|
fa823a37ae | ||
|
|
17f67fbe28 | ||
|
|
560b78bc35 | ||
|
|
f023f047ff | ||
|
|
5673359be2 | ||
|
|
666696d142 | ||
|
|
f95bc48f9b | ||
|
|
8d18fc09fa | ||
|
|
a3d42a90f2 | ||
|
|
1da5f2ecc6 | ||
|
|
d226e82f68 | ||
|
|
60cc2cb26c | ||
|
|
1ac90cf464 | ||
|
|
d3ffaebd17 | ||
|
|
bf01c549f5 | ||
|
|
6912d8c086 | ||
|
|
a89e1b5573 | ||
|
|
3a9c6088b2 | ||
|
|
fba629f385 | ||
|
|
90477825c2 | ||
|
|
1e56d39777 | ||
|
|
2829971412 | ||
|
|
c147fda6ca | ||
|
|
a27dfd8b7c | ||
|
|
a7c65efc45 | ||
|
|
646a524c8e | ||
|
|
f8d01d2c4f | ||
|
|
1d846f3744 | ||
|
|
6bbe58f876 | ||
|
|
36719fb086 | ||
|
|
78fd7a8458 | ||
|
|
f5976d0d63 | ||
|
|
b313fc3fbe | ||
|
|
cf667b8c47 | ||
|
|
7e3bd8586f | ||
|
|
d5df301317 | ||
|
|
628246c3da | ||
|
|
d228acd30a | ||
|
|
18cd97d914 | ||
|
|
5f13946bac | ||
|
|
8fb24fbc1e | ||
|
|
f0d4952055 | ||
|
|
7113580db2 | ||
|
|
163c211219 | ||
|
|
6993157b3a | ||
|
|
e2efdf674d | ||
|
|
9fcb8e5b6e | ||
|
|
a1a637c7f1 | ||
|
|
4f87f3e9f4 | ||
|
|
da6faa656c | ||
|
|
e1ad75e932 | ||
|
|
646bdbd002 | ||
|
|
9184de1997 | ||
|
|
eabc29ec4d | ||
|
|
e338cdab31 | ||
|
|
7001d609fa | ||
|
|
c8399ce3d2 | ||
|
|
3e1d701dce | ||
|
|
a385c39c2e | ||
|
|
5d1cf339a7 | ||
|
|
fafb8518e0 | ||
|
|
6807f4d4ea | ||
|
|
0ed7b2f3c0 | ||
|
|
116128ebb5 | ||
|
|
0c870c5fcd | ||
|
|
90bc3ab770 | ||
|
|
0021b0c242 | ||
|
|
6dc076f5d1 | ||
|
|
2dd1201c3f | ||
|
|
9be68966b4 | ||
|
|
47d482dca9 | ||
|
|
18dd13c872 | ||
|
|
c71bb877c3 | ||
|
|
eee8106f9e | ||
|
|
80ad596d3d | ||
|
|
9dbafcc368 | ||
|
|
9f5f113840 | ||
|
|
6afbdbe94f | ||
|
|
061a0c8cb6 |
8
.github/workflows/ci.py
vendored
8
.github/workflows/ci.py
vendored
@@ -31,7 +31,11 @@ def install_deps():
|
||||
sys.stdout.flush()
|
||||
if is_macos:
|
||||
items = (x.split()[1].strip('"') for x in open('Brewfile').readlines() if x.strip().startswith('brew '))
|
||||
run('brew', 'install', 'fish', *items)
|
||||
openssl = 'openssl'
|
||||
import ssl
|
||||
if ssl.OPENSSL_VERSION_INFO[0] == 1:
|
||||
openssl += '@1.1'
|
||||
run('brew', 'install', 'fish', openssl, *items)
|
||||
else:
|
||||
run('sudo apt-get update')
|
||||
run('sudo apt-get install -y libgl1-mesa-dev libxi-dev libxrandr-dev libxinerama-dev ca-certificates'
|
||||
@@ -59,7 +63,7 @@ def build_kitty():
|
||||
|
||||
|
||||
def test_kitty():
|
||||
run('./kitty/launcher/kitty +launch test.py')
|
||||
run('./test.py')
|
||||
|
||||
|
||||
def package_kitty():
|
||||
|
||||
18
.github/workflows/ci.yml
vendored
18
.github/workflows/ci.yml
vendored
@@ -6,6 +6,9 @@ env:
|
||||
LC_ALL: en_US.UTF-8
|
||||
LANG: en_US.UTF-8
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Linux (python=${{ matrix.pyver }} cc=${{ matrix.cc }} sanitize=${{ matrix.sanitize }})
|
||||
@@ -73,7 +76,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install build-only deps
|
||||
run: pip install -r docs/requirements.txt flake8 mypy types-requests types-docutils
|
||||
@@ -84,8 +87,11 @@ jobs:
|
||||
- name: Build kitty package
|
||||
run: python .github/workflows/ci.py package
|
||||
|
||||
- name: Build kitty
|
||||
run: python setup.py build --debug
|
||||
|
||||
- name: Run mypy
|
||||
run: python test.py mypy
|
||||
run: ./test.py mypy
|
||||
|
||||
- name: Build man page
|
||||
run: make FAIL_WARN=1 man
|
||||
@@ -120,7 +126,7 @@ jobs:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@master
|
||||
with:
|
||||
fetch-depth: 10
|
||||
fetch-depth: 0 # needed for :commit: docs role
|
||||
|
||||
- name: Build kitty
|
||||
run: python3 .github/workflows/ci.py build
|
||||
@@ -128,5 +134,11 @@ jobs:
|
||||
- name: Test kitty
|
||||
run: python3 .github/workflows/ci.py test
|
||||
|
||||
- name: Install deps for docs
|
||||
run: python3 -m pip install -r docs/requirements.txt
|
||||
|
||||
- name: Builds docs
|
||||
run: make FAIL_WARN=1 docs
|
||||
|
||||
- name: Build kitty package
|
||||
run: python3 .github/workflows/ci.py package
|
||||
|
||||
7
.github/workflows/codeql-analysis.yml
vendored
7
.github/workflows/codeql-analysis.yml
vendored
@@ -9,9 +9,16 @@ on:
|
||||
schedule:
|
||||
- cron: '0 22 * * 5'
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
security-events: write # to upload SARIF results (github/codeql-action/analyze)
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,6 +2,8 @@
|
||||
*.pyc
|
||||
*.pyo
|
||||
*_stub.pyi
|
||||
*_generated.go
|
||||
*_generated.h
|
||||
/.dmypy.json
|
||||
/tags
|
||||
/build/
|
||||
@@ -10,7 +12,7 @@
|
||||
/compile_commands.json
|
||||
/link_commands.json
|
||||
/glad/out/
|
||||
/kitty/launcher/
|
||||
/kitty/launcher/kitty*
|
||||
/*.dSYM/
|
||||
__pycache__/
|
||||
/glfw/wayland-*-client-protocol.[ch]
|
||||
|
||||
6
Makefile
6
Makefile
@@ -35,6 +35,12 @@ profile:
|
||||
app:
|
||||
python3 setup.py kitty.app $(VVAL)
|
||||
|
||||
linux-package: FORCE
|
||||
rm -rf linux-package
|
||||
python3 setup.py linux-package
|
||||
|
||||
FORCE:
|
||||
|
||||
man:
|
||||
$(MAKE) -C docs man
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@ def copy_python(env):
|
||||
iv['sanitize_source_folder'](os.path.join(env.py_dir, x))
|
||||
py_compile(env.py_dir)
|
||||
freeze_python(env.py_dir, pdir, env.obj_dir, ext_map, develop_mode_env_var='KITTY_DEVELOP_FROM', remove_pyc_files=True)
|
||||
shutil.rmtree(env.py_dir)
|
||||
|
||||
|
||||
def build_launcher(env):
|
||||
|
||||
@@ -369,6 +369,7 @@ class Freeze(object):
|
||||
iv['sanitize_source_folder'](join(self.python_stdlib, x))
|
||||
self.compile_py_modules()
|
||||
freeze_python(self.python_stdlib, pdir, self.obj_dir, ext_map, develop_mode_env_var='KITTY_DEVELOP_FROM', remove_pyc_files=True)
|
||||
shutil.rmtree(self.python_stdlib)
|
||||
iv['build_frozen_launcher']([path_to_freeze_dir(), self.obj_dir])
|
||||
os.rename(join(dirname(self.contents_dir), 'bin', 'kitty'), join(self.contents_dir, 'MacOS', 'kitty'))
|
||||
shutil.rmtree(join(dirname(self.contents_dir), 'bin'))
|
||||
|
||||
2
docs/_static/custom.css
vendored
2
docs/_static/custom.css
vendored
@@ -10,7 +10,7 @@
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
max-height: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
|
||||
.major-features li {
|
||||
|
||||
18
docs/_templates/base.html
vendored
18
docs/_templates/base.html
vendored
@@ -4,18 +4,14 @@
|
||||
{{ super() }}
|
||||
|
||||
{%- if analytics_id %}
|
||||
<script type="text/javascript">
|
||||
var _gaq = _gaq || [];
|
||||
_gaq.push(['_setAccount', '{{ analytics_id }}']);
|
||||
_gaq.push(['_setDomainName', 'none']);
|
||||
_gaq.push(['_setAllowLinker', true]);
|
||||
_gaq.push(['_trackPageview']);
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id={{ analytics_id }}"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
(function() {
|
||||
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
gtag('config', '{{ analytics_id }}');
|
||||
</script>
|
||||
{% endif -%}
|
||||
|
||||
|
||||
@@ -123,6 +123,7 @@ The full list of actions that can be mapped to key presses is available
|
||||
================================== =======================
|
||||
Action Shortcut
|
||||
================================== =======================
|
||||
Show this help :sc:`show_kitty_doc`
|
||||
Copy to clipboard :sc:`copy_to_clipboard` (also :kbd:`⌘+c` on macOS)
|
||||
Paste from clipboard :sc:`paste_from_clipboard` (also :kbd:`⌘+v` on macOS)
|
||||
Paste from selection :sc:`paste_from_selection`
|
||||
|
||||
@@ -85,7 +85,7 @@ Customizing the installation
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
_kitty_install_cmd \\
|
||||
_kitty_install_cmd \
|
||||
installer=nightly
|
||||
|
||||
If you want to install it in parallel to the released kitty specify a
|
||||
@@ -93,14 +93,14 @@ Customizing the installation
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
_kitty_install_cmd \\
|
||||
_kitty_install_cmd \
|
||||
installer=nightly dest=/some/other/location
|
||||
|
||||
* You can specify a different install location, with ``dest``:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
_kitty_install_cmd \\
|
||||
_kitty_install_cmd \
|
||||
dest=/some/other/location
|
||||
|
||||
* You can tell the installer not to launch |kitty| after installing it with
|
||||
@@ -108,14 +108,14 @@ Customizing the installation
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
_kitty_install_cmd \\
|
||||
_kitty_install_cmd \
|
||||
launch=n
|
||||
|
||||
* You can use a previously downloaded dmg/tarball, with ``installer``:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
_kitty_install_cmd \\
|
||||
_kitty_install_cmd \
|
||||
installer=/path/to/dmg or tarball
|
||||
|
||||
|
||||
|
||||
@@ -29,12 +29,13 @@ Dependencies
|
||||
|
||||
Run-time dependencies:
|
||||
|
||||
* ``python`` >= 3.6
|
||||
* ``python`` >= 3.8
|
||||
* ``harfbuzz`` >= 2.2.0
|
||||
* ``zlib``
|
||||
* ``libpng``
|
||||
* ``liblcms2``
|
||||
* ``librsync``
|
||||
* ``openssl``
|
||||
* ``freetype`` (not needed on macOS)
|
||||
* ``fontconfig`` (not needed on macOS)
|
||||
* ``libcanberra`` (not needed on macOS)
|
||||
@@ -82,14 +83,21 @@ You can run |kitty|, as::
|
||||
If that works, you can create a symlink to the launcher in :file:`~/bin` or some
|
||||
other directory on your PATH so that you can run |kitty| using just ``kitty``.
|
||||
|
||||
To have the kitty documentation available locally, run::
|
||||
|
||||
python3 -m pip install -r docs/requirements.txt && make docs
|
||||
|
||||
|
||||
Building kitty.app on macOS from source
|
||||
-------------------------------------------
|
||||
|
||||
Run::
|
||||
|
||||
python3 -m pip install -r docs/requirements.txt && make docs
|
||||
make app
|
||||
|
||||
Building the docs needs to be done only once.
|
||||
|
||||
This :file:`kitty.app` unlike the released one does not include its own copy of
|
||||
Python and the other dependencies. So if you ever un-install/upgrade those
|
||||
dependencies you might have to rebuild the app.
|
||||
@@ -152,7 +160,7 @@ While |kitty| does use Python, it is not a traditional Python package, so please
|
||||
do not install it in site-packages.
|
||||
Instead run::
|
||||
|
||||
python3 setup.py linux-package
|
||||
make linux-package
|
||||
|
||||
This will install |kitty| into the directory :file:`linux-package`. You can run
|
||||
|kitty| with :file:`linux-package/bin/kitty`. All the files needed to run kitty
|
||||
@@ -175,8 +183,11 @@ You should probably split |kitty| into three packages:
|
||||
:code:`kitty`
|
||||
Installs the main program
|
||||
|
||||
This allows users to install the terminfo and shell integration files on servers
|
||||
into which they ssh, without needing to install all of |kitty|.
|
||||
This allows users to install the terminfo and shell integration files on
|
||||
servers into which they ssh, without needing to install all of |kitty|. The
|
||||
shell integration files **must** still be present in
|
||||
:file:`lib/kitty/shell-integration` when installing the kitty main package as
|
||||
the kitty program expects to find them there.
|
||||
|
||||
.. note::
|
||||
You need a couple of extra dependencies to build linux-package. :file:`tic`
|
||||
|
||||
@@ -35,6 +35,164 @@ mouse anywhere in the current command to move the cursor there. See
|
||||
Detailed list of changes
|
||||
-------------------------------------
|
||||
|
||||
0.26.4 [2022-10-17]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- macOS: Allow changing the kitty icon by placing a custom icon in the kitty config folder (:pull:`5464`)
|
||||
|
||||
- Allow centering the :opt:`background_image` (:iss:`5525`)
|
||||
|
||||
- X11: Fix a regression in the previous release that caused pasting from GTK based applications to have extra newlines (:iss:`5528`)
|
||||
|
||||
- Tab bar: Improve empty space management when some tabs have short titles, allocate the saved space to the active tab (:iss:`5548`)
|
||||
|
||||
- Fix :opt:`background_tint` not applying to window margins and padding (:iss:`3933`)
|
||||
|
||||
- Wayland: Fix background image scaling using tiled mode on high DPI screens
|
||||
|
||||
- Wayland: Fix an abort when changing background colors with :opt:`wayland_titlebar_color` set to ``background`` (:iss:`5562`)
|
||||
|
||||
- Update to Unicode 15.0 (:pull:`5542`)
|
||||
|
||||
- GNOME Wayland: Fix a memory leak in gnome-shell when using client side decorations
|
||||
|
||||
|
||||
0.26.3 [2022-09-22]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Wayland: Mark windows in which a bell occurs as urgent on compositors that support the xdg-activation protocol
|
||||
|
||||
- Allow passing null bytes through the system clipboard (:iss:`5483`)
|
||||
|
||||
- ssh kitten: Fix :envvar:`KITTY_PUBLIC_KEY` not being encoded properly when transmitting (:iss:`5496`)
|
||||
|
||||
- Sessions: Allow controlling which OS Window is active via the ``focus_os_window`` directive
|
||||
|
||||
- Wayland: Fix for bug in NVIDIA drivers that prevents transparency working (:iss:`5479`)
|
||||
|
||||
- Wayland: Fix for a bug that could cause kitty to become non-responsive when
|
||||
using multiple OS windows in a single instance on some compositors (:iss:`5495`)
|
||||
|
||||
- Wayland: Fix for a bug preventing kitty from starting on Hyprland when using a non-unit scale (:iss:`5467`)
|
||||
|
||||
- Wayland: Generate a XDG_ACTIVATION_TOKEN when opening URLs or running programs in the background via the launch action
|
||||
|
||||
- Fix a regression that caused kitty not to restore SIGPIPE after python nukes it when launching children. Affects bash which does not sanitize its signal mask. (:iss:`5500`)
|
||||
|
||||
- Fix a use-after-free when handling fake mouse clicks and the action causes windows to be removed/re-allocated (:iss:`5506`)
|
||||
|
||||
|
||||
0.26.2 [2022-09-05]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Allow creating :code:`overlay-main` windows, which are treated as the active window unlike normal overlays (:iss:`5392`)
|
||||
|
||||
- hints kitten: Allow using :doc:`launch` as the program to run, to open the result in a new kitty tab/window/etc. (:iss:`5462`)
|
||||
|
||||
- hyperlinked_grep kitten: Allow control over which parts of ``rg`` output are hyperlinked (:pull:`5428`)
|
||||
|
||||
- Fix regression in 0.26.0 that caused launching kitty without working STDIO handles to result in high CPU usage and prewarming failing (:iss:`5444`)
|
||||
|
||||
- :doc:`/launch`: Allow setting the margin and padding for newly created windows (:iss:`5463`)
|
||||
|
||||
- macOS: Fix regression in 0.26.0 that caused asking the user for a line of input such as for :ac:`set_tab_title` to not work (:iss:`5447`)
|
||||
|
||||
- hints kitten: hyperlink matching: Fix hints occasionally matching text on subsequent line as part of hyperlink (:pull:`5450`)
|
||||
|
||||
- Fix a regression in 0.26.0 that broke mapping of native keys whose key codes did not fit in 21 bits (:iss:`5452`)
|
||||
|
||||
- Wayland: Fix remembering window size not accurate when client side decorations are present
|
||||
|
||||
- Fix an issue where notification identifiers were not sanitized leading to
|
||||
code execution if the user clicked on a notification popup from a malicious
|
||||
source. Thanks to Carter Sande for discovering this vulnerability.
|
||||
|
||||
0.26.1 [2022-08-30]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- ssh kitten: Fix executable permission missing from kitty bootstrap script (:iss:`5438`)
|
||||
|
||||
- Fix a regression in 0.26.0 that caused kitty to no longer set the ``LANG`` environment variable on macOS (:iss:`5439`)
|
||||
|
||||
- Allow specifying a title when using the :ac:`set_tab_title` action (:iss:`5441`)
|
||||
|
||||
0.26.0 [2022-08-29]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- A new option :opt:`remote_control_password` to use fine grained permissions for what can be remote controlled (:disc:`5320`)
|
||||
|
||||
- Reduce startup latency by ~30 milliseconds when running kittens via key bindings inside kitty (:iss:`5159`)
|
||||
|
||||
- A new option :opt:`modify_font` to adjust various font metrics like underlines, cell sizes etc. (:pull:`5265`)
|
||||
|
||||
- A new shortcut :sc:`show_kitty_doc` to display the kitty docs in a browser
|
||||
|
||||
- Graphics protocol: Only delete temp files if they have the string
|
||||
:code:`tty-graphics-protocol` in their file paths. This prevents deletion of arbitrary files in :file:`/tmp`.
|
||||
|
||||
- Deprecate the ``adjust_baseline``, ``adjust_line_height`` and ``adjust_column_width`` options in favor of :opt:`modify_font`
|
||||
|
||||
- Wayland: Fix a regression in the previous release that caused mouse cursor
|
||||
animation and keyboard repeat to stop working when switching seats (:iss:`5188`)
|
||||
|
||||
- Allow resizing windows created in session files (:pull:`5196`)
|
||||
|
||||
- Fix horizontal wheel events not being reported to client programs when they grab the mouse (:iss:`2819`)
|
||||
|
||||
- macOS: Remote control: Fix unable to launch a new OS window or background process when there is no OS window (:iss:`5210`)
|
||||
|
||||
- macOS: Fix unable to open new tab or new window when there is no OS window (:iss:`5276`)
|
||||
|
||||
- kitty @ set-colors: Fix changing inactive_tab_foreground not working (:iss:`5214`)
|
||||
|
||||
- macOS: Fix a regression that caused switching keyboard input using Eisu and
|
||||
Kana keys not working (:iss:`5232`)
|
||||
|
||||
- Add a mappable action to toggle the mirrored setting for the tall and fat
|
||||
layouts (:pull:`5344`)
|
||||
|
||||
- Add a mappable action to switch between predefined bias values for the tall and fat
|
||||
layouts (:pull:`5352`)
|
||||
|
||||
- Wayland: Reduce flicker at startup by not using render frames immediately after a resize (:iss:`5235`)
|
||||
|
||||
- Linux: Update cursor position after all key presses not just pre-edit text
|
||||
changes (:iss:`5241`)
|
||||
|
||||
- ssh kitten: Allow ssh kitten to work from inside tmux, provided the tmux
|
||||
session inherits the correct KITTY env vars (:iss:`5227`)
|
||||
|
||||
- ssh kitten: A new option :code:`--symlink-strategy` to control how symlinks
|
||||
are copied to the remote machine (:iss:`5249`)
|
||||
|
||||
- ssh kitten: Allow pressing :kbd:`Ctrl+C` to abort ssh before the connection is
|
||||
completed (:iss:`5271`)
|
||||
|
||||
- Bash integration: Fix declare not creating global variables in .bashrc (:iss:`5254`)
|
||||
|
||||
- Bash integration: Fix the inherit_errexit option being set by shell integration (:iss:`5349`)
|
||||
|
||||
- :command:`kitty @ scroll-window` allow scrolling by fractions of a screen
|
||||
(:iss:`5294`)
|
||||
|
||||
- remote files kitten: Fix working with files whose names have characters that
|
||||
need to be quoted in shell scripts (:iss:`5313`)
|
||||
|
||||
- Expand ~ in paths configured in :opt:`editor` and :opt:`exe_search_path` (:disc:`5298`)
|
||||
|
||||
- Allow showing the working directory of the active window in tab titles
|
||||
(:pull:`5314`)
|
||||
|
||||
- ssh kitten: Allow completion of ssh options between the destination and command (:iss:`5322`)
|
||||
|
||||
- macOS: Fix speaking selected text not working (:iss:`5357`)
|
||||
|
||||
- Allow ignoring failure to close windows/tabs via rc commands (:disc:`5406`)
|
||||
|
||||
- Fix hyperlinks not present when fetching text from the history buffer
|
||||
(:iss:`5427`)
|
||||
|
||||
|
||||
0.25.2 [2022-06-07]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -687,7 +845,7 @@ Detailed list of changes
|
||||
0.21.2 [2021-06-28]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- A new :opt:`adjust_baseline` option to adjust the vertical alignment of text
|
||||
- A new ``adjust_baseline`` option to adjust the vertical alignment of text
|
||||
inside a line (:pull:`3734`)
|
||||
|
||||
- A new :opt:`url_excluded_characters` option to exclude additional characters
|
||||
@@ -861,7 +1019,7 @@ Detailed list of changes
|
||||
scrollback buffer onto the screen. Useful, for example, to restore
|
||||
the screen after showing completions below the shell prompt.
|
||||
|
||||
- A new remote control command :ref:`at_env` to change the default
|
||||
- A new remote control command :ref:`at-env` to change the default
|
||||
environment passed to newly created windows (:iss:`3529`)
|
||||
|
||||
- Linux: Fix binary kitty builds not able to load fonts in WOFF2 format
|
||||
@@ -886,7 +1044,7 @@ Detailed list of changes
|
||||
kitty window title bar
|
||||
|
||||
- Fix reading :option:`kitty --session` from ``STDIN`` not working when the
|
||||
:option:`kitty --detach` option is used (:iss:`3523`)
|
||||
:code:`kitty --detach` option is used (:iss:`3523`)
|
||||
|
||||
- Special case rendering of the few remaining Powerline box drawing chars
|
||||
(:iss:`3535`)
|
||||
@@ -2029,7 +2187,7 @@ Detailed list of changes
|
||||
- Allow the user to control the resize debounce time via
|
||||
:opt:`resize_debounce_time`.
|
||||
|
||||
- Remote control: Make the :ref:`at_set-font-size` command more capable.
|
||||
- Remote control: Make the :ref:`at-set-font-size` command more capable.
|
||||
It can now increment font size and reset it. It also only acts on the
|
||||
active top-level window, by default (:iss:`1581`)
|
||||
|
||||
@@ -2338,7 +2496,7 @@ Detailed list of changes
|
||||
|
||||
- Fix a regression in 0.12.0 that broke dynamic resizing of layouts (:iss:`860`)
|
||||
|
||||
- Wayland: Allow using the :option:`kitty --class` command line flag
|
||||
- Wayland: Allow using the :code:`kitty --class` command line flag
|
||||
to set the app id (:iss:`862`)
|
||||
|
||||
- Add completion of the kitty command for the fish shell (:pull:`829`)
|
||||
@@ -2381,7 +2539,7 @@ Detailed list of changes
|
||||
window is not the active window (:iss:`729`)
|
||||
|
||||
- Remote control: Allow changing the current window layout with a new
|
||||
:ref:`at_goto-layout` command (:iss:`845`)
|
||||
:ref:`at-goto-layout` command (:iss:`845`)
|
||||
|
||||
- Remote control: Allow matching windows by the environment variables of their
|
||||
child process as well
|
||||
|
||||
21
docs/conf.py
21
docs/conf.py
@@ -120,10 +120,23 @@ html_title = 'kitty'
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
github_icon_path = 'M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z' # noqa
|
||||
|
||||
html_theme_options: Dict[str, Any] = {
|
||||
'sidebar_hide_name': True,
|
||||
'navigation_with_keys': True,
|
||||
'footer_icons': [
|
||||
{
|
||||
"name": "GitHub",
|
||||
"url": "https://github.com/kovidgoyal/kitty",
|
||||
"html": f"""
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="{github_icon_path}"></path>
|
||||
</svg>
|
||||
""",
|
||||
"class": "",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -236,7 +249,7 @@ if you specify a program-to-run you can use the special placeholder
|
||||
from kitty.rc.base import cli_params_for
|
||||
for cmd_name in sorted(all_command_names()):
|
||||
func = command_for_name(cmd_name)
|
||||
p(f'.. _at_{func.name}:\n')
|
||||
p(f'.. _at-{func.name}:\n')
|
||||
p('kitty @', func.name)
|
||||
p('-' * 120)
|
||||
p('.. program::', 'kitty @', func.name)
|
||||
@@ -264,7 +277,7 @@ def write_remote_control_protocol_docs() -> None: # {{{
|
||||
from kitty.rc.base import (
|
||||
RemoteCommand, all_command_names, command_for_name
|
||||
)
|
||||
field_pat = re.compile(r'\s*([a-zA-Z0-9_+]+)\s*:\s*(.+)')
|
||||
field_pat = re.compile(r'\s*([a-zA-Z0-9_+/]+)\s*:\s*(.+)')
|
||||
|
||||
def format_cmd(p: Callable[..., None], name: str, cmd: RemoteCommand) -> None:
|
||||
p(name)
|
||||
@@ -276,7 +289,7 @@ def write_remote_control_protocol_docs() -> None: # {{{
|
||||
if m is None:
|
||||
p(line)
|
||||
else:
|
||||
fields.append((m.group(1), m.group(2)))
|
||||
fields.append((m.group(1).split('/')[0], m.group(2)))
|
||||
if fields:
|
||||
p('\nFields are:\n')
|
||||
for (name, desc) in fields:
|
||||
|
||||
54
docs/extract-rst-targets.py
Executable file
54
docs/extract-rst-targets.py
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import os
|
||||
import re
|
||||
from typing import Dict, Iterator
|
||||
|
||||
tgt_pat = re.compile(r'^.. _(\S+?):$', re.MULTILINE)
|
||||
title_pat = re.compile('^(.+)\n[-=^#*]{5,}$', re.MULTILINE)
|
||||
|
||||
|
||||
def find_explicit_targets(text: str) -> Iterator[str]:
|
||||
for m in tgt_pat.finditer(text):
|
||||
yield m.group(1)
|
||||
|
||||
|
||||
def find_page_title(text: str) -> str:
|
||||
for m in title_pat.finditer(text):
|
||||
return m.group(1)
|
||||
return ''
|
||||
|
||||
|
||||
def main() -> Dict[str, Dict[str, str]]:
|
||||
refs = {}
|
||||
docs = {}
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
for dirpath, dirnames, filenames in os.walk(base):
|
||||
if 'generated' in dirnames:
|
||||
dirnames.remove('generated')
|
||||
for f in filenames:
|
||||
if f.endswith('.rst'):
|
||||
with open(os.path.join(dirpath, f)) as stream:
|
||||
raw = stream.read()
|
||||
href = os.path.relpath(stream.name, base).replace(os.sep, '/')
|
||||
href = href.rpartition('.')[0] + '/'
|
||||
docs[href.rstrip('/')] = find_page_title(raw)
|
||||
first_line = raw.lstrip('\n').partition('\n')[0]
|
||||
first_target_added = False
|
||||
for explicit_target in find_explicit_targets(raw):
|
||||
# Shorten the reference link to the top of the page.
|
||||
# Note that anchor links should still be used in HTML docs
|
||||
# to allow jumping within the same page.
|
||||
if not first_target_added:
|
||||
first_target_added = True
|
||||
if first_line.startswith(f'.. _{explicit_target}:'):
|
||||
refs[explicit_target] = href
|
||||
continue
|
||||
refs[explicit_target] = href + f'#{explicit_target.replace("_", "-")}'
|
||||
return {'ref': refs, 'doc': docs}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import json
|
||||
print(json.dumps(main(), indent=2))
|
||||
49
docs/faq.rst
49
docs/faq.rst
@@ -132,8 +132,8 @@ You can also 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`.
|
||||
:ref:`at-set-colors`. The shortcut mapping technique has the same syntax as the
|
||||
remote control command, for details, see :ref:`at-set-colors`.
|
||||
|
||||
To change colors when SSHing into a remote host, use the :opt:`color_scheme
|
||||
<kitten-ssh.color_scheme>` setting for the :doc:`ssh kitten <kittens/ssh>`.
|
||||
@@ -205,13 +205,15 @@ fonts to be freely resizable, so it does not support bitmapped fonts.
|
||||
.. note::
|
||||
If you are trying to use a font patched with `Nerd Fonts
|
||||
<https://nerdfonts.com/>`__ symbols, don't do that as patching destroys
|
||||
fonts. There is no need, simply install the standalone ``Symbols Nerd Font``
|
||||
fonts. There is no need, simply install the standalone ``Symbols Nerd Font Mono``
|
||||
(the file :file:`NerdFontsSymbolsOnly.zip` from the `Nerd Fonts releases page
|
||||
<https://github.com/ryanoasis/nerd-fonts/releases>`__). kitty should pick up
|
||||
symbols from it automatically, and you can tell it to do so explicitly in
|
||||
case it doesn't with the :opt:`symbol_map` directive::
|
||||
|
||||
symbol_map U+23FB-U+23FE,U+2665,U+26A1,U+2B58,U+E000-U+E00A,U+E0A0-U+E0A3,U+E0B0-U+E0C8,U+E0CA,U+E0CC-U+E0D2,U+E0D4,U+E200-U+E2A9,U+E300-U+E3E3,U+E5FA-U+E62F,U+E700-U+E7C5,U+F000-U+F2E0,U+F300-U+F31C,U+F400-U+F4A9,U+F500-U+F8FF Symbols Nerd Font
|
||||
# Nerd Fonts v2.2.2
|
||||
|
||||
symbol_map U+23FB-U+23FE,U+2665,U+26A1,U+2B58,U+E000-U+E00A,U+E0A0-U+E0A3,U+E0B0-U+E0C8,U+E0CA,U+E0CC-U+E0D2,U+E0D4,U+E200-U+E2A9,U+E300-U+E3E3,U+E5FA-U+E634,U+E700-U+E7C5,U+EA60-U+EBEB,U+F000-U+F2E0,U+F300-U+F32F,U+F400-U+F4A9,U+F500-U+F8FF Symbols Nerd Font Mono
|
||||
|
||||
Those Unicode symbols beyond the ``E000-F8FF`` Unicode private use area are
|
||||
not included.
|
||||
@@ -283,13 +285,40 @@ homepage:
|
||||
:target: https://github.com/igrmk/whiskers
|
||||
:width: 256
|
||||
|
||||
On macOS you can change the icon by following the steps:
|
||||
.. image:: https://github.com/samholmes/whiskers/raw/main/whiskers.png
|
||||
:target: https://github.com/samholmes/whiskers
|
||||
:width: 256
|
||||
|
||||
On macOS you can put :file:`kitty.app.icns` or :file:`kitty.app.png` in the
|
||||
:ref:`kitty configuration directory <confloc>`, and this icon will be applied
|
||||
automatically at startup. Unfortunately, Apple's Dock does not change its
|
||||
cached icon so the custom icon will revert when kitty is quit. Run the
|
||||
following to force the Dock to update its cached icons:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
rm /var/folders/*/*/*/com.apple.dock.iconcache; killall Dock
|
||||
|
||||
If you prefer not to keep a custom icon in the kitty config folder, you can
|
||||
also set it with the following command:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
# Set kitty.icns as the icon for currently running kitty
|
||||
kitty +runpy 'from kitty.fast_data_types import cocoa_set_app_icon; import sys; cocoa_set_app_icon(*sys.argv[1:]); print("OK")' kitty.icns
|
||||
|
||||
# Set the icon for app bundle specified by the path
|
||||
kitty +runpy 'from kitty.fast_data_types import cocoa_set_app_icon; import sys; cocoa_set_app_icon(*sys.argv[1:]); print("OK")' /path/to/icon.png /Applications/kitty.app
|
||||
|
||||
You can also change the icon manually by following the steps:
|
||||
|
||||
#. Find :file:`kitty.app` in the Applications folder, select it and press :kbd:`⌘+I`
|
||||
#. Drag :file:`kitty.icns` onto the application icon in the kitty info pane
|
||||
#. Delete the icon cache and restart Dock::
|
||||
#. Delete the icon cache and restart Dock:
|
||||
|
||||
$ rm /var/folders/*/*/*/com.apple.dock.iconcache; killall Dock
|
||||
.. code-block:: sh
|
||||
|
||||
rm /var/folders/*/*/*/com.apple.dock.iconcache; killall Dock
|
||||
|
||||
|
||||
How do I map key presses in kitty to different keys in the terminal program?
|
||||
@@ -308,7 +337,11 @@ the :sc:`send_text <send_text>` you can use the ``show_key`` kitten. Run::
|
||||
Then press the key you want to emulate. Note that this kitten will only show
|
||||
keys that actually reach the terminal program, in particular, keys mapped to
|
||||
actions in kitty will not be shown. To check those first map them to
|
||||
:ac:`no_op`.
|
||||
:ac:`no_op`. You can also start a kitty instance without any shortcuts to
|
||||
interfere::
|
||||
|
||||
kitty -o clear_all_shortcuts=yes kitty +kitten show_key
|
||||
|
||||
|
||||
How do I open a new window or tab with the same working directory as the current window?
|
||||
--------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -30,9 +30,14 @@ Glossary
|
||||
overlay
|
||||
An *overlay window* is a :term:`kitty window <window>` that is placed on
|
||||
top of an existing kitty window, entirely covering it. Overlays are used
|
||||
throught kitty, for example, to display the :ref:`the scrollback buffer <scrollback>`,
|
||||
throughout kitty, for example, to display the :ref:`the scrollback buffer <scrollback>`,
|
||||
to display :doc:`hints </kittens/hints>`, for :doc:`unicode input
|
||||
</kittens/unicode_input>` etc.
|
||||
</kittens/unicode_input>` etc. Normal overlays are meant for short
|
||||
duration popups and so are not considered the :italic:`active window`
|
||||
when determining the current working directory or getting input text for
|
||||
kittens, launch commands, etc. To create an overlay considered as a
|
||||
:italic:`main window` use the :code:`overlay-main` argument to
|
||||
:doc:`launch`.
|
||||
|
||||
hyperlinks
|
||||
Terminals can have hyperlinks, just like the internet. In kitty you can
|
||||
@@ -107,6 +112,11 @@ Variables that influence kitty behavior
|
||||
Set this to the directory path of the kitty source code and its Python code
|
||||
will be loaded from there. Only works with official binary builds.
|
||||
|
||||
.. envvar:: KITTY_RC_PASSWORD
|
||||
|
||||
Set this to a pass phrase to use the ``kitty @`` remote control command with
|
||||
:opt:`remote_control_password`.
|
||||
|
||||
|
||||
Variables that kitty sets when running child programs
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -116,53 +126,49 @@ Variables that kitty sets when running child programs
|
||||
This is only set on macOS. If the country and language from the macOS user
|
||||
settings form an invalid locale, it will be set to :code:`en_US.UTF-8`.
|
||||
|
||||
|
||||
.. envvar:: PATH
|
||||
|
||||
kitty prepends itself to the PATH of its own environment to ensure the
|
||||
functions calling :program:`kitty` will work properly.
|
||||
|
||||
|
||||
.. envvar:: KITTY_WINDOW_ID
|
||||
|
||||
An integer that is the id for the kitty :term:`window` the program is running in.
|
||||
Can be used with the :doc:`kitty remote control facility <remote-control>`.
|
||||
|
||||
|
||||
.. envvar:: KITTY_PID
|
||||
|
||||
An integer that is the process id for the kitty process in which the program
|
||||
is running. Allows programs to tell kitty to reload its config by sending it
|
||||
the SIGUSR1 signal.
|
||||
|
||||
.. envvar:: KITTY_PUBLIC_KEY
|
||||
|
||||
A public key that programs can use to communicate securely with kitty using
|
||||
the remote control protocol. The format is: :code:`protocol:key data`.
|
||||
|
||||
.. envvar:: WINDOWID
|
||||
|
||||
The id for the :term:`OS Window <os_window>` the program is running in. Only available
|
||||
on platforms that have ids for their windows, such as X11 and macOS.
|
||||
|
||||
|
||||
.. envvar:: TERM
|
||||
|
||||
The name of the terminal, defaults to ``xterm-kitty``. See :opt:`term`.
|
||||
|
||||
|
||||
.. envvar:: TERMINFO
|
||||
|
||||
Path to a directory containing the kitty terminfo database.
|
||||
|
||||
|
||||
.. envvar:: KITTY_INSTALLATION_DIR
|
||||
|
||||
Path to the kitty installation directory.
|
||||
|
||||
|
||||
.. envvar:: COLORTERM
|
||||
|
||||
Set to the value ``truecolor`` to indicate that kitty supports 16 million
|
||||
colors.
|
||||
|
||||
|
||||
.. envvar:: KITTY_LISTEN_ON
|
||||
|
||||
Set when the :doc:`remote control <remote-control>` facility is enabled and
|
||||
@@ -170,45 +176,38 @@ Variables that kitty sets when running child programs
|
||||
Contains the path to the socket. Avoid the need to use :option:`kitty @ --to` when
|
||||
issuing remote control commands.
|
||||
|
||||
|
||||
.. envvar:: KITTY_PIPE_DATA
|
||||
|
||||
Set to data describing the layout of the screen when running child
|
||||
programs using :option:`launch --stdin-source` with the contents of the
|
||||
screen/scrollback piped to them.
|
||||
|
||||
|
||||
.. envvar:: KITTY_CHILD_CMDLINE
|
||||
|
||||
Set to the command line of the child process running in the kitty
|
||||
window when calling the notification callback program on terminal bell, see
|
||||
:opt:`command_on_bell`.
|
||||
|
||||
|
||||
.. envvar:: KITTY_COMMON_OPTS
|
||||
|
||||
Set with the values of some common kitty options when running
|
||||
kittens, so kittens can use them without needing to load :file:`kitty.conf`.
|
||||
|
||||
|
||||
.. envvar:: KITTY_SHELL_INTEGRATION
|
||||
|
||||
Set when enabling :ref:`shell_integration`. It is automatically removed by
|
||||
the shell integration scripts.
|
||||
|
||||
|
||||
.. envvar:: ZDOTDIR
|
||||
|
||||
Set when enabling :ref:`shell_integration` with :program:`zsh`, allowing
|
||||
:program:`zsh` to automatically load the integration script.
|
||||
|
||||
|
||||
.. envvar:: XDG_DATA_DIRS
|
||||
|
||||
Set when enabling :ref:`shell_integration` with :program:`fish`, allowing
|
||||
:program:`fish` to automatically load the integration script.
|
||||
|
||||
|
||||
.. envvar:: ENV
|
||||
|
||||
Set when enabling :ref:`shell_integration` with :program:`bash`, allowing
|
||||
|
||||
@@ -31,7 +31,7 @@ Some programs and libraries that use the kitty graphics protocol:
|
||||
* `ranger <https://github.com/ranger/ranger>`_ - a terminal file manager, with
|
||||
image previews, see this `PR <https://github.com/ranger/ranger/pull/1077>`_
|
||||
* :doc:`kitty-diff <kittens/diff>` - a side-by-side terminal diff program with support for images
|
||||
* `tpix <https://github.com/jesvedberg/tpix>` - a statically compiled binary that can be used to display images and easily installed on remote servers without root access
|
||||
* `tpix <https://github.com/jesvedberg/tpix>`_ - a statically compiled binary that can be used to display images and easily installed on remote servers without root access
|
||||
* `pixcat <https://github.com/mirukana/pixcat>`_ - a third party CLI and python library that wraps the graphics protocol
|
||||
* `neofetch <https://github.com/dylanaraps/neofetch>`_ - A command line system
|
||||
information tool
|
||||
@@ -43,6 +43,7 @@ Some programs and libraries that use the kitty graphics protocol:
|
||||
* `rasterm <https://github.com/BourgeoisBear/rasterm>`_ - Go library to display images in the terminal
|
||||
* `chafa <https://github.com/hpjansson/chafa>`_ - a terminal image viewer
|
||||
* `hologram.nvim <https://github.com/edluffy/hologram.nvim>`_ - view images inside nvim
|
||||
* `term-image <https://github.com/AnonymouX47/term-image>`_ - A Python library, CLI and TUI to display and browse images in the terminal
|
||||
|
||||
Other terminals that have implemented the graphics protocol:
|
||||
|
||||
@@ -236,7 +237,8 @@ Value of `t` Meaning
|
||||
the terminal emulator should only delete the file if it
|
||||
is in a known temporary directory, such as :file:`/tmp`,
|
||||
:file:`/dev/shm`, :file:`TMPDIR env var if present` and any platform
|
||||
specific temporary directories.
|
||||
specific temporary directories and the file has the
|
||||
string :code:`tty-graphics-protocol` in its full file path.
|
||||
``s`` A *shared memory object*, which on POSIX systems is a
|
||||
`POSIX shared memory object <https://pubs.opengroup.org/onlinepubs/9699919799/functions/shm_open.html>`_
|
||||
and on Windows is a
|
||||
@@ -417,7 +419,9 @@ 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. Negative z-index values below
|
||||
INT32_MIN/2 (-1,073,741,824) will be drawn under cells with non-default background
|
||||
colors.
|
||||
colors. If two images with the same z-index overlap then the image with the
|
||||
lower id is considered to have the lower z-index. If the images have the same
|
||||
z-index and the same id, then the behavior is undefined.
|
||||
|
||||
.. note:: After placing an image on the screen the cursor must be moved to the
|
||||
right by the number of cols in the image placement rectangle and down by the
|
||||
|
||||
@@ -47,6 +47,12 @@ graphics protocol.
|
||||
Another terminal file manager, with previews of file contents powered by kitty's
|
||||
graphics protocol.
|
||||
|
||||
.. _tool_term_image:
|
||||
|
||||
`term-image <https://github.com/AnonymouX47/term-image>`__
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Tool to browse images in a terminal using kitty's graphics protocol.
|
||||
|
||||
.. _tool_koneko:
|
||||
|
||||
`koneko <https://github.com/twenty5151/koneko>`_
|
||||
|
||||
@@ -35,8 +35,11 @@ inside the kitty terminal to report key events.
|
||||
In addition to kitty, this protocol is also implemented in:
|
||||
|
||||
* The `foot terminal <https://codeberg.org/dnkl/foot/issues/319>`__
|
||||
* The `WezTerm terminal <https://wezfurlong.org/wezterm/config/lua/config/enable_kitty_keyboard.html>`__
|
||||
* The `notcurses library
|
||||
<https://github.com/dankamongmen/notcurses/issues/2131>`__
|
||||
* The `crossterm library
|
||||
<https://github.com/crossterm-rs/crossterm/pull/688>`__
|
||||
* The `neovim text editor <https://github.com/neovim/neovim/pull/18181>`__
|
||||
* The `kakoune text editor <https://github.com/mawww/kakoune/issues/4103>`__
|
||||
* The `dte text editor <https://gitlab.com/craigbarnes/dte/-/issues/138>`__
|
||||
|
||||
@@ -49,6 +49,24 @@ kittens. Look in the `kittens sub-directory
|
||||
code for those. Or see below for a list of :ref:`third-party kittens
|
||||
<external_kittens>`, that other kitty users have created.
|
||||
|
||||
kitty API to use with kittens
|
||||
-------------------------------
|
||||
|
||||
Kittens have full access to internal kitty APIs. However these are neither
|
||||
entirely stable nor documented. You can instead use the kitty
|
||||
:doc:`Remote control API </remote-control>`. Simply call
|
||||
:code:`boss.call_remote_control()`, with the same arguments you
|
||||
would pass to ``kitty @``. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def handle_result(args: List[str], answer: str, target_window_id: int, boss: Boss) -> None:
|
||||
# get the kitty window to which to send text
|
||||
w = boss.window_id_map.get(target_window_id)
|
||||
if w is not None:
|
||||
boss.call_remote_control(w, ('send-text', 'hello world'))
|
||||
|
||||
|
||||
|
||||
Passing arguments to kittens
|
||||
------------------------------
|
||||
|
||||
@@ -17,12 +17,12 @@ following contents:
|
||||
# by the hyperlink_grep kitten and nothing else so far.
|
||||
protocol file
|
||||
fragment_matches [0-9]+
|
||||
action launch --type=overlay vim +${FRAGMENT} ${FILE_PATH}
|
||||
action launch --type=overlay --cwd=current vim +${FRAGMENT} ${FILE_PATH}
|
||||
|
||||
# Open text files without fragments in the editor
|
||||
protocol file
|
||||
mime text/*
|
||||
action launch --type=overlay ${EDITOR} ${FILE_PATH}
|
||||
action launch --type=overlay --cwd=current ${EDITOR} ${FILE_PATH}
|
||||
|
||||
Now, run a search with::
|
||||
|
||||
@@ -74,6 +74,16 @@ to :program:`rg`. How to do that varies based on the shell:
|
||||
To learn more about kitty's powerful framework for customizing URL click
|
||||
actions, see :doc:`here </open_actions>`.
|
||||
|
||||
By default, this kitten adds hyperlinks for several parts of ripgrep output:
|
||||
the per-file header, match context lines, and match lines. You can control
|
||||
which items are linked with a :command:`--kitten hyperlink` flag. For example,
|
||||
:command:`--kitten hyperlink=matching_lines` will only add hyperlinks to the
|
||||
match lines. :command:`--kitten hyperlink=file_headers,context_lines` will link
|
||||
file headers and context lines but not match lines. :command:`--kitten
|
||||
hyperlink=none` will cause the command line to be passed to directly to
|
||||
:command:`rg` so no hyperlinking will be performed. :command:`--kitten
|
||||
hyperlink` may be specified multiple times.
|
||||
|
||||
Hopefully, someday this functionality will make it into some `upstream grep
|
||||
<https://github.com/BurntSushi/ripgrep/issues/665>`__ program directly removing
|
||||
the need for this kitten.
|
||||
|
||||
@@ -143,6 +143,16 @@ transmitted instantly without any roundtrip delay.
|
||||
is something POSIX sh compliant, and use :code:`python` as the
|
||||
:opt:`interpreter <kitten-ssh.interpreter>` in :file:`ssh.conf`.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
This may or may not work when using terminal multiplexers, depending on
|
||||
whether they passthrough the escape codes and if the values of the
|
||||
environment variables :envvar:`KITTY_PID` and :envvar:`KITTY_WINDOW_ID` are
|
||||
correct in the current session (they can be wrong when connecting to a tmux
|
||||
session running in a different window) and the ssh kitten is run in the
|
||||
currently active multiplexer window.
|
||||
|
||||
.. include:: /generated/conf-kitten-ssh.rst
|
||||
|
||||
|
||||
|
||||
295
docs/kitty_at_template.py
Normal file
295
docs/kitty_at_template.py
Normal file
@@ -0,0 +1,295 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import termios
|
||||
import time
|
||||
import tty
|
||||
from contextlib import contextmanager
|
||||
from ctypes import (
|
||||
CDLL, POINTER, byref, c_char_p, c_int, c_size_t, c_void_p,
|
||||
create_string_buffer
|
||||
)
|
||||
from ctypes.util import find_library
|
||||
|
||||
_plat = sys.platform.lower()
|
||||
is_macos: bool = 'darwin' in _plat
|
||||
|
||||
|
||||
def build_crypto_tools(): # {{{
|
||||
class EVP_PKEY_POINTER(c_void_p):
|
||||
|
||||
algorithm = 0
|
||||
|
||||
def __del__(self):
|
||||
EVP_PKEY_free(self)
|
||||
|
||||
@property
|
||||
def public(self):
|
||||
sz = c_size_t(0)
|
||||
EVP_PKEY_get_raw_public_key(self, None, byref(sz))
|
||||
buf = create_string_buffer(sz.value)
|
||||
EVP_PKEY_get_raw_public_key(self, buf, byref(sz))
|
||||
return buf.raw
|
||||
|
||||
def derive_secret(self, pubkey):
|
||||
pubkey = EVP_PKEY_new_raw_public_key(self.algorithm, None, pubkey, len(pubkey))
|
||||
ctx = EVP_PKEY_CTX_new(self, None)
|
||||
EVP_PKEY_derive_init(ctx)
|
||||
EVP_PKEY_derive_set_peer(ctx, pubkey)
|
||||
sz = c_size_t(0)
|
||||
EVP_PKEY_derive(ctx, None, byref(sz))
|
||||
buf = create_string_buffer(sz.value)
|
||||
EVP_PKEY_derive(ctx, buf, byref(sz))
|
||||
return hashlib.sha256(buf.raw).digest()
|
||||
|
||||
class EVP_PKEY_CTX_POINTER(c_void_p):
|
||||
|
||||
def __del__(self):
|
||||
EVP_PKEY_CTX_free(self)
|
||||
|
||||
class EVP_CIPHER_CTX_POINTER(c_void_p):
|
||||
|
||||
def __del__(self):
|
||||
EVP_CIPHER_CTX_free(self)
|
||||
|
||||
class EVP_CIPHER_POINTER(c_void_p):
|
||||
pass
|
||||
|
||||
cl = find_library('crypto')
|
||||
if not cl:
|
||||
raise SystemExit('Failed to find libcrypto on your system, make sure OpenSSL is installed')
|
||||
crypto = CDLL(cl)
|
||||
libc = CDLL(None)
|
||||
|
||||
def create_crypto_func(name, *argtypes, restype=c_int, int_return_ok=lambda x: x == 1):
|
||||
|
||||
impl = getattr(crypto, name)
|
||||
impl.restype = restype
|
||||
impl.argtypes = argtypes
|
||||
|
||||
def func(*a):
|
||||
res = impl(*a)
|
||||
if restype is c_int:
|
||||
if not int_return_ok(res):
|
||||
print('Call to', name, 'failed with return code:', res, file=sys.stderr)
|
||||
abort_on_openssl_error()
|
||||
elif restype is not None and issubclass(restype, c_void_p):
|
||||
if res.value is None:
|
||||
print('Call to', name, 'failed with NULL return', file=sys.stderr)
|
||||
abort_on_openssl_error()
|
||||
return res
|
||||
return func
|
||||
|
||||
OBJ_txt2nid = create_crypto_func('OBJ_txt2nid', c_char_p, int_return_ok=bool)
|
||||
EVP_PKEY_CTX_new_id = create_crypto_func('EVP_PKEY_CTX_new_id', c_int, c_void_p, restype=EVP_PKEY_CTX_POINTER)
|
||||
EVP_PKEY_CTX_new = create_crypto_func('EVP_PKEY_CTX_new', EVP_PKEY_POINTER, c_void_p, restype=EVP_PKEY_CTX_POINTER)
|
||||
EVP_PKEY_keygen_init = create_crypto_func('EVP_PKEY_keygen_init', EVP_PKEY_CTX_POINTER)
|
||||
EVP_PKEY_keygen = create_crypto_func('EVP_PKEY_keygen', EVP_PKEY_CTX_POINTER, POINTER(EVP_PKEY_POINTER))
|
||||
ERR_print_errors_fp = create_crypto_func('ERR_print_errors_fp', c_void_p, restype=None)
|
||||
EVP_PKEY_free = create_crypto_func('EVP_PKEY_free', EVP_PKEY_POINTER, restype=None)
|
||||
EVP_PKEY_CTX_free = create_crypto_func('EVP_PKEY_CTX_free', EVP_PKEY_CTX_POINTER, restype=None)
|
||||
EVP_PKEY_get_raw_public_key = create_crypto_func('EVP_PKEY_get_raw_public_key', EVP_PKEY_POINTER, c_char_p, POINTER(c_size_t))
|
||||
EVP_PKEY_new_raw_public_key = create_crypto_func('EVP_PKEY_new_raw_public_key', c_int, c_void_p, c_char_p, c_size_t, restype=EVP_PKEY_POINTER)
|
||||
EVP_PKEY_derive_init = create_crypto_func('EVP_PKEY_derive_init', EVP_PKEY_CTX_POINTER)
|
||||
EVP_PKEY_derive_set_peer = create_crypto_func('EVP_PKEY_derive_set_peer', EVP_PKEY_CTX_POINTER, EVP_PKEY_POINTER)
|
||||
EVP_PKEY_derive = create_crypto_func('EVP_PKEY_derive', EVP_PKEY_CTX_POINTER, c_char_p, POINTER(c_size_t))
|
||||
EVP_CIPHER_CTX_free = create_crypto_func('EVP_CIPHER_CTX_free', EVP_CIPHER_CTX_POINTER, restype=None)
|
||||
EVP_get_cipherbyname = create_crypto_func('EVP_get_cipherbyname', c_char_p, restype=EVP_CIPHER_POINTER)
|
||||
EVP_CIPHER_key_length = create_crypto_func('EVP_CIPHER_key_length', EVP_CIPHER_POINTER, int_return_ok=bool)
|
||||
EVP_CIPHER_iv_length = create_crypto_func('EVP_CIPHER_iv_length', EVP_CIPHER_POINTER, int_return_ok=bool)
|
||||
EVP_CIPHER_CTX_block_size = create_crypto_func('EVP_CIPHER_CTX_block_size', EVP_CIPHER_CTX_POINTER, int_return_ok=bool)
|
||||
EVP_CIPHER_CTX_new = create_crypto_func('EVP_CIPHER_CTX_new', restype=EVP_CIPHER_CTX_POINTER)
|
||||
EVP_EncryptInit_ex = create_crypto_func('EVP_EncryptInit_ex', EVP_CIPHER_CTX_POINTER, EVP_CIPHER_POINTER, c_void_p, c_char_p, c_char_p)
|
||||
EVP_EncryptUpdate = create_crypto_func('EVP_EncryptUpdate', EVP_CIPHER_CTX_POINTER, c_char_p, POINTER(c_int), c_char_p, c_int)
|
||||
EVP_EncryptFinal_ex = create_crypto_func('EVP_EncryptFinal_ex', EVP_CIPHER_CTX_POINTER, c_char_p, POINTER(c_int))
|
||||
EVP_CIPHER_CTX_ctrl = create_crypto_func('EVP_CIPHER_CTX_ctrl', EVP_CIPHER_CTX_POINTER, c_int, c_int, c_char_p)
|
||||
try:
|
||||
EVP_CIPHER_CTX_tag_length = create_crypto_func('EVP_CIPHER_CTX_tag_length', EVP_CIPHER_CTX_POINTER, int_return_ok=bool)
|
||||
except AttributeError: # need openssl >= 3
|
||||
def EVP_CIPHER_CTX_tag_length(cipher):
|
||||
return 16
|
||||
EVP_CTRL_AEAD_GET_TAG, EVP_CTRL_AEAD_SET_TAG = 0x10, 0x11 # these are defines in the header dont know how to get them programmatically
|
||||
EVP_CTRL_AEAD_SET_TAG
|
||||
|
||||
def abort_on_openssl_error():
|
||||
stderr = c_void_p.in_dll(libc, 'stderr')
|
||||
ERR_print_errors_fp(stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
def elliptic_curve_keypair(algorithm='X25519'):
|
||||
nid = OBJ_txt2nid(algorithm.encode())
|
||||
pctx = EVP_PKEY_CTX_new_id(nid, None)
|
||||
EVP_PKEY_keygen_init(pctx)
|
||||
key = EVP_PKEY_POINTER()
|
||||
EVP_PKEY_keygen(pctx, byref(key))
|
||||
key.algorithm = nid
|
||||
return key
|
||||
|
||||
def encrypt(plaintext, symmetric_key, algorithm='aes-256-gcm'):
|
||||
cipher = EVP_get_cipherbyname(algorithm.encode())
|
||||
if len(symmetric_key) != EVP_CIPHER_key_length(cipher):
|
||||
raise KeyError(f'The symmetric key has length {len(symmetric_key)} != {EVP_CIPHER_key_length(cipher)} needed for {algorithm}')
|
||||
ctx = EVP_CIPHER_CTX_new()
|
||||
iv = os.urandom(EVP_CIPHER_iv_length(cipher))
|
||||
EVP_EncryptInit_ex(ctx, cipher, None, symmetric_key, iv)
|
||||
bs = EVP_CIPHER_CTX_block_size(ctx)
|
||||
ciphertext = create_string_buffer(len(plaintext) + 2 * bs)
|
||||
outlen = c_int(len(ciphertext))
|
||||
EVP_EncryptUpdate(ctx, ciphertext, byref(outlen), plaintext, len(plaintext))
|
||||
ans = ciphertext[:outlen.value]
|
||||
outlen = c_int(len(ciphertext))
|
||||
EVP_EncryptFinal_ex(ctx, ciphertext, byref(outlen))
|
||||
if outlen.value:
|
||||
ans += ciphertext[:outlen.value]
|
||||
tag = create_string_buffer(EVP_CIPHER_CTX_tag_length(cipher))
|
||||
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, len(tag), tag)
|
||||
return iv, ans, tag.raw
|
||||
return elliptic_curve_keypair, encrypt
|
||||
# }}}
|
||||
|
||||
|
||||
# utils {{{
|
||||
def encrypt_cmd(cmd, password, pubkey=None):
|
||||
elliptic_curve_keypair, encrypt = build_crypto_tools()
|
||||
if pubkey is None:
|
||||
pubkey = os.environ['KITTY_PUBLIC_KEY']
|
||||
v, d = pubkey.split(':', 1)
|
||||
if v != '1':
|
||||
raise SystemExit(f'Unsupported encryption protocol: {v}')
|
||||
pubkey = base64.b85decode(d)
|
||||
k = elliptic_curve_keypair()
|
||||
sk = k.derive_secret(pubkey)
|
||||
cmd['timestamp'] = time.time_ns()
|
||||
cmd['password'] = password
|
||||
data = json.dumps(cmd).encode()
|
||||
iv, encrypted, tag = encrypt(data, sk)
|
||||
|
||||
def e(x):
|
||||
return base64.b85encode(x).decode('ascii')
|
||||
|
||||
return {
|
||||
'encrypted': e(encrypted), 'iv': e(iv), 'tag': e(tag), 'pubkey': e(k.public), 'version': cmd['version']
|
||||
}
|
||||
|
||||
|
||||
@contextmanager
|
||||
def raw_mode(fd):
|
||||
old = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(fd)
|
||||
yield
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
||||
|
||||
|
||||
def config_dir():
|
||||
if 'KITTY_CONFIG_DIRECTORY' in os.environ:
|
||||
return os.path.abspath(os.path.expanduser(os.environ['KITTY_CONFIG_DIRECTORY']))
|
||||
locations = []
|
||||
if 'XDG_CONFIG_HOME' in os.environ:
|
||||
locations.append(os.path.abspath(os.path.expanduser(os.environ['XDG_CONFIG_HOME'])))
|
||||
locations.append(os.path.expanduser('~/.config'))
|
||||
if is_macos:
|
||||
locations.append(os.path.expanduser('~/Library/Preferences'))
|
||||
for loc in filter(None, os.environ.get('XDG_CONFIG_DIRS', '').split(os.pathsep)):
|
||||
locations.append(os.path.abspath(os.path.expanduser(loc)))
|
||||
for loc in locations:
|
||||
if loc:
|
||||
q = os.path.join(loc, 'kitty')
|
||||
if os.access(q, os.W_OK) and os.path.exists(os.path.join(q, 'kitty.conf')):
|
||||
return q
|
||||
for loc in locations:
|
||||
if loc:
|
||||
q = os.path.join(loc, 'kitty')
|
||||
if os.path.isdir(q) and os.access(q, os.W_OK):
|
||||
return q
|
||||
return ''
|
||||
|
||||
|
||||
def resolve_custom_file(path):
|
||||
path = os.path.expanduser(path)
|
||||
path = os.path.expandvars(path)
|
||||
if not os.path.isabs(path):
|
||||
cdir = config_dir()
|
||||
if cdir:
|
||||
path = os.path.join(cdir, path)
|
||||
return path
|
||||
|
||||
|
||||
def get_password(opts):
|
||||
if opts.use_password == 'never':
|
||||
return ''
|
||||
ans = ''
|
||||
if opts.password:
|
||||
ans = opts.password
|
||||
if not ans and opts.password_file:
|
||||
if opts.password_file == '-':
|
||||
if sys.stdin.isatty():
|
||||
from getpass import getpass
|
||||
ans = getpass()
|
||||
else:
|
||||
ans = sys.stdin.read().rstrip()
|
||||
try:
|
||||
tty_fd = os.open(os.ctermid(), os.O_RDONLY | os.O_CLOEXEC)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
with open(tty_fd, closefd=True):
|
||||
os.dup2(tty_fd, sys.stdin.fileno())
|
||||
else:
|
||||
try:
|
||||
with open(resolve_custom_file(opts.password_file)) as f:
|
||||
ans = f.read().rstrip()
|
||||
except OSError:
|
||||
pass
|
||||
if not ans and opts.password_env:
|
||||
ans = os.environ.get(opts.password_env, '')
|
||||
if not ans and opts.use_password == 'always':
|
||||
raise SystemExit('No password was found')
|
||||
if ans and len(ans) > 1024:
|
||||
raise SystemExit('Specified password is too long')
|
||||
return ans
|
||||
# }}}
|
||||
|
||||
|
||||
arg_parser = argparse.ArgumentParser(prog='kitty@', description='Control kitty remotely.')
|
||||
arg_parser.add_argument('--password', default='', help='''\
|
||||
A password to use when contacting kitty. This will cause kitty to ask the user
|
||||
for permission to perform the specified action, unless the password has been
|
||||
accepted before or is pre-configured in kitty.conf''')
|
||||
arg_parser.add_argument('--password-file', default='rc-pass', help='''\
|
||||
A file from which to read the password. Trailing whitespace is ignored. Relative
|
||||
paths are resolved from the kitty configuration directory. Use - to read from STDIN.
|
||||
Used if no --password is supplied. Defaults to checking for the
|
||||
rc-pass file in the kitty configuration directory.''')
|
||||
arg_parser.add_argument('--password-env', default='KITTY_RC_PASSWORD', help='''\
|
||||
The name of an environment variable to read the password from.
|
||||
Used if no --password-file is supplied. Defaults to checking the KITTY_RC_PASSWORD.''')
|
||||
arg_parser.add_argument('--use-password', default='if-available', choices=('if-available', 'always', 'never'), help='''\
|
||||
If no password is available, kitty will usually just send the remote control command
|
||||
without a password. This option can be used to force it to always or never use
|
||||
the supplied password.''')
|
||||
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
|
||||
def populate_cmd(cmd):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
password = get_password(args)
|
||||
cmd = {'version': [0, 20, 0]} # use a random version that's fairly old
|
||||
populate_cmd(cmd)
|
||||
if password:
|
||||
encrypt_cmd(cmd, password)
|
||||
|
||||
# cmd = {'version': [0, 14, 2], 'cmd': 'ls'}
|
||||
# cmd = encrypt_cmd(cmd, 'test')
|
||||
# with open(os.open(os.ctermid(), os.O_RDWR | os.O_CLOEXEC), 'w') as tty_file, raw_mode(tty_file.fileno()):
|
||||
# print(end=f'\x1bP@kitty-cmd{json.dumps(cmd)}\x1b\\', flush=True, file=tty_file)
|
||||
# os.read(tty_file.fileno(), 4096)
|
||||
@@ -68,11 +68,20 @@ for the options is::
|
||||
└──────────────┴───────────────┘
|
||||
|
||||
In addition, you can map keys to increase or decrease the number of full-height
|
||||
windows, for example::
|
||||
windows, or toggle the mirrored setting, for example::
|
||||
|
||||
map ctrl+[ layout_action decrease_num_full_size_windows
|
||||
map ctrl+] layout_action increase_num_full_size_windows
|
||||
map ctrl+/ layout_action mirror toggle
|
||||
map ctrl+y layout_action mirror true
|
||||
map ctrl+n layout_action mirror false
|
||||
|
||||
You can also map a key to change the bias by providing a list of percentages
|
||||
and it will rotate through the list as you press the key. If you only provide
|
||||
one number it'll toggle between that percentage and 50, for example::
|
||||
|
||||
map ctrl+. layout_action bias 50 62 70
|
||||
map ctrl+, layout_action bias 62
|
||||
|
||||
The Fat Layout
|
||||
----------------
|
||||
@@ -101,8 +110,7 @@ on the bottom of the screen instead of the top. The syntax for the options is::
|
||||
└─────────┴──────────┴─────────┘
|
||||
|
||||
|
||||
This layout also supports ``decrease_num_full_size_windows`` layout action like
|
||||
the *Tall* layout, shown above.
|
||||
This layout also supports the same layout actions as the *Tall* layout, shown above.
|
||||
|
||||
|
||||
The Grid Layout
|
||||
|
||||
@@ -64,6 +64,10 @@ some special variables, documented below:
|
||||
``FRAGMENT``
|
||||
The fragment (unquoted), if any of the URL or the empty string.
|
||||
|
||||
``URL_PATH``
|
||||
The path, query and fragment portions of the URL, without any
|
||||
unquoting.
|
||||
|
||||
|
||||
.. note::
|
||||
You can use the :opt:`action_alias` option just as in :file:`kitty.conf` to
|
||||
|
||||
@@ -161,8 +161,12 @@ option in :file:`kitty.conf`. For example:
|
||||
# Set the --class for the new OS window
|
||||
os_window_class mywindow
|
||||
launch sh
|
||||
# Make the current window the active (focused) window
|
||||
# Resize the current window (see the resize_window action for details)
|
||||
resize_window wider 2
|
||||
# Make the current window the active (focused) window in its tab
|
||||
focus
|
||||
# Make the current OS Window the globally active window (not supported on Wayland)
|
||||
focus_os_window
|
||||
launch emacs
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -7,13 +7,15 @@ form::
|
||||
|
||||
<ESC>P@kitty-cmd<JSON object><ESC>\
|
||||
|
||||
Where ``<ESC>`` is the byte ``0x1b``. The JSON object has the form::
|
||||
Where ``<ESC>`` is the byte ``0x1b``. The JSON object has the form:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"cmd": "command name",
|
||||
"version": <kitty version>,
|
||||
"no_response": <Optional Boolean>,
|
||||
"payload": <Optional JSON object>,
|
||||
"version": "<kitty version>",
|
||||
"no_response": "<Optional Boolean>",
|
||||
"payload": "<Optional JSON object>"
|
||||
}
|
||||
|
||||
The ``version`` above is an array of the form :code:`[0, 14, 2]`. If you are
|
||||
@@ -38,4 +40,41 @@ with the following command line::
|
||||
|
||||
echo -en '\eP@kitty-cmd{"cmd":"ls","version":[0,14,2]}\e\\' | socat - unix:/tmp/test | awk '{ print substr($0, 13, length($0) - 14) }' | jq -c '.data | fromjson' | jq .
|
||||
|
||||
.. _rc_crypto:
|
||||
|
||||
Encrypted communication
|
||||
--------------------------
|
||||
|
||||
.. versionadded:: 0.26.0
|
||||
|
||||
When using the :opt:`remote_control_password` option communication to the
|
||||
terminal is encrypted to keep the password secure. A public key is used from
|
||||
the :envvar:`KITTY_PUBLIC_KEY` environment variable. Currently, only one
|
||||
encryption protocol is supported. The protocol number is present in
|
||||
:envvar:`KITTY_PUBLIC_KEY` as ``1``. The key data in this environment variable
|
||||
is :rfc:`Base-85 <1924>` encoded. The algorithm used is `Elliptic Curve Diffie
|
||||
Helman <https://en.wikipedia.org/wiki/Elliptic-curve_Diffie–Hellman>`__ with
|
||||
the `X25519 curve <https://en.wikipedia.org/wiki/Curve25519>`__. A time based
|
||||
nonce is used to minimise replay attacks. The original JSON command has the
|
||||
fields: ``password`` and ``timestamp`` added. The timestamp is the number of
|
||||
nanoseconds since the epoch, excluding leap seconds. Commands with a timestamp
|
||||
more than 5 minutes from the current time are rejected. The command is then
|
||||
encrypted using AES-256-GCM in authenticated encryption mode, with a symmetric
|
||||
key that is derived from the ECDH key-pair by running the shared secret through
|
||||
SHA-256 hashing, once. An IV of at least 96 bits of CSPRNG data is used. The
|
||||
tag for authenticated encryption **must** be at least 128 bits long. The tag
|
||||
**must** authenticate only the value of the ``encrypted`` field. A new command
|
||||
is created and transmitted that contains the fields:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"version": "<kitty version>",
|
||||
"iv": "base85 encoded IV",
|
||||
"tag": "base85 encoded AEAD tag",
|
||||
"pubkey": "base85 encoded ECDH public key of sender",
|
||||
"encrypted": "The original command encrypted and base85 encoded"
|
||||
}
|
||||
|
||||
|
||||
.. include:: generated/rc.rst
|
||||
|
||||
@@ -17,8 +17,9 @@ Start by running |kitty| as::
|
||||
|
||||
kitty -o allow_remote_control=yes -o enabled_layouts=tall
|
||||
|
||||
In order for control to work, :opt:`allow_remote_control` must be enabled in
|
||||
:file:`kitty.conf`. Here we turn it on explicitly at the command line.
|
||||
In order for control to work, :opt:`allow_remote_control` or
|
||||
:opt:`remote_control_password` must be enabled in :file:`kitty.conf`. Here we
|
||||
turn it on explicitly at the command line.
|
||||
|
||||
Now, in the new |kitty| window, enter the command::
|
||||
|
||||
@@ -35,12 +36,12 @@ This will make ``Hello, World`` show up in the window running the :program:`cat`
|
||||
program. The :option:`kitty @ send-text --match` option is very powerful, it
|
||||
allows selecting windows by their titles, the command line of the program
|
||||
running in the window, the working directory of the program running in the
|
||||
window, etc. See :ref:`kitty @ send-text --help <at_send-text>` for details.
|
||||
window, etc. See :ref:`kitty @ send-text --help <at-send-text>` for details.
|
||||
|
||||
More usefully, you can pipe the output of a command running in one window to
|
||||
another window, for example::
|
||||
|
||||
ls | kitty @ send-text --match title:^Output --stdin
|
||||
ls | kitty @ send-text --match 'title:^Output' --stdin
|
||||
|
||||
This will show the output of :program:`ls` in the output window instead of the
|
||||
current window. You can use this technique to, for example, show the output of
|
||||
@@ -49,7 +50,7 @@ are endless.
|
||||
|
||||
You can even have things you type show up in a different window. Run::
|
||||
|
||||
kitty @ send-text --match title:^Output --stdin
|
||||
kitty @ send-text --match 'title:^Output' --stdin
|
||||
|
||||
And type some text, it will show up in the output window, instead of the current
|
||||
window. Type :kbd:`Ctrl+D` when you are ready to stop.
|
||||
@@ -61,7 +62,7 @@ Now, let's open a new tab::
|
||||
This will open a new tab running the bash shell with the title "My Tab".
|
||||
We can change the title of the tab to "New Title" with::
|
||||
|
||||
kitty @ set-tab-title --match title:^My New Title
|
||||
kitty @ set-tab-title --match 'title:^My' New Title
|
||||
|
||||
Let's change the title of the current tab::
|
||||
|
||||
@@ -69,12 +70,12 @@ Let's change the title of the current tab::
|
||||
|
||||
Now lets switch to the newly opened tab::
|
||||
|
||||
kitty @ focus-tab --match title:^New
|
||||
kitty @ focus-tab --match 'title:^New'
|
||||
|
||||
Similarly, to focus the previously opened output window (which will also switch
|
||||
back to the old tab, automatically)::
|
||||
|
||||
kitty @ focus-window --match title:^Output
|
||||
kitty @ focus-window --match 'title:^Output'
|
||||
|
||||
You can get a listing of available tabs and windows, by running::
|
||||
|
||||
@@ -92,11 +93,23 @@ As you can see, it is very easy to control |kitty| using the ``kitty @``
|
||||
messaging system. This tutorial touches only the surface of what is possible.
|
||||
See ``kitty @ --help`` for more details.
|
||||
|
||||
Note that in the example's above, ``kitty @`` messaging works only when run
|
||||
In the example's above, ``kitty @`` messaging works only when run
|
||||
inside a |kitty| window, not anywhere. But, within a |kitty| window it even
|
||||
works over SSH. If you want to control |kitty| from programs/scripts not running
|
||||
inside a |kitty| window, you have to implement a couple of extra steps. First
|
||||
start |kitty| as::
|
||||
inside a |kitty| window, see the section on :ref:`using a socket for remote control <rc_via_socket>`
|
||||
below.
|
||||
|
||||
|
||||
Note that if all you want to do is run a single |kitty| "daemon" and have
|
||||
subsequent |kitty| invocations appear as new top-level windows, you can use the
|
||||
simpler :option:`kitty --single-instance` option, see ``kitty --help`` for that.
|
||||
|
||||
|
||||
.. _rc_via_socket:
|
||||
|
||||
Remote control via a socket
|
||||
--------------------------------
|
||||
First, start |kitty| as::
|
||||
|
||||
kitty -o allow_remote_control=yes --listen-on unix:/tmp/mykitty
|
||||
|
||||
@@ -108,11 +121,6 @@ command line argument to ``kitty @``. For example::
|
||||
kitty @ --to unix:/tmp/mykitty ls
|
||||
|
||||
|
||||
Note that if all you want to do is run a single |kitty| "daemon" and have
|
||||
subsequent |kitty| invocations appear as new top-level windows, you can use the
|
||||
simpler :option:`kitty --single-instance` option, see ``kitty --help`` for that.
|
||||
|
||||
|
||||
The builtin kitty shell
|
||||
--------------------------
|
||||
|
||||
@@ -147,6 +155,112 @@ other computers (for example, over SSH) or as other users.
|
||||
what programs you run in such windows, since they can effectively control
|
||||
kitty, as if you were running with :opt:`allow_remote_control` turned on.
|
||||
|
||||
You can further restrict what is allowed in these windows by using
|
||||
:option:`kitty @ launch --remote-control-password`.
|
||||
|
||||
|
||||
Fine grained permissions for remote control
|
||||
----------------------------------------------
|
||||
|
||||
.. versionadded:: 0.26.0
|
||||
|
||||
The :opt:`allow_remote_control` option discussed so far is a blunt
|
||||
instrument, granting the ability to any program running on your computer
|
||||
or even on remote computers via SSH the ability to use remote control.
|
||||
|
||||
You can instead define remote control passwords that can be used to grant
|
||||
different levels of control to different places. You can even write your
|
||||
own script to decide which remote control requests are allowed. This is
|
||||
done using the :opt:`remote_control_password` option in :file:`kitty.conf`.
|
||||
Set :opt:`allow_remote_control` to :code:`password` to use this feature.
|
||||
Let's see some examples:
|
||||
|
||||
.. code-block:: conf
|
||||
|
||||
remote_control_password "control colors" get-colors set-colors
|
||||
|
||||
Now, using this password, you can, in scripts run the command::
|
||||
|
||||
kitty @ --password="control colors" set-colors background=red
|
||||
|
||||
Any script with access to the password can now change colors in kitty using
|
||||
remote control, but only that and nothing else. You can even supply the
|
||||
password via the :envvar:`KITTY_RC_PASSWORD` environment variable, or the
|
||||
file :file:`~/.config/kitty/rc-password` to avoid having to type it repeatedly.
|
||||
See :option:`kitty @ --password-file` and :option:`kitty @ --password-env`.
|
||||
|
||||
The :opt:`remote_control_password` can be specified multiple times to create
|
||||
different passwords with different capabilities. Run the following to get a
|
||||
list of all action names::
|
||||
|
||||
kitty @ --help
|
||||
|
||||
You can even use glob patterns to match action names, for example:
|
||||
|
||||
.. code-block:: conf
|
||||
|
||||
remote_control_password "control colors" *-colors
|
||||
|
||||
If no action names are specified, all actions are allowed.
|
||||
|
||||
If ``kitty @`` is run with a password that is not present in
|
||||
:file:`kitty.conf`, then kitty will interactively prompt the user to allow or
|
||||
disallow the remote control request. The user can choose to allow or disallow
|
||||
either just that request or all requests using that password. The user's
|
||||
decision is remembered for the duration of that kitty instance.
|
||||
|
||||
.. note::
|
||||
For password based authentication to work over SSH, you must pass the
|
||||
:envvar:`KITTY_PUBLIC_KEY` environment variable to the remote host. The
|
||||
:doc:`ssh kitten <kittens/ssh>` does this for you automatically. When
|
||||
using a password, :ref:`rc_crypto` is used to ensure the password
|
||||
is kept secure. This does mean that using password based authentication
|
||||
is slower as the entire command is encrypted before transmission. This
|
||||
can be noticeable when using a command like ``kitty @ set-background-image``
|
||||
which transmits large amounts of image data. Also, the clock on the remote
|
||||
system must match (within a few minutes) the clock on the local system.
|
||||
kitty uses a time based nonce to minimise the potential for replay attacks.
|
||||
|
||||
.. _rc_custom_auth:
|
||||
|
||||
Customizing authorization with your own program
|
||||
____________________________________________________________
|
||||
|
||||
If the ability to control access by action names is not fine grained enough,
|
||||
you can define your own Python script to examine every remote control command
|
||||
and allow/disallow it. To do so create a file in the kitty configuration
|
||||
directory, :file:`~/.config/kitty/my_rc_auth.py` and add the following
|
||||
to :file:`kitty.conf`:
|
||||
|
||||
.. code-block:: conf
|
||||
|
||||
remote_control_password "testing custom auth" my_rc_auth.py
|
||||
|
||||
:file:`my_rc_auth.py` should define a :code:`is_cmd_allowed` function
|
||||
as shown below:
|
||||
|
||||
.. code-block:: py
|
||||
|
||||
def is_cmd_allowed(pcmd, window, from_socket, extra_data):
|
||||
cmd_name = pcmd['cmd'] # the name of the command
|
||||
cmd_payload = pcmd['payload'] # the arguments to the command
|
||||
# examine the cmd_name and cmd_payload and return True to allow
|
||||
# the command or False to disallow it. Return None to have no
|
||||
# effect on the command.
|
||||
|
||||
# The command payload will vary from command to command, see
|
||||
# the rc protocol docs for details. Below is an example of
|
||||
# restricting the launch command to allow only running the
|
||||
# default shell.
|
||||
|
||||
if cmd_name != 'launch':
|
||||
return None
|
||||
if cmd_payload.get('args') or cmd_payload.get('env') or cmd_payload.get('copy_cmdline') or cmd_payload.get('copy_env'):
|
||||
return False
|
||||
# prints in this function go to the parent kitty process STDOUT
|
||||
print('Allowing launch command:', cmd_payload)
|
||||
return True
|
||||
|
||||
|
||||
.. _rc_mapping:
|
||||
|
||||
@@ -162,6 +276,12 @@ Then pressing the :kbd:`F1` key will set the active window margins to
|
||||
:code:`30`. The syntax for what follows :ac:`remote_control` is exactly the same
|
||||
as the syntax for what follows :code:`kitty @` above.
|
||||
|
||||
If you wish to ignore errors from the command, prefix the command with an
|
||||
``!``. For example, the following will not return an error when no windows
|
||||
are matched::
|
||||
|
||||
map f1 remote_control !focus-window --match XXXXXX
|
||||
|
||||
.. note:: You do not need :opt:`allow_remote_control` to use these mappings,
|
||||
as they are not actual remote programs, but are simply a way to resuse the
|
||||
remote control infrastructure via keybings.
|
||||
|
||||
@@ -41,7 +41,7 @@ Features
|
||||
* Sophisticated completion for the :program:`kitty` command in the shell.
|
||||
|
||||
* When confirming a quit command if a window is sitting at a shell prompt,
|
||||
it is optionally, not counted (see :opt:`confirm_os_window_close`)
|
||||
it is not counted (for details, see :opt:`confirm_os_window_close`)
|
||||
|
||||
|
||||
Configuration
|
||||
@@ -274,8 +274,8 @@ Edit files in new kitty windows even over SSH
|
||||
|
||||
edit-in-kitty myfile.txt
|
||||
edit-in-kitty --type tab --title "Editing My File" myfile.txt
|
||||
# open file.txt at line 75 (works with vim, emacs, nano, micro)
|
||||
edit-in-kitty +75 file.txt
|
||||
# open myfile.txt at line 75 (works with vim, neovim, emacs, nano, micro)
|
||||
edit-in-kitty +75 myfile.txt
|
||||
|
||||
The :command:`edit-in-kitty` command allows you to seamlessly edit files
|
||||
in your default :opt:`editor` in new kitty windows. This works even over
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!./kitty/launcher/kitty +launch
|
||||
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
|
||||
@@ -770,6 +770,11 @@ is_useful_apple_global_shortcut(int sc) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
is_apple_jis_layout_function_key(NSEvent *event) {
|
||||
return [event keyCode] == 0x66 /* kVK_JIS_Eisu */ || [event keyCode] == 0x68 /* kVK_JIS_Kana */;
|
||||
}
|
||||
|
||||
GLFWAPI GLFWapplicationshouldhandlereopenfun glfwSetApplicationShouldHandleReopen(GLFWapplicationshouldhandlereopenfun callback) {
|
||||
GLFWapplicationshouldhandlereopenfun previous = handle_reopen_callback;
|
||||
handle_reopen_callback = callback;
|
||||
@@ -837,6 +842,14 @@ int _glfwPlatformInit(void)
|
||||
last_keydown_shortcut_event.timestamp = [event timestamp];
|
||||
return event;
|
||||
}
|
||||
// check for JIS keyboard layout function keys
|
||||
if (is_apple_jis_layout_function_key(event)) {
|
||||
debug_key("keyDown triggerred JIS layout function key ignoring\n");
|
||||
last_keydown_shortcut_event.virtual_key_code = [event keyCode];
|
||||
last_keydown_shortcut_event.input_source_switch_modifiers = 0;
|
||||
last_keydown_shortcut_event.timestamp = [event timestamp];
|
||||
return event;
|
||||
}
|
||||
}
|
||||
last_keydown_shortcut_event.virtual_key_code = 0xffff;
|
||||
NSWindow *kw = [NSApp keyWindow];
|
||||
@@ -979,8 +992,6 @@ void _glfwPlatformTerminate(void)
|
||||
_glfw.ns.appleSettings = nil;
|
||||
}
|
||||
|
||||
free(_glfw.ns.clipboardString);
|
||||
|
||||
_glfwTerminateNSGL();
|
||||
if (global_shortcuts != nil) { [global_shortcuts release]; global_shortcuts = nil; }
|
||||
|
||||
|
||||
1
glfw/cocoa_platform.h
vendored
1
glfw/cocoa_platform.h
vendored
@@ -183,7 +183,6 @@ typedef struct _GLFWlibraryNS
|
||||
|
||||
char keyName[64];
|
||||
char text[256];
|
||||
char* clipboardString;
|
||||
CGPoint cascadePoint;
|
||||
// Where to place the cursor when re-enabled
|
||||
double restoreCursorPosX, restoreCursorPosY;
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
#include "internal.h"
|
||||
#include "../kitty/monotonic.h"
|
||||
|
||||
#include <Availability.h>
|
||||
#import <CoreServices/CoreServices.h>
|
||||
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
||||
#include <float.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -1540,6 +1543,39 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
|
||||
debug_key("\n\tdoCommandBySelector: (%s)\n", [NSStringFromSelector(selector) UTF8String]);
|
||||
}
|
||||
|
||||
- (BOOL)isAccessibilityElement
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isAccessibilitySelectorAllowed:(SEL)selector
|
||||
{
|
||||
if (selector == @selector(accessibilityRole) || selector == @selector(accessibilitySelectedText)) return YES;
|
||||
return NO;
|
||||
}
|
||||
|
||||
#if (TARGET_OS_OSX && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101400)
|
||||
|
||||
- (NSAccessibilityRole)accessibilityRole
|
||||
{
|
||||
return NSAccessibilityTextAreaRole;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
- (NSString *)accessibilitySelectedText
|
||||
{
|
||||
NSString *text = nil;
|
||||
if (_glfw.callbacks.get_current_selection) {
|
||||
char *s = _glfw.callbacks.get_current_selection();
|
||||
if (s) {
|
||||
text = [NSString stringWithUTF8String:s];
|
||||
free(s);
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
@end
|
||||
// }}}
|
||||
|
||||
@@ -2472,48 +2508,150 @@ bool _glfwPlatformToggleFullscreen(_GLFWwindow* w, unsigned int flags) {
|
||||
return made_fullscreen;
|
||||
}
|
||||
|
||||
void _glfwPlatformSetClipboardString(const char* string)
|
||||
{
|
||||
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
|
||||
[pasteboard declareTypes:@[NSPasteboardTypeString] owner:nil];
|
||||
[pasteboard setString:@(string) forType:NSPasteboardTypeString];
|
||||
// Clipboard {{{
|
||||
|
||||
#define UTI_ROUNDTRIP_PREFIX @"uti-is-typical-apple-nih."
|
||||
|
||||
static NSString*
|
||||
mime_to_uti(const char *mime) {
|
||||
if (strcmp(mime, "text/plain") == 0) return NSPasteboardTypeString;
|
||||
if (@available(macOS 11.0, *)) {
|
||||
UTType *t = [UTType typeWithMIMEType:@(mime)]; // auto-released
|
||||
if (t != nil && !t.dynamic) return t.identifier;
|
||||
}
|
||||
return [NSString stringWithFormat:@"%@%s", UTI_ROUNDTRIP_PREFIX, mime]; // auto-released
|
||||
}
|
||||
|
||||
const char* _glfwPlatformGetClipboardString(void)
|
||||
{
|
||||
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
|
||||
free(_glfw.ns.clipboardString); _glfw.ns.clipboardString = NULL;
|
||||
static const char*
|
||||
uti_to_mime(NSString *uti) {
|
||||
if ([uti isEqualToString:NSPasteboardTypeString]) return "text/plain";
|
||||
if ([uti hasPrefix:UTI_ROUNDTRIP_PREFIX]) return [[uti substringFromIndex:[UTI_ROUNDTRIP_PREFIX length]] UTF8String];
|
||||
if (@available(macOS 11.0, *)) {
|
||||
UTType *t = [UTType typeWithIdentifier:uti]; // auto-released
|
||||
if (t.preferredMIMEType != nil) return [t.preferredMIMEType UTF8String];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
static void
|
||||
list_clipboard_mimetypes(GLFWclipboardwritedatafun write_data, void *object) {
|
||||
#define w(x) { if (ok) ok = write_data(object, x, strlen(x)); }
|
||||
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
|
||||
NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES};
|
||||
BOOL has_file_urls = [pasteboard canReadObjectForClasses:@[[NSURL class]] options:options];
|
||||
BOOL has_strings = [pasteboard canReadObjectForClasses:@[[NSString class]] options:nil];
|
||||
/* NSLog(@"has_file_urls: %d has_strings: %d", has_file_urls, has_strings); */
|
||||
bool ok = true;
|
||||
if (has_strings) w("text/plain");
|
||||
if (has_file_urls) w("text/local-path-list");
|
||||
for (NSPasteboardItem * item in pasteboard.pasteboardItems) {
|
||||
for (NSPasteboardType type in item.types) {
|
||||
/* NSLog(@"%@", type); */
|
||||
const char *mime = uti_to_mime(type);
|
||||
if (mime && mime[0] && ![@(mime) hasPrefix:@"text/plain"]) {
|
||||
/* NSLog(@"ut: %@ mt: %@ tags: %@", ut, ut.preferredMIMEType, ut.tags); */
|
||||
w(mime);
|
||||
}
|
||||
}
|
||||
}
|
||||
#undef w
|
||||
}
|
||||
|
||||
static void
|
||||
get_text_plain(GLFWclipboardwritedatafun write_data, void *object) {
|
||||
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
|
||||
NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES};
|
||||
NSArray* objs = [pasteboard readObjectsForClasses:@[[NSURL class], [NSString class]] options:options];
|
||||
bool found = false;
|
||||
if (objs) {
|
||||
const NSUInteger count = [objs count];
|
||||
if (count) {
|
||||
NSMutableString *path_list = [NSMutableString stringWithCapacity:4096]; // auto-released
|
||||
NSMutableString *text_list = [NSMutableString stringWithCapacity:4096]; // auto-released
|
||||
NSMutableData *path_list = [NSMutableData dataWithCapacity:4096]; // auto-released
|
||||
NSMutableData *text_list = [NSMutableData dataWithCapacity:4096]; // auto-released
|
||||
for (NSUInteger i = 0; i < count; i++) {
|
||||
id obj = objs[i];
|
||||
if ([obj isKindOfClass:[NSURL class]]) {
|
||||
NSURL *url = (NSURL*)obj;
|
||||
if (url.fileURL && url.fileSystemRepresentation) {
|
||||
if ([path_list length] > 0) [path_list appendString:@("\n")];
|
||||
[path_list appendString:@(url.fileSystemRepresentation)];
|
||||
if ([path_list length] > 0) [path_list appendBytes:"\n" length:1];
|
||||
[path_list appendBytes:url.fileSystemRepresentation length:strlen(url.fileSystemRepresentation)];
|
||||
}
|
||||
} else if ([obj isKindOfClass:[NSString class]]) {
|
||||
if ([text_list length] > 0) [text_list appendString:@("\n")];
|
||||
[text_list appendString:obj];
|
||||
if ([text_list length] > 0) [text_list appendBytes:"\n" length:1];
|
||||
[text_list appendData:[obj dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
}
|
||||
}
|
||||
const char *text = NULL;
|
||||
if (path_list.length > 0) text = [path_list UTF8String];
|
||||
else if (text_list.length > 0) text = [text_list UTF8String];
|
||||
if (text) _glfw.ns.clipboardString = _glfw_strdup(text);
|
||||
const NSMutableData *text = nil;
|
||||
if (path_list.length > 0) text = path_list;
|
||||
else if (text_list.length > 0) text = text_list;
|
||||
if (text) {
|
||||
found = true;
|
||||
write_data(object, text.mutableBytes, text.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!_glfw.ns.clipboardString) _glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to retrieve object from pasteboard");
|
||||
return _glfw.ns.clipboardString;
|
||||
if (!found) _glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to retrieve text/plain from pasteboard");
|
||||
}
|
||||
|
||||
void
|
||||
_glfwPlatformGetClipboard(GLFWClipboardType clipboard_type, const char* mime_type, GLFWclipboardwritedatafun write_data, void *object) {
|
||||
if (clipboard_type != GLFW_CLIPBOARD) return;
|
||||
if (mime_type == NULL) {
|
||||
list_clipboard_mimetypes(write_data, object);
|
||||
return;
|
||||
}
|
||||
if (strcmp(mime_type, "text/plain") == 0) {
|
||||
get_text_plain(write_data, object);
|
||||
return;
|
||||
}
|
||||
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
|
||||
/* NSLog(@"mime: %s uti: %@", mime_type, mime_to_uti(mime_type)); */
|
||||
NSPasteboardType t = [pasteboard availableTypeFromArray:@[mime_to_uti(mime_type)]];
|
||||
/* NSLog(@"available type: %@", t); */
|
||||
if (t != nil) {
|
||||
NSData *data = [pasteboard dataForType:t]; // auto-released
|
||||
/* NSLog(@"data: %@", data); */
|
||||
if (data != nil && data.length > 0) {
|
||||
write_data(object, data.bytes, data.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static NSMutableData*
|
||||
get_clipboard_data(const _GLFWClipboardData *cd, const char *mime) {
|
||||
NSMutableData *ans = [NSMutableData dataWithCapacity:8192];
|
||||
if (ans == nil) return nil;
|
||||
GLFWDataChunk chunk = cd->get_data(mime, NULL, cd->ctype);
|
||||
void *iter = chunk.iter;
|
||||
if (!iter) return ans;
|
||||
while (true) {
|
||||
chunk = cd->get_data(mime, iter, cd->ctype);
|
||||
if (!chunk.sz) break;
|
||||
[ans appendBytes:chunk.data length:chunk.sz];
|
||||
if (chunk.free) chunk.free((void*)chunk.free_data);
|
||||
}
|
||||
cd->get_data(NULL, iter, cd->ctype);
|
||||
return ans;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
_glfwPlatformSetClipboard(GLFWClipboardType t) {
|
||||
if (t != GLFW_CLIPBOARD) return;
|
||||
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
|
||||
NSMutableArray<NSPasteboardType> *ptypes = [NSMutableArray arrayWithCapacity:_glfw.clipboard.num_mime_types]; // auto-released
|
||||
for (size_t i = 0; i < _glfw.clipboard.num_mime_types; i++) {
|
||||
[ptypes addObject:mime_to_uti(_glfw.clipboard.mime_types[i])];
|
||||
}
|
||||
[pasteboard declareTypes:ptypes owner:nil];
|
||||
for (size_t i = 0; i < _glfw.clipboard.num_mime_types; i++) {
|
||||
NSMutableData *data = get_clipboard_data(&_glfw.clipboard, _glfw.clipboard.mime_types[i]); // auto-released
|
||||
/* NSLog(@"putting data: %@ for: %s with UTI: %@", data, _glfw.clipboard.mime_types[i], ptypes[i]); */
|
||||
if (data != nil) [pasteboard setData:data forType:ptypes[i]];
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
|
||||
EGLenum _glfwPlatformGetEGLPlatform(EGLint** attribs)
|
||||
{
|
||||
if (_glfw.egl.ANGLE_platform_angle)
|
||||
|
||||
9
glfw/context.c
vendored
9
glfw/context.c
vendored
@@ -396,15 +396,6 @@ bool _glfwRefreshContextAttribs(_GLFWwindow* window,
|
||||
window->context.release = GLFW_RELEASE_BEHAVIOR_FLUSH;
|
||||
}
|
||||
|
||||
// Clearing the front buffer to black to avoid garbage pixels left over from
|
||||
// previous uses of our bit of VRAM
|
||||
{
|
||||
PFNGLCLEARPROC glClear = (PFNGLCLEARPROC)
|
||||
window->context.getProcAddress("glClear");
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
window->context.swapBuffers(window);
|
||||
}
|
||||
|
||||
glfwMakeContextCurrent((GLFWwindow*) previous);
|
||||
return true;
|
||||
}
|
||||
|
||||
7
glfw/egl_context.c
vendored
7
glfw/egl_context.c
vendored
@@ -428,6 +428,8 @@ bool _glfwInitEGL(void)
|
||||
extensionSupportedEGL("EGL_KHR_get_all_proc_addresses");
|
||||
_glfw.egl.KHR_context_flush_control =
|
||||
extensionSupportedEGL("EGL_KHR_context_flush_control");
|
||||
_glfw.egl.EXT_present_opaque =
|
||||
extensionSupportedEGL("EGL_EXT_present_opaque");
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -598,6 +600,11 @@ bool _glfwCreateContextEGL(_GLFWwindow* window,
|
||||
if (_glfw.egl.KHR_gl_colorspace)
|
||||
setAttrib(EGL_GL_COLORSPACE_KHR, EGL_GL_COLORSPACE_SRGB_KHR);
|
||||
}
|
||||
// Disabled because it prevents transparency from working on NVIDIA drivers under Wayland
|
||||
// https://github.com/kovidgoyal/kitty/issues/5479
|
||||
// We anyway dont use the alpha bits for anything.
|
||||
/* if (_glfw.egl.EXT_present_opaque) */
|
||||
/* setAttrib(EGL_PRESENT_OPAQUE_EXT, !fbconfig->transparent); */
|
||||
|
||||
setAttrib(EGL_NONE, EGL_NONE);
|
||||
|
||||
|
||||
9
glfw/egl_context.h
vendored
9
glfw/egl_context.h
vendored
@@ -160,11 +160,11 @@ typedef EGLSurface (EGLAPIENTRY * PFNEGLCREATEPLATFORMWINDOWSURFACEEXTPROC)(EGLD
|
||||
//
|
||||
typedef struct _GLFWcontextEGL
|
||||
{
|
||||
EGLConfig config;
|
||||
EGLContext handle;
|
||||
EGLSurface surface;
|
||||
EGLConfig config;
|
||||
EGLContext handle;
|
||||
EGLSurface surface;
|
||||
|
||||
void* client;
|
||||
void* client;
|
||||
|
||||
} _GLFWcontextEGL;
|
||||
|
||||
@@ -186,6 +186,7 @@ typedef struct _GLFWlibraryEGL
|
||||
bool EXT_platform_base;
|
||||
bool EXT_platform_x11;
|
||||
bool EXT_platform_wayland;
|
||||
bool EXT_present_opaque;
|
||||
bool ANGLE_platform_angle;
|
||||
bool ANGLE_platform_angle_opengl;
|
||||
bool ANGLE_platform_angle_d3d;
|
||||
|
||||
@@ -111,7 +111,7 @@ def init_env(
|
||||
|
||||
elif module == 'cocoa':
|
||||
ans.cppflags.append('-DGL_SILENCE_DEPRECATION')
|
||||
for f_ in 'Cocoa IOKit CoreFoundation CoreVideo'.split():
|
||||
for f_ in 'Cocoa IOKit CoreFoundation CoreVideo UniformTypeIdentifiers'.split():
|
||||
ans.ldpaths.extend(('-framework', f_))
|
||||
|
||||
elif module == 'wayland':
|
||||
@@ -172,6 +172,8 @@ class Arg:
|
||||
while self.name.startswith('*'):
|
||||
self.name = self.name[1:]
|
||||
self.type = self.type + '*'
|
||||
if '[' in self.name:
|
||||
self.type += '[' + self.name.partition('[')[-1]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'Arg({self.type}, {self.name})'
|
||||
@@ -245,11 +247,15 @@ def generate_wrappers(glfw_header: str) -> None:
|
||||
const char* glfwGetPrimarySelectionString(GLFWwindow* window, void)
|
||||
int glfwGetNativeKeyForName(const char* key_name, int case_sensitive)
|
||||
void glfwRequestWaylandFrameEvent(GLFWwindow *handle, unsigned long long id, GLFWwaylandframecallbackfunc callback)
|
||||
void glfwWaylandActivateWindow(GLFWwindow *handle, const char *activation_token)
|
||||
void glfwWaylandRunWithActivationToken(GLFWwindow *handle, GLFWactivationcallback cb, void *cb_data)
|
||||
bool glfwWaylandSetTitlebarColor(GLFWwindow *handle, uint32_t color, bool use_system_color)
|
||||
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)
|
||||
void glfwDBusSetUserNotificationHandler(GLFWDBusnotificationactivatedfun handler)
|
||||
int glfwSetX11LaunchCommand(GLFWwindow *handle, char **argv, int argc)
|
||||
void glfwSetX11WindowAsDock(int32_t x11_window_id)
|
||||
void glfwSetX11WindowStrut(int32_t x11_window_id, uint32_t dimensions[12])
|
||||
'''.splitlines():
|
||||
if line:
|
||||
functions.append(Function(line.strip(), check_fail=False))
|
||||
|
||||
77
glfw/glfw3.h
vendored
77
glfw/glfw3.h
vendored
@@ -512,6 +512,7 @@ typedef enum {
|
||||
* GLFW_LOCK_KEY_MODS input mode is set.
|
||||
*/
|
||||
#define GLFW_MOD_NUM_LOCK 0x0080
|
||||
#define GLFW_MOD_LAST GLFW_MOD_NUM_LOCK
|
||||
#define GLFW_LOCK_MASK (GLFW_MOD_NUM_LOCK | GLFW_MOD_CAPS_LOCK)
|
||||
|
||||
/*! @} */
|
||||
@@ -1715,7 +1716,21 @@ typedef void (* GLFWjoystickfun)(int,int);
|
||||
|
||||
typedef void (* GLFWuserdatafun)(unsigned long long, void*);
|
||||
typedef void (* GLFWtickcallback)(void*);
|
||||
typedef void (* GLFWactivationcallback)(GLFWwindow *window, const char *token, void *data);
|
||||
typedef bool (* GLFWdrawtextfun)(GLFWwindow *window, const char *text, uint32_t fg, uint32_t bg, uint8_t *output_buf, size_t width, size_t height, float x_offset, float y_offset, size_t right_margin);
|
||||
typedef char* (* GLFWcurrentselectionfun)(void);
|
||||
typedef void (* GLFWclipboarddatafreefun)(void* data);
|
||||
typedef struct GLFWDataChunk {
|
||||
const char *data;
|
||||
size_t sz;
|
||||
GLFWclipboarddatafreefun free;
|
||||
void *iter, *free_data;
|
||||
} GLFWDataChunk;
|
||||
typedef enum {
|
||||
GLFW_CLIPBOARD, GLFW_PRIMARY_SELECTION
|
||||
} GLFWClipboardType;
|
||||
typedef GLFWDataChunk (* GLFWclipboarditerfun)(const char *mime_type, void *iter, GLFWClipboardType ctype);
|
||||
typedef bool (* GLFWclipboardwritedatafun)(void *object, const char *data, size_t sz);
|
||||
|
||||
/*! @brief Video mode type.
|
||||
*
|
||||
@@ -1873,6 +1888,7 @@ GLFWAPI unsigned long long glfwAddTimer(monotonic_t interval, bool repeats, GLFW
|
||||
GLFWAPI void glfwUpdateTimer(unsigned long long timer_id, monotonic_t interval, bool enabled);
|
||||
GLFWAPI void glfwRemoveTimer(unsigned long long);
|
||||
GLFWAPI GLFWdrawtextfun glfwSetDrawTextFunction(GLFWdrawtextfun function);
|
||||
GLFWAPI GLFWcurrentselectionfun glfwSetCurrentSelectionCallback(GLFWcurrentselectionfun callback);
|
||||
|
||||
/*! @brief Terminates the GLFW library.
|
||||
*
|
||||
@@ -2768,6 +2784,7 @@ GLFWAPI void glfwWindowHintString(int hint, const char* value);
|
||||
GLFWAPI GLFWwindow* glfwCreateWindow(int width, int height, const char* title, GLFWmonitor* monitor, GLFWwindow* share);
|
||||
GLFWAPI bool glfwToggleFullscreen(GLFWwindow *window, unsigned int flags);
|
||||
GLFWAPI bool glfwIsFullscreen(GLFWwindow *window, unsigned int flags);
|
||||
GLFWAPI bool glfwAreSwapsAllowed(const GLFWwindow* window);
|
||||
|
||||
/*! @brief Destroys the specified window and its context.
|
||||
*
|
||||
@@ -5216,64 +5233,8 @@ GLFWAPI const char* glfwGetGamepadName(int jid);
|
||||
*/
|
||||
GLFWAPI int glfwGetGamepadState(int jid, GLFWgamepadstate* state);
|
||||
|
||||
/*! @brief Sets the clipboard to the specified string.
|
||||
*
|
||||
* This function sets the system clipboard to the specified, UTF-8 encoded
|
||||
* string.
|
||||
*
|
||||
* @param[in] window Deprecated. Any valid window or `NULL`.
|
||||
* @param[in] string A UTF-8 encoded string.
|
||||
*
|
||||
* @errors Possible errors include @ref GLFW_NOT_INITIALIZED and @ref
|
||||
* GLFW_PLATFORM_ERROR.
|
||||
*
|
||||
* @remark @wayland Clipboard is currently unimplemented.
|
||||
*
|
||||
* @pointer_lifetime The specified string is copied before this function
|
||||
* returns.
|
||||
*
|
||||
* @thread_safety This function must only be called from the main thread.
|
||||
*
|
||||
* @sa @ref clipboard
|
||||
* @sa @ref glfwGetClipboardString
|
||||
*
|
||||
* @since Added in version 3.0.
|
||||
*
|
||||
* @ingroup input
|
||||
*/
|
||||
GLFWAPI void glfwSetClipboardString(GLFWwindow* window, const char* string);
|
||||
|
||||
/*! @brief Returns the contents of the clipboard as a string.
|
||||
*
|
||||
* This function returns the contents of the system clipboard, if it contains
|
||||
* or is convertible to a UTF-8 encoded string. If the clipboard is empty or
|
||||
* if its contents cannot be converted, `NULL` is returned and a @ref
|
||||
* GLFW_FORMAT_UNAVAILABLE error is generated.
|
||||
*
|
||||
* @param[in] window Deprecated. Any valid window or `NULL`.
|
||||
* @return The contents of the clipboard as a UTF-8 encoded string, or `NULL`
|
||||
* if an [error](@ref error_handling) occurred.
|
||||
*
|
||||
* @errors Possible errors include @ref GLFW_NOT_INITIALIZED and @ref
|
||||
* GLFW_PLATFORM_ERROR.
|
||||
*
|
||||
* @remark @wayland Clipboard is currently unimplemented.
|
||||
*
|
||||
* @pointer_lifetime The returned string is allocated and freed by GLFW. You
|
||||
* should not free it yourself. It is valid until the next call to @ref
|
||||
* glfwGetClipboardString or @ref glfwSetClipboardString, or until the library
|
||||
* is terminated.
|
||||
*
|
||||
* @thread_safety This function must only be called from the main thread.
|
||||
*
|
||||
* @sa @ref clipboard
|
||||
* @sa @ref glfwSetClipboardString
|
||||
*
|
||||
* @since Added in version 3.0.
|
||||
*
|
||||
* @ingroup input
|
||||
*/
|
||||
GLFWAPI const char* glfwGetClipboardString(GLFWwindow* window);
|
||||
GLFWAPI void glfwSetClipboardDataTypes(GLFWClipboardType clipboard_type, const char* const *mime_types, size_t num_mime_types, GLFWclipboarditerfun get_iter);
|
||||
GLFWAPI void glfwGetClipboard(GLFWClipboardType clipboard_type, const char* mime_type, GLFWclipboardwritedatafun write_data, void *object);
|
||||
|
||||
/*! @brief Returns the GLFW time.
|
||||
*
|
||||
|
||||
9
glfw/init.c
vendored
9
glfw/init.c
vendored
@@ -68,6 +68,8 @@ static void terminate(void)
|
||||
int i;
|
||||
|
||||
memset(&_glfw.callbacks, 0, sizeof(_glfw.callbacks));
|
||||
_glfw_free_clipboard_data(&_glfw.clipboard);
|
||||
_glfw_free_clipboard_data(&_glfw.primary);
|
||||
|
||||
while (_glfw.windowListHead)
|
||||
glfwDestroyWindow((GLFWwindow*) _glfw.windowListHead);
|
||||
@@ -386,3 +388,10 @@ GLFWAPI GLFWdrawtextfun glfwSetDrawTextFunction(GLFWdrawtextfun cbfun)
|
||||
_GLFW_SWAP_POINTERS(_glfw.callbacks.draw_text, cbfun);
|
||||
return cbfun;
|
||||
}
|
||||
|
||||
GLFWAPI GLFWcurrentselectionfun glfwSetCurrentSelectionCallback(GLFWcurrentselectionfun cbfun)
|
||||
{
|
||||
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
|
||||
_GLFW_SWAP_POINTERS(_glfw.callbacks.get_current_selection, cbfun);
|
||||
return cbfun;
|
||||
}
|
||||
|
||||
51
glfw/input.c
vendored
51
glfw/input.c
vendored
@@ -1526,36 +1526,41 @@ GLFWAPI int glfwGetGamepadState(int jid, GLFWgamepadstate* state)
|
||||
return true;
|
||||
}
|
||||
|
||||
GLFWAPI void glfwSetClipboardString(GLFWwindow* handle UNUSED, const char* string)
|
||||
{
|
||||
assert(string != NULL);
|
||||
void _glfw_free_clipboard_data(_GLFWClipboardData *cd) {
|
||||
if (cd->mime_types) {
|
||||
for (size_t i = 0; i < cd->num_mime_types; i++) free((void*)cd->mime_types[i]);
|
||||
free((void*)cd->mime_types);
|
||||
}
|
||||
memset(cd, 0, sizeof(cd[0]));
|
||||
}
|
||||
|
||||
GLFWAPI void glfwGetClipboard(GLFWClipboardType clipboard_type, const char* mime_type, GLFWclipboardwritedatafun write_data, void *object) {
|
||||
_GLFW_REQUIRE_INIT();
|
||||
_glfwPlatformSetClipboardString(string);
|
||||
_glfwPlatformGetClipboard(clipboard_type, mime_type, write_data, object);
|
||||
}
|
||||
|
||||
GLFWAPI const char* glfwGetClipboardString(GLFWwindow* handle UNUSED)
|
||||
{
|
||||
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
|
||||
return _glfwPlatformGetClipboardString();
|
||||
}
|
||||
|
||||
#if defined(_GLFW_X11) || defined(_GLFW_WAYLAND)
|
||||
GLFWAPI void glfwSetPrimarySelectionString(GLFWwindow* handle UNUSED, const char* string)
|
||||
{
|
||||
assert(string != NULL);
|
||||
|
||||
GLFWAPI void glfwSetClipboardDataTypes(GLFWClipboardType clipboard_type, const char* const *mime_types, size_t num_mime_types, GLFWclipboarditerfun get_data) {
|
||||
assert(mime_types != NULL);
|
||||
assert(get_data != NULL);
|
||||
_GLFW_REQUIRE_INIT();
|
||||
_glfwPlatformSetPrimarySelectionString(string);
|
||||
_GLFWClipboardData *cd = NULL;
|
||||
switch(clipboard_type) {
|
||||
case GLFW_CLIPBOARD: cd = &_glfw.clipboard; break;
|
||||
case GLFW_PRIMARY_SELECTION: cd = &_glfw.primary; break;
|
||||
}
|
||||
_glfw_free_clipboard_data(cd);
|
||||
cd->get_data = get_data;
|
||||
cd->mime_types = calloc(num_mime_types, sizeof(char*));
|
||||
cd->num_mime_types = 0;
|
||||
cd->ctype = clipboard_type;
|
||||
for (size_t i = 0; i < num_mime_types; i++) {
|
||||
if (mime_types[i]) {
|
||||
cd->mime_types[cd->num_mime_types++] = _glfw_strdup(mime_types[i]);
|
||||
}
|
||||
}
|
||||
_glfwPlatformSetClipboard(clipboard_type);
|
||||
}
|
||||
|
||||
GLFWAPI const char* glfwGetPrimarySelectionString(GLFWwindow* handle UNUSED)
|
||||
{
|
||||
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
|
||||
return _glfwPlatformGetPrimarySelectionString();
|
||||
}
|
||||
#endif
|
||||
|
||||
GLFWAPI monotonic_t glfwGetTime(void)
|
||||
{
|
||||
_GLFW_REQUIRE_INIT_OR_RETURN(0);
|
||||
|
||||
30
glfw/internal.h
vendored
30
glfw/internal.h
vendored
@@ -34,6 +34,9 @@
|
||||
#endif
|
||||
|
||||
#define arraysz(x) (sizeof(x)/sizeof(x[0]))
|
||||
#define MAX(x, y) __extension__ ({ \
|
||||
__typeof__ (x) a = (x); __typeof__ (y) b = (y); \
|
||||
a > b ? a : b;})
|
||||
|
||||
#if defined(GLFW_INCLUDE_GLCOREARB) || \
|
||||
defined(GLFW_INCLUDE_ES1) || \
|
||||
@@ -52,6 +55,8 @@
|
||||
#define GLFW_INCLUDE_NONE
|
||||
#include "glfw3.h"
|
||||
|
||||
#define EGL_PRESENT_OPAQUE_EXT 0x31df
|
||||
|
||||
#define _GLFW_INSERT_FIRST 0
|
||||
#define _GLFW_INSERT_LAST 1
|
||||
|
||||
@@ -439,6 +444,11 @@ struct _GLFWwindow
|
||||
bool rawMouseMotion;
|
||||
|
||||
_GLFWcontext context;
|
||||
#ifdef _GLFW_WAYLAND
|
||||
bool swaps_disallowed;
|
||||
#else
|
||||
const bool swaps_disallowed;
|
||||
#endif
|
||||
|
||||
struct {
|
||||
GLFWwindowposfun pos;
|
||||
@@ -554,6 +564,13 @@ struct _GLFWmutex
|
||||
_GLFW_PLATFORM_MUTEX_STATE;
|
||||
};
|
||||
|
||||
typedef struct _GLFWClipboardData {
|
||||
const char** mime_types;
|
||||
size_t num_mime_types;
|
||||
GLFWclipboarditerfun get_data;
|
||||
GLFWClipboardType ctype;
|
||||
} _GLFWClipboardData;
|
||||
|
||||
// Library global data
|
||||
//
|
||||
struct _GLFWlibrary
|
||||
@@ -568,6 +585,8 @@ struct _GLFWlibrary
|
||||
int refreshRate;
|
||||
} hints;
|
||||
|
||||
_GLFWClipboardData primary, clipboard;
|
||||
|
||||
_GLFWerror* errorListHead;
|
||||
_GLFWcursor* cursorListHead;
|
||||
_GLFWwindow* windowListHead;
|
||||
@@ -614,6 +633,7 @@ struct _GLFWlibrary
|
||||
GLFWjoystickfun joystick;
|
||||
GLFWapplicationclosefun application_close;
|
||||
GLFWdrawtextfun draw_text;
|
||||
GLFWcurrentselectionfun get_current_selection;
|
||||
} callbacks;
|
||||
|
||||
|
||||
@@ -666,12 +686,8 @@ void _glfwPlatformGetVideoMode(_GLFWmonitor* monitor, GLFWvidmode* mode);
|
||||
bool _glfwPlatformGetGammaRamp(_GLFWmonitor* monitor, GLFWgammaramp* ramp);
|
||||
void _glfwPlatformSetGammaRamp(_GLFWmonitor* monitor, const GLFWgammaramp* ramp);
|
||||
|
||||
void _glfwPlatformSetClipboardString(const char* string);
|
||||
const char* _glfwPlatformGetClipboardString(void);
|
||||
#if defined(_GLFW_X11) || defined(_GLFW_WAYLAND)
|
||||
void _glfwPlatformSetPrimarySelectionString(const char* string);
|
||||
const char* _glfwPlatformGetPrimarySelectionString(void);
|
||||
#endif
|
||||
void _glfwPlatformSetClipboard(GLFWClipboardType t);
|
||||
void _glfwPlatformGetClipboard(GLFWClipboardType clipboard_type, const char* mime_type, GLFWclipboardwritedatafun write_data, void *object);
|
||||
|
||||
bool _glfwPlatformInitJoysticks(void);
|
||||
void _glfwPlatformTerminateJoysticks(void);
|
||||
@@ -847,3 +863,5 @@ void _glfwPlatformUpdateTimer(unsigned long long timer_id, monotonic_t interval,
|
||||
void _glfwPlatformRemoveTimer(unsigned long long timer_id);
|
||||
|
||||
char* _glfw_strdup(const char* source);
|
||||
|
||||
void _glfw_free_clipboard_data(_GLFWClipboardData *cd);
|
||||
|
||||
4
glfw/linux_desktop_settings.c
vendored
4
glfw/linux_desktop_settings.c
vendored
@@ -83,8 +83,8 @@ process_settings_dict(DBusMessageIter *array_iter, void(process_setting)(const c
|
||||
|
||||
HANDLER(process_desktop_settings)
|
||||
cursor_theme_changed = false;
|
||||
DBusMessageIter root, array, item, settings;
|
||||
dbus_message_iter_init(msg, &root);
|
||||
DBusMessageIter root, array, item, settings;
|
||||
dbus_message_iter_init(msg, &root);
|
||||
#define die(...) { _glfwInputError(GLFW_PLATFORM_ERROR, __VA_ARGS__); return; }
|
||||
if (dbus_message_iter_get_arg_type(&root) != DBUS_TYPE_ARRAY) die("Reply to request for desktop settings is not an array");
|
||||
dbus_message_iter_recurse(&root, &array);
|
||||
|
||||
@@ -77,7 +77,8 @@
|
||||
"unstable/idle-inhibit/idle-inhibit-unstable-v1.xml",
|
||||
"unstable/xdg-decoration/xdg-decoration-unstable-v1.xml",
|
||||
"unstable/primary-selection/primary-selection-unstable-v1.xml",
|
||||
"unstable/text-input/text-input-unstable-v3.xml"
|
||||
"unstable/text-input/text-input-unstable-v3.xml",
|
||||
"staging/xdg-activation/xdg-activation-v1.xml"
|
||||
],
|
||||
"sources": [
|
||||
"wl_init.c",
|
||||
|
||||
6
glfw/window.c
vendored
6
glfw/window.c
vendored
@@ -71,7 +71,7 @@ void _glfwInputWindowFocus(_GLFWwindow* window, bool focused)
|
||||
_glfw.focusedWindowId = window->id;
|
||||
}
|
||||
|
||||
_GLFWwindow* _glfwFocusedWindow() {
|
||||
_GLFWwindow* _glfwFocusedWindow(void) {
|
||||
if (_glfw.focusedWindowId) {
|
||||
_GLFWwindow *w = _glfw.windowListHead;
|
||||
while (w) {
|
||||
@@ -1067,6 +1067,10 @@ GLFWAPI bool glfwIsFullscreen(GLFWwindow* wh, unsigned int flags) {
|
||||
return _glfwPlatformIsFullscreen((_GLFWwindow*)wh, flags);
|
||||
}
|
||||
|
||||
GLFWAPI bool glfwAreSwapsAllowed(const GLFWwindow* wh) {
|
||||
return !(((_GLFWwindow*)wh)->swaps_disallowed);
|
||||
}
|
||||
|
||||
GLFWAPI void glfwSetWindowUserPointer(GLFWwindow* handle, void* pointer)
|
||||
{
|
||||
_GLFWwindow* window = (_GLFWwindow*) handle;
|
||||
|
||||
33
glfw/wl_client_side_decorations.c
vendored
33
glfw/wl_client_side_decorations.c
vendored
@@ -31,13 +31,13 @@ static void
|
||||
build_blur_kernel(kernel_type *blur_kernel, const size_t size, kernel_type sigma) {
|
||||
// 1D Normalized Gaussian
|
||||
const kernel_type half = size / (kernel_type)2;
|
||||
kernel_type sum = 0;
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
kernel_type f = (i - half);
|
||||
blur_kernel[i] = (kernel_type)exp(- f * f / sigma);
|
||||
sum += blur_kernel[i];
|
||||
}
|
||||
for (size_t i = 0; i < size; i++) blur_kernel[i] /= sum;
|
||||
kernel_type sum = 0;
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
kernel_type f = (i - half);
|
||||
blur_kernel[i] = (kernel_type)exp(- f * f / sigma);
|
||||
sum += blur_kernel[i];
|
||||
}
|
||||
for (size_t i = 0; i < size; i++) blur_kernel[i] /= sum;
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -104,9 +104,10 @@ init_buffer_pair(_GLFWWaylandBufferPair *pair, size_t width, size_t height, unsi
|
||||
|
||||
static bool
|
||||
window_has_buffer(_GLFWwindow *window, struct wl_buffer *q) {
|
||||
#define Q(which) decs.which.buffer.a == q || decs.which.buffer.b == q
|
||||
return Q(left) || Q(top) || Q(right) || Q(bottom);
|
||||
#define Q(which) if (decs.which.buffer.a == q) { decs.which.buffer.a_needs_to_be_destroyed = false; return true; } if (decs.which.buffer.b == q) { decs.which.buffer.b_needs_to_be_destroyed = false; return true; }
|
||||
Q(left); Q(top); Q(right); Q(bottom);
|
||||
#undef Q
|
||||
return false;
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -122,10 +123,12 @@ static void
|
||||
alloc_buffer_pair(uintptr_t window_id, _GLFWWaylandBufferPair *pair, struct wl_shm_pool *pool, uint8_t *data, size_t *offset) {
|
||||
pair->data.a = data + *offset;
|
||||
pair->a = wl_shm_pool_create_buffer(pool, *offset, pair->width, pair->height, pair->stride, WL_SHM_FORMAT_ARGB8888);
|
||||
pair->a_needs_to_be_destroyed = true;
|
||||
wl_buffer_add_listener(pair->a, &handle_buffer_events, (void*)window_id);
|
||||
*offset += pair->size_in_bytes;
|
||||
pair->data.b = data + *offset;
|
||||
pair->b = wl_shm_pool_create_buffer(pool, *offset, pair->width, pair->height, pair->stride, WL_SHM_FORMAT_ARGB8888);
|
||||
pair->b_needs_to_be_destroyed = true;
|
||||
wl_buffer_add_listener(pair->b, &handle_buffer_events, (void*)window_id);
|
||||
*offset += pair->size_in_bytes;
|
||||
pair->front = pair->a; pair->back = pair->b;
|
||||
@@ -331,6 +334,8 @@ free_csd_surfaces(_GLFWwindow *window) {
|
||||
static void
|
||||
free_csd_buffers(_GLFWwindow *window) {
|
||||
#define d(which) { \
|
||||
if (decs.which.buffer.a_needs_to_be_destroyed && decs.which.buffer.a) wl_buffer_destroy(decs.which.buffer.a); \
|
||||
if (decs.which.buffer.b_needs_to_be_destroyed && decs.which.buffer.b) wl_buffer_destroy(decs.which.buffer.b); \
|
||||
memset(&decs.which.buffer, 0, sizeof(_GLFWWaylandBufferPair)); \
|
||||
}
|
||||
d(left); d(top); d(right); d(bottom);
|
||||
@@ -353,9 +358,10 @@ create_csd_surfaces(_GLFWwindow *window, _GLFWWaylandCSDEdge *s) {
|
||||
}
|
||||
|
||||
#define damage_csd(which, xbuffer) \
|
||||
wl_surface_attach(decs.which.surface, xbuffer, 0, 0); \
|
||||
wl_surface_attach(decs.which.surface, (xbuffer), 0, 0); \
|
||||
wl_surface_damage(decs.which.surface, 0, 0, decs.which.buffer.width, decs.which.buffer.height); \
|
||||
wl_surface_commit(decs.which.surface)
|
||||
wl_surface_commit(decs.which.surface); \
|
||||
if (decs.which.buffer.a == (xbuffer)) { decs.which.buffer.a_needs_to_be_destroyed = false; } else { decs.which.buffer.b_needs_to_be_destroyed = false; }
|
||||
|
||||
bool
|
||||
ensure_csd_resources(_GLFWwindow *window) {
|
||||
@@ -450,8 +456,5 @@ set_titlebar_color(_GLFWwindow *window, uint32_t color, bool use_system_color) {
|
||||
decs.use_custom_titlebar_color = use_custom_color;
|
||||
decs.titlebar_color = color;
|
||||
}
|
||||
if (window->decorated && decs.top.surface) {
|
||||
update_title_bar(window);
|
||||
damage_csd(top, decs.top.buffer.front);
|
||||
}
|
||||
change_csd_title(window);
|
||||
}
|
||||
|
||||
47
glfw/wl_init.c
vendored
47
glfw/wl_init.c
vendored
@@ -99,7 +99,7 @@ static void pointerHandleEnter(void* data UNUSED,
|
||||
return;
|
||||
}
|
||||
window->wl.decorations.focus = focus;
|
||||
_glfw.wl.serial = serial;
|
||||
_glfw.wl.serial = serial; _glfw.wl.input_serial = serial;
|
||||
_glfw.wl.pointerFocus = window;
|
||||
|
||||
window->wl.hovered = true;
|
||||
@@ -144,8 +144,12 @@ static void setCursor(GLFWCursorShape shape, _GLFWwindow* window)
|
||||
if (!image)
|
||||
return;
|
||||
if (image->width % scale || image->height % scale) {
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR, "WARNING: Cursor image size: %dx%d is not a multiple of window scale: %d. This will"
|
||||
" cause some compositors such as GNOME to crash. See https://github.com/kovidgoyal/kitty/issues/4878", image->width, image->height, scale);
|
||||
static uint32_t warned_width = 0, warned_height = 0;
|
||||
if (warned_width != image->width || warned_height != image->height) {
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR, "WARNING: Cursor image size: %dx%d is not a multiple of window scale: %d. This will"
|
||||
" cause some compositors such as GNOME to crash. See https://github.com/kovidgoyal/kitty/issues/4878", image->width, image->height, scale);
|
||||
warned_width = image->width; warned_height = image->height;
|
||||
}
|
||||
}
|
||||
|
||||
buffer = wl_cursor_image_get_buffer(image);
|
||||
@@ -307,7 +311,7 @@ static void pointerHandleButton(void* data UNUSED,
|
||||
if (window->wl.decorations.focus != CENTRAL_WINDOW)
|
||||
return;
|
||||
|
||||
_glfw.wl.serial = serial;
|
||||
_glfw.wl.serial = serial; _glfw.wl.input_serial = serial;
|
||||
|
||||
/* Makes left, right and middle 0, 1 and 2. Overall order follows evdev
|
||||
* codes. */
|
||||
@@ -459,7 +463,7 @@ static void keyboardHandleEnter(void* data UNUSED,
|
||||
return;
|
||||
}
|
||||
|
||||
_glfw.wl.serial = serial;
|
||||
_glfw.wl.serial = serial; _glfw.wl.input_serial = serial;
|
||||
_glfw.wl.keyboardFocusId = window->id;
|
||||
_glfwInputWindowFocus(window, true);
|
||||
uint32_t* key;
|
||||
@@ -512,7 +516,7 @@ static void keyboardHandleKey(void* data UNUSED,
|
||||
return;
|
||||
int action = state == WL_KEYBOARD_KEY_STATE_PRESSED ? GLFW_PRESS : GLFW_RELEASE;
|
||||
|
||||
_glfw.wl.serial = serial;
|
||||
_glfw.wl.serial = serial; _glfw.wl.input_serial = serial;
|
||||
glfw_xkb_handle_key_event(window, &_glfw.wl.xkb, key, action);
|
||||
|
||||
if (action == GLFW_PRESS && _glfw.wl.keyboardRepeatRate > 0 && glfw_xkb_should_repeat(&_glfw.wl.xkb, key))
|
||||
@@ -535,7 +539,7 @@ static void keyboardHandleModifiers(void* data UNUSED,
|
||||
uint32_t modsLocked,
|
||||
uint32_t group)
|
||||
{
|
||||
_glfw.wl.serial = serial;
|
||||
_glfw.wl.serial = serial; _glfw.wl.input_serial = serial;
|
||||
glfw_xkb_update_modifiers(&_glfw.wl.xkb, modsDepressed, modsLatched, modsLocked, 0, 0, group);
|
||||
}
|
||||
|
||||
@@ -573,10 +577,7 @@ static void seatHandleCapabilities(void* data UNUSED,
|
||||
{
|
||||
wl_pointer_destroy(_glfw.wl.pointer);
|
||||
_glfw.wl.pointer = NULL;
|
||||
if (_glfw.wl.cursorAnimationTimer) {
|
||||
removeTimer(&_glfw.wl.eventLoopData, _glfw.wl.cursorAnimationTimer);
|
||||
_glfw.wl.cursorAnimationTimer = 0;
|
||||
}
|
||||
if (_glfw.wl.cursorAnimationTimer) toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.cursorAnimationTimer, 0);
|
||||
}
|
||||
|
||||
if ((caps & WL_SEAT_CAPABILITY_KEYBOARD) && !_glfw.wl.keyboard)
|
||||
@@ -589,10 +590,7 @@ static void seatHandleCapabilities(void* data UNUSED,
|
||||
wl_keyboard_destroy(_glfw.wl.keyboard);
|
||||
_glfw.wl.keyboard = NULL;
|
||||
_glfw.wl.keyboardFocusId = 0;
|
||||
if (_glfw.wl.keyRepeatInfo.keyRepeatTimer) {
|
||||
removeTimer(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer);
|
||||
_glfw.wl.keyRepeatInfo.keyRepeatTimer = 0;
|
||||
}
|
||||
if (_glfw.wl.keyRepeatInfo.keyRepeatTimer) toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -721,6 +719,9 @@ static void registryHandleGlobal(void* data UNUSED,
|
||||
_glfwSetupWaylandPrimarySelectionDevice();
|
||||
}
|
||||
}
|
||||
else if (strstr(interface, "xdg_activation_v1") != 0) {
|
||||
_glfw.wl.xdg_activation_v1 = wl_registry_bind(registry, name, &xdg_activation_v1_interface, 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -884,6 +885,14 @@ int _glfwPlatformInit(void)
|
||||
|
||||
void _glfwPlatformTerminate(void)
|
||||
{
|
||||
if (_glfw.wl.activation_requests.array) {
|
||||
for (size_t i=0; i < _glfw.wl.activation_requests.sz; i++) {
|
||||
glfw_wl_xdg_activation_request *r = _glfw.wl.activation_requests.array + i;
|
||||
if (r->callback) r->callback(NULL, NULL, r->callback_data);
|
||||
xdg_activation_token_v1_destroy(r->token);
|
||||
}
|
||||
free(_glfw.wl.activation_requests.array);
|
||||
}
|
||||
_glfwTerminateEGL();
|
||||
if (_glfw.wl.egl.handle)
|
||||
{
|
||||
@@ -928,6 +937,8 @@ void _glfwPlatformTerminate(void)
|
||||
zwp_idle_inhibit_manager_v1_destroy(_glfw.wl.idleInhibitManager);
|
||||
if (_glfw.wl.dataSourceForClipboard)
|
||||
wl_data_source_destroy(_glfw.wl.dataSourceForClipboard);
|
||||
if (_glfw.wl.dataSourceForPrimarySelection)
|
||||
zwp_primary_selection_source_v1_destroy(_glfw.wl.dataSourceForPrimarySelection);
|
||||
for (size_t doi=0; doi < arraysz(_glfw.wl.dataOffers); doi++) {
|
||||
if (_glfw.wl.dataOffers[doi].id) {
|
||||
destroy_data_offer(&_glfw.wl.dataOffers[doi]);
|
||||
@@ -941,6 +952,8 @@ void _glfwPlatformTerminate(void)
|
||||
zwp_primary_selection_device_v1_destroy(_glfw.wl.primarySelectionDevice);
|
||||
if (_glfw.wl.primarySelectionDeviceManager)
|
||||
zwp_primary_selection_device_manager_v1_destroy(_glfw.wl.primarySelectionDeviceManager);
|
||||
if (_glfw.wl.xdg_activation_v1)
|
||||
xdg_activation_v1_destroy(_glfw.wl.xdg_activation_v1);
|
||||
if (_glfw.wl.registry)
|
||||
wl_registry_destroy(_glfw.wl.registry);
|
||||
if (_glfw.wl.display)
|
||||
@@ -949,10 +962,6 @@ void _glfwPlatformTerminate(void)
|
||||
wl_display_disconnect(_glfw.wl.display);
|
||||
}
|
||||
finalizePollData(&_glfw.wl.eventLoopData);
|
||||
free(_glfw.wl.clipboardString); _glfw.wl.clipboardString = NULL;
|
||||
free(_glfw.wl.primarySelectionString); _glfw.wl.primarySelectionString = NULL;
|
||||
free(_glfw.wl.pasteString); _glfw.wl.pasteString = NULL;
|
||||
|
||||
}
|
||||
|
||||
const char* _glfwPlatformGetVersionString(void)
|
||||
|
||||
34
glfw/wl_platform.h
vendored
34
glfw/wl_platform.h
vendored
@@ -58,7 +58,9 @@ typedef VkBool32 (APIENTRY *PFN_vkGetPhysicalDeviceWaylandPresentationSupportKHR
|
||||
#include "wayland-pointer-constraints-unstable-v1-client-protocol.h"
|
||||
#include "wayland-idle-inhibit-unstable-v1-client-protocol.h"
|
||||
#include "wayland-primary-selection-unstable-v1-client-protocol.h"
|
||||
#include "wayland-primary-selection-unstable-v1-client-protocol.h"
|
||||
#include "wl_text_input.h"
|
||||
#include "wayland-xdg-activation-v1-client-protocol.h"
|
||||
|
||||
#define _glfw_dlopen(name) dlopen(name, RTLD_LAZY | RTLD_LOCAL)
|
||||
#define _glfw_dlclose(handle) dlclose(handle)
|
||||
@@ -102,6 +104,7 @@ typedef struct _GLFWWaylandBufferPair {
|
||||
struct { uint8_t *a, *b, *front, *back; } data;
|
||||
bool has_pending_update;
|
||||
size_t size_in_bytes, width, height, stride;
|
||||
bool a_needs_to_be_destroyed, b_needs_to_be_destroyed;
|
||||
} _GLFWWaylandBufferPair;
|
||||
|
||||
typedef struct _GLFWWaylandCSDEdge {
|
||||
@@ -116,14 +119,22 @@ typedef enum WaylandWindowState {
|
||||
TOPLEVEL_STATE_NONE = 0,
|
||||
TOPLEVEL_STATE_MAXIMIZED = 1,
|
||||
TOPLEVEL_STATE_FULLSCREEN = 2,
|
||||
TOPLEVEL_STATE_RESIZING = 4,
|
||||
TOPLEVEL_STATE_ACTIVATED = 8,
|
||||
TOPLEVEL_STATE_TILED_LEFT = 16,
|
||||
TOPLEVEL_STATE_TILED_RIGHT = 32,
|
||||
TOPLEVEL_STATE_TILED_TOP = 64,
|
||||
TOPLEVEL_STATE_TILED_BOTTOM = 128,
|
||||
TOPLEVEL_STATE_RESIZING = 4,
|
||||
TOPLEVEL_STATE_ACTIVATED = 8,
|
||||
TOPLEVEL_STATE_TILED_LEFT = 16,
|
||||
TOPLEVEL_STATE_TILED_RIGHT = 32,
|
||||
TOPLEVEL_STATE_TILED_TOP = 64,
|
||||
TOPLEVEL_STATE_TILED_BOTTOM = 128,
|
||||
} WaylandWindowState;
|
||||
|
||||
typedef struct glfw_wl_xdg_activation_request {
|
||||
GLFWid window_id;
|
||||
GLFWactivationcallback callback;
|
||||
void *callback_data;
|
||||
uintptr_t request_id;
|
||||
void *token;
|
||||
} glfw_wl_xdg_activation_request;
|
||||
|
||||
|
||||
static const WaylandWindowState TOPLEVEL_STATE_DOCKED = TOPLEVEL_STATE_MAXIMIZED | TOPLEVEL_STATE_FULLSCREEN | TOPLEVEL_STATE_TILED_TOP | TOPLEVEL_STATE_TILED_LEFT | TOPLEVEL_STATE_TILED_RIGHT | TOPLEVEL_STATE_TILED_BOTTOM;
|
||||
|
||||
@@ -276,13 +287,14 @@ typedef struct _GLFWlibraryWayland
|
||||
struct zwp_primary_selection_device_manager_v1* primarySelectionDeviceManager;
|
||||
struct zwp_primary_selection_device_v1* primarySelectionDevice;
|
||||
struct zwp_primary_selection_source_v1* dataSourceForPrimarySelection;
|
||||
struct xdg_activation_v1* xdg_activation_v1;
|
||||
|
||||
int compositorVersion;
|
||||
int seatVersion;
|
||||
|
||||
struct wl_surface* cursorSurface;
|
||||
GLFWCursorShape cursorPreviousShape;
|
||||
uint32_t serial;
|
||||
uint32_t serial, input_serial;
|
||||
|
||||
int32_t keyboardRepeatRate;
|
||||
monotonic_t keyboardRepeatDelay;
|
||||
@@ -316,12 +328,14 @@ typedef struct _GLFWlibraryWayland
|
||||
PFN_wl_egl_window_resize window_resize;
|
||||
} egl;
|
||||
|
||||
struct {
|
||||
glfw_wl_xdg_activation_request *array;
|
||||
size_t capacity, sz;
|
||||
} activation_requests;
|
||||
|
||||
EventLoopData eventLoopData;
|
||||
char* pasteString;
|
||||
char* clipboardString;
|
||||
size_t dataOffersCounter;
|
||||
_GLFWWaylandDataOffer dataOffers[8];
|
||||
char* primarySelectionString;
|
||||
} _GLFWlibraryWayland;
|
||||
|
||||
// Wayland-specific per-monitor data
|
||||
|
||||
2
glfw/wl_text_input.c
vendored
2
glfw/wl_text_input.c
vendored
@@ -87,7 +87,7 @@ static void
|
||||
text_input_done(void *data UNUSED, struct zwp_text_input_v3 *txt_input UNUSED, uint32_t serial) {
|
||||
debug("text-input: done event: serial: %u current_commit_serial: %u\n", serial, commit_serial);
|
||||
if (serial != commit_serial) {
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: text_input_done serial mismatch, expected=%u got=%u\n", commit_serial, serial);
|
||||
if (serial > commit_serial) _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: text_input_done serial mismatch, expected=%u got=%u\n", commit_serial, serial);
|
||||
return;
|
||||
}
|
||||
if (pending_pre_edit) {
|
||||
|
||||
504
glfw/wl_window.c
vendored
504
glfw/wl_window.c
vendored
@@ -44,6 +44,59 @@
|
||||
|
||||
#define debug(...) if (_glfw.hints.init.debugRendering) fprintf(stderr, __VA_ARGS__);
|
||||
|
||||
static void
|
||||
activation_token_done(void *data, struct xdg_activation_token_v1 *xdg_token, const char *token) {
|
||||
for (size_t i = 0; i < _glfw.wl.activation_requests.sz; i++) {
|
||||
glfw_wl_xdg_activation_request *r = _glfw.wl.activation_requests.array + i;
|
||||
if (r->request_id == (uintptr_t)data) {
|
||||
_GLFWwindow *window = _glfwWindowForId(r->window_id);
|
||||
if (r->callback) r->callback((GLFWwindow*)window, token, r->callback_data);
|
||||
remove_i_from_array(_glfw.wl.activation_requests.array, i, _glfw.wl.activation_requests.sz);
|
||||
break;
|
||||
}
|
||||
}
|
||||
xdg_activation_token_v1_destroy(xdg_token);
|
||||
}
|
||||
|
||||
|
||||
static const struct
|
||||
xdg_activation_token_v1_listener activation_token_listener = {
|
||||
.done = &activation_token_done,
|
||||
};
|
||||
|
||||
|
||||
static bool
|
||||
get_activation_token(
|
||||
_GLFWwindow *window, uint32_t serial, GLFWactivationcallback cb, void *cb_data
|
||||
) {
|
||||
#define fail(msg) { _glfwInputError(GLFW_PLATFORM_ERROR, msg); if (cb) cb((GLFWwindow*)window, NULL, cb_data); return false; }
|
||||
if (_glfw.wl.xdg_activation_v1 == NULL) fail("Wayland: activation requests not supported by this Wayland compositor");
|
||||
struct xdg_activation_token_v1 *token = xdg_activation_v1_get_activation_token(_glfw.wl.xdg_activation_v1);
|
||||
if (token == NULL) fail("Wayland: failed to create activation request token");
|
||||
if (_glfw.wl.activation_requests.capacity < _glfw.wl.activation_requests.sz + 1) {
|
||||
_glfw.wl.activation_requests.capacity = MAX(64u, _glfw.wl.activation_requests.capacity * 2);
|
||||
_glfw.wl.activation_requests.array = realloc(_glfw.wl.activation_requests.array, _glfw.wl.activation_requests.capacity);
|
||||
if (!_glfw.wl.activation_requests.array) {
|
||||
_glfw.wl.activation_requests.capacity = 0;
|
||||
fail("Wayland: Out of memory while allocation activation request");
|
||||
}
|
||||
}
|
||||
glfw_wl_xdg_activation_request *r = _glfw.wl.activation_requests.array + _glfw.wl.activation_requests.sz++;
|
||||
memset(r, 0, sizeof(*r));
|
||||
static uintptr_t rq = 0;
|
||||
r->window_id = window->id;
|
||||
r->callback = cb; r->callback_data = cb_data;
|
||||
r->request_id = ++rq; r->token = token;
|
||||
if (serial != 0)
|
||||
xdg_activation_token_v1_set_serial(token, serial, _glfw.wl.seat);
|
||||
|
||||
xdg_activation_token_v1_set_surface(token, window->wl.surface);
|
||||
xdg_activation_token_v1_add_listener(token, &activation_token_listener, (void*)r->request_id);
|
||||
xdg_activation_token_v1_commit(token);
|
||||
return true;
|
||||
#undef fail
|
||||
}
|
||||
|
||||
static struct wl_buffer* createShmBuffer(const GLFWimage* image, bool is_opaque, bool init_data)
|
||||
{
|
||||
struct wl_shm_pool* pool;
|
||||
@@ -199,7 +252,7 @@ static bool checkScaleChange(_GLFWwindow* window)
|
||||
}
|
||||
|
||||
// Makes the surface considered as XRGB instead of ARGB.
|
||||
static void setOpaqueRegion(_GLFWwindow* window)
|
||||
static void setOpaqueRegion(_GLFWwindow* window, bool commit_surface)
|
||||
{
|
||||
struct wl_region* region;
|
||||
|
||||
@@ -209,10 +262,22 @@ static void setOpaqueRegion(_GLFWwindow* window)
|
||||
|
||||
wl_region_add(region, 0, 0, window->wl.width, window->wl.height);
|
||||
wl_surface_set_opaque_region(window->wl.surface, region);
|
||||
wl_surface_commit(window->wl.surface);
|
||||
if (commit_surface) wl_surface_commit(window->wl.surface);
|
||||
wl_region_destroy(region);
|
||||
}
|
||||
|
||||
static void
|
||||
swap_buffers(_GLFWwindow *window) {
|
||||
// this will attach the buffer to the surface,
|
||||
// the client is responsible for clearing the buffer to an appropriate blank
|
||||
window->swaps_disallowed = false;
|
||||
GLFWwindow *current = glfwGetCurrentContext();
|
||||
bool context_is_current = ((_GLFWwindow*)current)->id == window->id;
|
||||
if (!context_is_current) glfwMakeContextCurrent((GLFWwindow*)window);
|
||||
window->context.swapBuffers(window);
|
||||
if (!context_is_current) glfwMakeContextCurrent(current);
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
resizeFramebuffer(_GLFWwindow* window) {
|
||||
@@ -221,7 +286,7 @@ resizeFramebuffer(_GLFWwindow* window) {
|
||||
int scaledHeight = window->wl.height * scale;
|
||||
debug("Resizing framebuffer to: %dx%d at scale: %d\n", window->wl.width, window->wl.height, scale);
|
||||
wl_egl_window_resize(window->wl.native, scaledWidth, scaledHeight, 0, 0);
|
||||
if (!window->wl.transparent) setOpaqueRegion(window);
|
||||
if (!window->wl.transparent) setOpaqueRegion(window, false);
|
||||
_glfwInputFramebufferSize(window, scaledWidth, scaledHeight);
|
||||
}
|
||||
|
||||
@@ -236,9 +301,9 @@ clipboard_mime(void) {
|
||||
}
|
||||
|
||||
static bool
|
||||
dispatchChangesAfterConfigure(_GLFWwindow *window, int32_t width, int32_t height) {
|
||||
dispatchChangesAfterConfigure(_GLFWwindow *window, int32_t width, int32_t height, bool *scale_changed) {
|
||||
bool size_changed = width != window->wl.width || height != window->wl.height;
|
||||
bool scale_changed = checkScaleChange(window);
|
||||
*scale_changed = checkScaleChange(window);
|
||||
|
||||
if (size_changed) {
|
||||
_glfwInputWindowSize(window, width, height);
|
||||
@@ -246,7 +311,7 @@ dispatchChangesAfterConfigure(_GLFWwindow *window, int32_t width, int32_t height
|
||||
resizeFramebuffer(window);
|
||||
}
|
||||
|
||||
if (scale_changed) {
|
||||
if (*scale_changed) {
|
||||
debug("Scale changed to %d in dispatchChangesAfterConfigure\n", window->wl.scale);
|
||||
if (!size_changed) resizeFramebuffer(window);
|
||||
_glfwInputWindowContentScale(window, window->wl.scale, window->wl.scale);
|
||||
@@ -254,7 +319,7 @@ dispatchChangesAfterConfigure(_GLFWwindow *window, int32_t width, int32_t height
|
||||
|
||||
_glfwInputWindowDamage(window);
|
||||
|
||||
return size_changed || scale_changed;
|
||||
return size_changed || *scale_changed;
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -368,9 +433,22 @@ static bool createSurface(_GLFWwindow* window,
|
||||
|
||||
wl_surface_set_user_data(window->wl.surface, window);
|
||||
|
||||
window->wl.native = wl_egl_window_create(window->wl.surface,
|
||||
wndconfig->width,
|
||||
wndconfig->height);
|
||||
// If we already have been notified of the primary monitor scale, assume
|
||||
// the window will be created on it and so avoid a rescale roundtrip in the common
|
||||
// case of the window being shown on the primary monitor or all monitors having the same scale.
|
||||
// If you change this also change get_window_content_scale() in the kitty code.
|
||||
GLFWmonitor* monitor = glfwGetPrimaryMonitor();
|
||||
float xscale = 1.0, yscale = 1.0;
|
||||
int scale = 1;
|
||||
if (monitor) {
|
||||
glfwGetMonitorContentScale(monitor, &xscale, &yscale);
|
||||
// see wl_monitor.c xscale is always == yscale
|
||||
if (xscale <= 0.0001 || xscale != xscale || xscale >= 24) xscale = 1.0;
|
||||
if (xscale > 1) scale = (int)xscale;
|
||||
}
|
||||
|
||||
debug("Creating window at size: %dx%d and scale %d\n", wndconfig->width, wndconfig->height, scale);
|
||||
window->wl.native = wl_egl_window_create(window->wl.surface, wndconfig->width * scale, wndconfig->height * scale);
|
||||
if (!window->wl.native)
|
||||
return false;
|
||||
|
||||
@@ -378,11 +456,13 @@ static bool createSurface(_GLFWwindow* window,
|
||||
window->wl.height = wndconfig->height;
|
||||
window->wl.user_requested_content_size.width = wndconfig->width;
|
||||
window->wl.user_requested_content_size.height = wndconfig->height;
|
||||
window->wl.scale = 1;
|
||||
|
||||
window->wl.scale = scale;
|
||||
|
||||
if (!window->wl.transparent)
|
||||
setOpaqueRegion(window);
|
||||
setOpaqueRegion(window, false);
|
||||
|
||||
wl_surface_set_buffer_scale(window->wl.surface, scale);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -496,11 +576,7 @@ static void xdgSurfaceHandleConfigure(void* data,
|
||||
int height = window->wl.pending.height;
|
||||
if (!window->wl.surface_configured_once) {
|
||||
window->wl.surface_configured_once = true;
|
||||
if (!width && !height && !new_states && !window->wl.decorations.serverSide && getenv("XAUTHORITY") && strstr(getenv("XAUTHORITY"), "mutter")) {
|
||||
// https://github.com/kovidgoyal/kitty/issues/4802
|
||||
debug("Ignoring first empty surface configure event on mutter.\n");
|
||||
return;
|
||||
}
|
||||
swap_buffers(window);
|
||||
}
|
||||
|
||||
if (new_states != window->wl.current.toplevel_states ||
|
||||
@@ -524,10 +600,11 @@ static void xdgSurfaceHandleConfigure(void* data,
|
||||
}
|
||||
|
||||
bool resized = false;
|
||||
bool scale_changed = false;
|
||||
if (window->wl.pending_state) {
|
||||
int width = window->wl.pending.width, height = window->wl.pending.height;
|
||||
set_csd_window_geometry(window, &width, &height);
|
||||
resized = dispatchChangesAfterConfigure(window, width, height);
|
||||
resized = dispatchChangesAfterConfigure(window, width, height, &scale_changed);
|
||||
if (window->wl.decorations.serverSide) {
|
||||
free_csd_surfaces(window);
|
||||
} else {
|
||||
@@ -538,6 +615,10 @@ static void xdgSurfaceHandleConfigure(void* data,
|
||||
|
||||
inform_compositor_of_window_geometry(window, "configure");
|
||||
|
||||
// we need to swap buffers here to ensure the buffer attached to the surface is a multiple
|
||||
// of the new scale. See https://github.com/kovidgoyal/kitty/issues/5467
|
||||
if (scale_changed) swap_buffers(window);
|
||||
|
||||
// if a resize happened there will be a commit at the next render frame so
|
||||
// dont commit here, GNOME doesnt like it and its not really needed anyway
|
||||
if (!resized) wl_surface_commit(window->wl.surface);
|
||||
@@ -784,6 +865,7 @@ int _glfwPlatformCreateWindow(_GLFWwindow* window,
|
||||
initialize_csd_metrics(window);
|
||||
window->wl.transparent = fbconfig->transparent;
|
||||
strncpy(window->wl.appId, wndconfig->wl.appId, sizeof(window->wl.appId));
|
||||
window->swaps_disallowed = true;
|
||||
|
||||
if (!createSurface(window, wndconfig))
|
||||
return false;
|
||||
@@ -993,7 +1075,7 @@ void _glfwPlatformGetWindowFrameSize(_GLFWwindow* window,
|
||||
if (window->decorated && !window->monitor && !window->wl.decorations.serverSide)
|
||||
{
|
||||
if (top)
|
||||
*top = window->wl.decorations.metrics.top;
|
||||
*top = window->wl.decorations.metrics.top - window->wl.decorations.metrics.visible_titlebar_height;
|
||||
if (left)
|
||||
*left = window->wl.decorations.metrics.width;
|
||||
if (right)
|
||||
@@ -1062,38 +1144,51 @@ void _glfwPlatformHideWindow(_GLFWwindow* window)
|
||||
xdg_surface_destroy(window->wl.xdg.surface);
|
||||
window->wl.xdg.toplevel = NULL;
|
||||
window->wl.xdg.surface = NULL;
|
||||
window->wl.surface_configured_once = false;
|
||||
window->swaps_disallowed = true;
|
||||
}
|
||||
window->wl.visible = false;
|
||||
}
|
||||
|
||||
void _glfwPlatformRequestWindowAttention(_GLFWwindow* window UNUSED)
|
||||
{
|
||||
// TODO
|
||||
static bool notified = false;
|
||||
if (!notified) {
|
||||
_glfwInputError(GLFW_FEATURE_UNIMPLEMENTED,
|
||||
"Wayland: Window attention request not implemented yet");
|
||||
notified = true;
|
||||
static void
|
||||
request_attention(GLFWwindow *window, const char *token, void *data UNUSED) {
|
||||
if (window && token && token[0]) xdg_activation_v1_activate(_glfw.wl.xdg_activation_v1, token, ((_GLFWwindow*)window)->wl.surface);
|
||||
}
|
||||
|
||||
static bool
|
||||
has_activation_in_flight(_GLFWwindow* window, GLFWactivationcallback callback) {
|
||||
for (size_t i = 0; i < _glfw.wl.activation_requests.sz; i++) {
|
||||
glfw_wl_xdg_activation_request *r = _glfw.wl.activation_requests.array + i;
|
||||
if (r->window_id == window->id && r->callback == callback) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _glfwPlatformRequestWindowAttention(_GLFWwindow* window) {
|
||||
if (!has_activation_in_flight(window, request_attention)) get_activation_token(window, 0, request_attention, NULL);
|
||||
}
|
||||
|
||||
int _glfwPlatformWindowBell(_GLFWwindow* window UNUSED)
|
||||
{
|
||||
// TODO: Use an actual Wayland API to implement this when one becomes available
|
||||
static char tty[L_ctermid + 1];
|
||||
int fd = open(ctermid(tty), O_WRONLY | O_CLOEXEC);
|
||||
if (fd > -1) {
|
||||
int ret = write(fd, "\x07", 1) == 1 ? true : false;
|
||||
close(fd);
|
||||
return ret;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void
|
||||
focus_window(GLFWwindow *window, const char *token, void *data UNUSED) {
|
||||
if (!window) return;
|
||||
if (token && token[0]) xdg_activation_v1_activate(_glfw.wl.xdg_activation_v1, token, ((_GLFWwindow*)window)->wl.surface);
|
||||
else {
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Window focus request via xdg-activation protocol was denied by the compositor. Use a better compositor.");
|
||||
}
|
||||
}
|
||||
|
||||
void _glfwPlatformFocusWindow(_GLFWwindow* window UNUSED)
|
||||
{
|
||||
_glfwInputError(GLFW_FEATURE_UNAVAILABLE,
|
||||
"Wayland: The platform does not support setting the input focus");
|
||||
// Attempt to focus the window by using the activation protocol, whether it works
|
||||
// is entirely compositor dependent and as we all know Wayland and its ecosystem is
|
||||
// the product of morons.
|
||||
if (_glfw.wl.input_serial && !has_activation_in_flight(window, focus_window)) get_activation_token(window, _glfw.wl.input_serial, focus_window, NULL);
|
||||
}
|
||||
|
||||
void _glfwPlatformSetWindowMonitor(_GLFWwindow* window,
|
||||
@@ -1440,53 +1535,68 @@ void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor)
|
||||
}
|
||||
}
|
||||
|
||||
static void send_text(char *text, int fd)
|
||||
{
|
||||
if (text) {
|
||||
size_t len = strlen(text), pos = 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;
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||
"Wayland: Could not copy writing to destination fd failed with error: %s", strerror(errno));
|
||||
break;
|
||||
}
|
||||
if (ret > 0) {
|
||||
start = glfwGetTime();
|
||||
pos += ret;
|
||||
}
|
||||
static bool
|
||||
write_all(int fd, const char *data, size_t sz) {
|
||||
monotonic_t start = glfwGetTime();
|
||||
size_t pos = 0;
|
||||
while (pos < sz && glfwGetTime() - start < s_to_monotonic_t(2ll)) {
|
||||
ssize_t ret = write(fd, data + pos, sz - pos);
|
||||
if (ret < 0) {
|
||||
if (errno == EAGAIN || errno == EINTR) continue;
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||
"Wayland: Could not copy writing to destination fd failed with error: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (ret > 0) {
|
||||
start = glfwGetTime();
|
||||
pos += ret;
|
||||
}
|
||||
}
|
||||
return pos >= sz;
|
||||
}
|
||||
|
||||
static void
|
||||
send_clipboard_data(const _GLFWClipboardData *cd, const char *mime, int fd) {
|
||||
if (strcmp(mime, "text/plain;charset=utf-8") == 0 || strcmp(mime, "UTF8_STRING") == 0 || strcmp(mime, "TEXT") == 0 || strcmp(mime, "STRING") == 0) mime = "text/plain";
|
||||
GLFWDataChunk chunk = cd->get_data(mime, NULL, cd->ctype);
|
||||
void *iter = chunk.iter;
|
||||
if (!iter) return;
|
||||
bool keep_going = true;
|
||||
while (keep_going) {
|
||||
chunk = cd->get_data(mime, iter, cd->ctype);
|
||||
if (!chunk.sz) break;
|
||||
if (!write_all(fd, chunk.data, chunk.sz)) keep_going = false;
|
||||
if (chunk.free) chunk.free((void*)chunk.free_data);
|
||||
}
|
||||
cd->get_data(NULL, iter, cd->ctype);
|
||||
}
|
||||
|
||||
static void _glfwSendClipboardText(void *data UNUSED, struct wl_data_source *data_source UNUSED, const char *mime_type, int fd) {
|
||||
send_clipboard_data(&_glfw.clipboard, mime_type, fd);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
static void _glfwSendClipboardText(void *data UNUSED, struct wl_data_source *data_source UNUSED, const char *mime_type UNUSED, int fd) {
|
||||
send_text(_glfw.wl.clipboardString, fd);
|
||||
}
|
||||
|
||||
static void _glfwSendPrimarySelectionText(void *data UNUSED, struct zwp_primary_selection_source_v1 *primary_selection_source UNUSED,
|
||||
const char *mime_type UNUSED, int fd) {
|
||||
send_text(_glfw.wl.primarySelectionString, fd);
|
||||
const char *mime_type, int fd) {
|
||||
send_clipboard_data(&_glfw.primary, mime_type, fd);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
static char* read_offer_string(int data_pipe, size_t *data_sz) {
|
||||
static void
|
||||
read_offer(int data_pipe, GLFWclipboardwritedatafun write_data, void *object) {
|
||||
wl_display_flush(_glfw.wl.display);
|
||||
size_t capacity = 0;
|
||||
char *buf = NULL;
|
||||
*data_sz = 0;
|
||||
struct pollfd fds;
|
||||
fds.fd = data_pipe;
|
||||
fds.events = POLLIN;
|
||||
monotonic_t start = glfwGetTime();
|
||||
#define bail(...) { \
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR, __VA_ARGS__); \
|
||||
free(buf); buf = NULL; \
|
||||
close(data_pipe); \
|
||||
return NULL; \
|
||||
return; \
|
||||
}
|
||||
|
||||
char buf[8192];
|
||||
|
||||
while (glfwGetTime() - start < s_to_monotonic_t(2ll)) {
|
||||
int ret = poll(&fds, 1, 2000);
|
||||
if (ret == -1) {
|
||||
@@ -1496,34 +1606,65 @@ static char* read_offer_string(int data_pipe, size_t *data_sz) {
|
||||
if (!ret) {
|
||||
bail("Wayland: Failed to read clipboard data from pipe (timed out)");
|
||||
}
|
||||
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 + *data_sz, capacity - *data_sz - 1);
|
||||
ret = read(data_pipe, buf, sizeof(buf));
|
||||
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[*data_sz] = 0; return buf; }
|
||||
*data_sz += ret;
|
||||
if (ret == 0) { close(data_pipe); return; }
|
||||
if (!write_data(object, buf, ret)) bail("Wayland: call to write_data() failed with data from data offer");
|
||||
start = glfwGetTime();
|
||||
}
|
||||
bail("Wayland: Failed to read clipboard data from pipe (timed out)");
|
||||
#undef bail
|
||||
|
||||
}
|
||||
|
||||
static char* read_primary_selection_offer(struct zwp_primary_selection_offer_v1 *primary_selection_offer, const char *mime) {
|
||||
|
||||
typedef struct chunked_writer {
|
||||
char *buf; size_t sz, cap;
|
||||
} chunked_writer;
|
||||
|
||||
static bool
|
||||
write_chunk(void *object, const char *data, size_t sz) {
|
||||
chunked_writer *cw = object;
|
||||
if (cw->cap < cw->sz + sz) {
|
||||
cw->cap = MAX(cw->cap * 2, cw->sz + 8*sz);
|
||||
cw->buf = realloc(cw->buf, cw->cap);
|
||||
}
|
||||
memcpy(cw->buf + cw->sz, data, sz);
|
||||
cw->sz += sz;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static char*
|
||||
read_offer_string(int data_pipe, size_t *sz) {
|
||||
chunked_writer cw = {0};
|
||||
read_offer(data_pipe, write_chunk, &cw);
|
||||
if (cw.buf) {
|
||||
*sz = cw.sz;
|
||||
return cw.buf;
|
||||
}
|
||||
*sz = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
read_clipboard_data_offer(struct wl_data_offer *data_offer, const char *mime, GLFWclipboardwritedatafun write_data, void *object) {
|
||||
int pipefd[2];
|
||||
if (pipe2(pipefd, O_CLOEXEC) != 0) return NULL;
|
||||
if (pipe2(pipefd, O_CLOEXEC) != 0) return;
|
||||
wl_data_offer_receive(data_offer, mime, pipefd[1]);
|
||||
close(pipefd[1]);
|
||||
read_offer(pipefd[0], write_data, object);
|
||||
}
|
||||
|
||||
static void
|
||||
read_primary_selection_offer(struct zwp_primary_selection_offer_v1 *primary_selection_offer, const char *mime, GLFWclipboardwritedatafun write_data, void *object) {
|
||||
int pipefd[2];
|
||||
if (pipe2(pipefd, O_CLOEXEC) != 0) return;
|
||||
zwp_primary_selection_offer_v1_receive(primary_selection_offer, mime, pipefd[1]);
|
||||
close(pipefd[1]);
|
||||
size_t sz = 0;
|
||||
return read_offer_string(pipefd[0], &sz);
|
||||
read_offer(pipefd[0], write_data, object);
|
||||
}
|
||||
|
||||
static char* read_data_offer(struct wl_data_offer *data_offer, const char *mime, size_t *sz) {
|
||||
@@ -1535,24 +1676,24 @@ static char* read_data_offer(struct wl_data_offer *data_offer, const char *mime,
|
||||
}
|
||||
|
||||
static void data_source_canceled(void *data UNUSED, struct wl_data_source *wl_data_source) {
|
||||
if (_glfw.wl.dataSourceForClipboard == wl_data_source)
|
||||
if (_glfw.wl.dataSourceForClipboard == wl_data_source) {
|
||||
_glfw.wl.dataSourceForClipboard = NULL;
|
||||
_glfw_free_clipboard_data(&_glfw.clipboard);
|
||||
}
|
||||
wl_data_source_destroy(wl_data_source);
|
||||
}
|
||||
|
||||
static void primary_selection_source_canceled(void *data UNUSED, struct zwp_primary_selection_source_v1 *primary_selection_source) {
|
||||
if (_glfw.wl.dataSourceForPrimarySelection == primary_selection_source)
|
||||
if (_glfw.wl.dataSourceForPrimarySelection == primary_selection_source) {
|
||||
_glfw.wl.dataSourceForPrimarySelection = NULL;
|
||||
_glfw_free_clipboard_data(&_glfw.primary);
|
||||
}
|
||||
zwp_primary_selection_source_v1_destroy(primary_selection_source);
|
||||
}
|
||||
|
||||
static void data_source_target(void *data UNUSED, struct wl_data_source *wl_data_source UNUSED, const char* mime UNUSED) {
|
||||
}
|
||||
|
||||
static const struct wl_data_source_listener data_source_listener = {
|
||||
.send = _glfwSendClipboardText,
|
||||
.cancelled = data_source_canceled,
|
||||
.target = data_source_target,
|
||||
};
|
||||
|
||||
static const struct zwp_primary_selection_source_v1_listener primary_selection_source_listener = {
|
||||
@@ -1829,30 +1970,69 @@ static bool _glfwEnsureDataDevice(void) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void _glfwPlatformSetClipboardString(const char* string)
|
||||
{
|
||||
if (!_glfwEnsureDataDevice()) return;
|
||||
free(_glfw.wl.clipboardString);
|
||||
_glfw.wl.clipboardString = _glfw_strdup(string);
|
||||
if (_glfw.wl.dataSourceForClipboard)
|
||||
wl_data_source_destroy(_glfw.wl.dataSourceForClipboard);
|
||||
_glfw.wl.dataSourceForClipboard = wl_data_device_manager_create_data_source(_glfw.wl.dataDeviceManager);
|
||||
if (!_glfw.wl.dataSourceForClipboard)
|
||||
{
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||
"Wayland: Cannot copy failed to create data source");
|
||||
return;
|
||||
typedef void(*add_offer_func)(void*, const char *mime);
|
||||
|
||||
|
||||
void
|
||||
_glfwPlatformSetClipboard(GLFWClipboardType t) {
|
||||
_GLFWClipboardData *cd = NULL;
|
||||
void *data_source;
|
||||
add_offer_func f;
|
||||
if (t == GLFW_CLIPBOARD) {
|
||||
if (!_glfwEnsureDataDevice()) return;
|
||||
cd = &_glfw.clipboard;
|
||||
f = (add_offer_func)wl_data_source_offer;
|
||||
if (_glfw.wl.dataSourceForClipboard) wl_data_source_destroy(_glfw.wl.dataSourceForClipboard);
|
||||
_glfw.wl.dataSourceForClipboard = wl_data_device_manager_create_data_source(_glfw.wl.dataDeviceManager);
|
||||
if (!_glfw.wl.dataSourceForClipboard)
|
||||
{
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||
"Wayland: Cannot copy failed to create data source");
|
||||
return;
|
||||
}
|
||||
wl_data_source_add_listener(_glfw.wl.dataSourceForClipboard, &data_source_listener, NULL);
|
||||
data_source = _glfw.wl.dataSourceForClipboard;
|
||||
} else {
|
||||
if (!_glfw.wl.primarySelectionDevice) {
|
||||
static bool warned_about_primary_selection_device = false;
|
||||
if (!warned_about_primary_selection_device) {
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||
"Wayland: Cannot copy no primary selection device available");
|
||||
warned_about_primary_selection_device = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
cd = &_glfw.primary;
|
||||
f = (add_offer_func)zwp_primary_selection_source_v1_offer;
|
||||
if (_glfw.wl.dataSourceForPrimarySelection) zwp_primary_selection_source_v1_destroy(_glfw.wl.dataSourceForPrimarySelection);
|
||||
_glfw.wl.dataSourceForPrimarySelection = zwp_primary_selection_device_manager_v1_create_source(_glfw.wl.primarySelectionDeviceManager);
|
||||
if (!_glfw.wl.dataSourceForPrimarySelection)
|
||||
{
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||
"Wayland: Cannot copy failed to create primary selection source");
|
||||
return;
|
||||
}
|
||||
zwp_primary_selection_source_v1_add_listener(_glfw.wl.dataSourceForPrimarySelection, &primary_selection_source_listener, NULL);
|
||||
data_source = _glfw.wl.dataSourceForPrimarySelection;
|
||||
}
|
||||
f(data_source, clipboard_mime());
|
||||
for (size_t i = 0; i < cd->num_mime_types; i++) {
|
||||
if (strcmp(cd->mime_types[i], "text/plain") == 0) {
|
||||
f(data_source, "TEXT");
|
||||
f(data_source, "STRING");
|
||||
f(data_source, "UTF8_STRING");
|
||||
f(data_source, "text/plain;charset=utf-8");
|
||||
}
|
||||
f(data_source, cd->mime_types[i]);
|
||||
}
|
||||
wl_data_source_add_listener(_glfw.wl.dataSourceForClipboard, &data_source_listener, NULL);
|
||||
wl_data_source_offer(_glfw.wl.dataSourceForClipboard, clipboard_mime());
|
||||
wl_data_source_offer(_glfw.wl.dataSourceForClipboard, "text/plain");
|
||||
wl_data_source_offer(_glfw.wl.dataSourceForClipboard, "text/plain;charset=utf-8");
|
||||
wl_data_source_offer(_glfw.wl.dataSourceForClipboard, "TEXT");
|
||||
wl_data_source_offer(_glfw.wl.dataSourceForClipboard, "STRING");
|
||||
wl_data_source_offer(_glfw.wl.dataSourceForClipboard, "UTF8_STRING");
|
||||
struct wl_callback *callback = wl_display_sync(_glfw.wl.display);
|
||||
static const struct wl_callback_listener clipboard_copy_callback_listener = {.done = clipboard_copy_callback_done};
|
||||
wl_callback_add_listener(callback, &clipboard_copy_callback_listener, _glfw.wl.dataSourceForClipboard);
|
||||
if (t == GLFW_CLIPBOARD) {
|
||||
static const struct wl_callback_listener clipboard_copy_callback_listener = {.done = clipboard_copy_callback_done};
|
||||
wl_callback_add_listener(callback, &clipboard_copy_callback_listener, _glfw.wl.dataSourceForClipboard);
|
||||
} else {
|
||||
static const struct wl_callback_listener primary_selection_copy_callback_listener = {.done = primary_selection_copy_callback_done};
|
||||
wl_callback_add_listener(callback, &primary_selection_copy_callback_listener, _glfw.wl.dataSourceForPrimarySelection);
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
@@ -1870,81 +2050,47 @@ plain_text_mime_for_offer(const _GLFWWaylandDataOffer *d) {
|
||||
A("text/plain");
|
||||
A("UTF8_STRING");
|
||||
A("STRING");
|
||||
A("TEXT");
|
||||
#undef A
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char* _glfwPlatformGetClipboardString(void)
|
||||
{
|
||||
void
|
||||
_glfwPlatformGetClipboard(GLFWClipboardType clipboard_type, const char* mime_type, GLFWclipboardwritedatafun write_data, void *object) {
|
||||
_GLFWWaylandOfferType offer_type = clipboard_type == GLFW_PRIMARY_SELECTION ? PRIMARY_SELECTION : CLIPBOARD;
|
||||
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
|
||||
_GLFWWaylandDataOffer *d = _glfw.wl.dataOffers + i;
|
||||
if (d->id && d->offer_type == CLIPBOARD) {
|
||||
if (d->is_self_offer) return _glfw.wl.clipboardString;
|
||||
const char *mime = plain_text_mime_for_offer(d);
|
||||
if (mime) {
|
||||
free(_glfw.wl.pasteString);
|
||||
size_t sz = 0;
|
||||
_glfw.wl.pasteString = read_data_offer(d->id, mime, &sz);
|
||||
return _glfw.wl.pasteString;
|
||||
if (d->id && d->offer_type == offer_type) {
|
||||
if (d->is_self_offer) {
|
||||
write_data(object, NULL, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void _glfwPlatformSetPrimarySelectionString(const char* string)
|
||||
{
|
||||
if (!_glfw.wl.primarySelectionDevice) {
|
||||
static bool warned_about_primary_selection_device = false;
|
||||
if (!warned_about_primary_selection_device) {
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||
"Wayland: Cannot copy no primary selection device available");
|
||||
warned_about_primary_selection_device = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_glfw.wl.primarySelectionString == string) return;
|
||||
free(_glfw.wl.primarySelectionString);
|
||||
_glfw.wl.primarySelectionString = _glfw_strdup(string);
|
||||
|
||||
if (_glfw.wl.dataSourceForPrimarySelection)
|
||||
zwp_primary_selection_source_v1_destroy(_glfw.wl.dataSourceForPrimarySelection);
|
||||
_glfw.wl.dataSourceForPrimarySelection = zwp_primary_selection_device_manager_v1_create_source(_glfw.wl.primarySelectionDeviceManager);
|
||||
if (!_glfw.wl.dataSourceForPrimarySelection)
|
||||
{
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||
"Wayland: Cannot copy failed to create primary selection source");
|
||||
return;
|
||||
}
|
||||
zwp_primary_selection_source_v1_add_listener(_glfw.wl.dataSourceForPrimarySelection, &primary_selection_source_listener, NULL);
|
||||
zwp_primary_selection_source_v1_offer(_glfw.wl.dataSourceForPrimarySelection, clipboard_mime());
|
||||
zwp_primary_selection_source_v1_offer(_glfw.wl.dataSourceForPrimarySelection, "text/plain");
|
||||
zwp_primary_selection_source_v1_offer(_glfw.wl.dataSourceForPrimarySelection, "text/plain;charset=utf-8");
|
||||
zwp_primary_selection_source_v1_offer(_glfw.wl.dataSourceForPrimarySelection, "TEXT");
|
||||
zwp_primary_selection_source_v1_offer(_glfw.wl.dataSourceForPrimarySelection, "STRING");
|
||||
zwp_primary_selection_source_v1_offer(_glfw.wl.dataSourceForPrimarySelection, "UTF8_STRING");
|
||||
struct wl_callback *callback = wl_display_sync(_glfw.wl.display);
|
||||
static const struct wl_callback_listener primary_selection_copy_callback_listener = {.done = primary_selection_copy_callback_done};
|
||||
wl_callback_add_listener(callback, &primary_selection_copy_callback_listener, _glfw.wl.dataSourceForPrimarySelection);
|
||||
}
|
||||
|
||||
const char* _glfwPlatformGetPrimarySelectionString(void)
|
||||
{
|
||||
if (_glfw.wl.dataSourceForPrimarySelection != NULL) return _glfw.wl.primarySelectionString;
|
||||
|
||||
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) {
|
||||
if (d->is_self_offer) return _glfw.wl.primarySelectionString;
|
||||
const char *mime = plain_text_mime_for_offer(d);
|
||||
if (mime) {
|
||||
free(_glfw.wl.pasteString);
|
||||
_glfw.wl.pasteString = read_primary_selection_offer(d->id, mime);
|
||||
return _glfw.wl.pasteString;
|
||||
if (mime_type == NULL) {
|
||||
bool ok = true;
|
||||
for (size_t o = 0; o < d->mimes_count; o++) {
|
||||
const char *q = d->mimes[o];
|
||||
if (strchr(d->mimes[0], '/')) {
|
||||
if (strcmp(q, clipboard_mime()) == 0) continue;
|
||||
if (strcmp(q, "text/plain;charset=utf-8") == 0) q = "text/plain";
|
||||
} else {
|
||||
if (strcmp(q, "UTF8_STRING") == 0 || strcmp(q, "STRING") == 0 || strcmp(q, "TEXT") == 0) q = "text/plain";
|
||||
}
|
||||
if (ok) ok = write_data(object, q, strlen(q));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (strcmp(mime_type, "text/plain") == 0) {
|
||||
mime_type = plain_text_mime_for_offer(d);
|
||||
if (!mime_type) return;
|
||||
}
|
||||
if (d->is_primary) {
|
||||
read_primary_selection_offer(d->id, mime_type, write_data, object);
|
||||
} else {
|
||||
read_clipboard_data_offer(d->id, mime_type, write_data, object);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
EGLenum _glfwPlatformGetEGLPlatform(EGLint** attribs UNUSED)
|
||||
@@ -2066,6 +2212,18 @@ GLFWAPI struct wl_surface* glfwGetWaylandWindow(GLFWwindow* handle)
|
||||
return window->wl.surface;
|
||||
}
|
||||
|
||||
GLFWAPI void glfwWaylandActivateWindow(GLFWwindow* handle, const char *activation_token) {
|
||||
_GLFWwindow* window = (_GLFWwindow*) handle;
|
||||
_GLFW_REQUIRE_INIT();
|
||||
if (activation_token && activation_token[0]) xdg_activation_v1_activate(_glfw.wl.xdg_activation_v1, activation_token, window->wl.surface);
|
||||
}
|
||||
|
||||
GLFWAPI void glfwWaylandRunWithActivationToken(GLFWwindow *handle, GLFWactivationcallback cb, void *cb_data) {
|
||||
_GLFWwindow* window = (_GLFWwindow*) handle;
|
||||
_GLFW_REQUIRE_INIT();
|
||||
get_activation_token(window, _glfw.wl.input_serial, cb, cb_data);
|
||||
}
|
||||
|
||||
GLFWAPI int glfwGetNativeKeyForName(const char* keyName, bool caseSensitive) {
|
||||
return glfw_xkb_keysym_from_name(keyName, caseSensitive);
|
||||
}
|
||||
|
||||
14
glfw/x11_init.c
vendored
14
glfw/x11_init.c
vendored
@@ -137,6 +137,8 @@ static void detectEWMH(void)
|
||||
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WM_WINDOW_TYPE");
|
||||
_glfw.x11.NET_WM_WINDOW_TYPE_NORMAL =
|
||||
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WM_WINDOW_TYPE_NORMAL");
|
||||
_glfw.x11.NET_WM_WINDOW_TYPE_DOCK =
|
||||
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WM_WINDOW_TYPE_DOCK");
|
||||
_glfw.x11.NET_WORKAREA =
|
||||
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WORKAREA");
|
||||
_glfw.x11.NET_CURRENT_DESKTOP =
|
||||
@@ -147,6 +149,8 @@ static void detectEWMH(void)
|
||||
getAtomIfSupported(supportedAtoms, atomCount, "_NET_FRAME_EXTENTS");
|
||||
_glfw.x11.NET_REQUEST_FRAME_EXTENTS =
|
||||
getAtomIfSupported(supportedAtoms, atomCount, "_NET_REQUEST_FRAME_EXTENTS");
|
||||
_glfw.x11.NET_WM_STRUT_PARTIAL =
|
||||
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WM_STRUT_PARTIAL");
|
||||
|
||||
XFree(supportedAtoms);
|
||||
}
|
||||
@@ -680,8 +684,14 @@ void _glfwPlatformTerminate(void)
|
||||
|
||||
glfw_xkb_release(&_glfw.x11.xkb);
|
||||
glfw_dbus_terminate(&_glfw.x11.dbus);
|
||||
free(_glfw.x11.primarySelectionString);
|
||||
free(_glfw.x11.clipboardString);
|
||||
if (_glfw.x11.mime_atoms.array) {
|
||||
for (size_t i = 0; i < _glfw.x11.mime_atoms.sz; i++) {
|
||||
free((void*)_glfw.x11.mime_atoms.array[i].mime);
|
||||
}
|
||||
free(_glfw.x11.mime_atoms.array);
|
||||
}
|
||||
if (_glfw.x11.clipboard_atoms.array) { free(_glfw.x11.clipboard_atoms.array); }
|
||||
if (_glfw.x11.primary_atoms.array) { free(_glfw.x11.primary_atoms.array); }
|
||||
|
||||
if (_glfw.x11.display)
|
||||
{
|
||||
|
||||
18
glfw/x11_platform.h
vendored
18
glfw/x11_platform.h
vendored
@@ -207,6 +207,16 @@ typedef struct _GLFWwindowX11
|
||||
|
||||
} _GLFWwindowX11;
|
||||
|
||||
typedef struct MimeAtom {
|
||||
Atom atom;
|
||||
const char* mime;
|
||||
} MimeAtom;
|
||||
|
||||
typedef struct AtomArray {
|
||||
MimeAtom *array;
|
||||
size_t sz, capacity;
|
||||
} AtomArray;
|
||||
|
||||
// X11-specific global data
|
||||
//
|
||||
typedef struct _GLFWlibraryX11
|
||||
@@ -225,10 +235,6 @@ typedef struct _GLFWlibraryX11
|
||||
XContext context;
|
||||
// Most recent error code received by X error handler
|
||||
int errorCode;
|
||||
// Primary selection string (while the primary selection is owned)
|
||||
char* primarySelectionString;
|
||||
// Clipboard string (while the selection is owned)
|
||||
char* clipboardString;
|
||||
// Where to place the cursor when re-enabled
|
||||
double restoreCursorPosX, restoreCursorPosY;
|
||||
// The window whose disabled cursor mode is active
|
||||
@@ -247,6 +253,7 @@ typedef struct _GLFWlibraryX11
|
||||
Atom NET_WM_PING;
|
||||
Atom NET_WM_WINDOW_TYPE;
|
||||
Atom NET_WM_WINDOW_TYPE_NORMAL;
|
||||
Atom NET_WM_WINDOW_TYPE_DOCK;
|
||||
Atom NET_WM_STATE;
|
||||
Atom NET_WM_STATE_ABOVE;
|
||||
Atom NET_WM_STATE_FULLSCREEN;
|
||||
@@ -262,6 +269,7 @@ typedef struct _GLFWlibraryX11
|
||||
Atom NET_ACTIVE_WINDOW;
|
||||
Atom NET_FRAME_EXTENTS;
|
||||
Atom NET_REQUEST_FRAME_EXTENTS;
|
||||
Atom NET_WM_STRUT_PARTIAL;
|
||||
Atom MOTIF_WM_HINTS;
|
||||
|
||||
// Xdnd (drag and drop) atoms
|
||||
@@ -291,6 +299,8 @@ typedef struct _GLFWlibraryX11
|
||||
|
||||
// XRM database atom
|
||||
Atom RESOURCE_MANAGER;
|
||||
// Atoms for MIME types
|
||||
AtomArray mime_atoms, clipboard_atoms, primary_atoms;
|
||||
|
||||
struct {
|
||||
bool available;
|
||||
|
||||
366
glfw/x11_window.c
vendored
366
glfw/x11_window.c
vendored
@@ -716,19 +716,60 @@ static bool createNativeWindow(_GLFWwindow* window,
|
||||
return true;
|
||||
}
|
||||
|
||||
static size_t
|
||||
get_clipboard_data(const _GLFWClipboardData *cd, const char *mime, char **data) {
|
||||
*data = NULL;
|
||||
GLFWDataChunk chunk = cd->get_data(mime, NULL, cd->ctype);
|
||||
char *buf = NULL;
|
||||
size_t sz = 0, cap = 0;
|
||||
void *iter = chunk.iter;
|
||||
if (!iter) return 0;
|
||||
while (true) {
|
||||
chunk = cd->get_data(mime, iter, cd->ctype);
|
||||
if (!chunk.sz) break;
|
||||
if (cap < sz + chunk.sz) {
|
||||
cap = MAX(cap * 2, sz + 4 * chunk.sz);
|
||||
buf = realloc(buf, cap);
|
||||
}
|
||||
memcpy(buf + sz, chunk.data, chunk.sz);
|
||||
sz += chunk.sz;
|
||||
if (chunk.free) chunk.free((void*)chunk.free_data);
|
||||
}
|
||||
*data = buf;
|
||||
cd->get_data(NULL, iter, cd->ctype);
|
||||
return sz;
|
||||
}
|
||||
|
||||
static void
|
||||
get_atom_names(const Atom *atoms, int count, char **atom_names) {
|
||||
_glfwGrabErrorHandlerX11();
|
||||
XGetAtomNames(_glfw.x11.display, (Atom*)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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set the specified property to the selection converted to the requested target
|
||||
//
|
||||
static Atom writeTargetToProperty(const XSelectionRequestEvent* request)
|
||||
{
|
||||
int i;
|
||||
char* selectionString = NULL;
|
||||
const Atom formats[] = { _glfw.x11.UTF8_STRING, XA_STRING };
|
||||
const int formatCount = sizeof(formats) / sizeof(formats[0]);
|
||||
const AtomArray *aa;
|
||||
const _GLFWClipboardData *cd;
|
||||
|
||||
if (request->selection == _glfw.x11.PRIMARY)
|
||||
selectionString = _glfw.x11.primarySelectionString;
|
||||
else
|
||||
selectionString = _glfw.x11.clipboardString;
|
||||
if (request->selection == _glfw.x11.PRIMARY) {
|
||||
aa = &_glfw.x11.primary_atoms;
|
||||
cd = &_glfw.primary;
|
||||
} else {
|
||||
aa = &_glfw.x11.clipboard_atoms;
|
||||
cd = &_glfw.clipboard;
|
||||
}
|
||||
|
||||
if (request->property == None)
|
||||
{
|
||||
@@ -741,11 +782,10 @@ static Atom writeTargetToProperty(const XSelectionRequestEvent* request)
|
||||
{
|
||||
// The list of supported targets was requested
|
||||
|
||||
const Atom targets[] = { _glfw.x11.TARGETS,
|
||||
_glfw.x11.MULTIPLE,
|
||||
_glfw.x11.UTF8_STRING,
|
||||
XA_STRING };
|
||||
|
||||
Atom *targets = calloc(aa->sz + 2, sizeof(Atom));
|
||||
targets[0] = _glfw.x11.TARGETS;
|
||||
targets[1] = _glfw.x11.MULTIPLE;
|
||||
for (size_t i = 0; i < aa->sz; i++) targets[i+2] = aa->array[i].atom;
|
||||
XChangeProperty(_glfw.x11.display,
|
||||
request->requestor,
|
||||
request->property,
|
||||
@@ -753,8 +793,8 @@ static Atom writeTargetToProperty(const XSelectionRequestEvent* request)
|
||||
32,
|
||||
PropModeReplace,
|
||||
(unsigned char*) targets,
|
||||
sizeof(targets) / sizeof(targets[0]));
|
||||
|
||||
aa->sz + 2);
|
||||
free(targets);
|
||||
return request->property;
|
||||
}
|
||||
|
||||
@@ -763,7 +803,7 @@ static Atom writeTargetToProperty(const XSelectionRequestEvent* request)
|
||||
// Multiple conversions were requested
|
||||
|
||||
Atom* targets;
|
||||
unsigned long i, count;
|
||||
size_t i, j, count;
|
||||
|
||||
count = _glfwGetWindowPropertyX11(request->requestor,
|
||||
request->property,
|
||||
@@ -772,24 +812,25 @@ static Atom writeTargetToProperty(const XSelectionRequestEvent* request)
|
||||
|
||||
for (i = 0; i < count; i += 2)
|
||||
{
|
||||
int j;
|
||||
|
||||
for (j = 0; j < formatCount; j++)
|
||||
for (j = 0; j < aa->sz; j++)
|
||||
{
|
||||
if (targets[i] == formats[j])
|
||||
if (targets[i] == aa->array[j].atom)
|
||||
break;
|
||||
}
|
||||
|
||||
if (j < formatCount)
|
||||
if (j < aa->sz)
|
||||
{
|
||||
if (selectionString) XChangeProperty(_glfw.x11.display,
|
||||
char *data = NULL; size_t sz = get_clipboard_data(cd, aa->array[j].mime, &data);
|
||||
|
||||
if (data) XChangeProperty(_glfw.x11.display,
|
||||
request->requestor,
|
||||
targets[i + 1],
|
||||
targets[i],
|
||||
8,
|
||||
PropModeReplace,
|
||||
(unsigned char *) selectionString,
|
||||
strlen(selectionString));
|
||||
(unsigned char *) data,
|
||||
sz);
|
||||
free(data);
|
||||
}
|
||||
else
|
||||
targets[i + 1] = None;
|
||||
@@ -828,20 +869,22 @@ static Atom writeTargetToProperty(const XSelectionRequestEvent* request)
|
||||
|
||||
// Conversion to a data target was requested
|
||||
|
||||
for (i = 0; i < formatCount; i++)
|
||||
for (size_t i = 0; i < aa->sz; i++)
|
||||
{
|
||||
if (request->target == formats[i])
|
||||
if (request->target == aa->array[i].atom)
|
||||
{
|
||||
// The requested target is one we support
|
||||
|
||||
if (selectionString) XChangeProperty(_glfw.x11.display,
|
||||
char *data = NULL; size_t sz = get_clipboard_data(cd, aa->array[i].mime, &data);
|
||||
if (data) XChangeProperty(_glfw.x11.display,
|
||||
request->requestor,
|
||||
request->property,
|
||||
request->target,
|
||||
8,
|
||||
PropModeReplace,
|
||||
(unsigned char *) selectionString,
|
||||
strlen(selectionString));
|
||||
(unsigned char *) data,
|
||||
sz);
|
||||
free(data);
|
||||
|
||||
return request->property;
|
||||
}
|
||||
@@ -856,13 +899,11 @@ static void handleSelectionClear(XEvent* event)
|
||||
{
|
||||
if (event->xselectionclear.selection == _glfw.x11.PRIMARY)
|
||||
{
|
||||
free(_glfw.x11.primarySelectionString);
|
||||
_glfw.x11.primarySelectionString = NULL;
|
||||
_glfw_free_clipboard_data(&_glfw.primary);
|
||||
}
|
||||
else
|
||||
{
|
||||
free(_glfw.x11.clipboardString);
|
||||
_glfw.x11.clipboardString = NULL;
|
||||
_glfw_free_clipboard_data(&_glfw.clipboard);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -881,34 +922,21 @@ static void handleSelectionRequest(XEvent* event)
|
||||
XSendEvent(_glfw.x11.display, request->requestor, False, 0, &reply);
|
||||
}
|
||||
|
||||
static const char* getSelectionString(Atom selection)
|
||||
static void
|
||||
getSelectionString(Atom selection, Atom *targets, size_t num_targets, GLFWclipboardwritedatafun write_data, void *object, bool report_not_found)
|
||||
{
|
||||
char** selectionString = NULL;
|
||||
const Atom targets[] = { _glfw.x11.UTF8_STRING, XA_STRING };
|
||||
const size_t targetCount = sizeof(targets) / sizeof(targets[0]);
|
||||
|
||||
if (selection == _glfw.x11.PRIMARY)
|
||||
selectionString = &_glfw.x11.primarySelectionString;
|
||||
else
|
||||
selectionString = &_glfw.x11.clipboardString;
|
||||
|
||||
if (XGetSelectionOwner(_glfw.x11.display, selection) ==
|
||||
_glfw.x11.helperWindowHandle)
|
||||
{
|
||||
// Instead of doing a large number of X round-trips just to put this
|
||||
// string into a window property and then read it back, just return it
|
||||
return *selectionString;
|
||||
#define XFREE(x) { if (x) XFree(x); x = NULL; }
|
||||
if (XGetSelectionOwner(_glfw.x11.display, selection) == _glfw.x11.helperWindowHandle) {
|
||||
write_data(object, NULL, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
free(*selectionString);
|
||||
*selectionString = NULL;
|
||||
|
||||
for (size_t i = 0; i < targetCount; i++)
|
||||
bool found = false;
|
||||
for (size_t i = 0; !found && i < num_targets; i++)
|
||||
{
|
||||
char* data;
|
||||
Atom actualType;
|
||||
int actualFormat;
|
||||
unsigned long itemCount, bytesAfter;
|
||||
char* data = NULL;
|
||||
Atom actualType = None;
|
||||
int actualFormat = 0;
|
||||
unsigned long itemCount = 0, bytesAfter = 0;
|
||||
monotonic_t start = glfwGetTime();
|
||||
XEvent notification, dummy;
|
||||
|
||||
@@ -925,8 +953,7 @@ static const char* getSelectionString(Atom selection)
|
||||
¬ification))
|
||||
{
|
||||
monotonic_t time = glfwGetTime();
|
||||
if (time - start > s_to_monotonic_t(2ll))
|
||||
return "";
|
||||
if (time - start > s_to_monotonic_t(2ll)) return;
|
||||
waitForX11Event(s_to_monotonic_t(2ll) - (time - start));
|
||||
}
|
||||
|
||||
@@ -953,9 +980,6 @@ static const char* getSelectionString(Atom selection)
|
||||
|
||||
if (actualType == _glfw.x11.INCR)
|
||||
{
|
||||
size_t size = 1;
|
||||
char* string = NULL;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
start = glfwGetTime();
|
||||
@@ -966,13 +990,12 @@ static const char* getSelectionString(Atom selection)
|
||||
{
|
||||
monotonic_t time = glfwGetTime();
|
||||
if (time - start > s_to_monotonic_t(2ll)) {
|
||||
free(string);
|
||||
return "";
|
||||
return;
|
||||
}
|
||||
waitForX11Event(s_to_monotonic_t(2ll) - (time - start));
|
||||
}
|
||||
|
||||
XFree(data);
|
||||
XFREE(data);
|
||||
XGetWindowProperty(_glfw.x11.display,
|
||||
notification.xselection.requestor,
|
||||
notification.xselection.property,
|
||||
@@ -988,47 +1011,41 @@ static const char* getSelectionString(Atom selection)
|
||||
|
||||
if (itemCount)
|
||||
{
|
||||
size += itemCount;
|
||||
string = realloc(string, size);
|
||||
string[size - itemCount - 1] = '\0';
|
||||
strcat(string, data);
|
||||
}
|
||||
|
||||
if (!itemCount)
|
||||
{
|
||||
if (targets[i] == XA_STRING)
|
||||
{
|
||||
*selectionString = convertLatin1toUTF8(string);
|
||||
free(string);
|
||||
const char *string = data;
|
||||
if (targets[i] == XA_STRING) {
|
||||
string = convertLatin1toUTF8(data);
|
||||
itemCount = strlen(string);
|
||||
}
|
||||
else
|
||||
*selectionString = string;
|
||||
bool ok = write_data(object, string, itemCount);
|
||||
if (string != data) free((void*)string);
|
||||
if (!ok) { XFREE(data); break; }
|
||||
} else { found = true; break; }
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (actualType == targets[i])
|
||||
{
|
||||
if (targets[i] == XA_STRING)
|
||||
*selectionString = convertLatin1toUTF8(data);
|
||||
else
|
||||
*selectionString = _glfw_strdup(data);
|
||||
if (targets[i] == XA_STRING) {
|
||||
const char *string = convertLatin1toUTF8(data);
|
||||
write_data(object, string, strlen(string)); free((void*)string);
|
||||
} else write_data(object, data, itemCount);
|
||||
found = true;
|
||||
}
|
||||
else if (actualType == XA_ATOM && targets[i] == _glfw.x11.TARGETS) {
|
||||
found = true;
|
||||
write_data(object, data, itemCount);
|
||||
}
|
||||
|
||||
XFree(data);
|
||||
XFREE(data);
|
||||
|
||||
if (*selectionString)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!*selectionString)
|
||||
if (!found && report_not_found)
|
||||
{
|
||||
_glfwInputError(GLFW_FORMAT_UNAVAILABLE,
|
||||
"X11: Failed to convert selection to string");
|
||||
"X11: Failed to convert selection to data from clipboard");
|
||||
}
|
||||
|
||||
return *selectionString;
|
||||
#undef XFREE
|
||||
}
|
||||
|
||||
// Make the specified window and its video mode active on its monitor
|
||||
@@ -1098,21 +1115,6 @@ 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)
|
||||
@@ -2849,51 +2851,112 @@ void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor UNUSED)
|
||||
}
|
||||
}
|
||||
|
||||
void _glfwPlatformSetClipboardString(const char* string)
|
||||
{
|
||||
char* copy = _glfw_strdup(string);
|
||||
free(_glfw.x11.clipboardString);
|
||||
_glfw.x11.clipboardString = copy;
|
||||
static MimeAtom atom_for_mime(const char *mime) {
|
||||
for (size_t i = 0; i < _glfw.x11.mime_atoms.sz; i++) {
|
||||
MimeAtom ma = _glfw.x11.mime_atoms.array[i];
|
||||
if (strcmp(ma.mime, mime) == 0) {
|
||||
return ma;
|
||||
}
|
||||
}
|
||||
MimeAtom ma = {.mime=_glfw_strdup(mime), .atom=XInternAtom(_glfw.x11.display, mime, 0)};
|
||||
if (_glfw.x11.mime_atoms.capacity < _glfw.x11.mime_atoms.sz + 1) {
|
||||
_glfw.x11.mime_atoms.capacity += 32;
|
||||
_glfw.x11.mime_atoms.array = realloc(_glfw.x11.mime_atoms.array, _glfw.x11.mime_atoms.capacity);
|
||||
}
|
||||
_glfw.x11.mime_atoms.array[_glfw.x11.mime_atoms.sz++] = ma;
|
||||
return ma;
|
||||
}
|
||||
|
||||
XSetSelectionOwner(_glfw.x11.display,
|
||||
_glfw.x11.CLIPBOARD,
|
||||
_glfw.x11.helperWindowHandle,
|
||||
CurrentTime);
|
||||
|
||||
if (XGetSelectionOwner(_glfw.x11.display, _glfw.x11.CLIPBOARD) !=
|
||||
_glfw.x11.helperWindowHandle)
|
||||
{
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||
"X11: Failed to become owner of clipboard selection");
|
||||
void _glfwPlatformSetClipboard(GLFWClipboardType t) {
|
||||
Atom which = None;
|
||||
_GLFWClipboardData *cd = NULL;
|
||||
AtomArray *aa = NULL;
|
||||
switch (t) {
|
||||
case GLFW_CLIPBOARD: which = _glfw.x11.CLIPBOARD; cd = &_glfw.clipboard; aa = &_glfw.x11.clipboard_atoms; break;
|
||||
case GLFW_PRIMARY_SELECTION: which = _glfw.x11.PRIMARY; cd = &_glfw.primary; aa = &_glfw.x11.primary_atoms; break;
|
||||
}
|
||||
XSetSelectionOwner(_glfw.x11.display, which, _glfw.x11.helperWindowHandle, CurrentTime);
|
||||
if (XGetSelectionOwner(_glfw.x11.display, which) != _glfw.x11.helperWindowHandle) {
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR, "X11: Failed to become owner of clipboard selection");
|
||||
}
|
||||
if (aa->capacity < cd->num_mime_types + 32) {
|
||||
aa->capacity = cd->num_mime_types + 32;
|
||||
aa->array = reallocarray(aa->array, aa->capacity, sizeof(aa->array[0]));
|
||||
}
|
||||
aa->sz = 0;
|
||||
for (size_t i = 0; i < cd->num_mime_types; i++) {
|
||||
MimeAtom *a = aa->array + aa->sz++;
|
||||
*a = atom_for_mime(cd->mime_types[i]);
|
||||
if (strcmp(cd->mime_types[i], "text/plain") == 0) {
|
||||
a = aa->array + aa->sz++;
|
||||
a->atom = _glfw.x11.UTF8_STRING;
|
||||
a->mime = "text/plain";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const char* _glfwPlatformGetClipboardString(void)
|
||||
{
|
||||
return getSelectionString(_glfw.x11.CLIPBOARD);
|
||||
typedef struct chunked_writer {
|
||||
char *buf; size_t sz, cap;
|
||||
} chunked_writer;
|
||||
|
||||
static bool
|
||||
write_chunk(void *object, const char *data, size_t sz) {
|
||||
chunked_writer *cw = object;
|
||||
if (cw->cap < cw->sz + sz) {
|
||||
cw->cap = MAX(cw->cap * 2, cw->sz + 8*sz);
|
||||
cw->buf = realloc(cw->buf, cw->cap);
|
||||
}
|
||||
memcpy(cw->buf + cw->sz, data, sz);
|
||||
cw->sz += sz;
|
||||
return true;
|
||||
}
|
||||
|
||||
void _glfwPlatformSetPrimarySelectionString(const char* string)
|
||||
{
|
||||
free(_glfw.x11.primarySelectionString);
|
||||
_glfw.x11.primarySelectionString = _glfw_strdup(string);
|
||||
|
||||
XSetSelectionOwner(_glfw.x11.display,
|
||||
_glfw.x11.PRIMARY,
|
||||
_glfw.x11.helperWindowHandle,
|
||||
CurrentTime);
|
||||
|
||||
if (XGetSelectionOwner(_glfw.x11.display, _glfw.x11.PRIMARY) !=
|
||||
_glfw.x11.helperWindowHandle)
|
||||
{
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||
"X11: Failed to become owner of primary selection");
|
||||
static void
|
||||
get_available_mime_types(Atom which_clipboard, GLFWclipboardwritedatafun write_data, void *object) {
|
||||
chunked_writer cw = {0};
|
||||
getSelectionString(which_clipboard, &_glfw.x11.TARGETS, 1, write_chunk, &cw, false);
|
||||
size_t count = 0;
|
||||
bool ok = true;
|
||||
if (cw.buf) {
|
||||
Atom *atoms = (Atom*)cw.buf;
|
||||
count = cw.sz / sizeof(Atom);
|
||||
char **names = calloc(count, sizeof(char*));
|
||||
get_atom_names(atoms, count, names);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (strchr(names[i], '/')) {
|
||||
if (ok) ok = write_data(object, names[i], strlen(names[i]));
|
||||
} else {
|
||||
if (atoms[i] == _glfw.x11.UTF8_STRING || atoms[i] == XA_STRING) {
|
||||
if (ok) ok = write_data(object, "text/plain", strlen("text/plain"));
|
||||
}
|
||||
}
|
||||
XFree(names[i]);
|
||||
}
|
||||
free(cw.buf);
|
||||
free(names);
|
||||
}
|
||||
}
|
||||
|
||||
const char* _glfwPlatformGetPrimarySelectionString(void)
|
||||
{
|
||||
return getSelectionString(_glfw.x11.PRIMARY);
|
||||
void
|
||||
_glfwPlatformGetClipboard(GLFWClipboardType clipboard_type, const char* mime_type, GLFWclipboardwritedatafun write_data, void *object) {
|
||||
Atom atoms[4], which = clipboard_type == GLFW_PRIMARY_SELECTION ? _glfw.x11.PRIMARY : _glfw.x11.CLIPBOARD;
|
||||
if (mime_type == NULL) {
|
||||
get_available_mime_types(which, write_data, object);
|
||||
return;
|
||||
}
|
||||
size_t count = 0;
|
||||
if (strcmp(mime_type, "text/plain") == 0) {
|
||||
// we need to do this because GTK/GNOME is developed by morons
|
||||
// they convert text/plain to DOS line endings
|
||||
// https://gitlab.gnome.org/GNOME/gtk/-/issues/2307
|
||||
atoms[count++] = atom_for_mime("text/plain;charset=utf-8").atom;
|
||||
atoms[count++] = _glfw.x11.UTF8_STRING;
|
||||
atoms[count++] = atom_for_mime("text/plain").atom;
|
||||
atoms[count++] = XA_STRING;
|
||||
} else {
|
||||
atoms[count++] = atom_for_mime(mime_type).atom;
|
||||
}
|
||||
getSelectionString(which, atoms, count, write_data, object, true);
|
||||
}
|
||||
|
||||
EGLenum _glfwPlatformGetEGLPlatform(EGLint** attribs)
|
||||
@@ -3134,3 +3197,18 @@ GLFWAPI int glfwSetX11LaunchCommand(GLFWwindow *handle, char **argv, int argc)
|
||||
_GLFWwindow* window = (_GLFWwindow*) handle;
|
||||
return XSetCommand(_glfw.x11.display, window->x11.handle, argv, argc);
|
||||
}
|
||||
|
||||
GLFWAPI void glfwSetX11WindowAsDock(int32_t x11_window_id) {
|
||||
_GLFW_REQUIRE_INIT();
|
||||
Atom type = _glfw.x11.NET_WM_WINDOW_TYPE_DOCK;
|
||||
XChangeProperty(_glfw.x11.display, x11_window_id,
|
||||
_glfw.x11.NET_WM_WINDOW_TYPE, XA_ATOM, 32,
|
||||
PropModeReplace, (unsigned char*) &type, 1);
|
||||
}
|
||||
|
||||
GLFWAPI void glfwSetX11WindowStrut(int32_t x11_window_id, uint32_t dimensions[12]) {
|
||||
_GLFW_REQUIRE_INIT();
|
||||
XChangeProperty(_glfw.x11.display, x11_window_id,
|
||||
_glfw.x11.NET_WM_STRUT_PARTIAL, XA_CARDINAL, 32,
|
||||
PropModeReplace, (unsigned char*) dimensions, 12);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
from typing import (
|
||||
TYPE_CHECKING, Callable, Dict, List, NamedTuple, Optional, Tuple
|
||||
TYPE_CHECKING, Callable, Dict, Iterator, List, NamedTuple, Optional, Tuple
|
||||
)
|
||||
|
||||
from kitty.cli import parse_args
|
||||
@@ -16,11 +17,12 @@ from kitty.typing import BossType, KeyEventType, TypedDict
|
||||
from kitty.utils import ScreenSize
|
||||
|
||||
from ..tui.handler import Handler, result_handler
|
||||
from ..tui.loop import Loop, MouseEvent
|
||||
from ..tui.loop import Loop, MouseEvent, debug
|
||||
from ..tui.operations import MouseTracking, alternate_screen, styled
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import readline
|
||||
debug
|
||||
else:
|
||||
readline = None
|
||||
|
||||
@@ -108,6 +110,15 @@ The default choice is selected when the user presses the :kbd:`Enter` key.
|
||||
--prompt -p
|
||||
default="> "
|
||||
The prompt to use when inputting a line of text or a password.
|
||||
|
||||
|
||||
--unhide-key
|
||||
default=u
|
||||
The key to be pressed to unhide hidden text
|
||||
|
||||
|
||||
--hidden-text-placeholder
|
||||
The text in the message to be replaced by hidden text. The hidden text is read via STDIN.
|
||||
'''
|
||||
|
||||
|
||||
@@ -147,14 +158,17 @@ def extra_for(width: int, screen_width: int) -> int:
|
||||
|
||||
class Password(Handler):
|
||||
|
||||
def __init__(self, cli_opts: AskCLIOptions, prompt: str) -> None:
|
||||
def __init__(self, cli_opts: AskCLIOptions, prompt: str, is_password: bool = True, initial_text: str = '') -> None:
|
||||
self.cli_opts = cli_opts
|
||||
self.prompt = prompt
|
||||
self.initial_text = initial_text
|
||||
from kittens.tui.line_edit import LineEdit
|
||||
self.line_edit = LineEdit(is_password=True)
|
||||
self.line_edit = LineEdit(is_password=is_password)
|
||||
|
||||
def initialize(self) -> None:
|
||||
self.cmd.set_cursor_shape('beam')
|
||||
if self.initial_text:
|
||||
self.line_edit.on_text(self.initial_text, True)
|
||||
self.draw_screen()
|
||||
|
||||
@Handler.atomic_update
|
||||
@@ -194,10 +208,11 @@ class Password(Handler):
|
||||
return ''
|
||||
|
||||
|
||||
class Choose(Handler):
|
||||
class Choose(Handler): # {{{
|
||||
mouse_tracking = MouseTracking.buttons_only
|
||||
|
||||
def __init__(self, cli_opts: AskCLIOptions) -> None:
|
||||
self.prefix_style_pat = re.compile(r'(?:\x1b\[[^m]*?m)+')
|
||||
self.cli_opts = cli_opts
|
||||
self.choices: Dict[str, Choice] = {}
|
||||
self.clickable_ranges: Dict[str, List[Range]] = {}
|
||||
@@ -219,6 +234,18 @@ class Choose(Handler):
|
||||
self.response_on_accept = cli_opts.default or ''
|
||||
if cli_opts.type in ('yesno', 'choices') and self.response_on_accept not in self.allowed:
|
||||
self.response_on_accept = 'y' if cli_opts.type == 'yesno' else tuple(self.choices.keys())[0]
|
||||
self.message = cli_opts.message
|
||||
self.hidden_text_start_pos = self.hidden_text_end_pos = -1
|
||||
self.hidden_text = ''
|
||||
self.replacement_text = t = f'Press {styled(self.cli_opts.unhide_key, fg="green")} or click to show'
|
||||
self.replacement_range = Range(-1, -1, -1)
|
||||
if self.message and self.cli_opts.hidden_text_placeholder:
|
||||
self.hidden_text_start_pos = self.message.find(self.cli_opts.hidden_text_placeholder)
|
||||
if self.hidden_text_start_pos > -1:
|
||||
self.hidden_text = sys.stdin.read().rstrip()
|
||||
self.hidden_text_end_pos = self.hidden_text_start_pos + len(t)
|
||||
suffix = self.message[self.hidden_text_start_pos + len(self.cli_opts.hidden_text_placeholder):]
|
||||
self.message = self.message[:self.hidden_text_start_pos] + t + suffix
|
||||
|
||||
def initialize(self) -> None:
|
||||
self.cmd.set_cursor_visible(False)
|
||||
@@ -227,24 +254,35 @@ class Choose(Handler):
|
||||
def finalize(self) -> None:
|
||||
self.cmd.set_cursor_visible(True)
|
||||
|
||||
def draw_long_text(self, text: str) -> int:
|
||||
y = 0
|
||||
def draw_long_text(self, text: str) -> Iterator[str]:
|
||||
if not text:
|
||||
yield ''
|
||||
return
|
||||
width = self.screen_size.cols - 2
|
||||
m = self.prefix_style_pat.match(text)
|
||||
prefix = m.group() if m else ''
|
||||
while text:
|
||||
t, text = truncate_at_space(text, width)
|
||||
t = t.strip()
|
||||
self.print(' ' * extra_for(wcswidth(t), width), styled(t, bold=True), sep='')
|
||||
y += 1
|
||||
return y
|
||||
yield ' ' * extra_for(wcswidth(t), width) + styled(prefix + t, bold=True)
|
||||
|
||||
@Handler.atomic_update
|
||||
def draw_screen(self) -> None:
|
||||
self.cmd.clear_screen()
|
||||
y = max(0, self.screen_size.rows // 2 - 2)
|
||||
msg_lines: List[str] = []
|
||||
if self.message:
|
||||
for line in self.message.splitlines():
|
||||
msg_lines.extend(self.draw_long_text(line))
|
||||
y = self.screen_size.rows - len(msg_lines)
|
||||
y = max(0, (y // 2) - 2)
|
||||
self.print(end='\r\n'*y)
|
||||
if self.cli_opts.message:
|
||||
for line in self.cli_opts.message.splitlines():
|
||||
y += self.draw_long_text(line)
|
||||
for line in msg_lines:
|
||||
if self.replacement_text in line:
|
||||
idx = line.find(self.replacement_text)
|
||||
x = wcswidth(line[:idx])
|
||||
self.replacement_range = Range(x, x + wcswidth(self.replacement_text), y)
|
||||
self.print(line)
|
||||
y += 1
|
||||
if self.screen_size.rows > 2:
|
||||
self.print()
|
||||
y += 1
|
||||
@@ -259,7 +297,8 @@ class Choose(Handler):
|
||||
current_line_length = 0
|
||||
current_line: List[Tuple[str, str]] = []
|
||||
lines: List[List[Tuple[str, str]]] = []
|
||||
sep, sep_sz = ' ', 2
|
||||
sep = ' '
|
||||
sep_sz = len(sep) + 2 # for the borders
|
||||
|
||||
for choice in choices:
|
||||
self.clickable_ranges[choice.letter] = []
|
||||
@@ -272,7 +311,7 @@ class Choose(Handler):
|
||||
current_line = []
|
||||
current_line_length = 0
|
||||
current_line.append((choice.letter, text))
|
||||
current_line_length += sz
|
||||
current_line_length += sz + sep_sz
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
|
||||
@@ -311,10 +350,12 @@ class Choose(Handler):
|
||||
self.print(' ' * offset, line, sep='', end='' if is_last else '\r\n')
|
||||
y += 1
|
||||
|
||||
self.cmd.set_line_wrapping(False)
|
||||
for boxed_line in lines:
|
||||
print_line(top, *boxed_line)
|
||||
print_line(middle, *boxed_line)
|
||||
print_line(bottom, *boxed_line, is_last=boxed_line is lines[-1])
|
||||
self.cmd.set_line_wrapping(True)
|
||||
|
||||
def draw_choice(self, y: int) -> None:
|
||||
if y + 3 <= self.screen_size.rows:
|
||||
@@ -370,6 +411,14 @@ class Choose(Handler):
|
||||
self.quit_loop(0)
|
||||
elif self.cli_opts.type == 'yesno':
|
||||
self.on_interrupt()
|
||||
elif self.hidden_text and text == self.cli_opts.unhide_key:
|
||||
self.unhide()
|
||||
|
||||
def unhide(self) -> None:
|
||||
if self.hidden_text and self.message:
|
||||
self.message = self.message[:self.hidden_text_start_pos] + self.hidden_text + self.message[self.hidden_text_end_pos:]
|
||||
self.hidden_text = ''
|
||||
self.draw_screen()
|
||||
|
||||
def on_key(self, key_event: KeyEventType) -> None:
|
||||
if key_event.matches('esc'):
|
||||
@@ -385,6 +434,8 @@ class Choose(Handler):
|
||||
self.response = letter
|
||||
self.quit_loop(0)
|
||||
return
|
||||
if self.hidden_text and self.replacement_range.has_point(ev.cell_x, ev.cell_y):
|
||||
self.unhide()
|
||||
|
||||
def on_resize(self, screen_size: ScreenSize) -> None:
|
||||
self.screen_size = screen_size
|
||||
@@ -393,6 +444,7 @@ class Choose(Handler):
|
||||
def on_interrupt(self) -> None:
|
||||
self.quit_loop(1)
|
||||
on_eot = on_interrupt
|
||||
# }}}
|
||||
|
||||
|
||||
def main(args: List[str]) -> Response:
|
||||
|
||||
@@ -231,11 +231,11 @@ static PyMethodDef module_methods[] = {
|
||||
};
|
||||
|
||||
static struct PyModuleDef module = {
|
||||
.m_base = PyModuleDef_HEAD_INIT,
|
||||
.m_name = "subseq_matcher", /* name of module */
|
||||
.m_doc = NULL,
|
||||
.m_size = -1,
|
||||
.m_methods = module_methods
|
||||
.m_base = PyModuleDef_HEAD_INIT,
|
||||
.m_name = "subseq_matcher", /* name of module */
|
||||
.m_doc = NULL,
|
||||
.m_size = -1,
|
||||
.m_methods = module_methods
|
||||
};
|
||||
|
||||
EXPORTED PyMODINIT_FUNC
|
||||
|
||||
@@ -1,56 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import codecs
|
||||
import io
|
||||
import os
|
||||
import select
|
||||
import sys
|
||||
from typing import List, NoReturn, Optional
|
||||
|
||||
from kitty.cli import parse_args
|
||||
from kitty.cli_stub import ClipboardCLIOptions
|
||||
from kitty.fast_data_types import parse_input_from_terminal
|
||||
|
||||
from ..tui.handler import Handler
|
||||
from ..tui.loop import Loop
|
||||
|
||||
|
||||
class Clipboard(Handler):
|
||||
|
||||
def __init__(self, data_to_send: Optional[bytes], args: ClipboardCLIOptions):
|
||||
self.args = args
|
||||
self.clipboard_contents: Optional[str] = None
|
||||
self.data_to_send = data_to_send
|
||||
self.quit_on_write = False
|
||||
|
||||
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:
|
||||
if self.args.wait_for_completion:
|
||||
# ask kitty for the TN terminfo capability and
|
||||
# only quit after a response is received
|
||||
self.print('\x1bP+q544e\x1b\\', end='')
|
||||
self.print('Waiting for completion...')
|
||||
return
|
||||
self.quit_on_write = True
|
||||
return
|
||||
self.cmd.request_from_clipboard(self.args.use_primary)
|
||||
|
||||
def on_writing_finished(self) -> None:
|
||||
if self.quit_on_write:
|
||||
self.quit_loop(0)
|
||||
|
||||
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: str, val: str) -> None:
|
||||
self.quit_loop(0)
|
||||
|
||||
def on_interrupt(self) -> None:
|
||||
self.quit_loop(1)
|
||||
|
||||
def on_eot(self) -> None:
|
||||
self.quit_loop(1)
|
||||
|
||||
from ..tui.operations import (
|
||||
raw_mode, request_from_clipboard, write_to_clipboard
|
||||
)
|
||||
|
||||
OPTIONS = r'''
|
||||
--get-clipboard
|
||||
@@ -84,6 +48,62 @@ popup, see :opt:`clipboard_control` for details.
|
||||
'''
|
||||
|
||||
usage = ''
|
||||
got_capability_response = False
|
||||
got_clipboard_response = False
|
||||
clipboard_contents = ''
|
||||
clipboard_from_primary = False
|
||||
|
||||
|
||||
def ignore(x: str) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def on_text(x: str) -> None:
|
||||
if '\x03' in x:
|
||||
raise KeyboardInterrupt()
|
||||
if '\x04' in x:
|
||||
raise EOFError()
|
||||
|
||||
|
||||
def on_dcs(dcs: str) -> None:
|
||||
global got_capability_response
|
||||
if dcs.startswith('1+r'):
|
||||
got_capability_response = True
|
||||
|
||||
|
||||
def on_osc(osc: str) -> None:
|
||||
global clipboard_contents, clipboard_from_primary, got_clipboard_response
|
||||
idx = osc.find(';')
|
||||
if idx <= 0:
|
||||
return
|
||||
q = osc[:idx]
|
||||
if q == '52':
|
||||
got_clipboard_response = True
|
||||
widx = osc.find(';', idx + 1)
|
||||
if widx < idx:
|
||||
clipboard_from_primary = osc.find('p', idx + 1) > -1
|
||||
clipboard_contents = ''
|
||||
else:
|
||||
from base64 import standard_b64decode
|
||||
clipboard_from_primary = osc.find('p', idx+1, widx) > -1
|
||||
data = memoryview(osc.encode('ascii'))
|
||||
clipboard_contents = standard_b64decode(data[widx+1:]).decode('utf-8')
|
||||
|
||||
|
||||
def wait_loop(tty_fd: int) -> None:
|
||||
os.set_blocking(tty_fd, False)
|
||||
decoder = codecs.getincrementaldecoder('utf-8')('ignore')
|
||||
with raw_mode(tty_fd):
|
||||
buf = ''
|
||||
while not got_capability_response and not got_clipboard_response:
|
||||
rd = select.select([tty_fd], [], [])[0]
|
||||
if rd:
|
||||
raw = os.read(tty_fd, io.DEFAULT_BUFFER_SIZE)
|
||||
if not raw:
|
||||
raise EOFError()
|
||||
data = decoder.decode(raw)
|
||||
buf = (buf + data) if buf else data
|
||||
buf = parse_input_from_terminal(on_text, on_dcs, ignore, on_osc, ignore, ignore, buf, False)
|
||||
|
||||
|
||||
def main(args: List[str]) -> NoReturn:
|
||||
@@ -93,17 +113,34 @@ def main(args: List[str]) -> NoReturn:
|
||||
data: Optional[bytes] = None
|
||||
if not sys.stdin.isatty():
|
||||
data = sys.stdin.buffer.read()
|
||||
try:
|
||||
sys.stdin = open(os.ctermid())
|
||||
except FileNotFoundError:
|
||||
raise SystemExit('Not connected to a controlling terminal device, no /dev/tty')
|
||||
loop = Loop()
|
||||
handler = Clipboard(data, cli_opts)
|
||||
loop.loop(handler)
|
||||
if loop.return_code == 0 and handler.clipboard_contents:
|
||||
sys.stdout.write(handler.clipboard_contents)
|
||||
sys.stdout.flush()
|
||||
raise SystemExit(loop.return_code)
|
||||
wait_for_capability_response = False
|
||||
data_to_write = []
|
||||
if data:
|
||||
data_to_write.append(write_to_clipboard(data, cli_opts.use_primary).encode('ascii'))
|
||||
if not cli_opts.get_clipboard and cli_opts.wait_for_completion:
|
||||
data_to_write.append(b'\x1bP+q544e\x1b\\')
|
||||
wait_for_capability_response = True
|
||||
if cli_opts.get_clipboard:
|
||||
data_to_write.append(request_from_clipboard(cli_opts.use_primary).encode('ascii'))
|
||||
wait_for_capability_response = True
|
||||
tty_fd = os.open(os.ctermid(), os.O_RDWR | os.O_CLOEXEC)
|
||||
retcode = 0
|
||||
with open(tty_fd, 'wb', closefd=True) as ttyf:
|
||||
for x in data_to_write:
|
||||
ttyf.write(x)
|
||||
ttyf.flush()
|
||||
if wait_for_capability_response:
|
||||
try:
|
||||
wait_loop(tty_fd)
|
||||
except KeyboardInterrupt:
|
||||
sys.excepthook = lambda *a: None
|
||||
raise
|
||||
except EOFError:
|
||||
retcode = 1
|
||||
if clipboard_contents:
|
||||
print(end=clipboard_contents)
|
||||
|
||||
raise SystemExit(retcode)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
7
kittens/diff/collect.py
Executable file → Normal file
7
kittens/diff/collect.py
Executable file → Normal file
@@ -2,7 +2,6 @@
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import os
|
||||
import re
|
||||
from contextlib import suppress
|
||||
from fnmatch import fnmatch
|
||||
from functools import lru_cache
|
||||
@@ -12,6 +11,7 @@ from typing import (
|
||||
)
|
||||
|
||||
from kitty.guess_mime_type import guess_type
|
||||
from kitty.utils import control_codes_pat
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .highlight import DiffHighlight # noqa
|
||||
@@ -161,12 +161,9 @@ def collect_files(collection: Collection, left: str, right: str) -> None:
|
||||
collection.add_add(right_path_map[name])
|
||||
|
||||
|
||||
sanitize_pat = re.compile('[\x00-\x09\x0b-\x1f\x7f\x80-\x9f]')
|
||||
|
||||
|
||||
def sanitize(text: str) -> str:
|
||||
ntext = text.replace('\r\n', '⏎\n')
|
||||
return sanitize_pat.sub('░', ntext)
|
||||
return control_codes_pat().sub('░', ntext)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
|
||||
0
kittens/diff/main.py
Executable file → Normal file
0
kittens/diff/main.py
Executable file → Normal file
6
kittens/diff/options/definition.py
Executable file → Normal file
6
kittens/diff/options/definition.py
Executable file → Normal file
@@ -56,9 +56,9 @@ A glob pattern that is matched against only the filename of files and directorie
|
||||
files and directories are ignored when scanning the filesystem to look for files to diff.
|
||||
Can be specified multiple times to use multiple patterns. For example::
|
||||
|
||||
ignore_name .git
|
||||
ignore_name *~
|
||||
ignore_name *.pyc
|
||||
ignore_name .git
|
||||
ignore_name *~
|
||||
ignore_name *.pyc
|
||||
''',
|
||||
)
|
||||
|
||||
|
||||
0
kittens/diff/options/utils.py
Executable file → Normal file
0
kittens/diff/options/utils.py
Executable file → Normal file
@@ -187,11 +187,11 @@ static PyMethodDef module_methods[] = {
|
||||
};
|
||||
|
||||
static struct PyModuleDef module = {
|
||||
.m_base = PyModuleDef_HEAD_INIT,
|
||||
.m_name = "diff_speedup", /* name of module */
|
||||
.m_doc = NULL,
|
||||
.m_size = -1,
|
||||
.m_methods = module_methods
|
||||
.m_base = PyModuleDef_HEAD_INIT,
|
||||
.m_name = "diff_speedup", /* name of module */
|
||||
.m_doc = NULL,
|
||||
.m_size = -1,
|
||||
.m_methods = module_methods
|
||||
};
|
||||
|
||||
EXPORTED PyMODINIT_FUNC
|
||||
|
||||
@@ -15,13 +15,14 @@ from typing import (
|
||||
|
||||
from kitty.cli import parse_args
|
||||
from kitty.cli_stub import HintsCLIOptions
|
||||
from kitty.clipboard import set_clipboard_string, set_primary_selection
|
||||
from kitty.constants import website_url
|
||||
from kitty.fast_data_types import get_options, set_clipboard_string
|
||||
from kitty.fast_data_types import get_options, wcswidth
|
||||
from kitty.key_encoding import KeyEvent
|
||||
from kitty.typing import BossType, KittyCommonOpts
|
||||
from kitty.utils import (
|
||||
ScreenSize, kitty_ansi_sanitizer_pat, resolve_custom_file,
|
||||
screen_size_function, set_primary_selection
|
||||
screen_size_function
|
||||
)
|
||||
|
||||
from ..tui.handler import Handler, result_handler
|
||||
@@ -411,7 +412,10 @@ def convert_text(text: str, cols: int) -> str:
|
||||
appended = False
|
||||
for line in full_line.split('\r'):
|
||||
if line:
|
||||
lines.append(line.ljust(cols, '\0'))
|
||||
line_sz = wcswidth(line)
|
||||
if line_sz < cols:
|
||||
line += '\0' * (cols - line_sz)
|
||||
lines.append(line)
|
||||
lines.append('\r')
|
||||
appended = True
|
||||
if appended:
|
||||
@@ -536,12 +540,27 @@ OPTIONS = r'''
|
||||
--program
|
||||
type=list
|
||||
What program to use to open matched text. Defaults to the default open program
|
||||
for the operating system. Use a value of :code:`-` to paste the match into the
|
||||
terminal window instead. A value of :code:`@` will copy the match to the
|
||||
clipboard. A value of :code:`*` will copy the match to the primary selection
|
||||
(on systems that support primary selections). A value of :code:`default` will
|
||||
run the default open program. Can be specified multiple times to run multiple
|
||||
programs.
|
||||
for the operating system. Various special values are supported:
|
||||
|
||||
:code:`-`
|
||||
paste the match into the terminal window.
|
||||
|
||||
:code:`@`
|
||||
copy the match to the clipboard
|
||||
|
||||
:code:`*`
|
||||
copy the match to the primary selection (on systems that support primary selections)
|
||||
|
||||
:code:`default`
|
||||
run the default open program.
|
||||
|
||||
:code:`launch`
|
||||
run :doc:`/launch` to open the program in a new kitty tab, window, overlay, etc.
|
||||
For example::
|
||||
|
||||
--program "launch --type=tab vim"
|
||||
|
||||
Can be specified multiple times to run multiple programs.
|
||||
|
||||
|
||||
--type
|
||||
@@ -803,6 +822,7 @@ def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int,
|
||||
elif program == '*':
|
||||
set_primary_selection(joined_text())
|
||||
else:
|
||||
from kitty.conf.utils import to_cmdline
|
||||
cwd = data['cwd']
|
||||
program = get_options().open_url_with if program == 'default' else program
|
||||
if text_type == 'hyperlink':
|
||||
@@ -811,12 +831,20 @@ def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int,
|
||||
if w is not None:
|
||||
w.open_url(m, hyperlink_id=1, cwd=cwd)
|
||||
else:
|
||||
launch_args = []
|
||||
if isinstance(program, str) and program.startswith('launch '):
|
||||
launch_args = to_cmdline(program)
|
||||
launch_args.insert(1, '--cwd=' + cwd)
|
||||
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)
|
||||
if launch_args:
|
||||
w = boss.window_id_map.get(target_window_id)
|
||||
boss.call_remote_control(active_window=w, args=tuple(launch_args + ([m] if isinstance(m, str) else m)))
|
||||
else:
|
||||
boss.open_url(m, program, cwd=cwd)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# generated by gen-wcwidth.py, do not edit
|
||||
|
||||
url_delimiters = '\x00-\x09\x0b-\x0c\x0e-\x20\x7f-\xa0\xad\u0600-\u0605\u061c\u06dd\u070f\u0890-\u0891\u08e2\u1680\u180e\u2000-\u200f\u2028-\u202f\u205f-\u2064\u2066-\u206f\u3000\ud800-\uf8ff\ufeff\ufff9-\ufffb\U000110bd\U000110cd\U00013430-\U00013438\U0001bca0-\U0001bca3\U0001d173-\U0001d17a\U000e0001\U000e0020-\U000e007f\U000f0000-\U000ffffd\U00100000-\U0010fffd' # noqa
|
||||
url_delimiters = '\x00-\x09\x0b-\x0c\x0e-\x20\x7f-\xa0\xad\u0600-\u0605\u061c\u06dd\u070f\u0890-\u0891\u08e2\u1680\u180e\u2000-\u200f\u2028-\u202f\u205f-\u2064\u2066-\u206f\u3000\ud800-\uf8ff\ufeff\ufff9-\ufffb\U000110bd\U000110cd\U00013430-\U0001343f\U0001bca0-\U0001bca3\U0001d173-\U0001d17a\U000e0001\U000e0020-\U000e007f\U000f0000-\U000ffffd\U00100000-\U0010fffd' # noqa
|
||||
64
kittens/hyperlinked_grep/main.py
Normal file → Executable file
64
kittens/hyperlinked_grep/main.py
Normal file → Executable file
@@ -20,7 +20,49 @@ def write_hyperlink(write: Callable[[bytes], None], url: bytes, line: bytes, fra
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not sys.stdout.isatty() and '--pretty' not in sys.argv and '-p' not in sys.argv:
|
||||
i = 1
|
||||
all_link_options = {'matching_lines', 'context_lines', 'file_headers'}
|
||||
link_options = set()
|
||||
delegate_to_rg = False
|
||||
|
||||
def parse_link_options(raw: str) -> None:
|
||||
nonlocal delegate_to_rg
|
||||
if not raw:
|
||||
raise SystemExit('Must specify an argument for --kitten option')
|
||||
p, _, s = raw.partition('=')
|
||||
if p != 'hyperlink':
|
||||
raise SystemExit(f'Unknown argument for --kitten: {raw}')
|
||||
for option in s.split(','):
|
||||
if option == 'all':
|
||||
link_options.update(all_link_options)
|
||||
delegate_to_rg = False
|
||||
elif option == 'none':
|
||||
delegate_to_rg = True
|
||||
link_options.clear()
|
||||
elif option not in all_link_options:
|
||||
a = ', '.join(sorted(all_link_options))
|
||||
raise SystemExit(f"hyperlink option must be one of all, none, {a}, not '{option}'")
|
||||
else:
|
||||
link_options.add(option)
|
||||
delegate_to_rg = False
|
||||
|
||||
while i < len(sys.argv):
|
||||
if sys.argv[i] == '--kitten':
|
||||
next_item = '' if i + 1 >= len(sys.argv) else sys.argv[i + 1]
|
||||
parse_link_options(next_item)
|
||||
del sys.argv[i:i+2]
|
||||
elif sys.argv[i].startswith('--kitten='):
|
||||
parse_link_options(sys.argv[i][len('--kitten='):])
|
||||
del sys.argv[i]
|
||||
else:
|
||||
i += 1
|
||||
if not link_options: # Default to linking everything if no options given
|
||||
link_options.update(all_link_options)
|
||||
link_file_headers = 'file_headers' in link_options
|
||||
link_context_lines = 'context_lines' in link_options
|
||||
link_matching_lines = 'matching_lines' in link_options
|
||||
|
||||
if delegate_to_rg or (not sys.stdout.isatty() and '--pretty' not in sys.argv and '-p' not in sys.argv):
|
||||
os.execlp('rg', 'rg', *sys.argv[1:])
|
||||
cmdline = ['rg', '--pretty', '--with-filename'] + sys.argv[1:]
|
||||
try:
|
||||
@@ -31,7 +73,7 @@ def main() -> None:
|
||||
write: Callable[[bytes], None] = cast(Callable[[bytes], None], sys.stdout.buffer.write)
|
||||
sgr_pat = re.compile(br'\x1b\[.*?m')
|
||||
osc_pat = re.compile(b'\x1b\\].*?\x1b\\\\')
|
||||
num_pat = re.compile(br'^(\d+)[:-]')
|
||||
num_pat = re.compile(br'^(\d+)([:-])')
|
||||
|
||||
in_result: bytes = b''
|
||||
hostname = socket.gethostname().encode('utf-8')
|
||||
@@ -43,20 +85,22 @@ def main() -> None:
|
||||
if not clean_line:
|
||||
in_result = b''
|
||||
write(b'\n')
|
||||
continue
|
||||
if in_result:
|
||||
elif in_result:
|
||||
m = num_pat.match(clean_line)
|
||||
if m is not None:
|
||||
write_hyperlink(write, in_result, line, frag=m.group(1))
|
||||
else:
|
||||
write(line)
|
||||
is_match_line = m.group(2) == b':'
|
||||
if (is_match_line and link_matching_lines) or (not is_match_line and link_context_lines):
|
||||
write_hyperlink(write, in_result, line, frag=m.group(1))
|
||||
continue
|
||||
write(line)
|
||||
else:
|
||||
if line.strip():
|
||||
path = quote_from_bytes(os.path.abspath(clean_line)).encode('utf-8')
|
||||
in_result = b'file://' + hostname + path
|
||||
write_hyperlink(write, in_result, line)
|
||||
else:
|
||||
write(line)
|
||||
if link_file_headers:
|
||||
write_hyperlink(write, in_result, line)
|
||||
continue
|
||||
write(line)
|
||||
except KeyboardInterrupt:
|
||||
p.send_signal(signal.SIGINT)
|
||||
except (EOFError, BrokenPipeError):
|
||||
|
||||
8
kittens/icat/main.py
Executable file → Normal file
8
kittens/icat/main.py
Executable file → Normal file
@@ -353,7 +353,7 @@ def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfil
|
||||
else:
|
||||
import struct
|
||||
use_number = max(1, struct.unpack('@I', os.urandom(4))[0])
|
||||
with NamedTemporaryFile() as f:
|
||||
with NamedTemporaryFile(prefix='tty-graphics-protocol-') as f:
|
||||
prefix = f.name
|
||||
frame_data = render_image(
|
||||
path, prefix, m, available_width, available_height, args.scale_up,
|
||||
@@ -405,7 +405,7 @@ def detect_support(wait_for: float = 10, silent: bool = False) -> bool:
|
||||
parse_responses()
|
||||
return 1 not in responses or 2 not in responses
|
||||
|
||||
with NamedTemporaryFile() as f:
|
||||
with NamedTemporaryFile(prefix='tty-graphics-protocol') as f:
|
||||
f.write(b'abcd')
|
||||
f.flush()
|
||||
gc = GraphicsCommand()
|
||||
@@ -472,13 +472,13 @@ def process_single_item(
|
||||
file_removed = False
|
||||
try:
|
||||
if isinstance(item, bytes):
|
||||
with NamedTemporaryFile(prefix='stdin-image-data-', delete=False) as tf:
|
||||
with NamedTemporaryFile(prefix='tty-graphics-protocol-', delete=False) as tf:
|
||||
tf.write(item)
|
||||
item = tf.name
|
||||
is_tempfile = True
|
||||
if url_pat is not None and url_pat.match(item) is not None:
|
||||
from urllib.request import urlretrieve
|
||||
with NamedTemporaryFile(prefix='url-image-data-', delete=False) as tf:
|
||||
with NamedTemporaryFile(prefix='tty-graphics-protocol-', delete=False) as tf:
|
||||
try:
|
||||
with socket_timeout(30):
|
||||
urlretrieve(item, filename=tf.name)
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any, Callable, Dict, List, Tuple
|
||||
|
||||
@@ -11,6 +9,7 @@ from kitty.cli import parse_args
|
||||
from kitty.cli_stub import PanelCLIOptions
|
||||
from kitty.constants import appname, is_macos
|
||||
from kitty.os_window_size import WindowSizeData
|
||||
from kitty.fast_data_types import make_x11_window_a_dock_window
|
||||
|
||||
OPTIONS = r'''
|
||||
--lines
|
||||
@@ -65,14 +64,7 @@ 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: str, silent: bool = False) -> None:
|
||||
cmd_ = ['xprop'] + list(cmd)
|
||||
try:
|
||||
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:
|
||||
raise SystemExit(cp.returncode)
|
||||
Strut = Tuple[int, int, int, int, int, int, int, int, int, int, int, int]
|
||||
|
||||
|
||||
def create_strut(
|
||||
@@ -80,43 +72,33 @@ def create_strut(
|
||||
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',
|
||||
'-set', '_NET_WM_STRUT_PARTIAL',
|
||||
f'{left},{right},{top},{bottom},'
|
||||
f'{left_start_y},{left_end_y},{right_start_y},{right_end_y},'
|
||||
f'{top_start_x},{top_end_x},{bottom_start_x},{bottom_end_x}'
|
||||
)
|
||||
) -> Strut:
|
||||
return left, right, top, bottom, left_start_y, left_end_y, right_start_y, right_end_y, top_start_x, top_end_x, bottom_start_x, bottom_end_x
|
||||
|
||||
|
||||
def create_top_strut(win_id: int, width: int, height: int) -> None:
|
||||
create_strut(win_id, top=height, top_end_x=width)
|
||||
def create_top_strut(win_id: int, width: int, height: int) -> Strut:
|
||||
return create_strut(win_id, top=height, top_end_x=width)
|
||||
|
||||
|
||||
def create_bottom_strut(win_id: int, width: int, height: int) -> None:
|
||||
create_strut(win_id, bottom=height, bottom_end_x=width)
|
||||
def create_bottom_strut(win_id: int, width: int, height: int) -> Strut:
|
||||
return create_strut(win_id, bottom=height, bottom_end_x=width)
|
||||
|
||||
|
||||
def create_left_strut(win_id: int, width: int, height: int) -> None:
|
||||
create_strut(win_id, left=width, left_end_y=height)
|
||||
def create_left_strut(win_id: int, width: int, height: int) -> Strut:
|
||||
return create_strut(win_id, left=width, left_end_y=height)
|
||||
|
||||
|
||||
def create_right_strut(win_id: int, width: int, height: int) -> None:
|
||||
create_strut(win_id, right=width, right_end_y=height)
|
||||
def create_right_strut(win_id: int, width: int, height: int) -> Strut:
|
||||
return create_strut(win_id, right=width, right_end_y=height)
|
||||
|
||||
|
||||
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()[f'create_{args.edge}_strut']
|
||||
func(win_id, window_width, window_height)
|
||||
strut = func(win_id, window_width, window_height)
|
||||
make_x11_window_a_dock_window(win_id, strut)
|
||||
|
||||
|
||||
def initial_window_size_func(opts: WindowSizeData, cached_values: Dict[str, Any]) -> Callable[[int, int, float, float, float, float], Tuple[int, int]]:
|
||||
@@ -140,8 +122,6 @@ 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')
|
||||
if not shutil.which('xprop'):
|
||||
raise SystemExit('The xprop program is required for the panel kitten')
|
||||
args, items = parse_panel_args(sys_args[1:])
|
||||
if not items:
|
||||
raise SystemExit('You must specify the program to run')
|
||||
|
||||
@@ -1,453 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import select
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from importlib import import_module
|
||||
from typing import (
|
||||
IO, TYPE_CHECKING, Any, Dict, List, NoReturn, Optional, Union, cast
|
||||
)
|
||||
|
||||
from kitty.child import remove_cloexec
|
||||
from kitty.constants import kitty_exe
|
||||
from kitty.entry_points import main as main_entry_point
|
||||
from kitty.fast_data_types import establish_controlling_tty, safe_pipe
|
||||
from kitty.shm import SharedMemory
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _typeshed import ReadableBuffer, WriteableBuffer
|
||||
|
||||
|
||||
hangup_events = select.POLLHUP
|
||||
error_events = select.POLLERR | select.POLLNVAL
|
||||
basic_events = hangup_events | error_events
|
||||
|
||||
|
||||
class PrewarmProcessFailed(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Child:
|
||||
child_id: int
|
||||
child_process_pid: int
|
||||
|
||||
|
||||
class PrewarmProcess:
|
||||
|
||||
def __init__(self, create_file_to_read_from_worker: bool = False) -> None:
|
||||
self.from_worker_fd, self.in_worker_fd = safe_pipe()
|
||||
self.children: Dict[int, Child] = {}
|
||||
if create_file_to_read_from_worker:
|
||||
os.set_blocking(self.from_worker_fd, True)
|
||||
self.from_worker = open(self.from_worker_fd, mode='r', closefd=True)
|
||||
self.from_worker_fd = -1
|
||||
|
||||
def take_from_worker_fd(self) -> int:
|
||||
ans, self.from_worker_fd = self.from_worker_fd, -1
|
||||
return ans
|
||||
|
||||
def __del__(self) -> None:
|
||||
if self.from_worker_fd > -1:
|
||||
os.close(self.from_worker_fd)
|
||||
self.from_worker_fd = -1
|
||||
if hasattr(self, 'from_worker'):
|
||||
self.from_worker.close()
|
||||
del self.from_worker
|
||||
if self.worker_started:
|
||||
import subprocess
|
||||
self.process.stdin and self.process.stdin.close()
|
||||
self.process.stdout and self.process.stdout.close()
|
||||
try:
|
||||
self.process.wait(timeout=1.0)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
del self.process
|
||||
|
||||
@property
|
||||
def worker_started(self) -> bool:
|
||||
return self.in_worker_fd == -1
|
||||
|
||||
def ensure_worker(self) -> None:
|
||||
if not self.worker_started:
|
||||
import subprocess
|
||||
self.process = subprocess.Popen(
|
||||
[kitty_exe(), '+kitten', 'prewarm', str(self.in_worker_fd)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, pass_fds=(self.in_worker_fd,))
|
||||
os.close(self.in_worker_fd)
|
||||
self.in_worker_fd = -1
|
||||
assert self.process.stdin is not None and self.process.stdout is not None
|
||||
self.write_to_process_fd = self.process.stdin.fileno()
|
||||
self.read_from_process_fd = self.process.stdout.fileno()
|
||||
os.set_blocking(self.write_to_process_fd, False)
|
||||
os.set_blocking(self.read_from_process_fd, False)
|
||||
self.poll = select.poll()
|
||||
self.poll.register(self.process.stdout.fileno(), select.POLLIN | basic_events)
|
||||
|
||||
def poll_to_send(self, yes: bool = True) -> None:
|
||||
if yes:
|
||||
self.poll.register(self.write_to_process_fd, select.POLLOUT | basic_events)
|
||||
else:
|
||||
self.poll.unregister(self.write_to_process_fd)
|
||||
|
||||
def reload_kitty_config(self) -> None:
|
||||
if self.worker_started:
|
||||
self.send_to_prewarm_process('reload_kitty_config:\n')
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
tty_fd: int,
|
||||
argv: List[str],
|
||||
cwd: str = '',
|
||||
env: Optional[Dict[str, str]] = None,
|
||||
stdin_data: Optional[Union[str, bytes]] = None
|
||||
) -> Child:
|
||||
self.ensure_worker()
|
||||
tty_name = os.ttyname(tty_fd)
|
||||
if isinstance(stdin_data, str):
|
||||
stdin_data = stdin_data.encode()
|
||||
if env is None:
|
||||
env = dict(os.environ)
|
||||
cmd: Dict[str, Union[int, List[str], str, Dict[str, str]]] = {
|
||||
'tty_name': tty_name, 'cwd': cwd or os.getcwd(), 'argv': argv, 'env': env,
|
||||
}
|
||||
total_size = 0
|
||||
if stdin_data is not None:
|
||||
cmd['stdin_size'] = len(stdin_data)
|
||||
total_size += len(stdin_data)
|
||||
data = json.dumps(cmd).encode()
|
||||
total_size += len(data) + SharedMemory.num_bytes_for_size
|
||||
with SharedMemory(size=total_size, unlink_on_exit=True) as shm:
|
||||
shm.write_data_with_size(data)
|
||||
if stdin_data:
|
||||
shm.write(stdin_data)
|
||||
shm.flush()
|
||||
self.send_to_prewarm_process(f'fork:{shm.name}\n')
|
||||
input_buf = b''
|
||||
st = time.monotonic()
|
||||
while time.monotonic() - st < 2:
|
||||
for (fd, event) in self.poll.poll(0.2):
|
||||
if event & basic_events:
|
||||
raise PrewarmProcessFailed('Failed doing I/O with prewarm process')
|
||||
if fd == self.read_from_process_fd and event & select.POLLIN:
|
||||
d = os.read(self.read_from_process_fd, io.DEFAULT_BUFFER_SIZE)
|
||||
input_buf += d
|
||||
while (idx := input_buf.find(b'\n')) > -1:
|
||||
line = input_buf[:idx].decode()
|
||||
input_buf = input_buf[idx+1:]
|
||||
if line.startswith('CHILD:'):
|
||||
_, cid, pid = line.split(':')
|
||||
child = self.add_child(int(cid), int(pid))
|
||||
shm.unlink_on_exit = False
|
||||
return child
|
||||
if line.startswith('ERR:'):
|
||||
raise PrewarmProcessFailed(line.split(':', 1)[-1])
|
||||
raise PrewarmProcessFailed('Timed out waiting for I/O with prewarm process')
|
||||
|
||||
def add_child(self, child_id: int, pid: int) -> Child:
|
||||
self.children[child_id] = c = Child(child_id, pid)
|
||||
return c
|
||||
|
||||
def send_to_prewarm_process(self, output_buf: Union[str, bytes] = b'', timeout: float = 2) -> None:
|
||||
if isinstance(output_buf, str):
|
||||
output_buf = output_buf.encode()
|
||||
st = time.monotonic()
|
||||
while time.monotonic() - st < timeout and output_buf:
|
||||
self.poll_to_send(bool(output_buf))
|
||||
for (fd, event) in self.poll.poll(0.2):
|
||||
if event & basic_events:
|
||||
raise PrewarmProcessFailed('Failed doing I/O with prewarm process')
|
||||
if fd == self.write_to_process_fd and event & select.POLLOUT:
|
||||
n = os.write(self.write_to_process_fd, output_buf)
|
||||
output_buf = output_buf[n:]
|
||||
self.poll_to_send(False)
|
||||
if output_buf:
|
||||
raise PrewarmProcessFailed('Timed out waiting to write to prewarm process')
|
||||
|
||||
def mark_child_as_ready(self, child_id: int) -> bool:
|
||||
c = self.children.pop(child_id, None)
|
||||
if c is None:
|
||||
return False
|
||||
self.send_to_prewarm_process(f'ready:{child_id}\n')
|
||||
return True
|
||||
|
||||
|
||||
def reload_kitty_config() -> None:
|
||||
from kittens.tui.utils import kitty_opts
|
||||
kitty_opts.clear_cached()
|
||||
kitty_opts()
|
||||
|
||||
|
||||
def prewarm() -> None:
|
||||
reload_kitty_config()
|
||||
for kitten in ('hints', 'ssh', 'unicode_input', 'ask', 'show_error'):
|
||||
import_module(f'kittens.{kitten}.main')
|
||||
|
||||
|
||||
class MemoryViewReadWrapperBytes(io.BufferedIOBase):
|
||||
|
||||
def __init__(self, mw: memoryview):
|
||||
self.mw = mw
|
||||
self.pos = 0
|
||||
|
||||
def detach(self) -> io.RawIOBase:
|
||||
raise io.UnsupportedOperation('detach() not supported')
|
||||
|
||||
def read(self, size: Optional[int] = -1) -> bytes:
|
||||
if size is None or size < 0:
|
||||
size = max(0, len(self.mw) - self.pos)
|
||||
oldpos = self.pos
|
||||
self.pos = min(len(self.mw), self.pos + size)
|
||||
if self.pos <= oldpos:
|
||||
return b''
|
||||
return bytes(self.mw[oldpos:self.pos])
|
||||
|
||||
def readinto(self, b: 'WriteableBuffer') -> int:
|
||||
if not isinstance(b, memoryview):
|
||||
b = memoryview(b)
|
||||
b = b.cast('B')
|
||||
data = self.read(len(b))
|
||||
n = len(data)
|
||||
b[:n] = data
|
||||
return n
|
||||
readinto1 = readinto
|
||||
|
||||
def readall(self) -> bytes:
|
||||
return self.read()
|
||||
|
||||
def write(self, b: 'ReadableBuffer') -> int:
|
||||
raise io.UnsupportedOperation('readonly stream')
|
||||
|
||||
def readable(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class MemoryViewReadWrapper(io.TextIOWrapper):
|
||||
|
||||
def __init__(self, mw: memoryview):
|
||||
super().__init__(cast(IO[bytes], MemoryViewReadWrapperBytes(mw)), encoding='utf-8', errors='replace')
|
||||
|
||||
|
||||
def child_main(cmd: Dict[str, Any], ready_fd: int) -> NoReturn:
|
||||
cwd = cmd.get('cwd')
|
||||
if cwd:
|
||||
try:
|
||||
os.chdir(cwd)
|
||||
except OSError:
|
||||
with suppress(OSError):
|
||||
os.chdir('/')
|
||||
os.setsid()
|
||||
env = cmd.get('env')
|
||||
if env is not None:
|
||||
os.environ.clear()
|
||||
os.environ.update(env)
|
||||
argv = cmd.get('argv')
|
||||
if argv:
|
||||
sys.argv = list(argv)
|
||||
poll = select.poll()
|
||||
poll.register(ready_fd, select.POLLIN | select.POLLERR | select.POLLHUP)
|
||||
poll.poll()
|
||||
os.close(ready_fd)
|
||||
tty_name = cmd.get('tty_name')
|
||||
if tty_name:
|
||||
sys.__stdout__.flush()
|
||||
sys.__stderr__.flush()
|
||||
establish_controlling_tty(tty_name, sys.__stdin__.fileno(), sys.__stdout__.fileno(), sys.__stderr__.fileno())
|
||||
main_entry_point()
|
||||
raise SystemExit(0)
|
||||
|
||||
|
||||
def fork(shm_address: str, ready_fd: int) -> int:
|
||||
sz = pos = 0
|
||||
with SharedMemory(name=shm_address, unlink_on_exit=True) as shm:
|
||||
data = shm.read_data_with_size()
|
||||
cmd = json.loads(data)
|
||||
sz = cmd.get('stdin_size', 0)
|
||||
if sz:
|
||||
pos = shm.tell()
|
||||
shm.unlink_on_exit = False
|
||||
|
||||
try:
|
||||
child_pid = os.fork()
|
||||
except OSError:
|
||||
if sz:
|
||||
with SharedMemory(shm_address, unlink_on_exit=True):
|
||||
pass
|
||||
if child_pid:
|
||||
# master process
|
||||
return child_pid
|
||||
# child process
|
||||
if shm.unlink_on_exit:
|
||||
child_main(cmd, ready_fd)
|
||||
else:
|
||||
with SharedMemory(shm_address, unlink_on_exit=True) as shm:
|
||||
stdin_data = memoryview(shm.mmap)[pos:pos + sz]
|
||||
if stdin_data:
|
||||
sys.stdin = MemoryViewReadWrapper(stdin_data)
|
||||
try:
|
||||
child_main(cmd, ready_fd)
|
||||
finally:
|
||||
stdin_data.release()
|
||||
sys.stdin = sys.__stdin__
|
||||
|
||||
|
||||
def main(args: List[str] = sys.argv) -> None:
|
||||
read_signal_fd, write_signal_fd = safe_pipe()
|
||||
notify_child_death_fd = int(sys.argv[-1])
|
||||
os.set_blocking(notify_child_death_fd, False)
|
||||
signal.set_wakeup_fd(write_signal_fd)
|
||||
signal.signal(signal.SIGCHLD, lambda *a: None)
|
||||
signal.siginterrupt(signal.SIGCHLD, False)
|
||||
prewarm()
|
||||
stdin_fd = sys.__stdin__.fileno()
|
||||
os.set_blocking(stdin_fd, False)
|
||||
stdout_fd = sys.__stdout__.fileno()
|
||||
os.set_blocking(stdout_fd, False)
|
||||
poll = select.poll()
|
||||
poll.register(stdin_fd, select.POLLIN | basic_events)
|
||||
poll.register(read_signal_fd, select.POLLIN | basic_events)
|
||||
input_buf = output_buf = child_death_buf = b''
|
||||
child_ready_fds: Dict[int, int] = {}
|
||||
child_id_map: Dict[int, int] = {}
|
||||
self_pid = os.getpid()
|
||||
|
||||
def check_event(event: int, err_msg: str) -> None:
|
||||
if event & hangup_events:
|
||||
raise SystemExit(0)
|
||||
if event & error_events:
|
||||
raise SystemExit(err_msg)
|
||||
|
||||
def handle_input(event: int) -> None:
|
||||
nonlocal input_buf, output_buf
|
||||
check_event(event, 'Polling of STDIN failed')
|
||||
if not (event & select.POLLIN):
|
||||
return
|
||||
d = os.read(stdin_fd, io.DEFAULT_BUFFER_SIZE)
|
||||
if not d:
|
||||
raise SystemExit(0)
|
||||
input_buf += d
|
||||
while (idx := input_buf.find(b'\n')) > -1:
|
||||
line = input_buf[:idx].decode()
|
||||
input_buf = input_buf[idx+1:]
|
||||
cmd, _, payload = line.partition(':')
|
||||
if cmd == 'reload_kitty_config':
|
||||
reload_kitty_config()
|
||||
elif cmd == 'ready':
|
||||
child_id = int(payload)
|
||||
cfd = child_ready_fds.pop(child_id)
|
||||
if cfd is not None:
|
||||
os.write(cfd, b'1')
|
||||
os.close(cfd)
|
||||
elif cmd == 'fork':
|
||||
read_fd, write_fd = safe_pipe(False)
|
||||
remove_cloexec(read_fd)
|
||||
try:
|
||||
child_pid = fork(payload, read_fd)
|
||||
except Exception as e:
|
||||
es = str(e).replace('\n', ' ')
|
||||
output_buf += f'ERR:{es}\n'.encode()
|
||||
else:
|
||||
if os.getpid() == self_pid:
|
||||
child_id = len(child_id_map) + 1
|
||||
child_id_map[child_id] = child_pid
|
||||
child_ready_fds[child_id] = write_fd
|
||||
output_buf += f'CHILD:{child_id}:{child_pid}\n'.encode()
|
||||
finally:
|
||||
if os.getpid() == self_pid:
|
||||
os.close(read_fd)
|
||||
elif cmd == 'echo':
|
||||
output_buf += f'{payload}\n'.encode()
|
||||
|
||||
def handle_output(event: int) -> None:
|
||||
nonlocal output_buf
|
||||
check_event(event, 'Polling of STDOUT failed')
|
||||
if not (event & select.POLLOUT):
|
||||
return
|
||||
if output_buf:
|
||||
n = os.write(stdout_fd, output_buf)
|
||||
if not n:
|
||||
raise SystemExit(0)
|
||||
output_buf = output_buf[n:]
|
||||
if not output_buf:
|
||||
poll.unregister(stdout_fd)
|
||||
|
||||
def handle_notify_child_death(event: int) -> None:
|
||||
nonlocal child_death_buf
|
||||
check_event(event, 'Polling of notify child death fd failed')
|
||||
if not (event & select.POLLOUT):
|
||||
return
|
||||
if child_death_buf:
|
||||
n = os.write(notify_child_death_fd, child_death_buf)
|
||||
if not n:
|
||||
raise SystemExit(0)
|
||||
child_death_buf = child_death_buf[n:]
|
||||
if not child_death_buf:
|
||||
poll.unregister(notify_child_death_fd)
|
||||
|
||||
def handle_signal(event: int) -> None:
|
||||
nonlocal child_death_buf
|
||||
check_event(event, 'Polling of signal fd failed')
|
||||
if not (event & select.POLLIN):
|
||||
return
|
||||
d = os.read(read_signal_fd, io.DEFAULT_BUFFER_SIZE)
|
||||
if not d:
|
||||
raise SystemExit(0)
|
||||
signals = set(bytearray(d))
|
||||
if signal.SIGCHLD in signals:
|
||||
while True:
|
||||
try:
|
||||
pid, exit_status = os.waitpid(-1, os.WNOHANG)
|
||||
except ChildProcessError:
|
||||
break
|
||||
# a zero return means there is at least one child process
|
||||
# existing and no exit status is available
|
||||
if pid == 0:
|
||||
break
|
||||
matched_child_id = -1
|
||||
for child_id, child_pid in child_id_map.items():
|
||||
if child_pid == pid:
|
||||
matched_child_id = child_id
|
||||
break
|
||||
if matched_child_id > -1:
|
||||
del child_id_map[matched_child_id]
|
||||
child_ready_fds.pop(matched_child_id, None)
|
||||
child_death_buf += f'{pid}\n'.encode()
|
||||
try:
|
||||
while True:
|
||||
if output_buf:
|
||||
poll.register(stdout_fd, select.POLLOUT | basic_events)
|
||||
if child_death_buf:
|
||||
poll.register(notify_child_death_fd, select.POLLOUT | basic_events)
|
||||
for (q, event) in poll.poll():
|
||||
if q == stdin_fd:
|
||||
handle_input(event)
|
||||
elif q == stdout_fd:
|
||||
handle_output(event)
|
||||
elif q == read_signal_fd:
|
||||
handle_signal(event)
|
||||
elif q == notify_child_death_fd:
|
||||
handle_notify_child_death(event)
|
||||
except (KeyboardInterrupt, EOFError, BrokenPipeError):
|
||||
if os.getpid() == self_pid:
|
||||
raise SystemExit(1)
|
||||
raise
|
||||
except Exception:
|
||||
if os.getpid() == self_pid:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise
|
||||
finally:
|
||||
if os.getpid() == self_pid:
|
||||
for fmd in child_ready_fds.values():
|
||||
with suppress(OSError):
|
||||
os.close(fmd)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -206,7 +206,7 @@ class ControlMaster:
|
||||
show_error(msg)
|
||||
|
||||
def download(self) -> bool:
|
||||
cmdline = self.batch_cmd_prefix + [self.conn_data.hostname, 'cat', self.remote_path]
|
||||
cmdline = self.batch_cmd_prefix + [self.conn_data.hostname, 'cat', shlex.quote(self.remote_path)]
|
||||
with open(self.dest, 'wb') as f:
|
||||
cp = subprocess.run(cmdline, stdout=f, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL)
|
||||
if cp.returncode != 0:
|
||||
@@ -216,7 +216,7 @@ class ControlMaster:
|
||||
|
||||
def upload(self, suppress_output: bool = True) -> bool:
|
||||
cmd_prefix = self.cmd_prefix if suppress_output else self.batch_cmd_prefix
|
||||
cmd = cmd_prefix + [self.conn_data.hostname, 'cat', '>', self.remote_path]
|
||||
cmd = cmd_prefix + [self.conn_data.hostname, 'cat', '>', shlex.quote(self.remote_path)]
|
||||
if not suppress_output:
|
||||
print(shlex.join(cmd))
|
||||
with open(self.dest, 'rb') as f:
|
||||
@@ -273,9 +273,9 @@ def save_as(conn_data: SSHConnectionData, remote_path: str, cli_opts: RemoteFile
|
||||
)
|
||||
print('Relative paths will be resolved from:', styled(os.getcwd(), fg_intense=True, bold=True))
|
||||
print()
|
||||
from ..tui.path_completer import PathCompleter
|
||||
from ..tui.path_completer import get_path
|
||||
try:
|
||||
dest = PathCompleter().input()
|
||||
dest = get_path()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
return
|
||||
if dest:
|
||||
|
||||
@@ -122,7 +122,11 @@ def run_kitten(kitten: str, run_name: str = '__main__') -> None:
|
||||
print(kitten, file=sys.stderr)
|
||||
raise SystemExit(f'No kitten named {original_kitten_name}')
|
||||
m = runpy.run_path(path, init_globals={'sys': sys, 'os': os}, run_name='__run_kitten__')
|
||||
m['main'](sys.argv)
|
||||
from kitty.fast_data_types import set_options
|
||||
try:
|
||||
m['main'](sys.argv)
|
||||
finally:
|
||||
set_options(None)
|
||||
|
||||
|
||||
@run_once
|
||||
|
||||
@@ -34,7 +34,7 @@ def format_mods(mods: int) -> str:
|
||||
class KeysHandler(Handler):
|
||||
|
||||
def initialize(self) -> None:
|
||||
self.cmd.set_window_title('Kitty extended keyboard protocol demo')
|
||||
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')
|
||||
|
||||
@@ -47,7 +47,10 @@ class KeysHandler(Handler):
|
||||
mods = format_mods(key_event.mods)
|
||||
if mods:
|
||||
mods += '+'
|
||||
key = f'{mods}{key_event.key} '
|
||||
kk = key_event.key
|
||||
if kk == ' ':
|
||||
kk = 'SPACE'
|
||||
key = f'{mods}{kk} '
|
||||
self.cmd.colored(key, 'green')
|
||||
self.cmd.colored(etype + ' ', 'yellow')
|
||||
self.cmd.styled(key_event.text, italic=True)
|
||||
|
||||
@@ -45,7 +45,7 @@ def read_keys() -> None:
|
||||
|
||||
|
||||
def legacy_main() -> None:
|
||||
print('Press any keys - Ctrl-D will terminate this program', end='\r\n', flush=True)
|
||||
print('Press any keys - Ctrl+D will terminate this program', end='\r\n', flush=True)
|
||||
print(styled('UNIX', italic=True, fg='green'), styled('send_text', italic=True, fg='green'), sep='\t\t', end='\r\n')
|
||||
|
||||
with raw_mode():
|
||||
@@ -62,6 +62,7 @@ reset and :code:`application` mode is with DECCKM set. :code:`kitty` is the full
|
||||
kitty extended keyboard protocol.
|
||||
'''.format
|
||||
help_text = 'Show the codes generated by the terminal for key presses in various keyboard modes'
|
||||
usage = ''
|
||||
|
||||
|
||||
def main(args: List[str]) -> None:
|
||||
@@ -80,3 +81,8 @@ def main(args: List[str]) -> None:
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
elif __name__ == '__doc__':
|
||||
cd = sys.cli_docs # type: ignore
|
||||
cd['usage'] = usage
|
||||
cd['options'] = OPTIONS
|
||||
cd['help_text'] = help_text
|
||||
|
||||
@@ -291,6 +291,7 @@ def complete_option(ans: Completions, prefix: str = '-') -> None:
|
||||
def complete(ans: Completions, words: Sequence[str], new_word: bool) -> None:
|
||||
options = ssh_options()
|
||||
expecting_arg = False
|
||||
seen_destination = False
|
||||
types = ['' for i in range(len(words))]
|
||||
for i, word in enumerate(words):
|
||||
if expecting_arg:
|
||||
@@ -302,8 +303,10 @@ def complete(ans: Completions, words: Sequence[str], new_word: bool) -> None:
|
||||
if len(word) == 2 and options.get(word[1]):
|
||||
expecting_arg = True
|
||||
continue
|
||||
if seen_destination:
|
||||
break
|
||||
types[i] = 'destination'
|
||||
break
|
||||
seen_destination = True
|
||||
if new_word:
|
||||
if words:
|
||||
if expecting_arg:
|
||||
|
||||
@@ -38,7 +38,18 @@ type=list
|
||||
A glob pattern. Files with names matching this pattern are excluded from being
|
||||
transferred. Useful when adding directories. Can
|
||||
be specified multiple times, if any of the patterns match the file will be
|
||||
excluded.
|
||||
excluded. To exclude a directory use a pattern like */directory_name/*.
|
||||
|
||||
|
||||
--symlink-strategy
|
||||
default=preserve
|
||||
choices=preserve,resolve,keep-path
|
||||
Control what happens if the specified path is a symlink. The default is to preserve
|
||||
the symlink, re-creating it on the remote machine. Setting this to :code:`resolve`
|
||||
will cause the symlink to be followed and its target used as the file/directory to copy.
|
||||
The value of :code:`keep-path` is the same as :code:`resolve` except that the remote
|
||||
file path is derived from the symlink's path instead of the path of the symlink's target.
|
||||
Note that this option does not apply to symlinks encountered while recursively copying directories.
|
||||
'''
|
||||
|
||||
|
||||
@@ -100,5 +111,9 @@ def parse_copy_instructions(val: str, current_val: Dict[str, str]) -> Iterable[T
|
||||
raise CopyCLIError('Specifying a remote location with more than one file is not supported')
|
||||
home = home_path()
|
||||
for loc in locations:
|
||||
arcname = get_arcname(loc, opts.dest, home)
|
||||
yield str(uuid.uuid4()), CopyInstruction(loc, arcname, tuple(opts.exclude))
|
||||
if opts.symlink_strategy != 'preserve':
|
||||
rp = os.path.realpath(loc)
|
||||
else:
|
||||
rp = loc
|
||||
arcname = get_arcname(rp if opts.symlink_strategy == 'resolve' else loc, opts.dest, home)
|
||||
yield str(uuid.uuid4()), CopyInstruction(rp, arcname, tuple(opts.exclude))
|
||||
|
||||
@@ -27,7 +27,6 @@ from typing import (
|
||||
Tuple, Union, cast
|
||||
)
|
||||
|
||||
from kittens.tui.operations import restore_colors, save_colors
|
||||
from kitty.constants import (
|
||||
cache_dir, runtime_dir, shell_integration_dir, ssh_control_master_template,
|
||||
str_version, terminfo_dir
|
||||
@@ -40,7 +39,11 @@ from kitty.utils import (
|
||||
set_echo as turn_off_echo
|
||||
)
|
||||
|
||||
from ..tui.utils import kitty_opts
|
||||
from ..tui.operations import (
|
||||
RESTORE_PRIVATE_MODE_VALUES, SAVE_PRIVATE_MODE_VALUES, Mode,
|
||||
restore_colors, save_colors, set_mode
|
||||
)
|
||||
from ..tui.utils import kitty_opts, running_in_tmux
|
||||
from .config import init_config
|
||||
from .copy import CopyInstruction
|
||||
from .options.types import Options as SSHOptions
|
||||
@@ -168,6 +171,9 @@ def make_tarfile(ssh_opts: SSHOptions, base_env: Dict[str, str], compression: st
|
||||
env['KITTY_LOGIN_CWD'] = ssh_opts.cwd
|
||||
if ssh_opts.remote_kitty != 'no':
|
||||
env['KITTY_REMOTE'] = ssh_opts.remote_kitty
|
||||
if os.environ.get('KITTY_PUBLIC_KEY'):
|
||||
env.pop('KITTY_PUBLIC_KEY', None)
|
||||
literal_env['KITTY_PUBLIC_KEY'] = os.environ['KITTY_PUBLIC_KEY']
|
||||
env_script = serialize_env(literal_env, env, base_env, for_python=compression != 'gz')
|
||||
buf = io.BytesIO()
|
||||
with tarfile.open(mode=f'w:{compression}', fileobj=buf, encoding='utf-8') as tf:
|
||||
@@ -209,7 +215,7 @@ def get_ssh_data(msg: str, request_id: str) -> Iterator[bytes]:
|
||||
if pw != env_data['pw']:
|
||||
raise ValueError('Incorrect password')
|
||||
if rq_id != request_id:
|
||||
raise ValueError('Incorrect request id')
|
||||
raise ValueError(f'Incorrect request id: {rq_id!r} expecting the KITTY_PID-KITTY_WINDOW_ID for the current kitty window')
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
yield f'{e}\n'.encode('utf-8')
|
||||
@@ -409,9 +415,11 @@ class InvalidSSHArgs(ValueError):
|
||||
os.execlp(ssh_exe(), 'ssh')
|
||||
|
||||
|
||||
passthrough_args = {f'-{x}' for x in 'NnfGT'}
|
||||
|
||||
|
||||
def parse_ssh_args(args: List[str], extra_args: Tuple[str, ...] = ()) -> Tuple[List[str], List[str], bool, Tuple[str, ...]]:
|
||||
boolean_ssh_args, other_ssh_args = get_ssh_cli()
|
||||
passthrough_args = {f'-{x}' for x in 'NnfG'}
|
||||
ssh_args = []
|
||||
server_args: List[str] = []
|
||||
expecting_option_val = False
|
||||
@@ -508,7 +516,7 @@ def get_remote_command(
|
||||
return wrap_bootstrap_script(sh_script, interpreter), replacements, shm_name
|
||||
|
||||
|
||||
def connection_sharing_args(opts: SSHOptions, kitty_pid: int) -> List[str]:
|
||||
def connection_sharing_args(kitty_pid: int) -> List[str]:
|
||||
rd = runtime_dir()
|
||||
# Bloody OpenSSH generates a 40 char hash and in creating the socket
|
||||
# appends a 27 char temp suffix to it. Socket max path length is approx
|
||||
@@ -547,17 +555,29 @@ def connection_sharing_args(opts: SSHOptions, kitty_pid: int) -> List[str]:
|
||||
def restore_terminal_state() -> Iterator[bool]:
|
||||
with open(os.ctermid()) as f:
|
||||
val = termios.tcgetattr(f.fileno())
|
||||
print(end=SAVE_PRIVATE_MODE_VALUES)
|
||||
print(end=set_mode(Mode.HANDLE_TERMIOS_SIGNALS), flush=True)
|
||||
try:
|
||||
yield bool(val[3] & termios.ECHO)
|
||||
finally:
|
||||
termios.tcsetattr(f.fileno(), termios.TCSAFLUSH, val)
|
||||
print(end=RESTORE_PRIVATE_MODE_VALUES, flush=True)
|
||||
|
||||
|
||||
def dcs_to_kitty(payload: Union[bytes, str], type: str = 'ssh') -> bytes:
|
||||
if isinstance(payload, str):
|
||||
payload = payload.encode('utf-8')
|
||||
payload = standard_b64encode(payload)
|
||||
return b'\033P@kitty-' + type.encode('ascii') + b'|' + payload + b'\033\\'
|
||||
ans = b'\033P@kitty-' + type.encode('ascii') + b'|' + payload
|
||||
tmux = running_in_tmux()
|
||||
if tmux:
|
||||
cp = subprocess.run([tmux, 'set', '-p', 'allow-passthrough', 'on'])
|
||||
if cp.returncode != 0:
|
||||
raise SystemExit(cp.returncode)
|
||||
ans = b'\033Ptmux;\033' + ans + b'\033\033\\\033\\'
|
||||
else:
|
||||
ans += b'\033\\'
|
||||
return ans
|
||||
|
||||
|
||||
@run_once
|
||||
@@ -571,7 +591,6 @@ def ssh_version() -> Tuple[int, int]:
|
||||
|
||||
@contextmanager
|
||||
def drain_potential_tty_garbage(p: 'subprocess.Popen[bytes]', data_request: str) -> Iterator[None]:
|
||||
ssh_started_at = time.monotonic()
|
||||
with open(os.open(os.ctermid(), os.O_CLOEXEC | os.O_RDWR | os.O_NOCTTY), 'wb') as tty:
|
||||
if data_request:
|
||||
turn_off_echo(tty.fileno())
|
||||
@@ -580,14 +599,18 @@ def drain_potential_tty_garbage(p: 'subprocess.Popen[bytes]', data_request: str)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if p.returncode and time.monotonic() - ssh_started_at < 30:
|
||||
# discard queued input data on tty in case data transmission was
|
||||
# interrupted due to SSH failure, avoids spewing garbage to
|
||||
# screen
|
||||
data = b''
|
||||
give_up_at = time.monotonic() + 1
|
||||
tty_fd = tty.fileno()
|
||||
while time.monotonic() < give_up_at and b'KITTY_DATA_END' not in data:
|
||||
# discard queued input data on tty in case data transmission was
|
||||
# interrupted due to SSH failure, avoids spewing garbage to screen
|
||||
from uuid import uuid4
|
||||
canary = uuid4().hex.encode('ascii')
|
||||
turn_off_echo(tty.fileno())
|
||||
tty.write(dcs_to_kitty(canary + b'\n\r', type='echo'))
|
||||
tty.flush()
|
||||
data = b''
|
||||
give_up_at = time.monotonic() + 2
|
||||
tty_fd = tty.fileno()
|
||||
while time.monotonic() < give_up_at and canary not in data:
|
||||
with suppress(KeyboardInterrupt):
|
||||
rd, wr, err = select([tty_fd], [], [tty_fd], max(0, give_up_at - time.monotonic()))
|
||||
if err or not rd:
|
||||
break
|
||||
@@ -669,7 +692,7 @@ def run_ssh(ssh_args: List[str], server_args: List[str], found_extra_args: Tuple
|
||||
overrides.insert(0, f'hostname {uname}@{hostname_for_match}')
|
||||
host_opts = init_config(hostname_for_match, uname, overrides)
|
||||
if host_opts.share_connections:
|
||||
cmd[insertion_point:insertion_point] = connection_sharing_args(host_opts, int(os.environ['KITTY_PID']))
|
||||
cmd[insertion_point:insertion_point] = connection_sharing_args(int(os.environ['KITTY_PID']))
|
||||
use_kitty_askpass = host_opts.askpass == 'native' or (host_opts.askpass == 'unless-set' and 'SSH_ASKPASS' not in os.environ)
|
||||
need_to_request_data = True
|
||||
if use_kitty_askpass:
|
||||
@@ -699,16 +722,13 @@ def run_ssh(ssh_args: List[str], server_args: List[str], found_extra_args: Tuple
|
||||
else:
|
||||
rq = '' if need_to_request_data else 'id={REQUEST_ID}:pwfile={PASSWORD_FILENAME}:pw={DATA_PASSWORD}'.format(**replacements)
|
||||
with drain_potential_tty_garbage(p, rq):
|
||||
try:
|
||||
raise SystemExit(p.wait())
|
||||
except KeyboardInterrupt:
|
||||
raise SystemExit(1)
|
||||
raise SystemExit(p.wait())
|
||||
finally:
|
||||
if colors_changed:
|
||||
print(end=restore_colors(), flush=True)
|
||||
|
||||
|
||||
def main(args: List[str]) -> NoReturn:
|
||||
def main(args: List[str]) -> None:
|
||||
args = args[1:]
|
||||
if args and args[0] == 'use-python':
|
||||
args = args[1:] # backwards compat from when we had a python implementation
|
||||
@@ -716,13 +736,20 @@ def main(args: List[str]) -> NoReturn:
|
||||
ssh_args, server_args, passthrough, found_extra_args = parse_ssh_args(args, extra_args=('--kitten',))
|
||||
except InvalidSSHArgs as e:
|
||||
e.system_exit()
|
||||
if passthrough:
|
||||
if found_extra_args:
|
||||
raise SystemExit(f'The SSH kitten cannot work with the options: {", ".join(passthrough_args)}')
|
||||
os.execlp(ssh_exe(), 'ssh', *args)
|
||||
|
||||
if not os.environ.get('KITTY_WINDOW_ID') or not os.environ.get('KITTY_PID'):
|
||||
raise SystemExit('The SSH kitten is meant to run inside a kitty window')
|
||||
if passthrough:
|
||||
raise SystemExit('The SSH kitten is meant for interactive use via SSH only')
|
||||
if not sys.stdin.isatty():
|
||||
raise SystemExit('The SSH kitten is meant for interactive use only, STDIN must be a terminal')
|
||||
run_ssh(ssh_args, server_args, found_extra_args)
|
||||
try:
|
||||
run_ssh(ssh_args, server_args, found_extra_args)
|
||||
except KeyboardInterrupt:
|
||||
sys.excepthook = lambda *a: None
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Sequence
|
||||
|
||||
from kitty.types import run_once
|
||||
|
||||
@@ -50,12 +50,14 @@ def ssh_options() -> Dict[str, str]:
|
||||
return ans
|
||||
|
||||
|
||||
def is_kitten_cmdline(q: List[str]) -> bool:
|
||||
def is_kitten_cmdline(q: Sequence[str]) -> bool:
|
||||
if len(q) < 4:
|
||||
return False
|
||||
if os.path.basename(q[0]).lower() != 'kitty':
|
||||
return False
|
||||
return q[1:3] == ['+kitten', 'ssh'] or q[1:4] == ['+', 'kitten', 'ssh']
|
||||
if q[1:3] == ['+kitten', 'ssh'] or q[1:4] == ['+', 'kitten', 'ssh']:
|
||||
return True
|
||||
return q[1:3] == ['+runpy', 'from kittens.runner import main; main()'] and len(q) >= 6 and q[5] == 'ssh'
|
||||
|
||||
|
||||
def patch_cmdline(key: str, val: str, argv: List[str]) -> None:
|
||||
|
||||
@@ -243,14 +243,14 @@ static PyModuleDef_Slot slots[] = { {Py_mod_exec, (void*)exec_module}, {0, NULL}
|
||||
END_IGNORE_PEDANTIC_WARNINGS
|
||||
|
||||
static struct PyModuleDef module = {
|
||||
.m_base = PyModuleDef_HEAD_INIT,
|
||||
.m_name = "rsync", /* name of module */
|
||||
.m_doc = NULL,
|
||||
.m_slots = slots,
|
||||
.m_methods = module_methods
|
||||
.m_base = PyModuleDef_HEAD_INIT,
|
||||
.m_name = "rsync", /* name of module */
|
||||
.m_doc = NULL,
|
||||
.m_slots = slots,
|
||||
.m_methods = module_methods
|
||||
};
|
||||
|
||||
EXPORTED PyMODINIT_FUNC
|
||||
PyInit_rsync(void) {
|
||||
return PyModuleDef_Init(&module);
|
||||
return PyModuleDef_Init(&module);
|
||||
}
|
||||
|
||||
@@ -121,11 +121,22 @@ class Handler:
|
||||
self._tui_loop.quit(1)
|
||||
|
||||
def on_key_event(self, key_event: KeyEventType, in_bracketed_paste: bool = False) -> None:
|
||||
' Override this method and perform_default_key_action() to handle all key events '
|
||||
if key_event.text:
|
||||
self.on_text(key_event.text, in_bracketed_paste)
|
||||
else:
|
||||
self.on_key(key_event)
|
||||
|
||||
def perform_default_key_action(self, key_event: KeyEventType) -> bool:
|
||||
' Override in sub-class if you want to handle these key events yourself '
|
||||
if key_event.matches('ctrl+c'):
|
||||
self.on_interrupt()
|
||||
return True
|
||||
if key_event.matches('ctrl+d'):
|
||||
self.on_eot()
|
||||
return True
|
||||
return False
|
||||
|
||||
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@@ -301,7 +301,7 @@ def render_as_single_image(
|
||||
remove_alpha: str = '', flip: bool = False, flop: bool = False,
|
||||
) -> Tuple[str, int, int]:
|
||||
import tempfile
|
||||
fd, output = tempfile.mkstemp(prefix='icat-', suffix=f'.{m.mode}', dir=tdir)
|
||||
fd, output = tempfile.mkstemp(prefix='tty-graphics-protocol-', suffix=f'.{m.mode}', dir=tdir)
|
||||
os.close(fd)
|
||||
result = render_image(
|
||||
path, output, m, available_width, available_height, scale_up,
|
||||
|
||||
@@ -109,17 +109,17 @@ class TermManager:
|
||||
|
||||
|
||||
class MouseButton(IntFlag):
|
||||
NONE, LEFT, MIDDLE, RIGHT, FOURTH, FIFTH = 0, 1, 2, 4, 8, 16
|
||||
WHEEL_UP, WHEEL_DOWN = -1, -2
|
||||
NONE, LEFT, MIDDLE, RIGHT, FOURTH, FIFTH, SIXTH, SEVENTH = 0, 1, 2, 4, 8, 16, 32, 64
|
||||
WHEEL_UP, WHEEL_DOWN, WHEEL_LEFT, WHEEL_RIGHT = -1, -2, -4, -8
|
||||
|
||||
|
||||
bmap = {0: MouseButton.LEFT, 1: MouseButton.MIDDLE, 2: MouseButton.RIGHT}
|
||||
bmap = MouseButton.LEFT, MouseButton.MIDDLE, MouseButton.RIGHT
|
||||
ebmap = MouseButton.FOURTH, MouseButton.FIFTH, MouseButton.SIXTH, MouseButton.SEVENTH
|
||||
wbmap = MouseButton.WHEEL_UP, MouseButton.WHEEL_DOWN, MouseButton.WHEEL_LEFT, MouseButton.WHEEL_RIGHT
|
||||
SHIFT_INDICATOR = 1 << 2
|
||||
ALT_INDICATOR = 1 << 3
|
||||
CTRL_INDICATOR = 1 << 4
|
||||
MOTION_INDICATOR = 1 << 5
|
||||
SCROLL_BUTTON_INDICATOR = 1 << 6
|
||||
EXTRA_BUTTON_INDICATOR = 1 << 7
|
||||
|
||||
|
||||
class EventType(Enum):
|
||||
@@ -150,13 +150,12 @@ def decode_sgr_mouse(text: str, screen_size: ScreenSize) -> MouseEvent:
|
||||
typ = EventType.RELEASE if m == 'm' else (EventType.MOVE if cb & MOTION_INDICATOR else EventType.PRESS)
|
||||
buttons: MouseButton = MouseButton.NONE
|
||||
cb3 = cb & 3
|
||||
if cb3 != 3:
|
||||
if cb & SCROLL_BUTTON_INDICATOR:
|
||||
buttons = MouseButton.WHEEL_DOWN if cb3 & 1 else MouseButton.WHEEL_UP
|
||||
elif cb & EXTRA_BUTTON_INDICATOR:
|
||||
buttons |= MouseButton.FIFTH if cb3 & 1 else MouseButton.FOURTH
|
||||
else:
|
||||
buttons |= bmap[cb3]
|
||||
if cb >= 128:
|
||||
buttons |= ebmap[cb3]
|
||||
elif cb >= 64:
|
||||
buttons |= wbmap[cb3]
|
||||
elif cb3 < 3:
|
||||
buttons |= bmap[cb3]
|
||||
mods = 0
|
||||
if cb & SHIFT_INDICATOR:
|
||||
mods |= SHIFT
|
||||
@@ -331,13 +330,8 @@ class Loop:
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
if k.matches('ctrl+c'):
|
||||
self.handler.on_interrupt()
|
||||
return
|
||||
if k.matches('ctrl+d'):
|
||||
self.handler.on_eot()
|
||||
return
|
||||
self.handler.on_key_event(k)
|
||||
if not self.handler.perform_default_key_action(k):
|
||||
self.handler.on_key_event(k)
|
||||
|
||||
def _on_pm(self, pm: str) -> None:
|
||||
pass
|
||||
|
||||
@@ -47,6 +47,7 @@ class Mode(Enum):
|
||||
ALTERNATE_SCREEN = 1049, '?'
|
||||
BRACKETED_PASTE = 2004, '?'
|
||||
PENDING_UPDATE = 2026, '?'
|
||||
HANDLE_TERMIOS_SIGNALS = 19997, '?'
|
||||
|
||||
|
||||
def cmd(f: F) -> F:
|
||||
|
||||
@@ -147,5 +147,9 @@ class PathCompleter:
|
||||
return ''
|
||||
|
||||
|
||||
def get_path(prompt: str = '> ') -> str:
|
||||
return PathCompleter(prompt).input()
|
||||
|
||||
|
||||
def develop() -> None:
|
||||
PathCompleter().input()
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import os
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
from typing import TYPE_CHECKING, Tuple
|
||||
from typing import TYPE_CHECKING, Optional, Sequence, Tuple, cast
|
||||
|
||||
from kitty.types import run_once
|
||||
|
||||
@@ -55,12 +56,29 @@ def human_size(
|
||||
return format_number(size / 1024**exponent, max_num_of_decimals) + sep + unit_list[exponent]
|
||||
|
||||
|
||||
@run_once
|
||||
def kitty_opts() -> 'Options':
|
||||
from kitty.cli import create_default_opts
|
||||
from kitty.fast_data_types import get_options, set_options
|
||||
try:
|
||||
ans = cast(Optional['Options'], get_options())
|
||||
except RuntimeError:
|
||||
ans = None
|
||||
if ans is None:
|
||||
from kitty.cli import create_default_opts
|
||||
from kitty.utils import suppress_error_logging
|
||||
with suppress_error_logging():
|
||||
ans = create_default_opts()
|
||||
set_options(ans)
|
||||
return ans
|
||||
|
||||
|
||||
def set_kitty_opts(paths: Sequence[str], overrides: Sequence[str] = ()) -> 'Options':
|
||||
from kitty.config import load_config
|
||||
from kitty.fast_data_types import set_options
|
||||
from kitty.utils import suppress_error_logging
|
||||
with suppress_error_logging():
|
||||
return create_default_opts()
|
||||
opts = load_config(*paths, overrides=overrides or None)
|
||||
set_options(opts)
|
||||
return opts
|
||||
|
||||
|
||||
def report_error(msg: str = '', return_code: int = 1, print_exc: bool = False) -> None:
|
||||
@@ -82,3 +100,26 @@ def report_error(msg: str = '', return_code: int = 1, print_exc: bool = False) -
|
||||
def report_unhandled_error(msg: str = '') -> None:
|
||||
' Report an unhandled exception with the overlay ready message '
|
||||
return report_error(msg, print_exc=True)
|
||||
|
||||
|
||||
@run_once
|
||||
def running_in_tmux() -> str:
|
||||
socket = os.environ.get('TMUX')
|
||||
if not socket:
|
||||
return ''
|
||||
parts = socket.split(',')
|
||||
if len(parts) < 2:
|
||||
return ''
|
||||
try:
|
||||
if not os.access(parts[0], os.R_OK | os.W_OK):
|
||||
return ''
|
||||
except OSError:
|
||||
return ''
|
||||
from kitty.child import cmdline_of_pid
|
||||
c = cmdline_of_pid(int(parts[1]))
|
||||
if not c:
|
||||
return ''
|
||||
exe = os.path.basename(c[0])
|
||||
if exe.lower() == 'tmux':
|
||||
return exe
|
||||
return ''
|
||||
|
||||
63207
kittens/unicode_input/names.h
generated
63207
kittens/unicode_input/names.h
generated
File diff suppressed because one or more lines are too long
@@ -101,14 +101,14 @@ static PyModuleDef_Slot slots[] = { {Py_mod_exec, (void*)exec_module}, {0, NULL}
|
||||
END_IGNORE_PEDANTIC_WARNINGS
|
||||
|
||||
static struct PyModuleDef module = {
|
||||
.m_base = PyModuleDef_HEAD_INIT,
|
||||
.m_name = "unicode_names", /* name of module */
|
||||
.m_doc = NULL,
|
||||
.m_slots = slots,
|
||||
.m_methods = module_methods
|
||||
.m_base = PyModuleDef_HEAD_INIT,
|
||||
.m_name = "unicode_names", /* name of module */
|
||||
.m_doc = NULL,
|
||||
.m_slots = slots,
|
||||
.m_methods = module_methods
|
||||
};
|
||||
|
||||
EXPORTED PyMODINIT_FUNC
|
||||
PyInit_unicode_names(void) {
|
||||
return PyModuleDef_Init(&module);
|
||||
return PyModuleDef_Init(&module);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#version GLSL_VERSION
|
||||
#define left -1.0f
|
||||
#define top 1.0f
|
||||
#define right 1.0f
|
||||
#define bottom -1.0f
|
||||
#define left 0
|
||||
#define top 1
|
||||
#define right 2
|
||||
#define bottom 3
|
||||
#define tex_left 0
|
||||
#define tex_top 0
|
||||
#define tex_right 1
|
||||
@@ -14,15 +14,10 @@
|
||||
|
||||
uniform float tiled;
|
||||
uniform vec4 sizes; // [ window_width, window_height, image_width, image_height ]
|
||||
uniform vec4 positions; // [ left, top, right, bottom ]
|
||||
|
||||
out vec2 texcoord;
|
||||
|
||||
const vec2 pos_map[] = vec2[4](
|
||||
vec2(left, top),
|
||||
vec2(left, bottom),
|
||||
vec2(right, bottom),
|
||||
vec2(right, top)
|
||||
);
|
||||
const vec2 tex_map[] = vec2[4](
|
||||
vec2(tex_left, tex_top),
|
||||
vec2(tex_left, tex_bottom),
|
||||
@@ -39,6 +34,12 @@ float tiling_factor(int i) {
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 pos_map[] = vec2[4](
|
||||
vec2(positions[left], positions[top]),
|
||||
vec2(positions[left], positions[bottom]),
|
||||
vec2(positions[right], positions[bottom]),
|
||||
vec2(positions[right], positions[top])
|
||||
);
|
||||
vec2 tex_coords = tex_map[gl_VertexID];
|
||||
texcoord = vec2(
|
||||
tex_coords[x_axis] * tiling_factor(x_axis),
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#version GLSL_VERSION
|
||||
uniform float background_opacity;
|
||||
in vec3 color;
|
||||
in vec4 color;
|
||||
out vec4 final_color;
|
||||
|
||||
void main() {
|
||||
final_color = vec4(color * background_opacity, background_opacity);
|
||||
final_color = color;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ uniform vec3 inactive_border_color;
|
||||
uniform vec3 bell_border_color;
|
||||
uniform vec3 tab_bar_bg;
|
||||
uniform vec3 tab_bar_margin_color;
|
||||
uniform float background_opacity;
|
||||
uniform float tint_opacity, tint_premult;
|
||||
in vec4 rect; // left, top, right, bottom
|
||||
in uint rect_color;
|
||||
out vec3 color;
|
||||
out vec4 color;
|
||||
|
||||
// indices into the rect vector
|
||||
const int LEFT = 0;
|
||||
@@ -35,5 +37,9 @@ void main() {
|
||||
gl_Position = vec4(rect[pos.x], rect[pos.y], 0, 1);
|
||||
int rc = int(rect_color);
|
||||
vec3 window_bg = vec3(to_color(rect_color >> 24), to_color(rect_color >> 16), to_color(rect_color >> 8));
|
||||
color = W(0, default_bg) + W(1, active_border_color) + W(2, inactive_border_color) + W(3, window_bg) + W(4, bell_border_color) + W(5, tab_bar_bg) + W(6, tab_bar_margin_color);
|
||||
vec3 color3 = W(0, default_bg) + W(1, active_border_color) + W(2, inactive_border_color) + W(3, window_bg) + W(4, bell_border_color) + W(5, tab_bar_bg) + W(6, tab_bar_margin_color);
|
||||
float is_default_bg = float(rc & 1);
|
||||
float final_opacity = is_default_bg * tint_opacity + (1 - is_default_bg) * background_opacity;
|
||||
float final_premult_opacity = is_default_bg * tint_premult + (1 - is_default_bg) * background_opacity;
|
||||
color = vec4(color3 * final_premult_opacity, final_opacity);
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ class Borders:
|
||||
draw_minimal_borders = opts.draw_minimal_borders and max(opts.window_margin_width) < 1
|
||||
add_borders_rect(self.os_window_id, self.tab_id, 0, 0, 0, 0, BorderColor.default_bg)
|
||||
has_background_image = os_window_has_background_image(self.os_window_id)
|
||||
if not has_background_image:
|
||||
if not has_background_image or opts.background_tint > 0.0:
|
||||
for br in current_layout.blank_rects:
|
||||
add_borders_rect(self.os_window_id, self.tab_id, *br, BorderColor.default_bg)
|
||||
for tbr in tab_bar_rects:
|
||||
|
||||
430
kitty/boss.py
430
kitty/boss.py
@@ -2,6 +2,7 @@
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import atexit
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -11,35 +12,40 @@ from functools import partial
|
||||
from gettext import gettext as _
|
||||
from time import monotonic
|
||||
from typing import (
|
||||
Any, Callable, Container, Dict, Iterable, Iterator, List, Optional, Set,
|
||||
Tuple, Union
|
||||
TYPE_CHECKING, Any, Callable, Container, Dict, Iterable, Iterator, List,
|
||||
Optional, Set, Tuple, Union
|
||||
)
|
||||
from weakref import WeakValueDictionary
|
||||
|
||||
from .child import cached_process_data, default_env, set_default_env
|
||||
from .cli import create_opts, parse_args
|
||||
from .cli_stub import CLIOptions
|
||||
from .clipboard import (
|
||||
Clipboard, get_clipboard_string, get_primary_selection,
|
||||
set_clipboard_string, set_primary_selection
|
||||
)
|
||||
from .conf.utils import BadLine, KeyAction, to_cmdline
|
||||
from .config import common_opts_as_dict, prepare_config_file_for_editing
|
||||
from .constants import (
|
||||
appname, cache_dir, config_dir, is_macos, is_wayland, kitty_exe,
|
||||
RC_ENCRYPTION_PROTOCOL_VERSION, appname, cache_dir, clear_handled_signals,
|
||||
config_dir, handled_signals, is_macos, is_wayland, kitty_exe,
|
||||
logo_png_file, supports_primary_selection, website_url
|
||||
)
|
||||
from .fast_data_types import (
|
||||
CLOSE_BEING_CONFIRMED, GLFW_MOD_ALT, GLFW_MOD_CONTROL, GLFW_MOD_SHIFT,
|
||||
GLFW_MOD_SUPER, GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS,
|
||||
GLFW_MOD_SUPER, GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, GLFW_PRIMARY_SELECTION,
|
||||
IMPERATIVE_CLOSE_REQUESTED, NO_CLOSE_REQUESTED, ChildMonitor, Color,
|
||||
KeyEvent, add_timer, apply_options_update, background_opacity_of,
|
||||
change_background_opacity, change_os_window_state, cocoa_set_menubar_title,
|
||||
create_os_window, current_application_quit_request, current_os_window,
|
||||
destroy_global_data, focus_os_window, get_boss, get_clipboard_string,
|
||||
get_options, get_os_window_size, global_font_size,
|
||||
mark_os_window_for_close, os_window_font_size, patch_global_colors,
|
||||
redirect_mouse_handling, ring_bell, safe_pipe,
|
||||
EllipticCurveKey, KeyEvent, SingleKey, add_timer, apply_options_update,
|
||||
background_opacity_of, change_background_opacity, change_os_window_state,
|
||||
cocoa_set_menubar_title, create_os_window,
|
||||
current_application_quit_request, current_os_window, destroy_global_data,
|
||||
focus_os_window, get_boss, get_options, get_os_window_size,
|
||||
global_font_size, mark_os_window_for_close, os_window_font_size,
|
||||
patch_global_colors, redirect_mouse_handling, ring_bell,
|
||||
run_with_activation_token, safe_pipe, send_data_to_peer,
|
||||
set_application_quit_request, set_background_image, set_boss,
|
||||
set_clipboard_string, set_in_sequence_mode, set_options,
|
||||
set_os_window_size, set_os_window_title, thread_write, toggle_fullscreen,
|
||||
toggle_maximized, toggle_secure_input
|
||||
set_in_sequence_mode, set_options, set_os_window_size, set_os_window_title,
|
||||
thread_write, toggle_fullscreen, toggle_maximized, toggle_secure_input
|
||||
)
|
||||
from .key_encoding import get_name_to_functional_number_map
|
||||
from .keys import get_shortcut, shortcut_matches
|
||||
@@ -48,22 +54,27 @@ from .notify import notification_activated
|
||||
from .options.types import Options
|
||||
from .options.utils import MINIMUM_FONT_SIZE, KeyMap, SubSequenceMap
|
||||
from .os_window_size import initial_window_size_func
|
||||
from .prewarm import PrewarmProcess
|
||||
from .rgb import color_from_int
|
||||
from .session import Session, create_sessions, get_os_window_sizing_data
|
||||
from .tabs import (
|
||||
SpecialWindow, SpecialWindowInstance, Tab, TabDict, TabManager
|
||||
)
|
||||
from .types import _T, AsyncResponse, SingleKey, WindowSystemMouseEvent, ac
|
||||
from .types import _T, AsyncResponse, WindowSystemMouseEvent, ac
|
||||
from .typing import PopenType, TypedDict
|
||||
from .utils import (
|
||||
cleanup_ssh_control_masters, func_name, get_editor, get_new_os_window_size,
|
||||
get_primary_selection, is_path_in_temp_dir, less_version, log_error,
|
||||
macos_version, open_url, parse_address_spec, parse_uri_list,
|
||||
platform_window_id, remove_socket_file, safe_print, set_primary_selection,
|
||||
single_instance, startup_notification_handler, which
|
||||
is_path_in_temp_dir, less_version, log_error, macos_version, open_url,
|
||||
parse_address_spec, parse_uri_list, platform_window_id, remove_socket_file,
|
||||
safe_print, single_instance, startup_notification_handler, which
|
||||
)
|
||||
from .window import CommandOutput, CwdRequest, Window
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .rc.base import ResponseType
|
||||
|
||||
RCResponse = Union[Dict[str, Any], None, AsyncResponse]
|
||||
|
||||
|
||||
class OSWindowDict(TypedDict):
|
||||
id: int
|
||||
@@ -227,10 +238,15 @@ class Boss:
|
||||
opts: Options,
|
||||
args: CLIOptions,
|
||||
cached_values: Dict[str, Any],
|
||||
global_shortcuts: Dict[str, SingleKey]
|
||||
global_shortcuts: Dict[str, SingleKey],
|
||||
prewarm: PrewarmProcess,
|
||||
):
|
||||
set_layout_options(opts)
|
||||
self.clipboard = Clipboard()
|
||||
self.primary_selection = Clipboard(GLFW_PRIMARY_SELECTION)
|
||||
self.update_check_started = False
|
||||
self.encryption_key = EllipticCurveKey()
|
||||
self.encryption_public_key = f'{RC_ENCRYPTION_PROTOCOL_VERSION}:{base64.b85encode(self.encryption_key.public).decode("ascii")}'
|
||||
self.clipboard_buffers: Dict[str, str] = {}
|
||||
self.update_check_process: Optional['PopenType[bytes]'] = None
|
||||
self.window_id_map: WeakValueDictionary[int, Window] = WeakValueDictionary()
|
||||
@@ -250,12 +266,19 @@ class Boss:
|
||||
# we dont allow reloading the config file to change
|
||||
# allow_remote_control
|
||||
self.allow_remote_control = opts.allow_remote_control
|
||||
if args.listen_on and (self.allow_remote_control in ('y', 'socket-only')):
|
||||
if self.allow_remote_control in ('y', 'yes', 'true'):
|
||||
self.allow_remote_control = 'y'
|
||||
elif self.allow_remote_control in ('n', 'no', 'false'):
|
||||
self.allow_remote_control = 'n'
|
||||
self.listening_on = ''
|
||||
if args.listen_on and self.allow_remote_control in ('y', 'socket', 'socket-only', 'password'):
|
||||
listen_fd = listen_on(args.listen_on)
|
||||
self.listening_on = args.listen_on
|
||||
self.prewarm = prewarm
|
||||
self.child_monitor = ChildMonitor(
|
||||
self.on_child_death,
|
||||
DumpCommands(args) if args.dump_commands or args.dump_bytes else None,
|
||||
talk_fd, listen_fd
|
||||
talk_fd, listen_fd, self.prewarm.take_from_worker_fd()
|
||||
)
|
||||
set_boss(self)
|
||||
self.args = args
|
||||
@@ -276,14 +299,22 @@ class Boss:
|
||||
|
||||
def startup_first_child(self, os_window_id: Optional[int], startup_sessions: Iterable[Session] = ()) -> None:
|
||||
si = startup_sessions or create_sessions(get_options(), self.args, default_session=get_options().startup_session)
|
||||
focused_os_window = wid = 0
|
||||
token = os.environ.pop('XDG_ACTIVATION_TOKEN', '')
|
||||
for startup_session in si:
|
||||
self.add_os_window(startup_session, os_window_id=os_window_id)
|
||||
wid = self.add_os_window(startup_session, os_window_id=os_window_id)
|
||||
if startup_session.focus_os_window:
|
||||
focused_os_window = wid
|
||||
os_window_id = None
|
||||
if self.args.start_as != 'normal':
|
||||
if self.args.start_as == 'fullscreen':
|
||||
self.toggle_fullscreen()
|
||||
else:
|
||||
change_os_window_state(self.args.start_as)
|
||||
if focused_os_window > 0:
|
||||
focus_os_window(focused_os_window, True, token)
|
||||
elif token and is_wayland() and wid:
|
||||
focus_os_window(wid, True, token)
|
||||
|
||||
def add_os_window(
|
||||
self,
|
||||
@@ -434,26 +465,108 @@ class Boss:
|
||||
self.child_monitor.add_child(window.id, window.child.pid, window.child.child_fd, window.screen)
|
||||
self.window_id_map[window.id] = window
|
||||
|
||||
def _handle_remote_command(self, cmd: str, window: Optional[Window] = None, peer_id: int = 0) -> Union[Dict[str, Any], None, AsyncResponse]:
|
||||
from .remote_control import handle_cmd
|
||||
def _handle_remote_command(self, cmd: str, window: Optional[Window] = None, peer_id: int = 0) -> RCResponse:
|
||||
from .remote_control import is_cmd_allowed, parse_cmd
|
||||
response = None
|
||||
window = window or None
|
||||
if self.allow_remote_control == 'y' or peer_id > 0 or getattr(window, 'allow_remote_control', False):
|
||||
try:
|
||||
response = handle_cmd(self, window, cmd, peer_id)
|
||||
except Exception as err:
|
||||
import traceback
|
||||
response = {'ok': False, 'error': str(err)}
|
||||
if not getattr(err, 'hide_traceback', False):
|
||||
response['tb'] = traceback.format_exc()
|
||||
else:
|
||||
no_response = False
|
||||
try:
|
||||
no_response = json.loads(cmd).get('no_response')
|
||||
except Exception:
|
||||
pass
|
||||
window_has_remote_control = bool(window and window.allow_remote_control)
|
||||
if not window_has_remote_control:
|
||||
if self.allow_remote_control == 'n':
|
||||
return {'ok': False, 'error': 'Remote control is disabled'}
|
||||
if self.allow_remote_control == 'socket-only' and peer_id == 0:
|
||||
return {'ok': False, 'error': 'Remote control is allowed over a socket only'}
|
||||
try:
|
||||
pcmd = parse_cmd(cmd, self.encryption_key)
|
||||
except Exception as e:
|
||||
log_error(f'Failed to parse remote command with error: {e}')
|
||||
return response
|
||||
if not pcmd:
|
||||
return response
|
||||
extra_data: Dict[str, Any] = {}
|
||||
try:
|
||||
allowed_unconditionally = (
|
||||
self.allow_remote_control == 'y' or (peer_id > 0 and self.allow_remote_control in ('socket-only', 'socket')) or
|
||||
(window and window.remote_control_allowed(pcmd, extra_data)))
|
||||
except PermissionError:
|
||||
return {'ok': False, 'error': 'Remote control disallowed by window specific password'}
|
||||
if allowed_unconditionally:
|
||||
return self._execute_remote_command(pcmd, window, peer_id)
|
||||
q = is_cmd_allowed(pcmd, window, peer_id > 0, extra_data)
|
||||
if q is True:
|
||||
return self._execute_remote_command(pcmd, window, peer_id)
|
||||
if q is None:
|
||||
if self.ask_if_remote_cmd_is_allowed(pcmd, window, peer_id):
|
||||
return AsyncResponse()
|
||||
response = {'ok': False, 'error': 'Remote control is disabled. Add allow_remote_control to your kitty.conf'}
|
||||
if q is False and pcmd.get('password'):
|
||||
response['error'] = 'The user rejected this password or it is disallowed by remote_control_password in kitty.conf'
|
||||
no_response = pcmd.get('no_response') or False
|
||||
if no_response:
|
||||
return None
|
||||
return response
|
||||
|
||||
def ask_if_remote_cmd_is_allowed(self, pcmd: Dict[str, Any], window: Optional[Window] = None, peer_id: int = 0) -> bool:
|
||||
from kittens.tui.operations import styled
|
||||
in_flight = 0
|
||||
for w in self.window_id_map.values():
|
||||
if w.window_custom_type == 'remote_command_permission_dialog':
|
||||
in_flight += 1
|
||||
if in_flight > 4:
|
||||
log_error('Denying remote command permission as there are too many existing permission requests')
|
||||
return False
|
||||
wid = 0 if window is None else window.id
|
||||
hidden_text = styled(pcmd['password'], fg='yellow')
|
||||
overlay_window = self.choose(
|
||||
_('A program wishes to control kitty.\n'
|
||||
'Action: {1}\n' 'Password: {0}\n\n' '{2}'
|
||||
).format(
|
||||
hidden_text, styled(pcmd['cmd'], fg='magenta'),
|
||||
'\x1b[m' + styled(_(
|
||||
'Note that allowing the password will allow all future actions using the same password, in this kitty instance.'
|
||||
), dim=True, italic=True)),
|
||||
partial(self.remote_cmd_permission_received, pcmd, wid, peer_id),
|
||||
'a;green:Allow request', 'p;yellow:Allow password', 'r;magenta:Deny request', 'd;red:Deny password',
|
||||
window=window, default='a', hidden_text=hidden_text
|
||||
)
|
||||
if overlay_window is None:
|
||||
return False
|
||||
overlay_window.window_custom_type = 'remote_command_permission_dialog'
|
||||
return True
|
||||
|
||||
def remote_cmd_permission_received(self, pcmd: Dict[str, Any], window_id: int, peer_id: int, choice: str) -> None:
|
||||
from .remote_control import (
|
||||
encode_response_for_peer, set_user_password_allowed
|
||||
)
|
||||
response: RCResponse = None
|
||||
window = self.window_id_map.get(window_id)
|
||||
choice = choice or 'r'
|
||||
if choice in ('r', 'd'):
|
||||
if choice == 'd':
|
||||
set_user_password_allowed(pcmd['password'], False)
|
||||
no_response = pcmd.get('no_response') or False
|
||||
if not no_response:
|
||||
response = {'ok': False, 'error': 'Remote control is disabled. Add allow_remote_control to your kitty.conf'}
|
||||
response = {'ok': False, 'error': 'The user rejected this ' + ('request' if choice == 'r' else 'password')}
|
||||
elif choice in ('a', 'p'):
|
||||
if choice == 'p':
|
||||
set_user_password_allowed(pcmd['password'], True)
|
||||
response = self._execute_remote_command(pcmd, window, peer_id)
|
||||
if window is not None and response is not None and not isinstance(response, AsyncResponse):
|
||||
window.send_cmd_response(response)
|
||||
if peer_id > 0:
|
||||
if response is None:
|
||||
send_data_to_peer(peer_id, b'')
|
||||
elif not isinstance(response, AsyncResponse):
|
||||
send_data_to_peer(peer_id, encode_response_for_peer(response))
|
||||
|
||||
def _execute_remote_command(self, pcmd: Dict[str, Any], window: Optional[Window] = None, peer_id: int = 0) -> RCResponse:
|
||||
from .remote_control import handle_cmd
|
||||
try:
|
||||
response = handle_cmd(self, window, pcmd, peer_id)
|
||||
except Exception as err:
|
||||
import traceback
|
||||
response = {'ok': False, 'error': str(err)}
|
||||
if not getattr(err, 'hide_traceback', False):
|
||||
response['tb'] = traceback.format_exc()
|
||||
return response
|
||||
|
||||
@ac('misc', '''
|
||||
@@ -461,34 +574,51 @@ class Boss:
|
||||
|
||||
For example::
|
||||
|
||||
map F1 remote_control set-spacing margin=30
|
||||
map f1 remote_control set-spacing margin=30
|
||||
|
||||
See :ref:`rc_mapping` for details.
|
||||
''')
|
||||
def remote_control(self, *args: str) -> None:
|
||||
from .rc.base import (
|
||||
PayloadGetter, command_for_name, parse_subcommand_cli
|
||||
)
|
||||
from .remote_control import parse_rc_args
|
||||
try:
|
||||
global_opts, items = parse_rc_args(['@'] + list(args))
|
||||
if not items:
|
||||
return
|
||||
cmd = items[0]
|
||||
c = command_for_name(cmd)
|
||||
opts, items = parse_subcommand_cli(c, items)
|
||||
payload = c.message_to_kitty(global_opts, opts, items)
|
||||
import types
|
||||
if isinstance(payload, types.GeneratorType):
|
||||
for x in payload:
|
||||
c.response_from_kitty(self, self.active_window, PayloadGetter(c, x if isinstance(x, dict) else {}))
|
||||
else:
|
||||
c.response_from_kitty(self, self.active_window, PayloadGetter(c, payload if isinstance(payload, dict) else {}))
|
||||
self.call_remote_control(self.active_window, args)
|
||||
except (Exception, SystemExit):
|
||||
import traceback
|
||||
tb = traceback.format_exc()
|
||||
self.show_error(_('remote_control mapping failed'), tb)
|
||||
|
||||
def call_remote_control(self, active_window: Optional[Window], args: Tuple[str, ...]) -> 'ResponseType':
|
||||
from .rc.base import (
|
||||
PayloadGetter, command_for_name, parse_subcommand_cli
|
||||
)
|
||||
from .remote_control import parse_rc_args
|
||||
aa = list(args)
|
||||
silent = False
|
||||
if aa and aa[0].startswith('!'):
|
||||
aa[0] = aa[0][1:]
|
||||
silent = True
|
||||
try:
|
||||
global_opts, items = parse_rc_args(['@'] + aa)
|
||||
if not items:
|
||||
return None
|
||||
cmd = items[0]
|
||||
c = command_for_name(cmd)
|
||||
opts, items = parse_subcommand_cli(c, items)
|
||||
payload = c.message_to_kitty(global_opts, opts, items)
|
||||
except SystemExit as e:
|
||||
raise Exception(str(e)) from e
|
||||
import types
|
||||
try:
|
||||
if isinstance(payload, types.GeneratorType):
|
||||
for x in payload:
|
||||
c.response_from_kitty(self, active_window, PayloadGetter(c, x if isinstance(x, dict) else {}))
|
||||
return None
|
||||
return c.response_from_kitty(self, active_window, PayloadGetter(c, payload if isinstance(payload, dict) else {}))
|
||||
except Exception as e:
|
||||
if silent:
|
||||
log_error(f'Failed to run remote_control mapping: {aa} with error: {e}')
|
||||
return None
|
||||
raise
|
||||
|
||||
def peer_message_received(self, msg_bytes: bytes, peer_id: int) -> Union[bytes, bool, None]:
|
||||
cmd_prefix = b'\x1bP@kitty-cmd'
|
||||
terminator = b'\x1b\\'
|
||||
@@ -506,6 +636,7 @@ class Boss:
|
||||
if isinstance(data, dict) and data.get('cmd') == 'new_instance':
|
||||
from .cli_stub import CLIOptions
|
||||
startup_id = data.get('startup_id')
|
||||
activation_token = data.get('activation_token', '')
|
||||
args, rest = parse_args(data['args'][1:], result_class=CLIOptions)
|
||||
cmdline_args_for_open = data.get('cmdline_args_for_open')
|
||||
if cmdline_args_for_open:
|
||||
@@ -518,14 +649,21 @@ class Boss:
|
||||
args.session = PreReadSession(data['stdin'])
|
||||
if not os.path.isabs(args.directory):
|
||||
args.directory = os.path.join(data['cwd'], args.directory)
|
||||
focused_os_window = os_window_id = 0
|
||||
for session in create_sessions(opts, args, respect_cwd=True):
|
||||
os_window_id = self.add_os_window(
|
||||
session, wclass=args.cls, wname=args.name, opts_for_size=opts, startup_id=startup_id,
|
||||
override_title=args.title or None)
|
||||
if session.focus_os_window:
|
||||
focused_os_window = os_window_id
|
||||
if opts.background_opacity != get_options().background_opacity:
|
||||
self._set_os_window_background_opacity(os_window_id, opts.background_opacity)
|
||||
if data.get('notify_on_os_window_death'):
|
||||
self.os_window_death_actions[os_window_id] = partial(self.notify_on_os_window_death, data['notify_on_os_window_death'])
|
||||
if focused_os_window > 0:
|
||||
focus_os_window(focused_os_window, True, activation_token or '')
|
||||
elif activation_token and is_wayland() and os_window_id:
|
||||
focus_os_window(os_window_id, True, activation_token or '')
|
||||
else:
|
||||
log_error('Unknown message received from peer, ignoring')
|
||||
return None
|
||||
@@ -649,11 +787,19 @@ class Boss:
|
||||
confirm_on_cancel: bool = False, # on closing window
|
||||
confirm_on_accept: bool = True, # on pressing enter
|
||||
) -> None:
|
||||
result: bool = False
|
||||
|
||||
def callback_(res: Dict[str, Any], x: int, boss: Boss) -> None:
|
||||
callback(res.get('response') == 'y', *args)
|
||||
nonlocal result
|
||||
result = res.get('response') == 'y'
|
||||
|
||||
def on_popup_overlay_removal(wid: int, boss: Boss) -> None:
|
||||
callback(result, *args)
|
||||
|
||||
self.run_kitten_with_metadata(
|
||||
'ask', ['--type=yesno', '--message', msg, '--default', 'y' if confirm_on_accept else 'n'],
|
||||
window=window, custom_callback=callback_, default_data={'response': 'y' if confirm_on_cancel else 'n'})
|
||||
window=window, custom_callback=callback_, action_on_removal=on_popup_overlay_removal,
|
||||
default_data={'response': 'y' if confirm_on_cancel else 'n'})
|
||||
|
||||
def choose(
|
||||
self, msg: str, # can contain newlines and ANSI formatting
|
||||
@@ -661,15 +807,39 @@ class Boss:
|
||||
*choices: str, # The choices, see the help for the ask kitten for format of a choice
|
||||
window: Optional[Window] = None, # the window associated with the confirmation
|
||||
default: str = '', # the default choice when the user presses Enter
|
||||
) -> None:
|
||||
hidden_text: str = '', # text to hide in the message
|
||||
hidden_text_placeholder: str = 'HIDDEN_TEXT_PLACEHOLDER', # placeholder text to insert in to message
|
||||
unhide_key: str = 'u', # key to press to unhide hidden text
|
||||
) -> Optional[Window]:
|
||||
result: str = ''
|
||||
|
||||
def callback_(res: Dict[str, Any], x: int, boss: Boss) -> None:
|
||||
callback(res.get('response') or '')
|
||||
nonlocal result
|
||||
result = res.get('response') or ''
|
||||
|
||||
if hidden_text:
|
||||
msg = msg.replace(hidden_text, hidden_text_placeholder)
|
||||
cmd = ['--type=choices', '--message', msg]
|
||||
if default:
|
||||
cmd += ['-d', default]
|
||||
for c in choices:
|
||||
cmd += ['-c', c]
|
||||
self.run_kitten_with_metadata('ask', cmd, window=window, custom_callback=callback_, default_data={'response': ''})
|
||||
if hidden_text:
|
||||
cmd += ['--hidden-text-placeholder', hidden_text_placeholder, '--unhide-key', unhide_key]
|
||||
input_data = hidden_text
|
||||
else:
|
||||
input_data = None
|
||||
|
||||
def on_popup_overlay_removal(wid: int, boss: Boss) -> None:
|
||||
callback(result)
|
||||
|
||||
ans = self.run_kitten_with_metadata(
|
||||
'ask', cmd, window=window, custom_callback=callback_, input_data=input_data, default_data={'response': ''},
|
||||
action_on_removal=on_popup_overlay_removal
|
||||
)
|
||||
if isinstance(ans, Window):
|
||||
return ans
|
||||
return None
|
||||
|
||||
def get_line(
|
||||
self, msg: str, # can contain newlines and ANSI formatting
|
||||
@@ -678,10 +848,19 @@ class Boss:
|
||||
prompt: str = '> ',
|
||||
is_password: bool = False
|
||||
) -> None:
|
||||
result: str = ''
|
||||
|
||||
def callback_(res: Dict[str, Any], x: int, boss: Boss) -> None:
|
||||
callback(res.get('response') or '')
|
||||
nonlocal result
|
||||
result = res.get('response') or ''
|
||||
|
||||
def on_popup_overlay_removal(wid: int, boss: Boss) -> None:
|
||||
callback(result)
|
||||
|
||||
cmd = ['--type', 'password' if is_password else 'line', '--message', msg, '--prompt', prompt]
|
||||
self.run_kitten_with_metadata('ask', cmd, window=window, custom_callback=callback_, default_data={'response': ''})
|
||||
self.run_kitten_with_metadata(
|
||||
'ask', cmd, window=window, custom_callback=callback_, default_data={'response': ''}, action_on_removal=on_popup_overlay_removal
|
||||
)
|
||||
|
||||
def confirm_tab_close(self, tab: Tab) -> None:
|
||||
x = get_options().confirm_os_window_close
|
||||
@@ -733,6 +912,8 @@ class Boss:
|
||||
if not getattr(self, 'io_thread_started', False):
|
||||
self.child_monitor.start()
|
||||
self.io_thread_started = True
|
||||
for signum in self.child_monitor.handled_signals():
|
||||
handled_signals.add(signum)
|
||||
urls: List[str] = getattr(sys, 'cmdline_args_for_open', [])
|
||||
if urls:
|
||||
delattr(sys, 'cmdline_args_for_open')
|
||||
@@ -1100,6 +1281,13 @@ class Boss:
|
||||
w, h = get_new_os_window_size(metrics, width, height, unit, incremental, has_window_scaling)
|
||||
set_os_window_size(os_window_id, w, h)
|
||||
|
||||
def tab_for_id(self, tab_id: int) -> Optional[Tab]:
|
||||
for tm in self.os_window_map.values():
|
||||
tab = tm.tab_for_id(tab_id)
|
||||
if tab is not None:
|
||||
return tab
|
||||
return None
|
||||
|
||||
def default_bg_changed_for(self, window_id: int) -> None:
|
||||
w = self.window_id_map.get(window_id)
|
||||
if w is not None:
|
||||
@@ -1334,9 +1522,7 @@ class Boss:
|
||||
@ac('misc', 'Edit the kitty.conf config file in your favorite text editor')
|
||||
def edit_config_file(self, *a: Any) -> None:
|
||||
confpath = prepare_config_file_for_editing()
|
||||
# On macOS vim fails to handle SIGWINCH if it occurs early, so add a
|
||||
# small delay.
|
||||
cmd = [kitty_exe(), '+runpy', 'import os, sys, time; time.sleep(0.05); os.execvp(sys.argv[1], sys.argv[1:])'] + get_editor(get_options()) + [confpath]
|
||||
cmd = [kitty_exe(), '+edit'] + get_editor(get_options()) + [confpath]
|
||||
self.new_os_window(*cmd)
|
||||
|
||||
def run_kitten_with_metadata(
|
||||
@@ -1440,10 +1626,28 @@ class Boss:
|
||||
def input_unicode_character(self) -> None:
|
||||
self.run_kitten_with_metadata('unicode_input')
|
||||
|
||||
@ac('tab', 'Change the title of the active tab')
|
||||
def set_tab_title(self) -> None:
|
||||
@ac(
|
||||
'tab', '''
|
||||
Change the title of the active tab interactively, by typing in the new title.
|
||||
If you specify an argument to this action then that is used as the title instead of asking for it.
|
||||
Use the empty string ("") to reset the title to default. For example::
|
||||
|
||||
# interactive usage
|
||||
map f1 set_tab_title
|
||||
# set a specific title
|
||||
map f2 set_tab_title some title
|
||||
# reset to default
|
||||
map f3 set_tab_title ""
|
||||
'''
|
||||
)
|
||||
def set_tab_title(self, title: Optional[str] = None) -> None:
|
||||
tab = self.active_tab
|
||||
if tab:
|
||||
if title is not None:
|
||||
if title in ('""', "''"):
|
||||
title = ''
|
||||
tab.set_title(title)
|
||||
return
|
||||
args = [
|
||||
'--name=tab-title', '--message', _('Enter the new title for this tab below.'),
|
||||
'--default', tab.name or tab.title, 'do_set_tab_title', str(tab.id)]
|
||||
@@ -1496,31 +1700,34 @@ class Boss:
|
||||
cmd = [kitty_exe(), '@']
|
||||
aw = self.active_window
|
||||
if aw is not None:
|
||||
kw['env'] = {'KITTY_SHELL_ACTIVE_WINDOW_ID': str(aw.id)}
|
||||
env = {'KITTY_SHELL_ACTIVE_WINDOW_ID': str(aw.id)}
|
||||
at = self.active_tab
|
||||
if at is not None:
|
||||
env['KITTY_SHELL_ACTIVE_TAB_ID'] = str(at.id)
|
||||
kw['env'] = env
|
||||
if window_type == 'tab':
|
||||
tab = self._new_tab(SpecialWindow(cmd, **kw))
|
||||
if tab is not None:
|
||||
for w in tab:
|
||||
w.allow_remote_control = True
|
||||
window = w
|
||||
elif window_type == 'os_window':
|
||||
os_window_id = self._new_os_window(SpecialWindow(cmd, **kw))
|
||||
for tab in self.os_window_map[os_window_id]:
|
||||
for w in tab:
|
||||
w.allow_remote_control = True
|
||||
window = w
|
||||
elif window_type == 'overlay':
|
||||
tab = self.active_tab
|
||||
if aw is not None and tab is not None:
|
||||
kw['overlay_for'] = aw.id
|
||||
window = tab.new_special_window(SpecialWindow(cmd, **kw), allow_remote_control=True)
|
||||
window = tab.new_special_window(SpecialWindow(cmd, **kw))
|
||||
else:
|
||||
tab = self.active_tab
|
||||
if tab is not None:
|
||||
window = tab.new_special_window(SpecialWindow(cmd, **kw), allow_remote_control=True)
|
||||
window = tab.new_special_window(SpecialWindow(cmd, **kw))
|
||||
|
||||
path, ext = os.path.splitext(logo_png_file)
|
||||
window.set_logo(f'{path}-128{ext}', position='bottom-right', alpha=0.25)
|
||||
window.allow_remote_control = True
|
||||
|
||||
def switch_focus_to(self, window_id: int) -> None:
|
||||
tab = self.active_tab
|
||||
@@ -1546,7 +1753,19 @@ class Boss:
|
||||
if actions:
|
||||
self.drain_actions(actions)
|
||||
if not found_action:
|
||||
open_url(url, program or get_options().open_url_with, cwd=cwd)
|
||||
extra_env = {}
|
||||
if self.listening_on:
|
||||
extra_env['KITTY_LISTEN_ON'] = self.listening_on
|
||||
|
||||
def doit(activation_token: str = '') -> None:
|
||||
if activation_token:
|
||||
extra_env['XDG_ACTIVATION_TOKEN'] = activation_token
|
||||
open_url(url, program or get_options().open_url_with, cwd=cwd, extra_env=extra_env)
|
||||
|
||||
if is_wayland():
|
||||
run_with_activation_token(doit)
|
||||
else:
|
||||
doit()
|
||||
|
||||
@ac('misc', 'Click a URL using the keyboard')
|
||||
def open_url_with_hints(self) -> None:
|
||||
@@ -1602,6 +1821,12 @@ class Boss:
|
||||
if get_options().copy_on_select:
|
||||
self.copy_to_buffer(get_options().copy_on_select)
|
||||
|
||||
def get_active_selection(self) -> Optional[str]:
|
||||
w = self.active_window
|
||||
if w is not None and not w.destroyed:
|
||||
return w.text_for_selection()
|
||||
return None
|
||||
|
||||
@ac('cp', '''
|
||||
Copy the selection from the active window to the specified buffer
|
||||
|
||||
@@ -1734,18 +1959,28 @@ class Boss:
|
||||
with suppress(Exception):
|
||||
cwd = cwd_from.cwd_of_child
|
||||
|
||||
if stdin:
|
||||
r, w = safe_pipe(False)
|
||||
try:
|
||||
subprocess.Popen(cmd, env=env, stdin=r, cwd=cwd)
|
||||
except Exception:
|
||||
os.close(w)
|
||||
def doit(activation_token: str = '') -> None:
|
||||
nonlocal env
|
||||
if activation_token:
|
||||
if env is None:
|
||||
env = default_env().copy()
|
||||
env['XDG_ACTIVATION_TOKEN'] = activation_token
|
||||
if stdin:
|
||||
r, w = safe_pipe(False)
|
||||
try:
|
||||
subprocess.Popen(cmd, env=env, stdin=r, cwd=cwd, preexec_fn=clear_handled_signals)
|
||||
except Exception:
|
||||
os.close(w)
|
||||
else:
|
||||
thread_write(w, stdin)
|
||||
finally:
|
||||
os.close(r)
|
||||
else:
|
||||
thread_write(w, stdin)
|
||||
finally:
|
||||
os.close(r)
|
||||
subprocess.Popen(cmd, env=env, cwd=cwd, preexec_fn=clear_handled_signals)
|
||||
if is_wayland():
|
||||
run_with_activation_token(doit)
|
||||
else:
|
||||
subprocess.Popen(cmd, env=env, cwd=cwd)
|
||||
doit()
|
||||
|
||||
def pipe(self, source: str, dest: str, exe: str, *args: str) -> Optional[Window]:
|
||||
cmd = [exe] + list(args)
|
||||
@@ -1806,6 +2041,8 @@ class Boss:
|
||||
special_window = args
|
||||
else:
|
||||
special_window = self.args_to_special_window(args, cwd_from=cwd_from)
|
||||
if not self.os_window_map:
|
||||
self.add_os_window()
|
||||
tm = self.active_tab_manager
|
||||
if tm is not None:
|
||||
return tm.new_tab(special_window=special_window, cwd_from=cwd_from, as_neighbor=as_neighbor)
|
||||
@@ -1827,8 +2064,6 @@ class Boss:
|
||||
self._create_tab(list(args), cwd_from=CwdRequest(self.active_window_for_cwd))
|
||||
|
||||
def new_tab_with_wd(self, wd: Union[str, List[str]], str_is_multiple_paths: bool = False) -> None:
|
||||
if not self.os_window_map:
|
||||
self.add_os_window()
|
||||
if isinstance(wd, str):
|
||||
wd = wd.split(os.pathsep) if str_is_multiple_paths else [wd]
|
||||
for path in wd:
|
||||
@@ -1836,6 +2071,11 @@ class Boss:
|
||||
self._new_tab(special_window)
|
||||
|
||||
def _new_window(self, args: List[str], cwd_from: Optional[CwdRequest] = None) -> Optional[Window]:
|
||||
if not self.os_window_map:
|
||||
os_window_id = self.add_os_window()
|
||||
tm = self.os_window_map.get(os_window_id)
|
||||
if tm is not None and not tm.active_tab:
|
||||
tm.new_tab(empty_tab=True)
|
||||
tab = self.active_tab
|
||||
if tab is None:
|
||||
return None
|
||||
@@ -1954,6 +2194,7 @@ class Boss:
|
||||
for w in self.all_windows:
|
||||
self.default_bg_changed_for(w.id)
|
||||
w.refresh()
|
||||
self.prewarm.reload_kitty_config()
|
||||
|
||||
@ac('misc', '''
|
||||
Reload the config file
|
||||
@@ -1965,8 +2206,8 @@ class Boss:
|
||||
map f5 load_config_file /path/to/some/kitty.conf
|
||||
''')
|
||||
def load_config_file(self, *paths: str, apply_overrides: bool = True) -> None:
|
||||
from .config import load_config
|
||||
from .cli import default_config_paths
|
||||
from .config import load_config
|
||||
old_opts = get_options()
|
||||
prev_paths = old_opts.config_paths or default_config_paths(self.args.config)
|
||||
paths = paths or prev_paths
|
||||
@@ -1975,8 +2216,8 @@ class Boss:
|
||||
if bad_lines:
|
||||
self.show_bad_config_lines(bad_lines)
|
||||
self.apply_new_options(opts)
|
||||
from .open_actions import load_open_actions
|
||||
load_open_actions.clear_cached()
|
||||
from .open_actions import clear_caches
|
||||
clear_caches()
|
||||
|
||||
def safe_delete_temp_file(self, path: str) -> None:
|
||||
if is_path_in_temp_dir(path):
|
||||
@@ -2038,7 +2279,7 @@ class Boss:
|
||||
@ac('misc', '''
|
||||
Change colors in the specified windows
|
||||
|
||||
For details, see :ref:`at_set-colors`. For example::
|
||||
For details, see :ref:`at-set-colors`. For example::
|
||||
|
||||
map f5 set_colors --configured /path/to/some/config/file/colors.conf
|
||||
''')
|
||||
@@ -2246,8 +2487,11 @@ class Boss:
|
||||
@ac('debug', 'Show the environment variables that the kitty process sees')
|
||||
def show_kitty_env_vars(self) -> None:
|
||||
w = self.active_window
|
||||
env = os.environ.copy()
|
||||
if is_macos and env.get('LC_CTYPE') == 'UTF-8' and not getattr(sys, 'kitty_run_data').get('lc_ctype_before_python'):
|
||||
del env['LC_CTYPE']
|
||||
if w:
|
||||
output = '\n'.join(f'{k}={v}' for k, v in os.environ.items())
|
||||
output = '\n'.join(f'{k}={v}' for k, v in env.items())
|
||||
self.display_scrollback(w, output, title=_('Current kitty env vars'), report_cursor=False)
|
||||
|
||||
@ac('debug', '''
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <signal.h>
|
||||
extern PyTypeObject Screen_Type;
|
||||
|
||||
@@ -33,7 +34,7 @@ extern PyTypeObject Screen_Type;
|
||||
#define EVDBG(...)
|
||||
#endif
|
||||
|
||||
#define EXTRA_FDS 2
|
||||
#define EXTRA_FDS 3
|
||||
#ifndef MSG_NOSIGNAL
|
||||
// Apple does not implement MSG_NOSIGNAL
|
||||
#define MSG_NOSIGNAL 0
|
||||
@@ -56,7 +57,7 @@ typedef struct {
|
||||
bool shutting_down;
|
||||
pthread_t io_thread, talk_thread;
|
||||
|
||||
int talk_fd, listen_fd;
|
||||
int talk_fd, listen_fd, prewarm_fd;
|
||||
Message *messages;
|
||||
size_t messages_capacity, messages_count;
|
||||
LoopData io_loop_data;
|
||||
@@ -123,11 +124,11 @@ static PyObject *
|
||||
new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
|
||||
ChildMonitor *self;
|
||||
PyObject *dump_callback, *death_notify;
|
||||
int talk_fd = -1, listen_fd = -1;
|
||||
int talk_fd = -1, listen_fd = -1, prewarm_fd = -1;
|
||||
int ret;
|
||||
|
||||
if (the_monitor) { PyErr_SetString(PyExc_RuntimeError, "Can have only a single ChildMonitor instance"); return NULL; }
|
||||
if (!PyArg_ParseTuple(args, "OO|ii", &death_notify, &dump_callback, &talk_fd, &listen_fd)) return NULL;
|
||||
if (!PyArg_ParseTuple(args, "OO|iii", &death_notify, &dump_callback, &talk_fd, &listen_fd, &prewarm_fd)) return NULL;
|
||||
if ((ret = pthread_mutex_init(&children_lock, NULL)) != 0) {
|
||||
PyErr_Format(PyExc_RuntimeError, "Failed to create children_lock mutex: %s", strerror(ret));
|
||||
return NULL;
|
||||
@@ -137,9 +138,10 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
|
||||
return NULL;
|
||||
}
|
||||
self = (ChildMonitor *)type->tp_alloc(type, 0);
|
||||
if (!init_loop_data(&self->io_loop_data, 5, SIGINT, SIGTERM, SIGCHLD, SIGUSR1, SIGUSR2)) return PyErr_SetFromErrno(PyExc_OSError);
|
||||
if (!init_loop_data(&self->io_loop_data, SIGINT, SIGHUP, SIGTERM, SIGCHLD, SIGUSR1, SIGUSR2, 0)) return PyErr_SetFromErrno(PyExc_OSError);
|
||||
self->talk_fd = talk_fd;
|
||||
self->listen_fd = listen_fd;
|
||||
self->prewarm_fd = prewarm_fd;
|
||||
if (self == NULL) return PyErr_NoMemory();
|
||||
self->death_notify = death_notify; Py_INCREF(death_notify);
|
||||
if (dump_callback != Py_None) {
|
||||
@@ -148,7 +150,8 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
|
||||
} else parse_func = parse_worker;
|
||||
self->count = 0;
|
||||
children_fds[0].fd = self->io_loop_data.wakeup_read_fd; children_fds[1].fd = self->io_loop_data.signal_read_fd;
|
||||
children_fds[0].events = POLLIN; children_fds[1].events = POLLIN;
|
||||
children_fds[2].fd = self->prewarm_fd;
|
||||
children_fds[0].events = POLLIN; children_fds[1].events = POLLIN; children_fds[2].events = POLLIN;
|
||||
the_monitor = self;
|
||||
|
||||
return (PyObject*) self;
|
||||
@@ -174,9 +177,21 @@ dealloc(ChildMonitor* self) {
|
||||
FREE_CHILD(add_queue[add_queue_count]);
|
||||
}
|
||||
free_loop_data(&self->io_loop_data);
|
||||
safe_close(self->prewarm_fd, __FILE__, __LINE__); self->prewarm_fd = -1;
|
||||
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
handled_signals(ChildMonitor *self, PyObject *args UNUSED) {
|
||||
PyObject *ans = PyTuple_New(self->io_loop_data.num_handled_signals);
|
||||
if (ans) {
|
||||
for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(ans); i++) {
|
||||
PyTuple_SET_ITEM(ans, i, PyLong_FromLong((long)self->io_loop_data.handled_signals[i]));
|
||||
}
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
static void
|
||||
wakeup_io_loop(ChildMonitor *self, bool in_signal_handler) {
|
||||
wakeup_loop(&self->io_loop_data, in_signal_handler, "io_loop");
|
||||
@@ -427,7 +442,7 @@ parse_input(ChildMonitor *self) {
|
||||
}
|
||||
if (resp) {
|
||||
if (PyBytes_Check(resp)) send_response_to_peer(msg->peer_id, PyBytes_AS_STRING(resp), PyBytes_GET_SIZE(resp));
|
||||
else if (resp == Py_None || resp == Py_True) send_response_to_peer(msg->peer_id, NULL, 0);
|
||||
else if (resp == Py_None) send_response_to_peer(msg->peer_id, NULL, 0);
|
||||
Py_CLEAR(resp);
|
||||
} else send_response_to_peer(msg->peer_id, NULL, 0);
|
||||
}
|
||||
@@ -672,16 +687,9 @@ render_os_window(OSWindow *os_window, unsigned int active_window_id, color_type
|
||||
if (os_window->clear_count++ < 3) blank_os_window(os_window);
|
||||
Tab *tab = os_window->tabs + os_window->active_tab;
|
||||
BorderRects *br = &tab->border_rects;
|
||||
bool static_live_resize_in_progress = os_window->live_resize.in_progress && OPT(resize_draw_strategy) == RESIZE_DRAW_STATIC;
|
||||
float x_ratio = 1, y_ratio = 1;
|
||||
if (static_live_resize_in_progress) {
|
||||
x_ratio = (float) os_window->viewport_width / (float) os_window->live_resize.width;
|
||||
y_ratio = (float) os_window->viewport_height / (float) os_window->live_resize.height;
|
||||
}
|
||||
if (!static_live_resize_in_progress) {
|
||||
draw_borders(br->vao_idx, br->num_border_rects, br->rect_buf, br->is_dirty, os_window->viewport_width, os_window->viewport_height, active_window_bg, num_visible_windows, all_windows_have_same_bg, os_window);
|
||||
br->is_dirty = false;
|
||||
}
|
||||
draw_borders(br->vao_idx, br->num_border_rects, br->rect_buf, br->is_dirty, os_window->viewport_width, os_window->viewport_height, active_window_bg, num_visible_windows, all_windows_have_same_bg, os_window);
|
||||
br->is_dirty = false;
|
||||
if (TD.screen && os_window->num_tabs >= OPT(tab_bar_min_tabs)) draw_cells(TD.vao_idx, 0, &TD, x_ratio, y_ratio, os_window, true, false, NULL);
|
||||
for (unsigned int i = 0; i < tab->num_windows; i++) {
|
||||
Window *w = tab->windows + i;
|
||||
@@ -743,7 +751,6 @@ render(monotonic_t now, bool input_read) {
|
||||
|
||||
for (size_t i = 0; i < global_state.num_os_windows; i++) {
|
||||
OSWindow *w = global_state.os_windows + i;
|
||||
w->render_calls++;
|
||||
if (!w->num_tabs) continue;
|
||||
if (!should_os_window_be_rendered(w)) {
|
||||
update_os_window_title(w);
|
||||
@@ -752,8 +759,11 @@ render(monotonic_t now, bool input_read) {
|
||||
}
|
||||
if (USE_RENDER_FRAMES && w->render_state != RENDER_FRAME_READY) {
|
||||
if (w->render_state == RENDER_FRAME_NOT_REQUESTED || no_render_frame_received_recently(w, now, ms_to_monotonic_t(250ll))) request_frame_render(w);
|
||||
continue;
|
||||
// dont respect render frames soon after a resize on Wayland as they cause flicker because
|
||||
// we want to fill the newly resized buffer ASAP, not at compositors convenience
|
||||
if (!global_state.is_wayland || (monotonic() - w->viewport_resized_at) > s_double_to_monotonic_t(1)) continue;
|
||||
}
|
||||
w->render_calls++;
|
||||
make_os_window_context_current(w);
|
||||
if (w->live_resize.in_progress && OPT(resize_draw_strategy) >= RESIZE_DRAW_BLANK) {
|
||||
blank_os_window(w);
|
||||
@@ -957,7 +967,7 @@ process_pending_resizes(monotonic_t now) {
|
||||
|
||||
static void
|
||||
close_os_window(ChildMonitor *self, OSWindow *os_window) {
|
||||
int w = os_window->window_width, h = os_window->window_height;
|
||||
int w = os_window->content_area_width, h = os_window->content_area_height;
|
||||
if (os_window->before_fullscreen.is_set && is_os_window_fullscreen(os_window)) {
|
||||
w = os_window->before_fullscreen.w; h = os_window->before_fullscreen.h;
|
||||
}
|
||||
@@ -1240,12 +1250,13 @@ read_bytes(int fd, Screen *screen) {
|
||||
|
||||
typedef struct { bool kill_signal, child_died, reload_config; } SignalSet;
|
||||
|
||||
static void
|
||||
static bool
|
||||
handle_signal(const siginfo_t *siginfo, void *data) {
|
||||
SignalSet *ss = data;
|
||||
switch(siginfo->si_signo) {
|
||||
case SIGINT:
|
||||
case SIGTERM:
|
||||
case SIGHUP:
|
||||
ss->kill_signal = true;
|
||||
break;
|
||||
case SIGCHLD:
|
||||
@@ -1260,6 +1271,7 @@ handle_signal(const siginfo_t *siginfo, void *data) {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -1289,6 +1301,34 @@ mark_monitored_pids(pid_t pid, int status) {
|
||||
children_mutex(unlock);
|
||||
}
|
||||
|
||||
static void
|
||||
reap_prewarmed_children(ChildMonitor *self, int fd, bool enable_close_on_child_death) {
|
||||
static char buf[256];
|
||||
static size_t buf_pos = 0;
|
||||
while(true) {
|
||||
ssize_t len = read(fd, buf + buf_pos, sizeof(buf) - buf_pos);
|
||||
if (len < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
if (errno != EIO && errno != EAGAIN) log_error("Call to read() from reap_prewarmed_children() failed with error: %s", strerror(errno));
|
||||
break;
|
||||
}
|
||||
buf_pos += len;
|
||||
char *nl;
|
||||
while (buf_pos > 1 && (nl = memchr(buf, '\n', buf_pos)) != NULL) {
|
||||
size_t sz = nl - buf + 1;
|
||||
if (enable_close_on_child_death) {
|
||||
*nl = 0;
|
||||
int pid = atoi(buf);
|
||||
if (pid) mark_child_for_removal(self, pid);
|
||||
}
|
||||
memmove(buf, buf + sz, sz);
|
||||
buf_pos -= sz;
|
||||
}
|
||||
if (len == 0) break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void
|
||||
reap_children(ChildMonitor *self, bool enable_close_on_child_death) {
|
||||
int status;
|
||||
@@ -1379,6 +1419,9 @@ io_loop(void *data) {
|
||||
}
|
||||
if (ss.child_died) reap_children(self, OPT(close_on_child_death));
|
||||
}
|
||||
if (children_fds[2].revents && POLLIN) {
|
||||
reap_prewarmed_children(self, children_fds[2].fd, OPT(close_on_child_death));
|
||||
}
|
||||
for (i = 0; i < self->count; i++) {
|
||||
if (children_fds[EXTRA_FDS + i].revents & (POLLIN | POLLHUP)) {
|
||||
data_received = true;
|
||||
@@ -1682,8 +1725,10 @@ send_response_to_peer(id_type peer_id, const char *msg, size_t msg_sz) {
|
||||
peer->write.capacity += msg_sz;
|
||||
} else fatal("Out of memory");
|
||||
}
|
||||
if (msg) memcpy(peer->write.data + peer->write.used, msg, msg_sz);
|
||||
peer->write.used += msg_sz;
|
||||
if (msg_sz && msg) {
|
||||
memcpy(peer->write.data + peer->write.used, msg, msg_sz);
|
||||
peer->write.used += msg_sz;
|
||||
}
|
||||
}
|
||||
wakeup = true;
|
||||
break;
|
||||
@@ -1705,6 +1750,7 @@ static PyMethodDef methods[] = {
|
||||
METHOD(main_loop, METH_NOARGS)
|
||||
METHOD(mark_for_close, METH_VARARGS)
|
||||
METHOD(resize_pty, METH_VARARGS)
|
||||
METHODB(handled_signals, METH_NOARGS),
|
||||
{"set_iutf8_winid", (PyCFunction)pyset_iutf8, METH_VARARGS, ""},
|
||||
{NULL} /* Sentinel */
|
||||
};
|
||||
@@ -1743,7 +1789,6 @@ cocoa_set_menubar_title(PyObject *self UNUSED, PyObject *args UNUSED) {
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
|
||||
send_data_to_peer(PyObject *self UNUSED, PyObject *args) {
|
||||
char * msg; Py_ssize_t sz;
|
||||
unsigned long long peer_id;
|
||||
@@ -1752,8 +1797,28 @@ send_data_to_peer(PyObject *self UNUSED, PyObject *args) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
random_unix_socket(PyObject *self UNUSED, PyObject *args UNUSED) {
|
||||
#ifndef SO_PASSCRED
|
||||
errno = ENOTSUP;
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
#else
|
||||
int fd, optval = 1;
|
||||
struct sockaddr_un bind_addr = {.sun_family=AF_UNIX};
|
||||
fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) return PyErr_SetFromErrno(PyExc_OSError);
|
||||
if (setsockopt(fd, SOL_SOCKET, SO_PASSCRED, &optval, sizeof optval) != 0) goto fail;
|
||||
if (bind(fd, (struct sockaddr *)&bind_addr, sizeof(sa_family_t)) != 0) goto fail;
|
||||
return PyLong_FromLong((long)fd);
|
||||
fail:
|
||||
safe_close(fd, __FILE__, __LINE__);
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
#endif
|
||||
}
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
METHODB(safe_pipe, METH_VARARGS),
|
||||
METHODB(random_unix_socket, METH_NOARGS),
|
||||
{"add_timer", (PyCFunction)add_python_timer, METH_VARARGS, ""},
|
||||
{"remove_timer", (PyCFunction)remove_python_timer, METH_VARARGS, ""},
|
||||
METHODB(monitor_pid, METH_VARARGS),
|
||||
|
||||
@@ -79,14 +79,16 @@ wait_for_terminal_ready(int fd) {
|
||||
|
||||
static PyObject*
|
||||
spawn(PyObject *self UNUSED, PyObject *args) {
|
||||
PyObject *argv_p, *env_p;
|
||||
PyObject *argv_p, *env_p, *handled_signals_p;
|
||||
int master, slave, stdin_read_fd, stdin_write_fd, ready_read_fd, ready_write_fd;
|
||||
char *cwd, *exe;
|
||||
if (!PyArg_ParseTuple(args, "ssO!O!iiiiii", &exe, &cwd, &PyTuple_Type, &argv_p, &PyTuple_Type, &env_p, &master, &slave, &stdin_read_fd, &stdin_write_fd, &ready_read_fd, &ready_write_fd)) return NULL;
|
||||
if (!PyArg_ParseTuple(args, "ssO!O!iiiiiiO!", &exe, &cwd, &PyTuple_Type, &argv_p, &PyTuple_Type, &env_p, &master, &slave, &stdin_read_fd, &stdin_write_fd, &ready_read_fd, &ready_write_fd, &PyTuple_Type, &handled_signals_p)) return NULL;
|
||||
char name[2048] = {0};
|
||||
if (ttyname_r(slave, name, sizeof(name) - 1) != 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; }
|
||||
char **argv = serialize_string_tuple(argv_p);
|
||||
char **env = serialize_string_tuple(env_p);
|
||||
int handled_signals[16] = {0}, num_handled_signals = MIN((int)arraysz(handled_signals), PyTuple_GET_SIZE(handled_signals_p));
|
||||
for (Py_ssize_t i = 0; i < num_handled_signals; i++) handled_signals[i] = PyLong_AsLong(PyTuple_GET_ITEM(handled_signals_p, i));
|
||||
|
||||
#if PY_VERSION_HEX >= 0x03070000
|
||||
PyOS_BeforeFork();
|
||||
@@ -98,11 +100,14 @@ spawn(PyObject *self UNUSED, PyObject *args) {
|
||||
#if PY_VERSION_HEX >= 0x03070000
|
||||
PyOS_AfterFork_Child();
|
||||
#endif
|
||||
const struct sigaction act = {.sa_handler=SIG_DFL};
|
||||
|
||||
#define SA(which) if (sigaction(which, &act, NULL) != 0) exit_on_err("sigaction() in child process failed");
|
||||
for (int si = 0; si < num_handled_signals; si++) { SA(handled_signals[si]); }
|
||||
// See _Py_RestoreSignals in signalmodule.c for a list of signals python nukes
|
||||
sigset_t signals = {0};
|
||||
struct sigaction act = {.sa_handler=SIG_DFL};
|
||||
#define SA(which) { if (sigaction(which, &act, NULL) != 0) exit_on_err("sigaction() in child process failed"); }
|
||||
SA(SIGINT); SA(SIGTERM); SA(SIGCHLD); SA(SIGPIPE); SA(SIGUSR1); SA(SIGUSR2);
|
||||
#ifdef SIGPIPE
|
||||
SA(SIGPIPE)
|
||||
#endif
|
||||
#ifdef SIGXFSZ
|
||||
SA(SIGXFSZ);
|
||||
#endif
|
||||
@@ -110,29 +115,28 @@ spawn(PyObject *self UNUSED, PyObject *args) {
|
||||
SA(SIGXFZ);
|
||||
#endif
|
||||
#undef SA
|
||||
sigset_t signals; sigemptyset(&signals);
|
||||
if (sigprocmask(SIG_SETMASK, &signals, NULL) != 0) exit_on_err("sigprocmask() in child process failed");
|
||||
// Use only signal-safe functions (man 7 signal-safety)
|
||||
if (chdir(cwd) != 0) { if (chdir("/") != 0) {} }; // ignore failure to chdir to /
|
||||
if (setsid() == -1) exit_on_err("setsid() in child process failed");
|
||||
|
||||
// Establish the controlling terminal (see man 7 credentials)
|
||||
int tfd = safe_open(name, O_RDWR, 0);
|
||||
int tfd = safe_open(name, O_RDWR | O_CLOEXEC, 0);
|
||||
if (tfd == -1) exit_on_err("Failed to open controlling terminal");
|
||||
#ifdef TIOCSCTTY
|
||||
// On BSD open() does not establish the controlling terminal
|
||||
if (ioctl(tfd, TIOCSCTTY, 0) == -1) exit_on_err("Failed to set controlling terminal with TIOCSCTTY");
|
||||
#endif
|
||||
safe_close(tfd, __FILE__, __LINE__);
|
||||
|
||||
// Redirect stdin/stdout/stderr to the pty
|
||||
if (dup2(slave, 1) == -1) exit_on_err("dup2() failed for fd number 1");
|
||||
if (dup2(slave, 2) == -1) exit_on_err("dup2() failed for fd number 2");
|
||||
if (safe_dup2(slave, 1) == -1) exit_on_err("dup2() failed for fd number 1");
|
||||
if (safe_dup2(slave, 2) == -1) exit_on_err("dup2() failed for fd number 2");
|
||||
if (stdin_read_fd > -1) {
|
||||
if (dup2(stdin_read_fd, 0) == -1) exit_on_err("dup2() failed for fd number 0");
|
||||
if (safe_dup2(stdin_read_fd, 0) == -1) exit_on_err("dup2() failed for fd number 0");
|
||||
safe_close(stdin_read_fd, __FILE__, __LINE__);
|
||||
safe_close(stdin_write_fd, __FILE__, __LINE__);
|
||||
} else {
|
||||
if (dup2(slave, 0) == -1) exit_on_err("dup2() failed for fd number 0");
|
||||
if (safe_dup2(slave, 0) == -1) exit_on_err("dup2() failed for fd number 0");
|
||||
}
|
||||
safe_close(slave, __FILE__, __LINE__);
|
||||
safe_close(master, __FILE__, __LINE__);
|
||||
@@ -180,33 +184,36 @@ spawn(PyObject *self UNUSED, PyObject *args) {
|
||||
return PyLong_FromLong(pid);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
establish_controlling_tty(PyObject *self UNUSED, PyObject *args) {
|
||||
const char *ttyname;
|
||||
int stdin_fd, stdout_fd, stderr_fd;
|
||||
if (!PyArg_ParseTuple(args, "siii", &ttyname, &stdin_fd, &stdout_fd, &stderr_fd)) return NULL;
|
||||
int tfd = safe_open(ttyname, O_RDWR, 0);
|
||||
if (tfd == -1) return PyErr_SetFromErrnoWithFilename(PyExc_OSError, ttyname);
|
||||
#ifdef TIOCSCTTY
|
||||
// On BSD open() does not establish the controlling terminal
|
||||
if (ioctl(tfd, TIOCSCTTY, 0) == -1) return PyErr_SetFromErrno(PyExc_OSError);
|
||||
#ifdef __APPLE__
|
||||
#include <crt_externs.h>
|
||||
#else
|
||||
extern char **environ;
|
||||
#endif
|
||||
|
||||
static PyObject*
|
||||
clearenv_py(PyObject *self UNUSED, PyObject *args UNUSED) {
|
||||
#ifdef __APPLE__
|
||||
char **e = *_NSGetEnviron();
|
||||
if (e) *e = NULL;
|
||||
#else
|
||||
if (environ) *environ = NULL;
|
||||
#endif
|
||||
if (dup2(tfd, stdin_fd) == -1) return PyErr_SetFromErrno(PyExc_OSError);
|
||||
if (dup2(tfd, stdout_fd) == -1) return PyErr_SetFromErrno(PyExc_OSError);
|
||||
if (dup2(tfd, stderr_fd) == -1) return PyErr_SetFromErrno(PyExc_OSError);
|
||||
safe_close(tfd, __FILE__, __LINE__);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
METHODB(spawn, METH_VARARGS),
|
||||
METHODB(establish_controlling_tty, METH_VARARGS),
|
||||
{"clearenv", clearenv_py, METH_NOARGS, ""},
|
||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
|
||||
bool
|
||||
init_child(PyObject *module) {
|
||||
PyModule_AddIntMacro(module, CLD_KILLED);
|
||||
PyModule_AddIntMacro(module, CLD_STOPPED);
|
||||
PyModule_AddIntMacro(module, CLD_EXITED);
|
||||
PyModule_AddIntMacro(module, CLD_CONTINUED);
|
||||
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
131
kitty/child.py
131
kitty/child.py
@@ -13,7 +13,8 @@ from typing import (
|
||||
import kitty.fast_data_types as fast_data_types
|
||||
|
||||
from .constants import (
|
||||
is_freebsd, is_macos, kitty_base_dir, shell_path, terminfo_dir
|
||||
handled_signals, is_freebsd, is_macos, kitty_base_dir, shell_path,
|
||||
terminfo_dir
|
||||
)
|
||||
from .types import run_once
|
||||
from .utils import log_error, which
|
||||
@@ -142,19 +143,12 @@ def environ_of_process(pid: int) -> Dict[str, str]:
|
||||
return parse_environ_block(_environ_of_process(pid))
|
||||
|
||||
|
||||
def remove_cloexec(fd: int) -> None:
|
||||
os.set_inheritable(fd, True)
|
||||
|
||||
|
||||
def remove_blocking(fd: int) -> None:
|
||||
os.set_blocking(fd, False)
|
||||
|
||||
|
||||
def process_env() -> Dict[str, str]:
|
||||
ans = dict(os.environ)
|
||||
ssl_env_var = getattr(sys, 'kitty_ssl_env_var', None)
|
||||
if ssl_env_var is not None:
|
||||
ans.pop(ssl_env_var, None)
|
||||
ans.pop('XDG_ACTIVATION_TOKEN', None)
|
||||
return ans
|
||||
|
||||
|
||||
@@ -175,9 +169,14 @@ def set_default_env(val: Optional[Dict[str, str]] = None) -> None:
|
||||
setattr(default_env, 'lc_ctype_set_by_user', has_lctype)
|
||||
|
||||
|
||||
def set_LANG_in_default_env(val: str) -> None:
|
||||
default_env().setdefault('LANG', val)
|
||||
|
||||
|
||||
def openpty() -> Tuple[int, int]:
|
||||
master, slave = os.openpty() # Note that master and slave are in blocking mode
|
||||
remove_cloexec(slave)
|
||||
os.set_inheritable(slave, True)
|
||||
os.set_inheritable(master, False)
|
||||
fast_data_types.set_iutf8_fd(master, True)
|
||||
return master, slave
|
||||
|
||||
@@ -193,11 +192,33 @@ class ProcessDesc(TypedDict):
|
||||
cmdline: Optional[Sequence[str]]
|
||||
|
||||
|
||||
def is_prewarmable(argv: Sequence[str]) -> bool:
|
||||
if len(argv) < 3 or os.path.basename(argv[0]) != 'kitty':
|
||||
return False
|
||||
if argv[1][:1] not in '@+':
|
||||
return False
|
||||
if argv[1][0] == '@':
|
||||
return True
|
||||
if argv[1] == '+':
|
||||
return argv[2] != 'open'
|
||||
return argv[1] != '+open'
|
||||
|
||||
|
||||
@run_once
|
||||
def cmdline_of_prewarmer() -> List[str]:
|
||||
# we need this check in case the prewarmed process has done an exec and
|
||||
# changed its cmdline
|
||||
with suppress(Exception):
|
||||
return cmdline_of_pid(fast_data_types.get_boss().prewarm.worker_pid)
|
||||
return ['']
|
||||
|
||||
|
||||
class Child:
|
||||
|
||||
child_fd: Optional[int] = None
|
||||
pid: Optional[int] = None
|
||||
forked = False
|
||||
is_prewarmed = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -206,11 +227,11 @@ class Child:
|
||||
stdin: Optional[bytes] = None,
|
||||
env: Optional[Dict[str, str]] = None,
|
||||
cwd_from: Optional['CwdRequest'] = None,
|
||||
allow_remote_control: bool = False,
|
||||
is_clone_launch: str = '',
|
||||
add_listen_on_env_var: bool = True,
|
||||
):
|
||||
self.allow_remote_control = allow_remote_control
|
||||
self.is_clone_launch = is_clone_launch
|
||||
self.add_listen_on_env_var = add_listen_on_env_var
|
||||
self.argv = list(argv)
|
||||
if cwd_from:
|
||||
try:
|
||||
@@ -226,13 +247,19 @@ class Child:
|
||||
def final_env(self) -> Dict[str, str]:
|
||||
from kitty.options.utils import DELETE_ENV_VAR
|
||||
env = default_env().copy()
|
||||
if is_macos and env.get('LC_CTYPE') == 'UTF-8' and not sys._xoptions.get(
|
||||
boss = fast_data_types.get_boss()
|
||||
if is_macos and env.get('LC_CTYPE') == 'UTF-8' and not getattr(sys, 'kitty_run_data').get(
|
||||
'lc_ctype_before_python') and not getattr(default_env, 'lc_ctype_set_by_user', False):
|
||||
del env['LC_CTYPE']
|
||||
env.update(self.env)
|
||||
env['TERM'] = fast_data_types.get_options().term
|
||||
env['COLORTERM'] = 'truecolor'
|
||||
env['KITTY_PID'] = getpid()
|
||||
env['KITTY_PUBLIC_KEY'] = boss.encryption_public_key
|
||||
if self.add_listen_on_env_var and boss.listening_on:
|
||||
env['KITTY_LISTEN_ON'] = boss.listening_on
|
||||
else:
|
||||
env.pop('KITTY_LISTEN_ON', None)
|
||||
if self.cwd:
|
||||
# needed in case cwd is a symlink, in which case shells
|
||||
# can use it to display the current directory name rather
|
||||
@@ -261,14 +288,18 @@ class Child:
|
||||
self.forked = True
|
||||
master, slave = openpty()
|
||||
stdin, self.stdin = self.stdin, None
|
||||
ready_read_fd, ready_write_fd = os.pipe()
|
||||
remove_cloexec(ready_read_fd)
|
||||
if stdin is not None:
|
||||
stdin_read_fd, stdin_write_fd = os.pipe()
|
||||
remove_cloexec(stdin_read_fd)
|
||||
else:
|
||||
stdin_read_fd = stdin_write_fd = -1
|
||||
env = tuple(f'{k}={v}' for k, v in self.final_env().items())
|
||||
self.is_prewarmed = is_prewarmable(self.argv)
|
||||
if not self.is_prewarmed:
|
||||
ready_read_fd, ready_write_fd = os.pipe()
|
||||
os.set_inheritable(ready_write_fd, False)
|
||||
os.set_inheritable(ready_read_fd, True)
|
||||
if stdin is not None:
|
||||
stdin_read_fd, stdin_write_fd = os.pipe()
|
||||
os.set_inheritable(stdin_write_fd, False)
|
||||
os.set_inheritable(stdin_read_fd, True)
|
||||
else:
|
||||
stdin_read_fd = stdin_write_fd = -1
|
||||
env = tuple(f'{k}={v}' for k, v in self.final_env().items())
|
||||
argv = list(self.argv)
|
||||
exe = argv[0]
|
||||
if is_macos and exe == shell_path:
|
||||
@@ -289,29 +320,46 @@ class Child:
|
||||
argv[0] = (f'-{exe.split("/")[-1]}')
|
||||
self.final_exe = which(exe) or exe
|
||||
self.final_argv0 = argv[0]
|
||||
pid = fast_data_types.spawn(self.final_exe, self.cwd, tuple(argv), env, master, slave, stdin_read_fd, stdin_write_fd, ready_read_fd, ready_write_fd)
|
||||
if self.is_prewarmed:
|
||||
fe = self.final_env()
|
||||
self.prewarmed_child = fast_data_types.get_boss().prewarm(slave, self.argv, self.cwd, fe, stdin)
|
||||
pid = self.prewarmed_child.child_process_pid
|
||||
else:
|
||||
pid = fast_data_types.spawn(
|
||||
self.final_exe, self.cwd, tuple(argv), env, master, slave, stdin_read_fd, stdin_write_fd,
|
||||
ready_read_fd, ready_write_fd, tuple(handled_signals))
|
||||
os.close(slave)
|
||||
self.pid = pid
|
||||
self.child_fd = master
|
||||
if stdin is not None:
|
||||
os.close(stdin_read_fd)
|
||||
fast_data_types.thread_write(stdin_write_fd, stdin)
|
||||
os.close(ready_read_fd)
|
||||
self.terminal_ready_fd = ready_write_fd
|
||||
if not self.is_prewarmed:
|
||||
if stdin is not None:
|
||||
os.close(stdin_read_fd)
|
||||
fast_data_types.thread_write(stdin_write_fd, stdin)
|
||||
os.close(ready_read_fd)
|
||||
self.terminal_ready_fd = ready_write_fd
|
||||
if self.child_fd is not None:
|
||||
remove_blocking(self.child_fd)
|
||||
os.set_blocking(self.child_fd, False)
|
||||
return pid
|
||||
|
||||
def mark_terminal_ready(self) -> None:
|
||||
os.close(self.terminal_ready_fd)
|
||||
def __del__(self) -> None:
|
||||
fd = getattr(self, 'terminal_ready_fd', -1)
|
||||
if fd > -1:
|
||||
os.close(fd)
|
||||
self.terminal_ready_fd = -1
|
||||
|
||||
def mark_terminal_ready(self) -> None:
|
||||
if self.is_prewarmed:
|
||||
fast_data_types.get_boss().prewarm.mark_child_as_ready(self.prewarmed_child.child_id)
|
||||
else:
|
||||
os.close(self.terminal_ready_fd)
|
||||
self.terminal_ready_fd = -1
|
||||
|
||||
def cmdline_of_pid(self, pid: int) -> List[str]:
|
||||
try:
|
||||
ans = cmdline_of_pid(pid)
|
||||
except Exception:
|
||||
ans = []
|
||||
if not ans and pid == self.pid:
|
||||
if pid == self.pid and (not ans or (self.is_prewarmed and ans == cmdline_of_prewarmer())):
|
||||
ans = list(self.argv)
|
||||
return ans
|
||||
|
||||
@@ -411,3 +459,24 @@ class Child:
|
||||
with suppress(Exception):
|
||||
return environ_of_process(pid)
|
||||
return {}
|
||||
|
||||
def send_signal_for_key(self, key_num: int) -> bool:
|
||||
import signal
|
||||
import termios
|
||||
if self.child_fd is None:
|
||||
return False
|
||||
t = termios.tcgetattr(self.child_fd)
|
||||
if not t[3] & termios.ISIG:
|
||||
return False
|
||||
cc = t[-1]
|
||||
if key_num == cc[termios.VINTR]:
|
||||
s = signal.SIGINT
|
||||
elif key_num == cc[termios.VSUSP]:
|
||||
s = signal.SIGTSTP
|
||||
elif key_num == cc[termios.VQUIT]:
|
||||
s = signal.SIGQUIT
|
||||
else:
|
||||
return False
|
||||
pgrp = os.tcgetpgrp(self.child_fd)
|
||||
os.killpg(pgrp, s)
|
||||
return True
|
||||
|
||||
233
kitty/cli.py
233
kitty/cli.py
@@ -2,6 +2,7 @@
|
||||
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
from collections import deque
|
||||
from typing import (
|
||||
@@ -11,8 +12,12 @@ from typing import (
|
||||
|
||||
from .cli_stub import CLIOptions
|
||||
from .conf.utils import resolve_config
|
||||
from .constants import appname, defconf, is_macos, str_version, website_url
|
||||
from .constants import (
|
||||
appname, clear_handled_signals, defconf, is_macos, str_version, website_url
|
||||
)
|
||||
from .fast_data_types import wcswidth
|
||||
from .options.types import Options as KittyOpts
|
||||
from .types import run_once
|
||||
from .typing import BadLineType, TypedDict
|
||||
|
||||
|
||||
@@ -24,6 +29,7 @@ class OptionDict(TypedDict):
|
||||
type: str
|
||||
default: Optional[str]
|
||||
condition: bool
|
||||
completion: Dict[str, str]
|
||||
|
||||
|
||||
CONFIG_HELP = '''\
|
||||
@@ -118,8 +124,7 @@ def option(x: str) -> str:
|
||||
idx = x.find('-')
|
||||
if idx > -1:
|
||||
x = x[idx:]
|
||||
parts = map(bold, x.split())
|
||||
return ' '.join(parts)
|
||||
return bold(x.rstrip('>'))
|
||||
|
||||
|
||||
@role
|
||||
@@ -127,6 +132,16 @@ def code(x: str) -> str:
|
||||
return cyan(x)
|
||||
|
||||
|
||||
def text_and_target(x: str) -> Tuple[str, str]:
|
||||
parts = x.split('<', 1)
|
||||
return parts[0].strip(), parts[-1].rstrip('>')
|
||||
|
||||
|
||||
@role
|
||||
def term(x: str) -> str:
|
||||
return ref_hyperlink(x, 'term-')
|
||||
|
||||
|
||||
@role
|
||||
def kbd(x: str) -> str:
|
||||
return x
|
||||
@@ -134,12 +149,18 @@ def kbd(x: str) -> str:
|
||||
|
||||
@role
|
||||
def env(x: str) -> str:
|
||||
return italic(x)
|
||||
return ref_hyperlink(x, 'envvar-')
|
||||
|
||||
|
||||
role_map['envvar'] = role_map['env']
|
||||
|
||||
|
||||
def hyperlink_for_url(url: str, text: str) -> str:
|
||||
if sys.stdout.isatty():
|
||||
return f'\x1b]8;;{url}\x1b\\\x1b[4:3;58:5:4m{text}\x1b[4:0;59m\x1b]8;;\x1b\\'
|
||||
return text
|
||||
|
||||
|
||||
@role
|
||||
def file(x: str) -> str:
|
||||
return italic(x)
|
||||
@@ -147,12 +168,52 @@ def file(x: str) -> str:
|
||||
|
||||
@role
|
||||
def doc(x: str) -> str:
|
||||
return website_url(x)
|
||||
t, q = text_and_target(x)
|
||||
if t == q:
|
||||
from .conf.types import ref_map
|
||||
m = ref_map()['doc']
|
||||
q = q.strip('/')
|
||||
if q in m:
|
||||
x = f'{m[q]} <{t}>'
|
||||
return ref_hyperlink(x, 'doc-')
|
||||
|
||||
|
||||
@run_once
|
||||
def hostname() -> str:
|
||||
import socket
|
||||
return socket.gethostname() or 'localhost'
|
||||
|
||||
|
||||
def ref_hyperlink(x: str, prefix: str = '') -> str:
|
||||
t, q = text_and_target(x)
|
||||
url = f'kitty+doc://{hostname()}/#ref={prefix}{q}'
|
||||
t = re.sub(r':([a-z]+):`([^`]+)`', r'\2', t)
|
||||
return hyperlink_for_url(url, t)
|
||||
|
||||
|
||||
@role
|
||||
def ref(x: str) -> str:
|
||||
return re.sub(r'\s*<\S+?>', '', x)
|
||||
return ref_hyperlink(x)
|
||||
|
||||
|
||||
@role
|
||||
def ac(x: str) -> str:
|
||||
return ref_hyperlink(x, 'action-')
|
||||
|
||||
|
||||
@role
|
||||
def iss(x: str) -> str:
|
||||
return ref_hyperlink(x, 'issues-')
|
||||
|
||||
|
||||
@role
|
||||
def pull(x: str) -> str:
|
||||
return ref_hyperlink(x, 'pull-')
|
||||
|
||||
|
||||
@role
|
||||
def disc(x: str) -> str:
|
||||
return ref_hyperlink(x, 'discussions-')
|
||||
|
||||
|
||||
OptionSpecSeq = List[Union[str, OptionDict]]
|
||||
@@ -165,15 +226,19 @@ def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, Option
|
||||
state = NORMAL
|
||||
lines = spec.splitlines()
|
||||
prev_line = ''
|
||||
prev_indent = 0
|
||||
seq: OptionSpecSeq = []
|
||||
disabled: OptionSpecSeq = []
|
||||
mpat = re.compile('([a-z]+)=(.+)')
|
||||
current_cmd: OptionDict = {
|
||||
'dest': '', 'aliases': frozenset(), 'help': '', 'choices': frozenset(),
|
||||
'type': '', 'condition': False, 'default': None
|
||||
'type': '', 'condition': False, 'default': None, 'completion': {},
|
||||
}
|
||||
empty_cmd = current_cmd
|
||||
|
||||
def indent_of_line(x: str) -> int:
|
||||
return len(x) - len(x.lstrip())
|
||||
|
||||
for line in lines:
|
||||
line = line.rstrip()
|
||||
if state is NORMAL:
|
||||
@@ -187,7 +252,7 @@ def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, Option
|
||||
current_cmd = {
|
||||
'dest': parts[0][2:].replace('-', '_'), 'aliases': frozenset(parts), 'help': '',
|
||||
'choices': frozenset(), 'type': '',
|
||||
'default': None, 'condition': True
|
||||
'default': None, 'condition': True, 'completion': {}
|
||||
}
|
||||
state = METADATA
|
||||
continue
|
||||
@@ -210,13 +275,26 @@ def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, Option
|
||||
current_cmd['dest'] = v
|
||||
elif k == 'condition':
|
||||
current_cmd['condition'] = bool(eval(v))
|
||||
elif k == 'completion':
|
||||
cv = current_cmd['completion'] = {}
|
||||
for x in shlex.split(v):
|
||||
ck, vv = x.split(':', 1)
|
||||
cv[ck] = vv
|
||||
elif state is HELP:
|
||||
if line:
|
||||
current_indent = indent_of_line(line)
|
||||
if current_indent > 1:
|
||||
if prev_indent == 0:
|
||||
current_cmd['help'] += '\n'
|
||||
else:
|
||||
line = line.strip()
|
||||
prev_indent = current_indent
|
||||
spc = '' if current_cmd['help'].endswith('\n') else ' '
|
||||
current_cmd['help'] += spc + line
|
||||
else:
|
||||
prev_indent = 0
|
||||
if prev_line:
|
||||
current_cmd['help'] += '\n\n'
|
||||
current_cmd['help'] += '\n' if current_cmd['help'].endswith('::') else '\n\n'
|
||||
else:
|
||||
state = NORMAL
|
||||
(seq if current_cmd.get('condition', True) else disabled).append(current_cmd)
|
||||
@@ -254,36 +332,56 @@ def version(add_rev: bool = False) -> str:
|
||||
|
||||
|
||||
def wrap(text: str, limit: int = 80) -> Iterator[str]:
|
||||
NORMAL, IN_FORMAT = 'NORMAL', 'IN_FORMAT'
|
||||
state = NORMAL
|
||||
last_space_at = None
|
||||
chars_in_line = 0
|
||||
breaks = []
|
||||
for i, ch in enumerate(text):
|
||||
if state is IN_FORMAT:
|
||||
if ch == 'm':
|
||||
state = NORMAL
|
||||
continue
|
||||
if ch == '\033':
|
||||
state = IN_FORMAT
|
||||
continue
|
||||
if ch == ' ':
|
||||
last_space_at = i
|
||||
if chars_in_line < limit:
|
||||
chars_in_line += 1
|
||||
continue
|
||||
if last_space_at is not None:
|
||||
breaks.append(last_space_at)
|
||||
last_space_at = None
|
||||
chars_in_line = i - breaks[-1]
|
||||
if not text.strip():
|
||||
yield ''
|
||||
return
|
||||
in_escape = 0
|
||||
current_line: List[str] = []
|
||||
escapes: List[str] = []
|
||||
current_word: List[str] = []
|
||||
current_line_length = 0
|
||||
|
||||
lines: List[str] = []
|
||||
for b in reversed(breaks):
|
||||
lines.append(text[b:].lstrip())
|
||||
text = text[:b]
|
||||
if text:
|
||||
lines.append(text)
|
||||
return reversed(lines)
|
||||
def print_word(ch: str = '') -> Iterator[str]:
|
||||
nonlocal current_word, current_line, escapes, current_line_length
|
||||
cw = ''.join(current_word)
|
||||
w = wcswidth(cw)
|
||||
if current_line_length + w > limit:
|
||||
yield ''.join(current_line)
|
||||
current_line = []
|
||||
current_line_length = 0
|
||||
cw = cw.strip()
|
||||
current_word = [cw]
|
||||
if escapes:
|
||||
current_line.append(''.join(escapes))
|
||||
escapes = []
|
||||
if current_word:
|
||||
current_line.append(cw)
|
||||
current_line_length += w
|
||||
current_word = []
|
||||
if ch:
|
||||
current_word.append(ch)
|
||||
|
||||
for i, ch in enumerate(text):
|
||||
if in_escape > 0:
|
||||
if in_escape == 1 and ch in '[]':
|
||||
in_escape = 2 if ch == '[' else 3
|
||||
if (in_escape == 2 and ch == 'm') or (in_escape == 3 and ch == '\\' and text[i-1] == '\x1b'):
|
||||
in_escape = 0
|
||||
escapes.append(ch)
|
||||
continue
|
||||
if ch == '\x1b':
|
||||
in_escape = 1
|
||||
if current_word:
|
||||
yield from print_word()
|
||||
escapes.append(ch)
|
||||
continue
|
||||
if current_word and ch.isspace() and ch != '\xa0':
|
||||
yield from print_word(ch)
|
||||
else:
|
||||
current_word.append(ch)
|
||||
yield from print_word()
|
||||
if current_line:
|
||||
yield ''.join(current_line)
|
||||
|
||||
|
||||
def get_defaults_from_seq(seq: OptionSpecSeq) -> Dict[str, Any]:
|
||||
@@ -324,10 +422,7 @@ class PrintHelpForSeq:
|
||||
j = '\n' + (' ' * indent)
|
||||
lines: List[str] = []
|
||||
for ln in text.splitlines():
|
||||
if ln:
|
||||
lines.extend(wrap(ln, limit=linesz - indent))
|
||||
else:
|
||||
lines.append('')
|
||||
lines.extend(wrap(ln, limit=linesz - indent))
|
||||
a((' ' * leading_indent) + j.join(lines))
|
||||
|
||||
usage = '[program-to-run ...]' if usage is None else usage
|
||||
@@ -335,6 +430,8 @@ class PrintHelpForSeq:
|
||||
a('{}: {} {}{}'.format(title('Usage'), bold(yellow(appname)), optstring, usage))
|
||||
a('')
|
||||
message = message or default_msg
|
||||
# replace rst literal code block syntax
|
||||
message = message.replace('::\n\n', ':\n\n')
|
||||
wa(prettify(message))
|
||||
a('')
|
||||
if seq:
|
||||
@@ -346,15 +443,18 @@ class PrintHelpForSeq:
|
||||
help_text = opt['help']
|
||||
if help_text == '!':
|
||||
continue # hidden option
|
||||
a(' ' + ', '.join(map(green, sorted(opt['aliases']))))
|
||||
a(' ' + ', '.join(map(green, sorted(opt['aliases'], reverse=True))))
|
||||
defval = opt.get('default')
|
||||
if not opt.get('type', '').startswith('bool-'):
|
||||
blocks[-1] += '={}'.format(italic(opt['dest'].upper()))
|
||||
if defval:
|
||||
dt = '=[{}]'.format(italic(defval))
|
||||
blocks[-1] += dt
|
||||
if opt.get('help'):
|
||||
defval = opt.get('default')
|
||||
t = help_text.replace('%default', str(defval))
|
||||
wa(prettify(t.strip()), indent=4)
|
||||
if defval is not None:
|
||||
wa(f'Default: {defval}', indent=4)
|
||||
t = help_text.replace('%default', str(defval)).strip()
|
||||
# replace rst literal code block syntax
|
||||
t = t.replace('::\n\n', ':\n\n')
|
||||
t = t.replace('#placeholder_for_formatting#', '')
|
||||
wa(prettify(t), indent=4)
|
||||
if opt.get('choices'):
|
||||
wa('Choices: {}'.format(', '.join(opt['choices'])), indent=4)
|
||||
a('')
|
||||
@@ -362,7 +462,7 @@ class PrintHelpForSeq:
|
||||
text = '\n'.join(blocks) + '\n\n' + version()
|
||||
if print_help_for_seq.allow_pager and sys.stdout.isatty():
|
||||
import subprocess
|
||||
p = subprocess.Popen(['less', '-isRXF'], stdin=subprocess.PIPE)
|
||||
p = subprocess.Popen(['less', '-isRXF'], stdin=subprocess.PIPE, preexec_fn=clear_handled_signals)
|
||||
try:
|
||||
p.communicate(text.encode('utf-8'))
|
||||
except KeyboardInterrupt:
|
||||
@@ -416,6 +516,7 @@ def seq_as_rst(
|
||||
if opt.get('help'):
|
||||
defval = opt.get('default')
|
||||
t = help_text.replace('%default', str(defval)).strip()
|
||||
t = t.replace('#placeholder_for_formatting#', '')
|
||||
a('')
|
||||
a(textwrap.indent(prettify_rst(t), ' ' * 4))
|
||||
if defval is not None:
|
||||
@@ -603,6 +704,7 @@ are running a program that does not set titles.
|
||||
|
||||
--config -c
|
||||
type=list
|
||||
completion=type:file ext:conf group:"Config files" kwds:none,NONE
|
||||
{config_help}
|
||||
|
||||
|
||||
@@ -614,6 +716,7 @@ Syntax: :italic:`name=value`. For example: :option:`{appname} -o` font_size=20
|
||||
|
||||
--directory --working-directory -d
|
||||
default=.
|
||||
completion=type:directory
|
||||
Change to the specified directory when launching.
|
||||
|
||||
|
||||
@@ -624,9 +727,11 @@ Detach from the controlling terminal, if any.
|
||||
|
||||
|
||||
--session
|
||||
completion=type:file ext:session relative:conf group:"Session files"
|
||||
Path to a file containing the startup :italic:`session` (tabs, windows, layout,
|
||||
programs). Use - to read from STDIN. See the :file:`README` file for details and
|
||||
an example.
|
||||
an example. Environment variables are expanded, relative paths are resolved relative
|
||||
to the kitty configuration directory.
|
||||
|
||||
|
||||
--hold
|
||||
@@ -664,19 +769,18 @@ regardless of this option.
|
||||
|
||||
--listen-on
|
||||
Listen on the specified socket address for control messages. For example,
|
||||
:option:`{appname} --listen-on`=unix:/tmp/mykitty or
|
||||
:option:`{appname} --listen-on`=tcp:localhost:12345. On Linux systems, you can
|
||||
also use abstract UNIX sockets, not associated with a file, like this:
|
||||
:option:`{appname} --listen-on`=unix:@mykitty. Environment variables are
|
||||
expanded and relative paths are resolved with respect to the temporary
|
||||
directory. To control kitty, you can send commands to it with
|
||||
:italic:`{appname} @` using the :option:`{appname} @ --to` option to specify
|
||||
this address. Unless you enabled :opt:`allow_remote_control` in
|
||||
:file:`{conf_name}.conf`, this option will be ignored. Note that if you run
|
||||
:italic:`{appname} @` within a kitty window, there is
|
||||
no need to specify the :option:`{appname} @ --to` option as it will
|
||||
automatically read from the environment. For UNIX sockets, this can also be
|
||||
specified in :file:`{conf_name}.conf`.
|
||||
:option:`{appname} --listen-on`=unix:/tmp/mykitty or :option:`{appname}
|
||||
--listen-on`=tcp:localhost:12345. On Linux systems, you can also use abstract
|
||||
UNIX sockets, not associated with a file, like this: :option:`{appname}
|
||||
--listen-on`=unix:@mykitty. Environment variables are expanded and relative
|
||||
paths are resolved with respect to the temporary directory. To control kitty,
|
||||
you can send commands to it with :italic:`{appname} @` using the
|
||||
:option:`{appname} @ --to` option to specify this address. Note that if you run
|
||||
:italic:`{appname} @` within a kitty window, there is no need to specify the
|
||||
:option:`{appname} @ --to` option as it will automatically read from the
|
||||
environment. Note that this will be ignored unless :opt:`allow_remote_control`
|
||||
is set to either: :code:`yes`, :code:`socket` or :code:`socket-only`. For UNIX
|
||||
sockets, this can also be specified in :file:`{conf_name}.conf`.
|
||||
|
||||
|
||||
--start-as
|
||||
@@ -730,6 +834,7 @@ present in the main font.
|
||||
|
||||
|
||||
--watcher
|
||||
completion=type:file ext:py relative:conf group:"Watcher files"
|
||||
This option is deprecated in favor of the :opt:`watcher` option in
|
||||
:file:`{conf_name}.conf` and should not be used.
|
||||
|
||||
|
||||
121
kitty/clipboard.py
Normal file
121
kitty/clipboard.py
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import io
|
||||
from typing import IO, Callable, Dict, List, Tuple, Union
|
||||
|
||||
from .conf.utils import uniq
|
||||
from .constants import supports_primary_selection
|
||||
from .fast_data_types import (
|
||||
GLFW_CLIPBOARD, GLFW_PRIMARY_SELECTION, get_boss, get_clipboard_mime,
|
||||
set_clipboard_data_types
|
||||
)
|
||||
|
||||
DataType = Union[bytes, 'IO[bytes]']
|
||||
|
||||
|
||||
class Clipboard:
|
||||
|
||||
def __init__(self, clipboard_type: int = GLFW_CLIPBOARD) -> None:
|
||||
self.data: Dict[str, DataType] = {}
|
||||
self.clipboard_type = clipboard_type
|
||||
self.enabled = self.clipboard_type == GLFW_CLIPBOARD or supports_primary_selection
|
||||
|
||||
def set_text(self, x: Union[str, bytes]) -> None:
|
||||
if isinstance(x, str):
|
||||
x = x.encode('utf-8')
|
||||
self.set_mime({'text/plain': x})
|
||||
|
||||
def set_mime(self, data: Dict[str, DataType]) -> None:
|
||||
if self.enabled and isinstance(data, dict):
|
||||
self.data = data
|
||||
set_clipboard_data_types(self.clipboard_type, tuple(self.data))
|
||||
|
||||
def get_text(self) -> str:
|
||||
parts: List[bytes] = []
|
||||
self.get_mime("text/plain", parts.append)
|
||||
return b''.join(parts).decode('utf-8', 'replace')
|
||||
|
||||
def get_mime(self, mime: str, output: Callable[[bytes], None]) -> None:
|
||||
if self.enabled:
|
||||
try:
|
||||
get_clipboard_mime(self.clipboard_type, mime, output)
|
||||
except RuntimeError as err:
|
||||
if str(err) != 'is_self_offer':
|
||||
raise
|
||||
data = self.data.get(mime, b'')
|
||||
if isinstance(data, bytes):
|
||||
output(data)
|
||||
else:
|
||||
data.seek(0, 0)
|
||||
q = b' '
|
||||
while q:
|
||||
q = data.read(io.DEFAULT_BUFFER_SIZE)
|
||||
output(q)
|
||||
|
||||
def get_mime_data(self, mime: str) -> bytes:
|
||||
ans: List[bytes] = []
|
||||
self.get_mime(mime, ans.append)
|
||||
return b''.join(ans)
|
||||
|
||||
def get_available_mime_types_for_paste(self) -> Tuple[str, ...]:
|
||||
if self.enabled:
|
||||
parts: List[bytes] = []
|
||||
try:
|
||||
get_clipboard_mime(self.clipboard_type, None, parts.append)
|
||||
except RuntimeError as err:
|
||||
if str(err) != 'is_self_offer':
|
||||
raise
|
||||
return tuple(self.data)
|
||||
return tuple(x.decode('utf-8', 'replace') for x in uniq(parts))
|
||||
return ()
|
||||
|
||||
def __call__(self, mime: str) -> Callable[[], bytes]:
|
||||
data = self.data.get(mime, b'')
|
||||
if isinstance(data, str): # type: ignore
|
||||
data = data.encode('utf-8') # type: ignore
|
||||
if isinstance(data, bytes):
|
||||
def chunker() -> bytes:
|
||||
nonlocal data
|
||||
assert isinstance(data, bytes)
|
||||
ans = data
|
||||
data = b''
|
||||
return ans
|
||||
return chunker
|
||||
|
||||
data.seek(0, 0)
|
||||
|
||||
def io_chunker() -> bytes:
|
||||
assert not isinstance(data, bytes)
|
||||
return data.read(io.DEFAULT_BUFFER_SIZE)
|
||||
return io_chunker
|
||||
|
||||
|
||||
def set_clipboard_string(x: Union[str, bytes]) -> None:
|
||||
get_boss().clipboard.set_text(x)
|
||||
|
||||
|
||||
def get_clipboard_string() -> str:
|
||||
return get_boss().clipboard.get_text()
|
||||
|
||||
|
||||
def set_primary_selection(x: Union[str, bytes]) -> None:
|
||||
get_boss().primary_selection.set_text(x)
|
||||
|
||||
|
||||
def get_primary_selection() -> str:
|
||||
return get_boss().primary_selection.get_text()
|
||||
|
||||
|
||||
def develop() -> Tuple[Clipboard, Clipboard]:
|
||||
from .constants import is_macos, detect_if_wayland_ok
|
||||
from .fast_data_types import set_boss
|
||||
from .main import init_glfw_module
|
||||
glfw_module = 'cocoa' if is_macos else ('wayland' if detect_if_wayland_ok() else 'x11')
|
||||
|
||||
class Boss:
|
||||
clipboard = Clipboard()
|
||||
primary_selection = Clipboard(GLFW_PRIMARY_SELECTION)
|
||||
init_glfw_module(glfw_module)
|
||||
set_boss(Boss()) # type: ignore
|
||||
return Boss.clipboard, Boss.primary_selection
|
||||
@@ -862,6 +862,65 @@ cocoa_set_url_handler(PyObject UNUSED *self, PyObject *args) {
|
||||
} // autoreleasepool
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
cocoa_set_app_icon(PyObject UNUSED *self, PyObject *args) {
|
||||
@autoreleasepool {
|
||||
|
||||
const char *icon_path = NULL, *app_path = NULL;
|
||||
if (!PyArg_ParseTuple(args, "s|z", &icon_path, &app_path)) return NULL;
|
||||
if (!icon_path || icon_path[0] == '\0') {
|
||||
PyErr_SetString(PyExc_TypeError, "Empty icon file path");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
NSString *custom_icon_path = [NSString stringWithUTF8String:icon_path];
|
||||
NSString *bundle_path = @"";
|
||||
if (!app_path) {
|
||||
bundle_path = [[NSBundle mainBundle] bundlePath];
|
||||
if (!bundle_path || bundle_path.length == 0) bundle_path = @"/Applications/kitty.app";
|
||||
} else if (app_path[0] != '\0') {
|
||||
bundle_path = [NSString stringWithUTF8String:app_path];
|
||||
}
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:custom_icon_path]) {
|
||||
PyErr_Format(PyExc_FileNotFoundError, "Icon file not found: %s", [custom_icon_path UTF8String]);
|
||||
return NULL;
|
||||
}
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:bundle_path]) {
|
||||
PyErr_Format(PyExc_FileNotFoundError, "Application bundle not found: %s", [bundle_path UTF8String]);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
NSImage *icon_image = [[NSImage alloc] initWithContentsOfFile:custom_icon_path];
|
||||
BOOL result = [[NSWorkspace sharedWorkspace] setIcon:icon_image forFile:bundle_path options:NSExcludeQuickDrawElementsIconCreationOption];
|
||||
[icon_image release];
|
||||
if (result) Py_RETURN_NONE;
|
||||
PyErr_Format(PyExc_OSError, "Failed to set custom icon %s for %s", [custom_icon_path UTF8String], [bundle_path UTF8String]);
|
||||
return NULL;
|
||||
|
||||
} // autoreleasepool
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
cocoa_set_dock_icon(PyObject UNUSED *self, PyObject *args) {
|
||||
@autoreleasepool {
|
||||
|
||||
const char *icon_path = NULL;
|
||||
if (!PyArg_ParseTuple(args, "s", &icon_path)) return NULL;
|
||||
if (!icon_path || icon_path[0] == '\0') {
|
||||
PyErr_SetString(PyExc_TypeError, "Empty icon file path");
|
||||
return NULL;
|
||||
}
|
||||
NSString *custom_icon_path = [NSString stringWithUTF8String:icon_path];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:custom_icon_path]) {
|
||||
NSImage *icon_image = [[[NSImage alloc] initWithContentsOfFile:custom_icon_path] autorelease];
|
||||
[NSApplication sharedApplication].applicationIconImage = icon_image;
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
return NULL;
|
||||
|
||||
} // autoreleasepool
|
||||
}
|
||||
|
||||
static NSSound *beep_sound = nil;
|
||||
|
||||
static void
|
||||
@@ -906,26 +965,32 @@ cocoa_system_beep(const char *path) {
|
||||
else NSBeep();
|
||||
}
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
{"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""},
|
||||
{"cocoa_set_global_shortcut", (PyCFunction)cocoa_set_global_shortcut, METH_VARARGS, ""},
|
||||
{"cocoa_send_notification", (PyCFunction)cocoa_send_notification, METH_VARARGS, ""},
|
||||
{"cocoa_set_notification_activated_callback", (PyCFunction)set_notification_activated_callback, METH_O, ""},
|
||||
{"cocoa_set_url_handler", (PyCFunction)cocoa_set_url_handler, METH_VARARGS, ""},
|
||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
static void
|
||||
uncaughtExceptionHandler(NSException *exception) {
|
||||
log_error("Unhandled exception in Cocoa: %s", [[exception description] UTF8String]);
|
||||
log_error("Stack trace:\n%s", [[exception.callStackSymbols description] UTF8String]);
|
||||
}
|
||||
|
||||
void
|
||||
cocoa_set_uncaught_exception_handler(void) {
|
||||
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
|
||||
}
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
{"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""},
|
||||
{"cocoa_set_global_shortcut", (PyCFunction)cocoa_set_global_shortcut, METH_VARARGS, ""},
|
||||
{"cocoa_send_notification", (PyCFunction)cocoa_send_notification, METH_VARARGS, ""},
|
||||
{"cocoa_set_notification_activated_callback", (PyCFunction)set_notification_activated_callback, METH_O, ""},
|
||||
{"cocoa_set_url_handler", (PyCFunction)cocoa_set_url_handler, METH_VARARGS, ""},
|
||||
{"cocoa_set_app_icon", (PyCFunction)cocoa_set_app_icon, METH_VARARGS, ""},
|
||||
{"cocoa_set_dock_icon", (PyCFunction)cocoa_set_dock_icon, METH_VARARGS, ""},
|
||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
bool
|
||||
init_cocoa(PyObject *module) {
|
||||
memset(&global_shortcuts, 0, sizeof(global_shortcuts));
|
||||
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
||||
register_at_exit_cleanup_func(COCOA_CLEANUP_FUNC, cleanup);
|
||||
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ from kittens.runner import (
|
||||
)
|
||||
|
||||
from .cli import (
|
||||
OptionDict, OptionSpecSeq, options_for_completion, parse_option_spec,
|
||||
prettify
|
||||
OptionDict, options_for_completion, parse_option_spec, prettify
|
||||
)
|
||||
from .constants import shell_integration_dir
|
||||
from .remote_control import global_options_spec
|
||||
from .constants import config_dir, shell_integration_dir
|
||||
from .fast_data_types import truncate_point_for_length, wcswidth
|
||||
from .rc.base import all_command_names, command_for_name
|
||||
from .shell import options_for_cmd
|
||||
@@ -278,8 +278,8 @@ def zsh_output_serializer(ans: Completions) -> str:
|
||||
allm = tuple(matches)
|
||||
if len(allm) > 1:
|
||||
common_prefix = os.path.commonprefix(allm)
|
||||
if common_prefix and '/' in common_prefix:
|
||||
common_prefix = os.path.dirname(common_prefix) + '/'
|
||||
if common_prefix and os.sep in common_prefix:
|
||||
common_prefix = os.path.dirname(common_prefix).rstrip(os.sep) + os.sep
|
||||
cmd.extend(('-p', shlex.quote(common_prefix)))
|
||||
matches = MatchGroup({k[len(common_prefix):]: v for k, v in matches.items()})
|
||||
has_descriptions = any(matches.values())
|
||||
@@ -387,20 +387,6 @@ def complete_kitty_cli_arg(ans: Completions, opt: Optional[OptionDict], prefix:
|
||||
from kitty.config import option_names_for_completion
|
||||
k = 'Config directives'
|
||||
ans.add_match_group(k, {k+'=': '' for k in option_names_for_completion() if k.startswith(prefix)}, trailing_space=False)
|
||||
elif dest == 'config':
|
||||
|
||||
def is_conf_file(x: str) -> bool:
|
||||
if os.path.isdir(x):
|
||||
return True
|
||||
return x.lower().endswith('.conf')
|
||||
|
||||
complete_files_and_dirs(ans, prefix, files_group_name='Config files', predicate=is_conf_file)
|
||||
elif dest == 'session':
|
||||
complete_files_and_dirs(ans, prefix, files_group_name='Session files')
|
||||
elif dest == 'watcher':
|
||||
complete_files_and_dirs(ans, prefix, files_group_name='Watcher files')
|
||||
elif dest == 'directory':
|
||||
complete_dirs(ans, prefix)
|
||||
elif dest == 'listen_on':
|
||||
if ':' not in prefix:
|
||||
k = 'Address type'
|
||||
@@ -411,6 +397,15 @@ def complete_kitty_cli_arg(ans: Completions, opt: Optional[OptionDict], prefix:
|
||||
complete_basic_option_args(ans, opt, prefix)
|
||||
|
||||
|
||||
def basic_option_arg_completer(ans: Completions, opt: Optional[OptionDict], prefix: str, unknown_args: Delegate) -> None:
|
||||
prefix = prefix or ''
|
||||
if not opt:
|
||||
if unknown_args.num_of_unknown_args > 0:
|
||||
ans.delegate = unknown_args
|
||||
return
|
||||
complete_basic_option_args(ans, opt, prefix)
|
||||
|
||||
|
||||
CompleteArgsFunc = Callable[[Completions, Optional[OptionDict], str, Delegate], None]
|
||||
|
||||
|
||||
@@ -419,7 +414,7 @@ def complete_alias_map(
|
||||
words: Sequence[str],
|
||||
new_word: bool,
|
||||
option_map: Dict[str, OptionDict],
|
||||
complete_args: Optional[CompleteArgsFunc] = None
|
||||
complete_args: CompleteArgsFunc = basic_option_arg_completer,
|
||||
) -> None:
|
||||
expecting_arg = False
|
||||
opt: Optional[OptionDict] = None
|
||||
@@ -431,11 +426,11 @@ def complete_alias_map(
|
||||
if w is not last_word:
|
||||
continue
|
||||
long_opt = option_map.get(prev_word)
|
||||
if long_opt is not None and complete_args is not None:
|
||||
if long_opt is not None:
|
||||
complete_args(ans, long_opt, '', Delegate())
|
||||
return
|
||||
if w is last_word and not new_word:
|
||||
if opt is not None and complete_args is not None:
|
||||
if opt is not None:
|
||||
complete_args(ans, opt, w, Delegate())
|
||||
return
|
||||
expecting_arg = False
|
||||
@@ -444,7 +439,7 @@ def complete_alias_map(
|
||||
parts = w.split('=', 1)
|
||||
if len(parts) == 2:
|
||||
long_opt = option_map.get(parts[0])
|
||||
if long_opt is not None and complete_args is not None:
|
||||
if long_opt is not None:
|
||||
complete_args(ans, long_opt, parts[1], Delegate())
|
||||
ans.add_prefix(f'{parts[0]}=')
|
||||
return
|
||||
@@ -453,23 +448,20 @@ def complete_alias_map(
|
||||
if w.startswith('-'):
|
||||
ans.add_match_group('Options', {k: opt['help'] for k, opt in option_map.items() if k.startswith(last_word)})
|
||||
else:
|
||||
if complete_args is not None:
|
||||
complete_args(ans, None, last_word, Delegate(words, i))
|
||||
complete_args(ans, None, last_word, Delegate(words, i))
|
||||
return
|
||||
if opt is None:
|
||||
if complete_args is not None:
|
||||
complete_args(ans, None, '' if new_word else last_word, Delegate(words, i, new_word))
|
||||
complete_args(ans, None, '' if new_word else last_word, Delegate(words, i, new_word))
|
||||
if w.startswith('--') and '=' in w:
|
||||
continue
|
||||
return # some non-option word encountered
|
||||
expecting_arg = not opt.get('type', '').startswith('bool-')
|
||||
if expecting_arg:
|
||||
if opt is not None and complete_args is not None:
|
||||
if opt is not None:
|
||||
complete_args(ans, opt, '' if new_word else last_word, Delegate())
|
||||
else:
|
||||
prefix = '' if new_word else last_word
|
||||
if complete_args is not None:
|
||||
complete_args(ans, None, prefix, Delegate())
|
||||
complete_args(ans, None, prefix, Delegate())
|
||||
ans.add_match_group('Options', {k: opt['help'] for k, opt in option_map.items() if k.startswith(prefix)})
|
||||
|
||||
|
||||
@@ -477,23 +469,33 @@ def complete_cli(
|
||||
ans: Completions,
|
||||
words: Sequence[str],
|
||||
new_word: bool,
|
||||
seq: OptionSpecSeq,
|
||||
complete_args: Optional[CompleteArgsFunc] = None
|
||||
) -> None:
|
||||
option_map = {}
|
||||
for opt in seq:
|
||||
for opt in options_for_completion():
|
||||
if not isinstance(opt, str):
|
||||
for alias in opt['aliases']:
|
||||
option_map[alias] = opt
|
||||
complete_alias_map(ans, words, new_word, option_map, complete_args)
|
||||
complete_alias_map(ans, words, new_word, option_map, complete_kitty_cli_arg)
|
||||
|
||||
|
||||
def global_options_for_remote_cmd() -> Dict[str, OptionDict]:
|
||||
seq, disabled = parse_option_spec(global_options_spec())
|
||||
ans: Dict[str, OptionDict] = {}
|
||||
for opt in seq:
|
||||
if isinstance(opt, str):
|
||||
continue
|
||||
for alias in opt['aliases']:
|
||||
ans[alias] = opt
|
||||
return ans
|
||||
|
||||
|
||||
def complete_remote_command(ans: Completions, cmd_name: str, words: Sequence[str], new_word: bool) -> None:
|
||||
aliases, alias_map = options_for_cmd(cmd_name)
|
||||
if not alias_map:
|
||||
try:
|
||||
args_completion = command_for_name(cmd_name).args_completion
|
||||
except KeyError:
|
||||
return
|
||||
args_completer: Optional[CompleteArgsFunc] = None
|
||||
args_completion = command_for_name(cmd_name).args_completion
|
||||
args_completer: CompleteArgsFunc = basic_option_arg_completer
|
||||
if args_completion:
|
||||
if 'files' in args_completion:
|
||||
title, matchers = args_completion['files']
|
||||
@@ -509,7 +511,7 @@ def complete_launch_wrapper(ans: Completions, words: Sequence[str], new_word: bo
|
||||
from kitty.launch import clone_safe_opts
|
||||
aliases, alias_map = options_for_cmd('launch')
|
||||
alias_map = {k: v for k, v in alias_map.items() if v['dest'] in clone_safe_opts()}
|
||||
args_completer: Optional[CompleteArgsFunc] = None
|
||||
args_completer: CompleteArgsFunc = basic_option_arg_completer
|
||||
if allow_files:
|
||||
args_completer = remote_files_completer('Files', ('*',))
|
||||
complete_alias_map(ans, words, new_word, alias_map, complete_args=args_completer)
|
||||
@@ -564,9 +566,102 @@ def complete_files_and_dirs(
|
||||
ans.add_match_group(files_group_name, files, is_files=True)
|
||||
|
||||
|
||||
def filter_files_from_completion_spec(spec: Dict[str, str]) -> Callable[['os.DirEntry[str]', str], bool]:
|
||||
|
||||
if 'ext' in spec:
|
||||
extensions = frozenset(os.extsep + x.lower() for x in spec['ext'].split(','))
|
||||
else:
|
||||
extensions = frozenset()
|
||||
|
||||
if 'mime' in spec:
|
||||
import re
|
||||
from fnmatch import translate
|
||||
mimes = tuple(re.compile(translate(x)) for x in spec['mime'].split(','))
|
||||
from .guess_mime_type import guess_type
|
||||
else:
|
||||
mimes = ()
|
||||
|
||||
if mimes or extensions:
|
||||
def check_file(x: 'os.DirEntry[str]', result: str) -> bool:
|
||||
if extensions:
|
||||
q = result.lower()
|
||||
for ext in extensions:
|
||||
if q.endswith(ext):
|
||||
return True
|
||||
if mimes:
|
||||
mq = guess_type(result)
|
||||
if mq:
|
||||
for mime in mimes:
|
||||
if mime.match(mq):
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
def check_file(x: 'os.DirEntry[str]', result: str) -> bool:
|
||||
return True
|
||||
|
||||
return check_file
|
||||
|
||||
|
||||
def complete_file_path(ans: Completions, spec: Dict[str, str], prefix: str, only_dirs: bool = False) -> None:
|
||||
prefix = prefix.replace(r'\ ', ' ')
|
||||
relative_to = spec.get('relative', '')
|
||||
if relative_to:
|
||||
if relative_to == 'conf':
|
||||
relative_to = config_dir
|
||||
else:
|
||||
relative_to = os.getcwd()
|
||||
src_dir = relative_to
|
||||
check_against = prefix
|
||||
prefix_result_with = prefix
|
||||
files, dirs = [], []
|
||||
if prefix:
|
||||
expanded_prefix = os.path.expandvars(os.path.expanduser(prefix))
|
||||
check_against = os.path.basename(expanded_prefix)
|
||||
prefix_result_with = os.path.dirname(expanded_prefix).rstrip(os.sep) + os.sep
|
||||
if os.path.isabs(expanded_prefix):
|
||||
src_dir = os.path.dirname(expanded_prefix)
|
||||
elif os.sep in expanded_prefix or (os.altsep and os.altsep in expanded_prefix):
|
||||
src_dir = os.path.join(relative_to, os.path.dirname(expanded_prefix))
|
||||
else:
|
||||
prefix_result_with = ''
|
||||
try:
|
||||
items: Iterable['os.DirEntry[str]'] = os.scandir(src_dir)
|
||||
except OSError:
|
||||
items = ()
|
||||
check_file = filter_files_from_completion_spec(spec)
|
||||
for x in items:
|
||||
if not x.name.startswith(check_against):
|
||||
continue
|
||||
result = prefix_result_with + x.name
|
||||
if x.is_dir():
|
||||
dirs.append(result.rstrip(os.sep) + os.sep)
|
||||
else:
|
||||
if check_file(x, result):
|
||||
files.append(result)
|
||||
if dirs:
|
||||
ans.add_match_group('Directories', dirs, trailing_space=False, is_files=True)
|
||||
if not only_dirs and files:
|
||||
ans.add_match_group(spec.get('group') or 'Files', files, is_files=True)
|
||||
|
||||
|
||||
def complete_path(ans: Completions, opt: OptionDict, prefix: str) -> None:
|
||||
spec = opt['completion']
|
||||
t = spec['type']
|
||||
if 'kwds' in spec:
|
||||
kwds = [x for x in spec['kwds'].split(',') if x.startswith(prefix)]
|
||||
if kwds:
|
||||
ans.add_match_group('Keywords', kwds)
|
||||
if t == 'file':
|
||||
complete_file_path(ans, spec, prefix)
|
||||
elif t == 'directory':
|
||||
complete_file_path(ans, spec, prefix, only_dirs=True)
|
||||
|
||||
|
||||
def complete_basic_option_args(ans: Completions, opt: OptionDict, prefix: str) -> None:
|
||||
if opt['choices']:
|
||||
ans.add_match_group(f'Choices for {opt["dest"]}', tuple(k for k in opt['choices'] if k.startswith(prefix)))
|
||||
elif opt['completion'].get('type') in ('file', 'directory'):
|
||||
complete_path(ans, opt, prefix)
|
||||
|
||||
|
||||
def complete_dirs(ans: Completions, prefix: str = '') -> None:
|
||||
@@ -630,6 +725,19 @@ def remote_args_completer(title: str, words: Iterable[str]) -> CompleteArgsFunc:
|
||||
return complete_names_for_arg
|
||||
|
||||
|
||||
def remote_command_completer(ans: Completions, opt: Optional[OptionDict], prefix: str, unknown_args: Delegate) -> None:
|
||||
if opt is None:
|
||||
words = unknown_args.words[unknown_args.pos:]
|
||||
new_word = unknown_args.new_word
|
||||
if not words or (len(words) == 1 and not new_word):
|
||||
prefix = (words or ('',))[0]
|
||||
ans.add_match_group('Remote control commands', {c: '' for c in remote_control_command_names() if c.startswith(prefix)})
|
||||
else:
|
||||
complete_remote_command(ans, words[0], words[1:], new_word)
|
||||
else:
|
||||
basic_option_arg_completer(ans, opt, prefix, unknown_args)
|
||||
|
||||
|
||||
def config_file_predicate(filename: str) -> bool:
|
||||
return filename.endswith('.conf')
|
||||
|
||||
@@ -660,42 +768,38 @@ def complete_kitten(ans: Completions, kitten: str, words: Sequence[str], new_wor
|
||||
options = cd['options']()
|
||||
seq = parse_option_spec(options)[0]
|
||||
option_map = {}
|
||||
if not new_word:
|
||||
for opt in seq:
|
||||
if not isinstance(opt, str):
|
||||
for alias in opt['aliases']:
|
||||
option_map[alias] = opt
|
||||
for opt in seq:
|
||||
if not isinstance(opt, str):
|
||||
for alias in opt['aliases']:
|
||||
option_map[alias] = opt
|
||||
complete_alias_map(ans, words, new_word, option_map, {
|
||||
'icat': complete_icat_args,
|
||||
'diff': complete_diff_args,
|
||||
'themes': complete_themes_args,
|
||||
}.get(kitten))
|
||||
}.get(kitten, basic_option_arg_completer))
|
||||
|
||||
|
||||
def find_completions(words: Sequence[str], new_word: bool, entry_points: Iterable[str], namespaced_entry_points: Iterable[str]) -> Completions:
|
||||
ans = Completions()
|
||||
if not words:
|
||||
return ans
|
||||
if words[0] in ('edit-in-kitty', 'clone-in-kitty'):
|
||||
exe = os.path.basename(words[0])
|
||||
if exe in ('edit-in-kitty', 'clone-in-kitty'):
|
||||
complete_launch_wrapper(ans, words[1:], new_word, allow_files=words[0] != 'clone-in-kitty')
|
||||
return ans
|
||||
if words[0] != 'kitty':
|
||||
if exe != 'kitty':
|
||||
return ans
|
||||
words = words[1:]
|
||||
if not words or (len(words) == 1 and not new_word):
|
||||
if words and words[0].startswith('--') and '=' in words[0]:
|
||||
complete_cli(ans, words, new_word, options_for_completion(), complete_kitty_cli_arg)
|
||||
complete_cli(ans, words, new_word)
|
||||
return ans
|
||||
prefix = words[0] if words else ''
|
||||
completions_for_first_word(ans, prefix, entry_points, namespaced_entry_points)
|
||||
kitty_cli_opts(ans, prefix)
|
||||
return ans
|
||||
if words[0] == '@':
|
||||
if len(words) == 1 or (len(words) == 2 and not new_word):
|
||||
prefix = words[1] if len(words) > 1 else ''
|
||||
ans.add_match_group('Remote control commands', {c: '' for c in remote_control_command_names() if c.startswith(prefix)})
|
||||
else:
|
||||
complete_remote_command(ans, words[1], words[2:], new_word)
|
||||
complete_alias_map(ans, words[1:], new_word, global_options_for_remote_cmd(), remote_command_completer)
|
||||
return ans
|
||||
if words[0].startswith('@'):
|
||||
if len(words) == 1 and not new_word:
|
||||
@@ -703,6 +807,7 @@ def find_completions(words: Sequence[str], new_word: bool, entry_points: Iterabl
|
||||
ans.add_match_group('Remote control commands', {f'@{c}': '' for c in remote_control_command_names() if c.startswith(prefix)})
|
||||
else:
|
||||
complete_remote_command(ans, words[0][1:], words[1:], new_word)
|
||||
return ans
|
||||
if words[0] == '+':
|
||||
if len(words) == 1 or (len(words) == 2 and not new_word):
|
||||
prefix = words[1] if len(words) > 1 else ''
|
||||
@@ -714,7 +819,7 @@ def find_completions(words: Sequence[str], new_word: bool, entry_points: Iterabl
|
||||
else:
|
||||
complete_kitten(ans, words[2], words[3:], new_word)
|
||||
elif words[1] == 'open':
|
||||
complete_cli(ans, words[2:], new_word, options_for_completion(), complete_kitty_cli_arg)
|
||||
complete_cli(ans, words[2:], new_word)
|
||||
return ans
|
||||
if words[0].startswith('+'):
|
||||
if len(words) == 1:
|
||||
@@ -722,7 +827,7 @@ def find_completions(words: Sequence[str], new_word: bool, entry_points: Iterabl
|
||||
if words[0] == '+kitten':
|
||||
ans.add_match_group('Kittens', all_kitten_names())
|
||||
elif words[0] == '+open':
|
||||
complete_cli(ans, words[1:], new_word, options_for_completion(), complete_kitty_cli_arg)
|
||||
complete_cli(ans, words[1:], new_word)
|
||||
else:
|
||||
prefix = words[0]
|
||||
ans.add_match_group('Entry points', (c for c in namespaced_entry_points if c.startswith(prefix)))
|
||||
@@ -733,9 +838,9 @@ def find_completions(words: Sequence[str], new_word: bool, entry_points: Iterabl
|
||||
else:
|
||||
complete_kitten(ans, words[1], words[2:], new_word)
|
||||
elif words[0] == '+open':
|
||||
complete_cli(ans, words[1:], new_word, options_for_completion(), complete_kitty_cli_arg)
|
||||
complete_cli(ans, words[1:], new_word)
|
||||
else:
|
||||
complete_cli(ans, words, new_word, options_for_completion(), complete_kitty_cli_arg)
|
||||
complete_cli(ans, words, new_word)
|
||||
|
||||
return ans
|
||||
|
||||
|
||||
@@ -390,7 +390,7 @@ def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
|
||||
return class_def, '\n'.join(preamble + ['', ''] + tc_lines)
|
||||
|
||||
|
||||
def generate_c_conversion(loc: str, ctypes: List[Option]) -> str:
|
||||
def generate_c_conversion(loc: str, ctypes: List[Union[Option, MultiOption]]) -> str:
|
||||
lines: List[str] = []
|
||||
basic_converters = {
|
||||
'int': 'PyLong_AsLong', 'uint': 'PyLong_AsUnsignedLong', 'bool': 'PyObject_IsTrue',
|
||||
@@ -437,7 +437,7 @@ def write_output(loc: str, defn: Definition) -> None:
|
||||
f.write(f'{tc}\n')
|
||||
ctypes = []
|
||||
for opt in defn.root_group.iter_all_non_groups():
|
||||
if isinstance(opt, Option) and opt.ctype:
|
||||
if isinstance(opt, (Option, MultiOption)) and opt.ctype:
|
||||
ctypes.append(opt)
|
||||
if ctypes:
|
||||
c = generate_c_conversion(loc, ctypes)
|
||||
|
||||
@@ -45,27 +45,50 @@ def expand_opt_references(conf_name: str, text: str) -> str:
|
||||
|
||||
|
||||
@run_once
|
||||
def ref_map() -> Dict[str, str]:
|
||||
from kitty.actions import get_all_actions
|
||||
ref_map = {
|
||||
'layouts': f'{website_url("overview")}#layouts',
|
||||
'include': f'{website_url("conf")}#include',
|
||||
'watchers': f'{website_url("launch")}#watchers',
|
||||
'sessions': f'{website_url("overview")}#startup-sessions',
|
||||
'functional': f'{website_url("keyboard-protocol")}#functional-key-definitions',
|
||||
'ssh_copy_command': f'{website_url("kittens/ssh")}#ssh-copy-command',
|
||||
'shell_integration': website_url("shell-integration"),
|
||||
'clone_shell': f'{website_url("shell-integration")}#clone-shell',
|
||||
'github_discussions': 'https://github.com/kovidgoyal/kitty/discussions',
|
||||
}
|
||||
for actions in get_all_actions().values():
|
||||
for ac in actions:
|
||||
ref_map[f'action-{ac.name}'] = f'{website_url("actions")}#' + ac.name.replace('_', '-')
|
||||
return ref_map
|
||||
def ref_map() -> Dict[str, Dict[str, str]]:
|
||||
import json
|
||||
from ..fast_data_types import get_docs_ref_map
|
||||
ans: Dict[str, Dict[str, str]] = json.loads(get_docs_ref_map())
|
||||
return ans
|
||||
|
||||
|
||||
def resolve_ref(ref: str, website_url: Callable[[str], str] = website_url) -> str:
|
||||
m = ref_map()
|
||||
href = m['ref'].get(ref, '')
|
||||
prefix, rest = ref.partition('-')[::2]
|
||||
if href:
|
||||
pass
|
||||
elif ref.startswith('conf-kitty-'):
|
||||
href = f'conf#{ref}'
|
||||
elif ref.startswith('conf-kitten-'):
|
||||
parts = ref.split('-')
|
||||
href = "kittens/" + parts[2] + f'/#{ref}'
|
||||
elif ref.startswith('at_'):
|
||||
base = ref.split('_', 1)[1]
|
||||
href = "remote-control/#at-" + base.replace('_', '-')
|
||||
elif ref.startswith('at-'):
|
||||
base = ref.split('-', 1)[1]
|
||||
href = "remote-control/#at-" + base.replace('_', '-')
|
||||
elif ref.startswith('action-group-'):
|
||||
href = f'actions/#{ref}'
|
||||
elif prefix == 'action':
|
||||
href = f'actions/#{rest.replace("_", "-")}'
|
||||
elif prefix in ('term', 'envvar'):
|
||||
href = 'glossary/#' + ref
|
||||
elif prefix == 'doc':
|
||||
href = rest.lstrip('/')
|
||||
elif prefix in ('issues', 'pull', 'discussions'):
|
||||
t, num = ref.partition(':')[::2]
|
||||
href = f'https://github.com/kovidgoyal/kitty/{prefix}/{rest}'
|
||||
if not (href.startswith('https://') or href.startswith('http://')):
|
||||
href = website_url(href)
|
||||
return href
|
||||
|
||||
|
||||
def remove_markup(text: str) -> str:
|
||||
|
||||
imap = {'iss': 'issues-', 'pull': 'pull-', 'disc': 'discussions-'}
|
||||
|
||||
def extract(m: 'Match[str]') -> Tuple[str, str]:
|
||||
parts = m.group(2).split('<')
|
||||
t = parts[0].strip()
|
||||
@@ -73,24 +96,24 @@ def remove_markup(text: str) -> str:
|
||||
return t, q
|
||||
|
||||
def sub(m: 'Match[str]') -> str:
|
||||
if m.group(1) == 'ref':
|
||||
key = m.group(1)
|
||||
if key in ('ref', 'iss', 'pull', 'disc'):
|
||||
t, q = extract(m)
|
||||
return f'{t} <{ref_map()[q]}>'
|
||||
if m.group(1) == 'doc':
|
||||
q = imap.get(key, '') + q
|
||||
url = resolve_ref(q)
|
||||
if not url:
|
||||
raise KeyError(f'Failed to resolve :{m.group(1)}: {q}')
|
||||
return f'{t} <{url}>'
|
||||
if key == 'doc':
|
||||
t, q = extract(m)
|
||||
return f'{t} <{website_url(q.lstrip("/"))}>'
|
||||
if m.group(1) == 'ac':
|
||||
q = m.group(2).split('<')[-1].rstrip('>')
|
||||
return q
|
||||
if m.group(1) == 'term':
|
||||
return f'{t} <{website_url(q)}>'
|
||||
if key in ('term', 'option'):
|
||||
t, _ = extract(m)
|
||||
return t
|
||||
if m.group(1) == 'option':
|
||||
t, _ = extract(m)
|
||||
return t
|
||||
if m.group(1) == 'disc':
|
||||
if key in ('ac', 'opt'):
|
||||
t, q = extract(m)
|
||||
return f'{t} {ref_map()["github_discussions"]}/{q}'
|
||||
return f'{t} {q}' if q and q != t else t
|
||||
|
||||
return str(m.group(2))
|
||||
|
||||
return re.sub(r':([a-zA-Z0-9]+):`(.+?)`', sub, text, flags=re.DOTALL)
|
||||
|
||||
@@ -79,24 +79,27 @@ class ToCmdline:
|
||||
self.override_env.update(override)
|
||||
return self
|
||||
|
||||
def __call__(self, x: str) -> List[str]:
|
||||
return list(
|
||||
map(
|
||||
lambda y: expandvars(
|
||||
os.path.expanduser(y),
|
||||
os.environ if self.override_env is None else self.override_env,
|
||||
fallback_to_os_env=False
|
||||
),
|
||||
shlex.split(x)
|
||||
def __call__(self, x: str, expand: bool = True) -> List[str]:
|
||||
ans = shlex.split(x)
|
||||
if expand:
|
||||
ans = list(
|
||||
map(
|
||||
lambda y: expandvars(
|
||||
os.path.expanduser(y),
|
||||
os.environ if self.override_env is None else self.override_env,
|
||||
fallback_to_os_env=False
|
||||
),
|
||||
ans
|
||||
)
|
||||
)
|
||||
)
|
||||
return ans
|
||||
|
||||
|
||||
to_cmdline_implementation = ToCmdline()
|
||||
|
||||
|
||||
def to_cmdline(x: str) -> List[str]:
|
||||
return to_cmdline_implementation(x)
|
||||
def to_cmdline(x: str, expand: bool = True) -> List[str]:
|
||||
return to_cmdline_implementation(x, expand)
|
||||
|
||||
|
||||
def python_string(text: str) -> str:
|
||||
@@ -185,7 +188,7 @@ def parse_line(
|
||||
val = os.path.expandvars(os.path.expanduser(val.strip()))
|
||||
if key == 'globinclude':
|
||||
from pathlib import Path
|
||||
vals = tuple(map(lambda x: str(os.fspath(x)), Path(base_path_for_includes).glob(val)))
|
||||
vals = tuple(map(lambda x: str(os.fspath(x)), sorted(Path(base_path_for_includes).glob(val))))
|
||||
elif key == 'envinclude':
|
||||
from fnmatch import fnmatchcase
|
||||
for x in os.environ:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user