Compare commits
763 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 | ||
|
|
ad128a3c29 | ||
|
|
b71d2e04ca | ||
|
|
cc3c7b0286 | ||
|
|
bf6c90a69a | ||
|
|
4c9a75c1ba | ||
|
|
98f46f8bd7 | ||
|
|
dec62b1929 | ||
|
|
79f7954048 | ||
|
|
fc217dafba | ||
|
|
a6aff817cf | ||
|
|
38cb18fe92 | ||
|
|
20b6a97159 | ||
|
|
98f6e24106 | ||
|
|
4ed6364b61 | ||
|
|
cd7751681d | ||
|
|
76531d2f7a | ||
|
|
eea652f1d0 | ||
|
|
f20f4762a8 | ||
|
|
c004fd1f4d | ||
|
|
f29039db29 | ||
|
|
fbf1ec43c7 | ||
|
|
a42200a430 | ||
|
|
ad576c333c | ||
|
|
5a0a980648 | ||
|
|
b3578a4fa5 | ||
|
|
3c952b01d3 | ||
|
|
b1453a51f6 | ||
|
|
2c3a7b5f41 | ||
|
|
0df5a6c6c8 | ||
|
|
bf4dc6365a | ||
|
|
ce3322bf91 | ||
|
|
835cb31501 | ||
|
|
762dd065f3 | ||
|
|
7b91447d89 | ||
|
|
1146c4ea3f | ||
|
|
729f7b8e03 | ||
|
|
d3ec632b9f | ||
|
|
cb01b5ee44 | ||
|
|
0828096e9e | ||
|
|
ebcbed290f | ||
|
|
d6afe6f2cb | ||
|
|
4daf70f636 | ||
|
|
2f3dd72c84 | ||
|
|
dca8c65376 | ||
|
|
12d8175d9a | ||
|
|
609bcd68dd | ||
|
|
313136803c | ||
|
|
b937033411 | ||
|
|
55eeb9c11e | ||
|
|
8fbec2dfd5 | ||
|
|
c4521290b8 | ||
|
|
e12a9f3caf | ||
|
|
56f8a06362 | ||
|
|
5c4abe749b | ||
|
|
62c27b3db7 | ||
|
|
1283a2b7e5 | ||
|
|
bec620a8d3 | ||
|
|
6b0bb2e845 | ||
|
|
135818f34b | ||
|
|
2a361d1de1 | ||
|
|
761a941d43 | ||
|
|
c03630a487 | ||
|
|
0b79cbdb3b | ||
|
|
1dc6b49e02 | ||
|
|
1962f8e5a0 | ||
|
|
cabfcbcb69 | ||
|
|
48bb43a2c4 | ||
|
|
af5e3e0c18 | ||
|
|
fb8398c2eb | ||
|
|
eefd4bb3e3 | ||
|
|
581c388838 | ||
|
|
7a63ea6480 | ||
|
|
65f1329635 | ||
|
|
0df9a5d5c5 | ||
|
|
6405849a1b | ||
|
|
e9340dfaaf | ||
|
|
0509855930 | ||
|
|
4c74462763 | ||
|
|
7f69381221 | ||
|
|
75942fb47e | ||
|
|
0893d7164a | ||
|
|
27906ea853 | ||
|
|
8540ca3990 | ||
|
|
e82c5a63b5 | ||
|
|
83602d7e53 | ||
|
|
64e30e5299 | ||
|
|
83ce23ff9f | ||
|
|
c5e5ec618d | ||
|
|
bd84ec03ce | ||
|
|
d71f18566e | ||
|
|
76e75d9f89 | ||
|
|
f5695ac4f4 | ||
|
|
b748cd21cf | ||
|
|
3a8bdf3b48 | ||
|
|
196c6b07aa | ||
|
|
1978cc5b55 | ||
|
|
ea47cee23f | ||
|
|
e6844ad6f2 | ||
|
|
02d5b3eafa | ||
|
|
151dacf17f | ||
|
|
117d1b02be | ||
|
|
cb55878efd | ||
|
|
ea4751eae5 |
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():
|
||||
|
||||
26
.github/workflows/ci.yml
vendored
26
.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 }})
|
||||
@@ -20,15 +23,15 @@ jobs:
|
||||
cc: [gcc, clang]
|
||||
include:
|
||||
- python: a
|
||||
pyver: 3.7
|
||||
pyver: "3.8"
|
||||
sanitize: 0
|
||||
|
||||
- python: b
|
||||
pyver: 3.8
|
||||
pyver: "3.9"
|
||||
sanitize: 1
|
||||
|
||||
- python: c
|
||||
pyver: 3.9
|
||||
pyver: "3.10"
|
||||
sanitize: 1
|
||||
|
||||
|
||||
@@ -65,7 +68,7 @@ jobs:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 10
|
||||
fetch-depth: 0 # needed for :commit: docs role
|
||||
|
||||
- name: Test for trailing whitespace
|
||||
run: if grep -Inr '\s$' kitty kitty_tests kittens docs *.py *.asciidoc *.rst .gitattributes .gitignore; then echo Trailing whitespace found, aborting.; exit 1; fi
|
||||
@@ -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
|
||||
|
||||
|
||||
180
__main__.py
180
__main__.py
@@ -1,185 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import List
|
||||
|
||||
|
||||
def icat(args: List[str]) -> None:
|
||||
from kittens.runner import run_kitten as rk
|
||||
sys.argv = args
|
||||
rk('icat')
|
||||
|
||||
|
||||
def list_fonts(args: List[str]) -> None:
|
||||
from kitty.fonts.list import main as list_main
|
||||
list_main(args)
|
||||
|
||||
|
||||
def remote_control(args: List[str]) -> None:
|
||||
from kitty.remote_control import main as rc_main
|
||||
rc_main(args)
|
||||
|
||||
|
||||
def runpy(args: List[str]) -> None:
|
||||
if len(args) < 2:
|
||||
raise SystemExit('Usage: kitty +runpy "some python code"')
|
||||
sys.argv = ['kitty'] + args[2:]
|
||||
exec(args[1])
|
||||
|
||||
|
||||
def hold(args: List[str]) -> None:
|
||||
import subprocess
|
||||
ret = 1
|
||||
try:
|
||||
ret = subprocess.Popen(args[1:]).wait()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except FileNotFoundError:
|
||||
print(f'Could not find {args[1]!r} to execute', file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(e, file=sys.stderr)
|
||||
from kitty.utils import hold_till_enter
|
||||
hold_till_enter()
|
||||
raise SystemExit(ret)
|
||||
|
||||
|
||||
def complete(args: List[str]) -> None:
|
||||
from kitty.complete import main as complete_main
|
||||
complete_main(args[1:], entry_points, namespaced_entry_points)
|
||||
|
||||
|
||||
def open_urls(args: List[str]) -> None:
|
||||
setattr(sys, 'cmdline_args_for_open', True)
|
||||
sys.argv = ['kitty'] + args[1:]
|
||||
from kitty.main import main as kitty_main
|
||||
kitty_main()
|
||||
|
||||
|
||||
def launch(args: List[str]) -> None:
|
||||
import runpy
|
||||
sys.argv = args[1:]
|
||||
try:
|
||||
exe = args[1]
|
||||
except IndexError:
|
||||
raise SystemExit(
|
||||
'usage: kitty +launch script.py [arguments to be passed to script.py ...]\n\n'
|
||||
'script.py will be run with full access to kitty code. If script.py is '
|
||||
'prefixed with a : it will be searched for in PATH'
|
||||
)
|
||||
if exe.startswith(':'):
|
||||
import shutil
|
||||
q = shutil.which(exe[1:])
|
||||
if not q:
|
||||
raise SystemExit(f'{exe[1:]} not found in PATH')
|
||||
exe = q
|
||||
if not os.path.exists(exe):
|
||||
raise SystemExit(f'{exe} does not exist')
|
||||
runpy.run_path(exe, run_name='__main__')
|
||||
|
||||
|
||||
def shebang(args: List[str]) -> None:
|
||||
script_path = args[1]
|
||||
cmd = args[2:]
|
||||
if cmd == ['__ext__']:
|
||||
cmd = [os.path.splitext(script_path)[1][1:].lower()]
|
||||
try:
|
||||
f = open(script_path)
|
||||
except FileNotFoundError:
|
||||
raise SystemExit(f'The file {script_path} does not exist')
|
||||
with f:
|
||||
if f.read(2) == '#!':
|
||||
line = f.readline().strip()
|
||||
_plat = sys.platform.lower()
|
||||
is_macos: bool = 'darwin' in _plat
|
||||
if is_macos:
|
||||
cmd = line.split(' ')
|
||||
else:
|
||||
cmd = line.split(' ', maxsplit=1)
|
||||
os.execvp(cmd[0], cmd + [script_path])
|
||||
|
||||
|
||||
def run_kitten(args: List[str]) -> None:
|
||||
try:
|
||||
kitten = args[1]
|
||||
except IndexError:
|
||||
from kittens.runner import list_kittens
|
||||
list_kittens()
|
||||
raise SystemExit(1)
|
||||
sys.argv = args[1:]
|
||||
from kittens.runner import run_kitten as rk
|
||||
rk(kitten)
|
||||
|
||||
|
||||
def edit_config_file(args: List[str]) -> None:
|
||||
from kitty.cli import create_default_opts
|
||||
from kitty.fast_data_types import set_options
|
||||
from kitty.utils import edit_config_file as f
|
||||
set_options(create_default_opts())
|
||||
f()
|
||||
|
||||
|
||||
def namespaced(args: List[str]) -> None:
|
||||
try:
|
||||
func = namespaced_entry_points[args[1]]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
func(args[1:])
|
||||
return
|
||||
raise SystemExit(f'{args[1]} is not a known entry point. Choices are: ' + ', '.join(namespaced_entry_points))
|
||||
|
||||
|
||||
entry_points = {
|
||||
# These two are here for backwards compat
|
||||
'icat': icat,
|
||||
'list-fonts': list_fonts,
|
||||
|
||||
'@': remote_control,
|
||||
'+': namespaced,
|
||||
}
|
||||
namespaced_entry_points = {k: v for k, v in entry_points.items() if k[0] not in '+@'}
|
||||
namespaced_entry_points['hold'] = hold
|
||||
namespaced_entry_points['complete'] = complete
|
||||
namespaced_entry_points['runpy'] = runpy
|
||||
namespaced_entry_points['launch'] = launch
|
||||
namespaced_entry_points['open'] = open_urls
|
||||
namespaced_entry_points['kitten'] = run_kitten
|
||||
namespaced_entry_points['edit-config'] = edit_config_file
|
||||
namespaced_entry_points['shebang'] = shebang
|
||||
|
||||
|
||||
def setup_openssl_environment() -> None:
|
||||
# Use our bundled CA certificates instead of the system ones, since
|
||||
# many systems come with no certificates in a useable form or have various
|
||||
# locations for the certificates.
|
||||
d = os.path.dirname
|
||||
ext_dir: str = getattr(sys, 'kitty_extensions_dir')
|
||||
if 'darwin' in sys.platform.lower():
|
||||
cert_file = os.path.join(d(d(d(ext_dir))), 'cacert.pem')
|
||||
else:
|
||||
cert_file = os.path.join(d(ext_dir), 'cacert.pem')
|
||||
os.environ['SSL_CERT_FILE'] = cert_file
|
||||
setattr(sys, 'kitty_ssl_env_var', 'SSL_CERT_FILE')
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if getattr(sys, 'frozen', False) and getattr(sys, 'kitty_extensions_dir', ''):
|
||||
setup_openssl_environment()
|
||||
first_arg = '' if len(sys.argv) < 2 else sys.argv[1]
|
||||
func = entry_points.get(first_arg)
|
||||
if func is None:
|
||||
if first_arg.startswith('@'):
|
||||
remote_control(['@', first_arg[1:]] + sys.argv[2:])
|
||||
elif first_arg.startswith('+'):
|
||||
namespaced(['+', first_arg[1:]] + sys.argv[2:])
|
||||
else:
|
||||
from kitty.main import main as kitty_main
|
||||
kitty_main()
|
||||
else:
|
||||
func(sys.argv[1:])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from kitty.entry_points import main
|
||||
main()
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -16,6 +16,7 @@ The :doc:`ssh kitten <kittens/ssh>` is redesigned with powerful new features:
|
||||
|
||||
* Automatic :ref:`shell_integration` on remote machines
|
||||
* Easily :ref:`clone local shell/editor config <real_world_ssh_kitten_config>` on remote machines
|
||||
* Easily :ref:`edit files in your local editor <edit_file>` on remote machines
|
||||
* Automatic :opt:`re-use of existing connections <kitten-ssh.share_connections>` to avoid connection setup latency
|
||||
|
||||
|
||||
@@ -34,6 +35,202 @@ 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]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- A new command :command:`edit-in-kitty` to :ref:`edit_file`
|
||||
|
||||
- Allow getting the last non-empty command output easily via an action or
|
||||
remote control (:pull:`4973`)
|
||||
|
||||
- Fix a bug that caused :opt:`macos_colorspace` to always be ``default`` regardless of its actual value (:iss:`5129`)
|
||||
|
||||
- diff kitten: A new option :opt:`kitten-diff.ignore_name` to exclude files and directories from being scanned (:pull:`5171`)
|
||||
|
||||
- ssh kitten: Fix bash not being executed as a login shell since kitty 0.25.0 (:iss:`5130`)
|
||||
|
||||
- macOS: When pasting text and the clipboard has a filesystem path, paste the
|
||||
full path instead of the text, which is sometimes just the file name (:pull:`5142`)
|
||||
|
||||
- macOS: Allow opening executables without a file extension with kitty as well
|
||||
(:iss:`5160`)
|
||||
|
||||
- Themes kitten: Add a tab to show user defined custom color themes separately
|
||||
(:pull:`5150`)
|
||||
|
||||
- Iosevka: Fix incorrect rendering when there is a combining char that does not
|
||||
group with its neighbors (:iss:`5153`)
|
||||
|
||||
- Weston: Fix client side decorations flickering on slow computers during
|
||||
window resize (:iss:`5162`)
|
||||
|
||||
- Remote control: Fix commands with large or asynchronous payloads like
|
||||
:command:`kitty @ set-backround-image`, :command:`kitty @ set-window-logo`
|
||||
and :command:`kitty @ select-window` not working correctly
|
||||
when using a socket (:iss:`5165`)
|
||||
|
||||
- hints kitten: Fix surrounding quotes/brackets and embedded carriage returns
|
||||
not being removed when using line number processing (:iss:`5170`)
|
||||
|
||||
|
||||
0.25.1 [2022-05-26]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -648,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
|
||||
@@ -822,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
|
||||
@@ -847,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`)
|
||||
@@ -1990,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`)
|
||||
|
||||
@@ -2299,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`)
|
||||
@@ -2342,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
|
||||
|
||||
25
docs/conf.py
25
docs/conf.py
@@ -13,7 +13,7 @@ import subprocess
|
||||
import sys
|
||||
import time
|
||||
from functools import partial
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
|
||||
from typing import Any, Callable, Dict, Iterable, List, Tuple
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.parsers.rst.roles import set_classes
|
||||
@@ -85,7 +85,7 @@ master_doc = 'index'
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language: Optional[str] = None
|
||||
language: str = 'en'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
@@ -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
|
||||
@@ -378,7 +380,9 @@ id. To do so add the ``p`` key with a number between ``1`` and ``4294967295``.
|
||||
When you specify a placement id, it will be added to the acknowledgement code
|
||||
above. Every placement is uniquely identified by the pair of the ``image id``
|
||||
and the ``placement id``. If you specify a placement id for an image that does
|
||||
not have an id, it will be ignored. An example response::
|
||||
not have an id (i.e. has id=0), it will be ignored. In particular this means
|
||||
there can exist multiple images with ``image id=0, placement id=0``.
|
||||
An example response::
|
||||
|
||||
<ESC>_Gi=<image id>,p=<placement id>;OK<ESC>\
|
||||
|
||||
@@ -415,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.
|
||||
|
||||
@@ -22,6 +22,8 @@ transferred to the remote computer. Note that this happens without needing
|
||||
to install *any* special software on the server, beyond :program:`ls` that
|
||||
supports hyperlinks.
|
||||
|
||||
.. seealso:: See the :ref:`edit-in-kitty <edit_file>` command
|
||||
|
||||
.. seealso:: See the :doc:`transfer` kitten
|
||||
|
||||
.. versionadded:: 0.19.0
|
||||
|
||||
@@ -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
|
||||
@@ -144,33 +148,7 @@ You can customize these actions by creating a :file:`launch-actions.conf` file
|
||||
in the :ref:`kitty config directory <confloc>`, just like the
|
||||
:file:`open-actions.conf` file above. For example:
|
||||
|
||||
.. code:: conf
|
||||
|
||||
# Open script files based on their shebangs
|
||||
protocol file
|
||||
ext sh,command,tool
|
||||
action launch --hold --type=os-window kitty +shebang $FILE_PATH {SHELL}
|
||||
|
||||
# Open shell specific script files
|
||||
protocol file
|
||||
ext fish,bash,zsh
|
||||
action launch --hold --type=os-window kitty +shebang $FILE_PATH __ext__
|
||||
|
||||
# Open directories
|
||||
protocol file
|
||||
mime inode/directory
|
||||
action launch --type=os-window --cwd $FILE_PATH
|
||||
|
||||
# Open text files without fragments in the editor
|
||||
protocol file
|
||||
mime text/*
|
||||
action launch --type=os-window $EDITOR $FILE_PATH
|
||||
|
||||
# Open image files with icat
|
||||
protocol file
|
||||
mime image/*
|
||||
action launch --type=os-window kitty +kitten icat --hold $FILE_PATH
|
||||
|
||||
# Open ssh URLs with ssh command
|
||||
protocol ssh
|
||||
action launch --type=os-window ssh $URL
|
||||
.. literalinclude:: ../kitty/open_actions.py
|
||||
:language: conf
|
||||
:start-at: # Open script files
|
||||
:end-before: '''.splitlines()))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -33,13 +33,15 @@ Features
|
||||
* :ref:`clone_shell` with all environment variables and the working directory
|
||||
copied
|
||||
|
||||
* :ref:`Edit files in new kitty windows <edit_file>` even over SSH
|
||||
|
||||
* Glitch free window resizing even with complex prompts. Achieved by erasing
|
||||
the prompt on resize and allowing the shell to redraw it cleanly.
|
||||
|
||||
* 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
|
||||
@@ -263,6 +265,33 @@ state (mainly CWD and env vars) and this state is transmitted to kitty and
|
||||
restored by the shell integration scripts in the cloned window.
|
||||
|
||||
|
||||
.. _edit_file:
|
||||
|
||||
Edit files in new kitty windows even over SSH
|
||||
------------------------------------------------
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
edit-in-kitty myfile.txt
|
||||
edit-in-kitty --type tab --title "Editing My File" myfile.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
|
||||
SSH (if you use the :doc:`ssh kitten <kittens/ssh>`), allowing you
|
||||
to easily edit remote files in your local editor with all its bells and
|
||||
whistles.
|
||||
|
||||
The :command:`edit-in-kitty` command takes almost all the same arguments as the
|
||||
:doc:`launch <launch>` command, so you can open a new tab instead or a new OS
|
||||
window, etc. Not all arguments are supported, see the discussion in the
|
||||
:ref:`clone_shell` section above.
|
||||
|
||||
In order to avoid remote code execution, kitty will only execute the configured
|
||||
editor and pass the file path to edit to it.
|
||||
|
||||
|
||||
.. _manual_shell_integration:
|
||||
|
||||
Manual shell integration
|
||||
|
||||
@@ -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,38 +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];
|
||||
|
||||
if (![[pasteboard types] containsObject:NSPasteboardTypeString])
|
||||
{
|
||||
_glfwInputError(GLFW_FORMAT_UNAVAILABLE,
|
||||
"Cocoa: Failed to retrieve string from pasteboard");
|
||||
return 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];
|
||||
}
|
||||
|
||||
NSString* object = [pasteboard stringForType:NSPasteboardTypeString];
|
||||
if (!object)
|
||||
{
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||
"Cocoa: Failed to retrieve object from pasteboard");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
free(_glfw.ns.clipboardString);
|
||||
_glfw.ns.clipboardString = _glfw_strdup([object UTF8String]);
|
||||
|
||||
return _glfw.ns.clipboardString;
|
||||
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) {
|
||||
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 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 appendBytes:"\n" length:1];
|
||||
[text_list appendData:[obj dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
}
|
||||
}
|
||||
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 (!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;
|
||||
|
||||
61
glfw/wl_client_side_decorations.c
vendored
61
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
|
||||
@@ -102,13 +102,34 @@ init_buffer_pair(_GLFWWaylandBufferPair *pair, size_t width, size_t height, unsi
|
||||
return 2 * pair->size_in_bytes;
|
||||
}
|
||||
|
||||
static bool
|
||||
window_has_buffer(_GLFWwindow *window, struct wl_buffer *q) {
|
||||
#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
|
||||
alloc_buffer_pair(_GLFWWaylandBufferPair *pair, struct wl_shm_pool *pool, uint8_t *data, size_t *offset) {
|
||||
buffer_release_event(void *data, struct wl_buffer *buffer) {
|
||||
wl_buffer_destroy(buffer);
|
||||
_GLFWwindow *window = _glfwWindowForId((uintptr_t)data);
|
||||
if (window && window_has_buffer(window, buffer)) decs.buffer_destroyed = true;
|
||||
}
|
||||
|
||||
static struct wl_buffer_listener handle_buffer_events = {.release = buffer_release_event};
|
||||
|
||||
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;
|
||||
pair->data.front = pair->data.a; pair->data.back = pair->data.b;
|
||||
@@ -287,7 +308,7 @@ create_shm_buffers(_GLFWwindow* window) {
|
||||
struct wl_shm_pool* pool = wl_shm_create_pool(_glfw.wl.shm, fd, decs.mapping.size);
|
||||
close(fd);
|
||||
size_t offset = 0;
|
||||
#define a(which) alloc_buffer_pair(&decs.which.buffer, pool, decs.mapping.data, &offset)
|
||||
#define a(which) alloc_buffer_pair(window->id, &decs.which.buffer, pool, decs.mapping.data, &offset)
|
||||
a(top); a(left); a(bottom); a(right);
|
||||
#undef a
|
||||
wl_shm_pool_destroy(pool);
|
||||
@@ -313,8 +334,8 @@ free_csd_surfaces(_GLFWwindow *window) {
|
||||
static void
|
||||
free_csd_buffers(_GLFWwindow *window) {
|
||||
#define d(which) { \
|
||||
if (decs.which.buffer.a) wl_buffer_destroy(decs.which.buffer.a); \
|
||||
if (decs.which.buffer.b) wl_buffer_destroy(decs.which.buffer.b); \
|
||||
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);
|
||||
@@ -337,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) {
|
||||
@@ -352,12 +374,14 @@ ensure_csd_resources(_GLFWwindow *window) {
|
||||
decs.for_window_state.scale != window->wl.scale ||
|
||||
!decs.mapping.data
|
||||
);
|
||||
const bool needs_update = focus_changed || size_changed || !decs.left.surface;
|
||||
debug("CSD: old.size: %dx%d new.size: %dx%d needs_update: %d size_changed: %d\n", decs.for_window_state.width, decs.for_window_state.height, window->wl.width, window->wl.height, needs_update, size_changed);
|
||||
const bool needs_update = focus_changed || size_changed || !decs.left.surface || decs.buffer_destroyed;
|
||||
debug("CSD: old.size: %dx%d new.size: %dx%d needs_update: %d size_changed: %d buffer_destroyed: %d\n",
|
||||
decs.for_window_state.width, decs.for_window_state.height, window->wl.width, window->wl.height, needs_update, size_changed, decs.buffer_destroyed);
|
||||
if (!needs_update) return false;
|
||||
if (size_changed) {
|
||||
if (size_changed || decs.buffer_destroyed) {
|
||||
free_csd_buffers(window);
|
||||
if (!create_shm_buffers(window)) return false;
|
||||
decs.buffer_destroyed = false;
|
||||
}
|
||||
|
||||
int32_t x, y, scale = window->wl.scale < 1 ? 1 : window->wl.scale;
|
||||
@@ -432,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);
|
||||
}
|
||||
|
||||
40
glfw/wl_init.c
vendored
40
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,6 +577,7 @@ static void seatHandleCapabilities(void* data UNUSED,
|
||||
{
|
||||
wl_pointer_destroy(_glfw.wl.pointer);
|
||||
_glfw.wl.pointer = NULL;
|
||||
if (_glfw.wl.cursorAnimationTimer) toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.cursorAnimationTimer, 0);
|
||||
}
|
||||
|
||||
if ((caps & WL_SEAT_CAPABILITY_KEYBOARD) && !_glfw.wl.keyboard)
|
||||
@@ -584,6 +589,8 @@ 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) toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -875,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)
|
||||
{
|
||||
@@ -919,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]);
|
||||
@@ -932,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)
|
||||
@@ -940,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)
|
||||
|
||||
36
glfw/wl_platform.h
vendored
36
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;
|
||||
|
||||
@@ -172,7 +183,7 @@ typedef struct _GLFWwindowWayland
|
||||
struct zwp_idle_inhibitor_v1* idleInhibitor;
|
||||
|
||||
struct {
|
||||
bool serverSide;
|
||||
bool serverSide, buffer_destroyed;
|
||||
_GLFWdecorationSideWayland focus;
|
||||
_GLFWWaylandCSDEdge top, left, right, 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__':
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
# 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
|
||||
from hashlib import md5
|
||||
from typing import (
|
||||
TYPE_CHECKING, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Union
|
||||
)
|
||||
|
||||
from kitty.guess_mime_type import guess_type
|
||||
from typing import TYPE_CHECKING, Dict, List, Set, Optional, Iterator, Tuple, Union
|
||||
from kitty.utils import control_codes_pat
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .highlight import DiffHighlight # noqa
|
||||
@@ -37,6 +41,8 @@ class Segment:
|
||||
|
||||
class Collection:
|
||||
|
||||
ignore_names: Tuple[str, ...] = ()
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.changes: Dict[str, str] = {}
|
||||
self.renames: Dict[str, str] = {}
|
||||
@@ -105,22 +111,34 @@ def resolve_remote_name(path: str, default: str) -> str:
|
||||
return default
|
||||
|
||||
|
||||
def allowed_items(items: Sequence[str], ignore_patterns: Sequence[str]) -> Iterator[str]:
|
||||
for name in items:
|
||||
for pat in ignore_patterns:
|
||||
if fnmatch(name, pat):
|
||||
break
|
||||
else:
|
||||
yield name
|
||||
|
||||
|
||||
def walk(base: str, names: Set[str], pmap: Dict[str, str], ignore_names: Tuple[str, ...]) -> None:
|
||||
for dirpath, dirnames, filenames in os.walk(base):
|
||||
dirnames[:] = allowed_items(dirnames, ignore_names)
|
||||
for filename in allowed_items(filenames, ignore_names):
|
||||
path = os.path.abspath(os.path.join(dirpath, filename))
|
||||
path_name_map[path] = name = os.path.relpath(path, base)
|
||||
names.add(name)
|
||||
pmap[name] = path
|
||||
|
||||
|
||||
def collect_files(collection: Collection, left: str, right: str) -> None:
|
||||
left_names: Set[str] = set()
|
||||
right_names: Set[str] = set()
|
||||
left_path_map: Dict[str, str] = {}
|
||||
right_path_map: Dict[str, str] = {}
|
||||
|
||||
def walk(base: str, names: Set[str], pmap: Dict[str, str]) -> None:
|
||||
for dirpath, dirnames, filenames in os.walk(base):
|
||||
for filename in filenames:
|
||||
path = os.path.abspath(os.path.join(dirpath, filename))
|
||||
path_name_map[path] = name = os.path.relpath(path, base)
|
||||
names.add(name)
|
||||
pmap[name] = path
|
||||
walk(left, left_names, left_path_map, collection.ignore_names)
|
||||
walk(right, right_names, right_path_map, collection.ignore_names)
|
||||
|
||||
walk(left, left_names, left_path_map)
|
||||
walk(right, right_names, right_path_map)
|
||||
common_names = left_names & right_names
|
||||
changed_names = {n for n in common_names if data_for_path(left_path_map[n]) != data_for_path(right_path_map[n])}
|
||||
for n in changed_names:
|
||||
@@ -143,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)
|
||||
|
||||
@@ -655,6 +655,7 @@ def main(args: List[str]) -> None:
|
||||
opts = init_config(cli_opts)
|
||||
set_diff_command(opts.diff_cmd)
|
||||
lines_for_path.replace_tab_by = opts.replace_tab_by
|
||||
Collection.ignore_names = tuple(opts.ignore_name)
|
||||
left, right = map(get_remote_file, (left, right))
|
||||
if os.path.isdir(left) != os.path.isdir(right):
|
||||
raise SystemExit('The items to be diffed should both be either directories or files. Comparing a directory to a file is not valid.')
|
||||
|
||||
@@ -47,6 +47,21 @@ opt('replace_tab_by', '\\x20\\x20\\x20\\x20',
|
||||
option_type='python_string',
|
||||
long_text='The string to replace tabs with. Default is to use four spaces.'
|
||||
)
|
||||
|
||||
opt('+ignore_name', '',
|
||||
option_type='store_multiple',
|
||||
add_to_default=False,
|
||||
long_text='''
|
||||
A glob pattern that is matched against only the filename of files and directories. Matching
|
||||
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
|
||||
''',
|
||||
)
|
||||
|
||||
egr() # }}}
|
||||
|
||||
# colors {{{
|
||||
|
||||
7
kittens/diff/options/parse.py
generated
7
kittens/diff/options/parse.py
generated
@@ -1,7 +1,7 @@
|
||||
# generated by gen-config.py DO NOT edit
|
||||
|
||||
import typing
|
||||
from kittens.diff.options.utils import parse_map, syntax_aliases
|
||||
from kittens.diff.options.utils import parse_map, store_multiple, syntax_aliases
|
||||
from kitty.conf.utils import merge_dicts, positive_int, python_string, to_color, to_color_or_none
|
||||
|
||||
|
||||
@@ -37,6 +37,10 @@ class Parser:
|
||||
def hunk_margin_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['hunk_margin_bg'] = to_color(val)
|
||||
|
||||
def ignore_name(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
for k, v in store_multiple(val, ans["ignore_name"]):
|
||||
ans["ignore_name"][k] = v
|
||||
|
||||
def margin_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['margin_bg'] = to_color(val)
|
||||
|
||||
@@ -89,6 +93,7 @@ class Parser:
|
||||
|
||||
def create_result_dict() -> typing.Dict[str, typing.Any]:
|
||||
return {
|
||||
'ignore_name': {},
|
||||
'map': [],
|
||||
}
|
||||
|
||||
|
||||
3
kittens/diff/options/types.py
generated
3
kittens/diff/options/types.py
generated
@@ -20,6 +20,7 @@ option_names = ( # {{{
|
||||
'highlight_removed_bg',
|
||||
'hunk_bg',
|
||||
'hunk_margin_bg',
|
||||
'ignore_name',
|
||||
'map',
|
||||
'margin_bg',
|
||||
'margin_fg',
|
||||
@@ -64,6 +65,7 @@ class Options:
|
||||
syntax_aliases: typing.Dict[str, str] = {'pyj': 'py', 'pyi': 'py', 'recipe': 'py'}
|
||||
title_bg: Color = Color(255, 255, 255)
|
||||
title_fg: Color = Color(0, 0, 0)
|
||||
ignore_name: typing.Dict[str, str] = {}
|
||||
map: typing.List[typing.Tuple[kitty.types.ParsedShortcut, kitty.conf.utils.KeyAction]] = []
|
||||
key_definitions: KittensKeyMap = {}
|
||||
config_paths: typing.Tuple[str, ...] = ()
|
||||
@@ -116,6 +118,7 @@ class Options:
|
||||
|
||||
|
||||
defaults = Options()
|
||||
defaults.ignore_name = {}
|
||||
defaults.map = [
|
||||
# quit
|
||||
(ParsedShortcut(mods=0, key_name='q'), KeyAction('quit')), # noqa
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
from typing import Any, Dict, Iterable, Tuple, Union
|
||||
from typing import Any, Container, Dict, Iterable, Tuple, Union
|
||||
|
||||
from kitty.conf.utils import (
|
||||
KeyFuncWrapper, KittensKeyDefinition, parse_kittens_key
|
||||
@@ -58,6 +58,12 @@ def syntax_aliases(raw: str) -> Dict[str, str]:
|
||||
return ans
|
||||
|
||||
|
||||
def store_multiple(val: str, current_val: Container[str]) -> Iterable[Tuple[str, str]]:
|
||||
val = val.strip()
|
||||
if val not in current_val:
|
||||
yield val, val
|
||||
|
||||
|
||||
def parse_map(val: str) -> Iterable[KittensKeyDefinition]:
|
||||
x = parse_kittens_key(val, func_with_args.args_funcs)
|
||||
if x is not None:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,19 +9,20 @@ from functools import lru_cache
|
||||
from gettext import gettext as _
|
||||
from itertools import repeat
|
||||
from typing import (
|
||||
Any, Callable, Dict, Generator, Iterable, List, Optional, Pattern,
|
||||
Sequence, Set, Tuple, Type, cast
|
||||
Any, Callable, Dict, Generator, Iterable, Iterator, List, Optional,
|
||||
Pattern, Sequence, Set, Tuple, Type, cast
|
||||
)
|
||||
|
||||
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
|
||||
@@ -44,6 +45,7 @@ DEFAULT_HINT_ALPHABET = string.digits + string.ascii_lowercase
|
||||
DEFAULT_REGEX = r'(?m)^\s*(.+)\s*$'
|
||||
FILE_EXTENSION = r'\.(?:[a-zA-Z0-9]{2,7}|[ahcmo])(?!\.)'
|
||||
PATH_REGEX = fr'(?:\S*?/[\r\S]+)|(?:\S[\r\S]*{FILE_EXTENSION})\b'
|
||||
DEFAULT_LINENUM_REGEX = fr'(?P<path>{PATH_REGEX}):(?P<line>\d+)'
|
||||
|
||||
|
||||
class Mark:
|
||||
@@ -64,6 +66,10 @@ class Mark:
|
||||
self.is_hyperlink = is_hyperlink
|
||||
self.group_id = group_id
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (f'Mark(index={self.index!r}, start={self.start!r}, end={self.end!r},'
|
||||
f' text={self.text!r}, groupdict={self.groupdict!r}, is_hyperlink={self.is_hyperlink!r}, group_id={self.group_id!r})')
|
||||
|
||||
|
||||
@lru_cache(maxsize=2048)
|
||||
def encode_hint(num: int, alphabet: str) -> str:
|
||||
@@ -225,14 +231,14 @@ class Hints(Handler):
|
||||
self.write(self.current_text)
|
||||
|
||||
|
||||
def regex_finditer(pat: 'Pattern[str]', minimum_match_length: int, text: str) -> Generator[Tuple[int, int, Dict[str, str]], None, None]:
|
||||
def regex_finditer(pat: 'Pattern[str]', minimum_match_length: int, text: str) -> Iterator[Tuple[int, int, 're.Match[str]']]:
|
||||
has_named_groups = bool(pat.groupindex)
|
||||
for m in pat.finditer(text):
|
||||
s, e = m.span(0 if has_named_groups else pat.groups)
|
||||
while e > s + 1 and text[e-1] == '\0':
|
||||
e -= 1
|
||||
if e - s >= minimum_match_length:
|
||||
yield s, e, m.groupdict()
|
||||
yield s, e, m
|
||||
|
||||
|
||||
closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>', '*': '*', '"': '"', "'": "'", "“": "”", "‘": "’"}
|
||||
@@ -318,16 +324,22 @@ def ip(text: str, s: int, e: int) -> Tuple[int, int]:
|
||||
return s, e
|
||||
|
||||
|
||||
def mark(pattern: str, post_processors: Iterable[PostprocessorFunc], text: str, args: HintsCLIOptions) -> Generator[Mark, None, None]:
|
||||
def mark(pattern: str, post_processors: Iterable[PostprocessorFunc], text: str, args: HintsCLIOptions) -> Iterator[Mark]:
|
||||
pat = re.compile(pattern)
|
||||
for idx, (s, e, groupdict) in enumerate(regex_finditer(pat, args.minimum_match_length, text)):
|
||||
sanitize_pat = re.compile('[\r\n\0]')
|
||||
for idx, (s, e, match_object) in enumerate(regex_finditer(pat, args.minimum_match_length, text)):
|
||||
try:
|
||||
for func in post_processors:
|
||||
s, e = func(text, s, e)
|
||||
except InvalidMatch:
|
||||
continue
|
||||
|
||||
mark_text = re.sub('[\r\n\0]', '', text[s:e])
|
||||
groupdict = match_object.groupdict()
|
||||
for group_name in groupdict:
|
||||
group_idx = pat.groupindex[group_name]
|
||||
gs, ge = match_object.span(group_idx)
|
||||
gs, ge = max(gs, s), min(ge, e)
|
||||
groupdict[group_name] = sanitize_pat.sub('', text[gs:ge])
|
||||
mark_text = sanitize_pat.sub('', text[s:e])
|
||||
yield Mark(idx, s, e, mark_text, groupdict)
|
||||
|
||||
|
||||
@@ -400,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:
|
||||
@@ -420,7 +435,7 @@ def parse_input(text: str) -> str:
|
||||
def linenum_marks(text: str, args: HintsCLIOptions, Mark: Type[Mark], extra_cli_args: Sequence[str], *a: Any) -> Generator[Mark, None, None]:
|
||||
regex = args.regex
|
||||
if regex == DEFAULT_REGEX:
|
||||
regex = fr'(?P<path>{PATH_REGEX}):(?P<line>\d+)'
|
||||
regex = DEFAULT_LINENUM_REGEX
|
||||
yield from mark(regex, [brackets, quotes], text, args)
|
||||
|
||||
|
||||
@@ -525,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
|
||||
@@ -699,18 +729,14 @@ def main(args: List[str]) -> Optional[Dict[str, Any]]:
|
||||
|
||||
|
||||
def linenum_process_result(data: Dict[str, Any]) -> Tuple[str, int]:
|
||||
pat = re.compile(r':(\d+)$')
|
||||
for m, g in zip(data['match'], data['groupdicts']):
|
||||
if m:
|
||||
path, line = g['path'], g['line']
|
||||
# look for trailers on path of the for :number
|
||||
while True:
|
||||
m = pat.search(path)
|
||||
if m is None:
|
||||
break
|
||||
line = m.group(1)
|
||||
path = path[:-len(m.group())]
|
||||
|
||||
lnum_pat = re.compile(r'(:\d+)$')
|
||||
for match, g in zip(data['match'], data['groupdicts']):
|
||||
path, line = g['path'], g['line']
|
||||
if path and line:
|
||||
m = lnum_pat.search(path)
|
||||
if m is not None:
|
||||
line = m.group(1)[1:]
|
||||
path = path.rpartition(':')[0]
|
||||
return os.path.expanduser(path), int(line)
|
||||
return '', -1
|
||||
|
||||
@@ -796,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':
|
||||
@@ -804,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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Callable, Dict, Iterable, Iterator, Sequence, Tuple
|
||||
|
||||
from kitty.complete import Completions, complete_files_and_dirs, debug
|
||||
from kitty.types import run_once
|
||||
from .utils import ssh_options
|
||||
|
||||
debug
|
||||
|
||||
@@ -71,47 +72,6 @@ def known_hosts() -> Tuple[str, ...]:
|
||||
return tuple(sorted(filter(lambda x: '*' not in x and '[' not in x, set(iter_known_hosts()))))
|
||||
|
||||
|
||||
@run_once
|
||||
def ssh_options() -> Dict[str, str]:
|
||||
try:
|
||||
p = subprocess.run(['ssh'], stderr=subprocess.PIPE, encoding='utf-8')
|
||||
raw = p.stderr or ''
|
||||
except FileNotFoundError:
|
||||
return {
|
||||
'4': '', '6': '', 'A': '', 'a': '', 'C': '', 'f': '', 'G': '', 'g': '', 'K': '', 'k': '',
|
||||
'M': '', 'N': '', 'n': '', 'q': '', 's': '', 'T': '', 't': '', 'V': '', 'v': '', 'X': '',
|
||||
'x': '', 'Y': '', 'y': '', 'B': 'bind_interface', 'b': 'bind_address', 'c': 'cipher_spec',
|
||||
'D': '[bind_address:]port', 'E': 'log_file', 'e': 'escape_char', 'F': 'configfile', 'I': 'pkcs11',
|
||||
'i': 'identity_file', 'J': '[user@]host[:port]', 'L': 'address', 'l': 'login_name', 'm': 'mac_spec',
|
||||
'O': 'ctl_cmd', 'o': 'option', 'p': 'port', 'Q': 'query_option', 'R': 'address',
|
||||
'S': 'ctl_path', 'W': 'host:port', 'w': 'local_tun[:remote_tun]'
|
||||
}
|
||||
|
||||
ans: Dict[str, str] = {}
|
||||
pos = 0
|
||||
while True:
|
||||
pos = raw.find('[', pos)
|
||||
if pos < 0:
|
||||
break
|
||||
num = 1
|
||||
epos = pos
|
||||
while num > 0:
|
||||
epos += 1
|
||||
if raw[epos] not in '[]':
|
||||
continue
|
||||
num += 1 if raw[epos] == '[' else -1
|
||||
q = raw[pos+1:epos]
|
||||
pos = epos
|
||||
if len(q) < 2 or q[0] != '-':
|
||||
continue
|
||||
if ' ' in q:
|
||||
opt, desc = q.split(' ', 1)
|
||||
ans[opt[1:]] = desc
|
||||
else:
|
||||
ans.update(dict.fromkeys(q[1:], ''))
|
||||
return ans
|
||||
|
||||
|
||||
# option help {{{
|
||||
@run_once
|
||||
def option_help_map() -> Dict[str, str]:
|
||||
@@ -331,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:
|
||||
@@ -342,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,26 +27,28 @@ 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
|
||||
)
|
||||
from kitty.options.types import Options
|
||||
from kitty.shell_integration import as_str_literal
|
||||
from kitty.shm import SharedMemory
|
||||
from kitty.types import run_once
|
||||
from kitty.utils import (
|
||||
SSHConnectionData, expandvars, resolve_abs_or_config_path,
|
||||
set_echo as turn_off_echo, suppress_error_logging
|
||||
set_echo as turn_off_echo
|
||||
)
|
||||
|
||||
from .completion import complete, ssh_options
|
||||
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
|
||||
from .options.utils import DELETE_ENV_VAR
|
||||
from .utils import create_shared_memory
|
||||
from .utils import create_shared_memory, ssh_options
|
||||
|
||||
|
||||
@run_once
|
||||
@@ -111,18 +113,14 @@ def serialize_env(literal_env: Dict[str, str], env: Dict[str, str], base_env: Di
|
||||
return '\n'.join(lines).encode('utf-8')
|
||||
|
||||
|
||||
@run_once
|
||||
def kitty_opts() -> Options:
|
||||
from kitty.cli import create_default_opts
|
||||
with suppress_error_logging():
|
||||
return create_default_opts()
|
||||
|
||||
|
||||
def make_tarfile(ssh_opts: SSHOptions, base_env: Dict[str, str], compression: str = 'gz', literal_env: Dict[str, str] = {}) -> bytes:
|
||||
|
||||
def normalize_tarinfo(tarinfo: tarfile.TarInfo) -> tarfile.TarInfo:
|
||||
tarinfo.uname = tarinfo.gname = ''
|
||||
tarinfo.uid = tarinfo.gid = 0
|
||||
# some distro's like nix mess with installed file permissions so ensure
|
||||
# files are at least readable and writable by owning user
|
||||
tarinfo.mode |= stat.S_IWUSR | stat.S_IRUSR
|
||||
return tarinfo
|
||||
|
||||
def add_data_as_file(tf: tarfile.TarFile, arcname: str, data: Union[str, bytes]) -> tarfile.TarInfo:
|
||||
@@ -152,6 +150,7 @@ def make_tarfile(ssh_opts: SSHOptions, base_env: Dict[str, str], compression: st
|
||||
if ssh_opts.shell_integration == 'inherited':
|
||||
ksi = get_effective_ksi_env_var(kitty_opts())
|
||||
else:
|
||||
from kitty.options.types import Options
|
||||
from kitty.options.utils import shell_integration
|
||||
ksi = get_effective_ksi_env_var(Options({'shell_integration': shell_integration(ssh_opts.shell_integration)}))
|
||||
|
||||
@@ -172,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:
|
||||
@@ -213,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')
|
||||
@@ -413,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
|
||||
@@ -512,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
|
||||
@@ -551,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
|
||||
@@ -575,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())
|
||||
@@ -584,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
|
||||
@@ -673,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:
|
||||
@@ -703,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
|
||||
@@ -720,18 +736,26 @@ 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__':
|
||||
main(sys.argv)
|
||||
elif __name__ == '__completer__':
|
||||
from .completion import complete
|
||||
setattr(sys, 'kitten_completer', complete)
|
||||
elif __name__ == '__conf__':
|
||||
from .options.definition import definition
|
||||
|
||||
@@ -3,15 +3,61 @@
|
||||
|
||||
|
||||
import os
|
||||
from typing import Any, Dict, List
|
||||
import subprocess
|
||||
from typing import Any, Dict, List, Sequence
|
||||
|
||||
from kitty.types import run_once
|
||||
|
||||
|
||||
def is_kitten_cmdline(q: List[str]) -> bool:
|
||||
@run_once
|
||||
def ssh_options() -> Dict[str, str]:
|
||||
try:
|
||||
p = subprocess.run(['ssh'], stderr=subprocess.PIPE, encoding='utf-8')
|
||||
raw = p.stderr or ''
|
||||
except FileNotFoundError:
|
||||
return {
|
||||
'4': '', '6': '', 'A': '', 'a': '', 'C': '', 'f': '', 'G': '', 'g': '', 'K': '', 'k': '',
|
||||
'M': '', 'N': '', 'n': '', 'q': '', 's': '', 'T': '', 't': '', 'V': '', 'v': '', 'X': '',
|
||||
'x': '', 'Y': '', 'y': '', 'B': 'bind_interface', 'b': 'bind_address', 'c': 'cipher_spec',
|
||||
'D': '[bind_address:]port', 'E': 'log_file', 'e': 'escape_char', 'F': 'configfile', 'I': 'pkcs11',
|
||||
'i': 'identity_file', 'J': '[user@]host[:port]', 'L': 'address', 'l': 'login_name', 'm': 'mac_spec',
|
||||
'O': 'ctl_cmd', 'o': 'option', 'p': 'port', 'Q': 'query_option', 'R': 'address',
|
||||
'S': 'ctl_path', 'W': 'host:port', 'w': 'local_tun[:remote_tun]'
|
||||
}
|
||||
|
||||
ans: Dict[str, str] = {}
|
||||
pos = 0
|
||||
while True:
|
||||
pos = raw.find('[', pos)
|
||||
if pos < 0:
|
||||
break
|
||||
num = 1
|
||||
epos = pos
|
||||
while num > 0:
|
||||
epos += 1
|
||||
if raw[epos] not in '[]':
|
||||
continue
|
||||
num += 1 if raw[epos] == '[' else -1
|
||||
q = raw[pos+1:epos]
|
||||
pos = epos
|
||||
if len(q) < 2 or q[0] != '-':
|
||||
continue
|
||||
if ' ' in q:
|
||||
opt, desc = q.split(' ', 1)
|
||||
ans[opt[1:]] = desc
|
||||
else:
|
||||
ans.update(dict.fromkeys(q[1:], ''))
|
||||
return ans
|
||||
|
||||
|
||||
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:
|
||||
|
||||
@@ -494,6 +494,7 @@ class Theme:
|
||||
is_dark: bool = False
|
||||
blurb: str = ''
|
||||
num_settings: int = 0
|
||||
is_user_defined: bool = False
|
||||
|
||||
def apply_dict(self, d: Dict[str, Any]) -> None:
|
||||
self.name = str(d['name'])
|
||||
@@ -501,7 +502,7 @@ class Theme:
|
||||
a = d.get(x)
|
||||
if isinstance(a, str):
|
||||
setattr(self, x, a)
|
||||
for x in ('is_dark', 'num_settings'):
|
||||
for x in ('is_dark', 'num_settings', 'is_user_defined'):
|
||||
a = d.get(x)
|
||||
if isinstance(a, int):
|
||||
setattr(self, x, a)
|
||||
@@ -594,6 +595,7 @@ class Themes:
|
||||
d = parse_theme(name, raw)
|
||||
except (Exception, SystemExit):
|
||||
continue
|
||||
d['is_user_defined'] = True
|
||||
t = Theme(raw.__str__)
|
||||
t.apply_dict(d)
|
||||
if t.name:
|
||||
|
||||
@@ -69,6 +69,10 @@ def create_recent_filter(names: Iterable[str]) -> Callable[[Theme], bool]:
|
||||
return recent_filter
|
||||
|
||||
|
||||
def user_filter(q: Theme) -> bool:
|
||||
return q.is_user_defined
|
||||
|
||||
|
||||
def mark_shortcut(text: str, acc: str) -> str:
|
||||
acc_idx = text.lower().index(acc.lower())
|
||||
return text[:acc_idx] + styled(text[acc_idx], underline='straight', bold=True, fg_intense=True) + text[acc_idx+1:]
|
||||
@@ -153,12 +157,13 @@ class ThemesHandler(Handler):
|
||||
self.report_traceback_on_exit: Optional[str] = None
|
||||
self.filter_map: Dict[str, Callable[[Theme], bool]] = {
|
||||
'dark': dark_filter, 'light': light_filter, 'all': all_filter,
|
||||
'recent': create_recent_filter(self.cached_values.get('recent', ()))
|
||||
'recent': create_recent_filter(self.cached_values.get('recent', ())),
|
||||
'user': user_filter
|
||||
}
|
||||
self.themes_list = ThemesList()
|
||||
self.colors_set_once = False
|
||||
self.line_edit = LineEdit()
|
||||
self.tabs = tuple('all dark light recent'.split())
|
||||
self.tabs = tuple('all dark light recent user'.split())
|
||||
self.quit_on_next_key_release = -1
|
||||
|
||||
def update_recent(self) -> None:
|
||||
@@ -389,7 +394,7 @@ class ThemesHandler(Handler):
|
||||
if key_event.matches('esc') or key_event.matches_text('q'):
|
||||
self.quit_on_next_key_release = 0
|
||||
return
|
||||
for cat in 'all dark light recent'.split():
|
||||
for cat in self.tabs:
|
||||
if key_event.matches_text(cat[0]) or key_event.matches(f'alt+{cat[0]}'):
|
||||
if cat != self.current_category:
|
||||
self.current_category = cat
|
||||
|
||||
@@ -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,12 +1,18 @@
|
||||
#!/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 Tuple
|
||||
from typing import TYPE_CHECKING, Optional, Sequence, Tuple, cast
|
||||
|
||||
from kitty.types import run_once
|
||||
|
||||
from .operations import raw_mode, set_cursor_visible
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from kitty.options.types import Options
|
||||
|
||||
|
||||
def get_key_press(allowed: str, default: str) -> str:
|
||||
response = default
|
||||
@@ -50,6 +56,31 @@ def human_size(
|
||||
return format_number(size / 1024**exponent, max_num_of_decimals) + sep + unit_list[exponent]
|
||||
|
||||
|
||||
def kitty_opts() -> 'Options':
|
||||
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():
|
||||
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:
|
||||
' Report an error also sending the overlay ready message to ensure kitten is visible '
|
||||
from .operations import overlay_ready
|
||||
@@ -69,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 +0,0 @@
|
||||
# empty
|
||||
|
||||
@@ -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:
|
||||
|
||||
455
kitty/boss.py
455
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,10 +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)
|
||||
self._run_kitten('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'})
|
||||
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_, 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
|
||||
@@ -660,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('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
|
||||
@@ -677,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('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
|
||||
@@ -732,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')
|
||||
@@ -1073,7 +1255,7 @@ class Boss:
|
||||
w = self.active_window
|
||||
if w is None:
|
||||
return
|
||||
overlay_window = self._run_kitten('resize_window', args=[
|
||||
overlay_window = self.run_kitten_with_metadata('resize_window', args=[
|
||||
f'--horizontal-increment={get_options().window_resize_step_cells}',
|
||||
f'--vertical-increment={get_options().window_resize_step_lines}'
|
||||
])
|
||||
@@ -1099,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:
|
||||
@@ -1333,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(
|
||||
@@ -1419,10 +1606,10 @@ class Boss:
|
||||
|
||||
@ac('misc', 'Run the specified kitten. See :doc:`/kittens/custom` for details')
|
||||
def kitten(self, kitten: str, *kargs: str) -> None:
|
||||
self._run_kitten(kitten, kargs)
|
||||
self.run_kitten_with_metadata(kitten, kargs)
|
||||
|
||||
def run_kitten(self, kitten: str, *args: str) -> None:
|
||||
self._run_kitten(kitten, args)
|
||||
self.run_kitten_with_metadata(kitten, args)
|
||||
|
||||
def on_kitten_finish(
|
||||
self, target_window_id: int, end_kitten: Callable[[Dict[str, Any], int, 'Boss'], None],
|
||||
@@ -1437,16 +1624,34 @@ class Boss:
|
||||
|
||||
@ac('misc', 'Input an arbitrary unicode character. See :doc:`/kittens/unicode_input` for details.')
|
||||
def input_unicode_character(self) -> None:
|
||||
self._run_kitten('unicode_input')
|
||||
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)]
|
||||
self._run_kitten('ask', args)
|
||||
self.run_kitten_with_metadata('ask', args)
|
||||
|
||||
def do_set_tab_title(self, title: str, tab_id: int) -> None:
|
||||
tm = self.active_tab_manager
|
||||
@@ -1463,7 +1668,7 @@ class Boss:
|
||||
if ec != (None, None, None):
|
||||
import traceback
|
||||
tb = traceback.format_exc()
|
||||
self._run_kitten('show_error', args=['--title', title], input_data=json.dumps({'msg': msg, 'tb': tb}))
|
||||
self.run_kitten_with_metadata('show_error', args=['--title', title], input_data=json.dumps({'msg': msg, 'tb': tb}))
|
||||
|
||||
@ac('mk', 'Create a new marker')
|
||||
def create_marker(self) -> None:
|
||||
@@ -1483,7 +1688,7 @@ class Boss:
|
||||
except Exception as err:
|
||||
self.show_error(_('Invalid marker specification'), str(err))
|
||||
|
||||
self._run_kitten('ask', [
|
||||
self.run_kitten_with_metadata('ask', [
|
||||
'--name=create-marker', '--message',
|
||||
_('Create marker, for example:\ntext 1 ERROR\nSee {}\n').format(website_url('marks'))
|
||||
],
|
||||
@@ -1495,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
|
||||
@@ -1545,11 +1753,23 @@ 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:
|
||||
self._run_kitten('hints')
|
||||
self.run_kitten_with_metadata('hints')
|
||||
|
||||
def drain_actions(self, actions: List[KeyAction], window_for_dispatch: Optional[Window] = None, dispatch_type: str = 'KeyPress') -> None:
|
||||
|
||||
@@ -1601,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
|
||||
|
||||
@@ -1733,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)
|
||||
@@ -1805,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)
|
||||
@@ -1826,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:
|
||||
@@ -1835,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
|
||||
@@ -1953,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
|
||||
@@ -1964,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
|
||||
@@ -1974,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):
|
||||
@@ -2037,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
|
||||
''')
|
||||
@@ -2139,7 +2381,7 @@ class Boss:
|
||||
def done2(target_window_id: int, self: Boss) -> None:
|
||||
callback(ans)
|
||||
|
||||
q = self._run_kitten(
|
||||
q = self.run_kitten_with_metadata(
|
||||
'hints', args=(
|
||||
'--ascending', '--customize-processing=::import::kitty.choose_entry',
|
||||
'--window-title', title,
|
||||
@@ -2245,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', '''
|
||||
@@ -2285,8 +2530,8 @@ class Boss:
|
||||
|
||||
if failures:
|
||||
from kittens.tui.operations import styled
|
||||
spec = '\n '.join(styled(u, fg='red') for u in failures)
|
||||
bdata = f"Unknown URL type, cannot open:\n\n {spec}".encode('utf-8')
|
||||
spec = '\n '.join(styled(u, fg='yellow') for u in failures)
|
||||
bdata = json.dumps({'msg': f"Unknown URL type, cannot open:\n {spec}"}).encode('utf-8')
|
||||
special_window = SpecialWindow([kitty_exe(), '+kitten', 'show_error', '--title', 'Open URL Error'], bdata, 'Open URL Error')
|
||||
if needs_window_replaced and tab is not None:
|
||||
tab.new_special_window(special_window)
|
||||
|
||||
@@ -20,16 +20,21 @@
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <signal.h>
|
||||
extern PyTypeObject Screen_Type;
|
||||
|
||||
#if defined(__APPLE__) || defined(__OpenBSD__)
|
||||
#define NO_SIGQUEUE 1
|
||||
#endif
|
||||
|
||||
#ifdef DEBUG_EVENT_LOOP
|
||||
#define EVDBG(...) log_event(__VA_ARGS__)
|
||||
#else
|
||||
#define EVDBG(...)
|
||||
#endif
|
||||
|
||||
#define EXTRA_FDS 2
|
||||
#define EXTRA_FDS 3
|
||||
#ifndef MSG_NOSIGNAL
|
||||
// Apple does not implement MSG_NOSIGNAL
|
||||
#define MSG_NOSIGNAL 0
|
||||
@@ -52,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;
|
||||
@@ -119,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;
|
||||
@@ -133,10 +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)) return PyErr_SetFromErrno(PyExc_OSError);
|
||||
if (!install_signal_handlers(&self->io_loop_data)) 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) {
|
||||
@@ -145,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;
|
||||
@@ -171,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");
|
||||
@@ -669,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;
|
||||
@@ -740,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);
|
||||
@@ -749,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);
|
||||
@@ -801,11 +814,25 @@ free_twd(ThreadWriteData *x) {
|
||||
free(x);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
sig_queue(PyObject *self UNUSED, PyObject *args) {
|
||||
int pid, signal, value;
|
||||
if (!PyArg_ParseTuple(args, "iii", &pid, &signal, &value)) return NULL;
|
||||
#ifdef NO_SIGQUEUE
|
||||
if (kill(pid, signal) != 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; }
|
||||
#else
|
||||
union sigval v;
|
||||
v.sival_int = value;
|
||||
if (sigqueue(pid, signal, v) != 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; }
|
||||
#endif
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
monitor_pid(PyObject *self UNUSED, PyObject *args) {
|
||||
long pid;
|
||||
int pid;
|
||||
bool ok = true;
|
||||
if (!PyArg_ParseTuple(args, "l", &pid)) return NULL;
|
||||
if (!PyArg_ParseTuple(args, "i", &pid)) return NULL;
|
||||
children_mutex(lock);
|
||||
if (monitored_pids_count >= arraysz(monitored_pids)) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Too many monitored pids");
|
||||
@@ -940,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;
|
||||
}
|
||||
@@ -1223,12 +1250,13 @@ read_bytes(int fd, Screen *screen) {
|
||||
|
||||
typedef struct { bool kill_signal, child_died, reload_config; } SignalSet;
|
||||
|
||||
static void
|
||||
handle_signal(int signum, void *data) {
|
||||
static bool
|
||||
handle_signal(const siginfo_t *siginfo, void *data) {
|
||||
SignalSet *ss = data;
|
||||
switch(signum) {
|
||||
switch(siginfo->si_signo) {
|
||||
case SIGINT:
|
||||
case SIGTERM:
|
||||
case SIGHUP:
|
||||
ss->kill_signal = true;
|
||||
break;
|
||||
case SIGCHLD:
|
||||
@@ -1237,9 +1265,13 @@ handle_signal(int signum, void *data) {
|
||||
case SIGUSR1:
|
||||
ss->reload_config = true;
|
||||
break;
|
||||
case SIGUSR2:
|
||||
log_error("Received SIGUSR2: %d\n", siginfo->si_value.sival_int);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -1269,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;
|
||||
@@ -1359,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;
|
||||
@@ -1416,7 +1479,7 @@ io_loop(void *data) {
|
||||
|
||||
typedef struct {
|
||||
id_type id;
|
||||
size_t num_of_unresponded_messages_sent_to_main_thread;
|
||||
size_t num_of_unresponded_messages_sent_to_main_thread, fd_array_idx;
|
||||
bool finished_reading;
|
||||
int fd;
|
||||
struct {
|
||||
@@ -1570,8 +1633,7 @@ wakeup_talk_loop(bool in_signal_handler) {
|
||||
|
||||
static void
|
||||
prune_peers(void) {
|
||||
for (size_t i = 0; i < talk_data.num_peers; i++) {
|
||||
size_t idx = talk_data.num_peers - 1 - i;
|
||||
for (size_t idx = talk_data.num_peers; idx-- > 0;) {
|
||||
Peer *p = talk_data.peers + idx;
|
||||
if (p->read.finished && !p->num_of_unresponded_messages_sent_to_main_thread && !p->write.used) {
|
||||
free_peer(p);
|
||||
@@ -1585,7 +1647,7 @@ talk_loop(void *data) {
|
||||
// The talk thread loop
|
||||
ChildMonitor *self = (ChildMonitor*)data;
|
||||
set_thread_name("KittyPeerMon");
|
||||
if (!init_loop_data(&talk_data.loop_data)) { log_error("Failed to create wakeup fd for talk thread with error: %s", strerror(errno)); }
|
||||
if (!init_loop_data(&talk_data.loop_data, 0)) { log_error("Failed to create wakeup fd for talk thread with error: %s", strerror(errno)); }
|
||||
PollFD fds[PEER_LIMIT + 8] = {{0}};
|
||||
size_t num_listen_fds = 0, num_peer_fds = 0;
|
||||
#define add_listener(which) \
|
||||
@@ -1604,13 +1666,14 @@ talk_loop(void *data) {
|
||||
for (size_t i = 0; i < talk_data.num_peers; i++) {
|
||||
Peer *p = talk_data.peers + i;
|
||||
if (!p->read.finished || p->write.used) {
|
||||
fds[num_listen_fds + num_peer_fds].fd = p->fd;
|
||||
fds[num_listen_fds + num_peer_fds].revents = 0;
|
||||
p->fd_array_idx = num_listen_fds + num_peer_fds++;
|
||||
fds[p->fd_array_idx].fd = p->fd;
|
||||
fds[p->fd_array_idx].revents = 0;
|
||||
int flags = 0;
|
||||
if (!p->read.finished) flags |= POLLIN;
|
||||
if (p->write.used) flags |= POLLOUT;
|
||||
fds[num_listen_fds + num_peer_fds++].events = flags;
|
||||
}
|
||||
fds[p->fd_array_idx].events = flags;
|
||||
} else p->fd_array_idx = 0;
|
||||
}
|
||||
talk_mutex(unlock);
|
||||
}
|
||||
@@ -1625,13 +1688,16 @@ talk_loop(void *data) {
|
||||
if (fds[num_listen_fds - 1].revents & POLLIN) {
|
||||
drain_fd(fds[num_listen_fds - 1].fd); // wakeup
|
||||
}
|
||||
for (size_t i = num_listen_fds, k = 0; i < num_peer_fds + num_listen_fds; i++, k++) {
|
||||
for (size_t k = 0; k < talk_data.num_peers; k++) {
|
||||
Peer *p = talk_data.peers + k;
|
||||
if (fds[i].revents & (POLLIN | POLLHUP)) read_from_peer(self, p);
|
||||
if (fds[i].revents & POLLOUT) write_to_peer(p);
|
||||
if (fds[i].revents & POLLNVAL) {
|
||||
p->read.finished = true;
|
||||
p->write.failed = true; p->write.used = 0;
|
||||
if (p->fd_array_idx) {
|
||||
if (fds[p->fd_array_idx].revents & (POLLIN | POLLHUP)) read_from_peer(self, p);
|
||||
if (fds[p->fd_array_idx].revents & POLLOUT) write_to_peer(p);
|
||||
if (fds[p->fd_array_idx].revents & POLLNVAL) {
|
||||
p->read.finished = true;
|
||||
p->write.failed = true; p->write.used = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (ret < 0) { if (errno != EAGAIN && errno != EINTR) perror("poll() on talk fds failed"); }
|
||||
@@ -1659,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;
|
||||
@@ -1682,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 */
|
||||
};
|
||||
@@ -1720,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;
|
||||
@@ -1729,13 +1797,34 @@ 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),
|
||||
METHODB(send_data_to_peer, METH_VARARGS),
|
||||
METHODB(cocoa_set_menubar_title, METH_VARARGS),
|
||||
{"sigqueue", (PyCFunction)sig_queue, METH_VARARGS, ""},
|
||||
{NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
@@ -1745,6 +1834,11 @@ init_child_monitor(PyObject *module) {
|
||||
if (PyModule_AddObject(module, "ChildMonitor", (PyObject *)&ChildMonitor_Type) != 0) return false;
|
||||
Py_INCREF(&ChildMonitor_Type);
|
||||
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
||||
#ifdef NO_SIGQUEUE
|
||||
PyModule_AddIntConstant(module, "has_sigqueue", 0);
|
||||
#else
|
||||
PyModule_AddIntConstant(module, "has_sigqueue", 1);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,34 +100,43 @@ spawn(PyObject *self UNUSED, PyObject *args) {
|
||||
#if PY_VERSION_HEX >= 0x03070000
|
||||
PyOS_AfterFork_Child();
|
||||
#endif
|
||||
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);
|
||||
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
|
||||
#ifdef SIGPIPE
|
||||
SA(SIGPIPE)
|
||||
#endif
|
||||
#ifdef SIGXFSZ
|
||||
SA(SIGXFSZ);
|
||||
#endif
|
||||
#ifdef SIGXFZ
|
||||
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__);
|
||||
@@ -139,9 +150,6 @@ spawn(PyObject *self UNUSED, PyObject *args) {
|
||||
for (int c = 3; c < 201; c++) safe_close(c, __FILE__, __LINE__);
|
||||
|
||||
environ = env;
|
||||
// for some reason SIGPIPE is set to SIG_IGN, so reset it, needed by bash,
|
||||
// which does not reset signal handlers on its own
|
||||
signal(SIGPIPE, SIG_DFL);
|
||||
execvp(exe, argv);
|
||||
// Report the failure and exec a shell instead, so that we are not left
|
||||
// with a forked but not exec'ed process
|
||||
@@ -176,14 +184,36 @@ spawn(PyObject *self UNUSED, PyObject *args) {
|
||||
return PyLong_FromLong(pid);
|
||||
}
|
||||
|
||||
#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
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
METHODB(spawn, 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;
|
||||
}
|
||||
|
||||
149
kitty/child.py
149
kitty/child.py
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import fcntl
|
||||
import os
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
@@ -14,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
|
||||
@@ -43,11 +43,11 @@ if is_macos:
|
||||
ans[pgid].append(pid)
|
||||
return ans
|
||||
|
||||
def cmdline_of_process(pid: int) -> List[str]:
|
||||
def cmdline_of_pid(pid: int) -> List[str]:
|
||||
return cmdline_(pid)
|
||||
else:
|
||||
|
||||
def cmdline_of_process(pid: int) -> List[str]:
|
||||
def cmdline_of_pid(pid: int) -> List[str]:
|
||||
with open(f'/proc/{pid}/cmdline', 'rb') as f:
|
||||
return list(filter(None, f.read().decode('utf-8').split('\0')))
|
||||
|
||||
@@ -143,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:
|
||||
fcntl.fcntl(fd, fcntl.F_SETFD, fcntl.fcntl(fd, fcntl.F_GETFD) & ~fcntl.FD_CLOEXEC)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -176,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
|
||||
|
||||
@@ -194,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,
|
||||
@@ -207,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:
|
||||
@@ -227,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
|
||||
@@ -262,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:
|
||||
@@ -290,23 +320,49 @@ 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 pid == self.pid and (not ans or (self.is_prewarmed and ans == cmdline_of_prewarmer())):
|
||||
ans = list(self.argv)
|
||||
return ans
|
||||
|
||||
@property
|
||||
def foreground_processes(self) -> List[ProcessDesc]:
|
||||
if self.child_fd is None:
|
||||
@@ -318,7 +374,7 @@ class Child:
|
||||
def process_desc(pid: int) -> ProcessDesc:
|
||||
ans: ProcessDesc = {'pid': pid, 'cmdline': None, 'cwd': None}
|
||||
with suppress(Exception):
|
||||
ans['cmdline'] = cmdline_of_process(pid)
|
||||
ans['cmdline'] = self.cmdline_of_pid(pid)
|
||||
with suppress(Exception):
|
||||
ans['cwd'] = cwd_of_process(pid) or None
|
||||
return ans
|
||||
@@ -331,7 +387,7 @@ class Child:
|
||||
def cmdline(self) -> List[str]:
|
||||
try:
|
||||
assert self.pid is not None
|
||||
return cmdline_of_process(self.pid) or list(self.argv)
|
||||
return self.cmdline_of_pid(self.pid) or list(self.argv)
|
||||
except Exception:
|
||||
return list(self.argv)
|
||||
|
||||
@@ -339,7 +395,7 @@ class Child:
|
||||
def foreground_cmdline(self) -> List[str]:
|
||||
try:
|
||||
assert self.pid_for_cwd is not None
|
||||
return cmdline_of_process(self.pid_for_cwd) or self.cmdline
|
||||
return self.cmdline_of_pid(self.pid_for_cwd) or self.cmdline
|
||||
except Exception:
|
||||
return self.cmdline
|
||||
|
||||
@@ -403,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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user