Compare commits
753 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d8d669a43 | ||
|
|
f4d44f30b4 | ||
|
|
4c77b0c562 | ||
|
|
2f367fa90c | ||
|
|
970ec9c839 | ||
|
|
be52f29792 | ||
|
|
8ae2f06828 | ||
|
|
601b2a05ac | ||
|
|
b78fbb4521 | ||
|
|
c40ef01445 | ||
|
|
ab8a4c6b9f | ||
|
|
dd331ca12e | ||
|
|
c3b23679f3 | ||
|
|
d0398dca28 | ||
|
|
e66c732b41 | ||
|
|
e7b216ba68 | ||
|
|
838a85ee8b | ||
|
|
3ec93e0b4e | ||
|
|
e8a9bbb51d | ||
|
|
25c2a0e241 | ||
|
|
de12dddbef | ||
|
|
24eeeedb1f | ||
|
|
c2bc24de35 | ||
|
|
21446e36c1 | ||
|
|
0f734719e7 | ||
|
|
f3c96f308d | ||
|
|
8ff841f1ac | ||
|
|
709c4abb53 | ||
|
|
c1f06941f5 | ||
|
|
ecb74aed29 | ||
|
|
c72154782b | ||
|
|
9520006466 | ||
|
|
40f04284d4 | ||
|
|
832534aac9 | ||
|
|
6cc89416ce | ||
|
|
b3fc2cb118 | ||
|
|
69efd9801b | ||
|
|
c07f164154 | ||
|
|
a2d1140229 | ||
|
|
bae9b095b4 | ||
|
|
efe6ff6188 | ||
|
|
3257fdf24f | ||
|
|
3284d71300 | ||
|
|
cdb3e2c1cd | ||
|
|
57cffc71b7 | ||
|
|
a4f1caeb4e | ||
|
|
45de091ee6 | ||
|
|
00c1802319 | ||
|
|
02c2d24360 | ||
|
|
499c255e81 | ||
|
|
30cad2e0a6 | ||
|
|
6e838b83d8 | ||
|
|
5297223ee3 | ||
|
|
2340d5be8d | ||
|
|
7de1a145aa | ||
|
|
32426d61c3 | ||
|
|
3ce47660a0 | ||
|
|
0dff455ffa | ||
|
|
81739288e8 | ||
|
|
5641668bc1 | ||
|
|
5c03a52a0b | ||
|
|
087b52e3e0 | ||
|
|
733b8e7c1c | ||
|
|
aa83f42f2d | ||
|
|
4ed2854791 | ||
|
|
73cdd87d91 | ||
|
|
51c8e3b2c6 | ||
|
|
e522095fae | ||
|
|
6fc1226028 | ||
|
|
fb40280d3c | ||
|
|
746cb3684a | ||
|
|
16c6545e93 | ||
|
|
0c4b20aa4e | ||
|
|
15e3e8d8b1 | ||
|
|
6123de52c2 | ||
|
|
3db0aab24b | ||
|
|
ae77f696ce | ||
|
|
98369db7f5 | ||
|
|
bc9d6892d4 | ||
|
|
5c02c370d4 | ||
|
|
b944cdddeb | ||
|
|
a1d203d34a | ||
|
|
e151b8e604 | ||
|
|
3c67e991c2 | ||
|
|
cb7aed3234 | ||
|
|
ff80b906d0 | ||
|
|
8300436481 | ||
|
|
6b13454091 | ||
|
|
9eae4ad913 | ||
|
|
bfb8532c52 | ||
|
|
c41405fd57 | ||
|
|
276ba7754a | ||
|
|
8569be81ea | ||
|
|
a765d551e4 | ||
|
|
4309aa1ace | ||
|
|
6ef83a09d3 | ||
|
|
adcc616c92 | ||
|
|
6dc1617429 | ||
|
|
1837168b0b | ||
|
|
1be1864657 | ||
|
|
49c335972f | ||
|
|
aabc99c7bf | ||
|
|
7c91dc6183 | ||
|
|
ff8a99211d | ||
|
|
efda0ea455 | ||
|
|
906be21b8d | ||
|
|
5a36fbfe7b | ||
|
|
f9f69a0577 | ||
|
|
8ccbb96b8d | ||
|
|
8ae7256fd6 | ||
|
|
ba401c19c2 | ||
|
|
11343d42c3 | ||
|
|
6c628bc594 | ||
|
|
f6cbca1aeb | ||
|
|
51e7c8c136 | ||
|
|
f952694ffd | ||
|
|
59afdfc4e9 | ||
|
|
27ec1e4b3c | ||
|
|
e791a8a7bb | ||
|
|
66fe53ceb1 | ||
|
|
de3cc11423 | ||
|
|
901eae9604 | ||
|
|
1962519666 | ||
|
|
1e84cbe2ab | ||
|
|
06da2b88ff | ||
|
|
91b9110dcc | ||
|
|
d57abb56ce | ||
|
|
f6edb774fc | ||
|
|
ec0f3e9128 | ||
|
|
6f1404d17b | ||
|
|
3d9e47c295 | ||
|
|
afa6128155 | ||
|
|
72b1996423 | ||
|
|
6b1a9d635e | ||
|
|
dea79f16d0 | ||
|
|
2d772d7243 | ||
|
|
09fb246c8f | ||
|
|
0a5c16363c | ||
|
|
b444f4636e | ||
|
|
2b8acebd6e | ||
|
|
766010c292 | ||
|
|
0a2768e496 | ||
|
|
0fd7f3f7b5 | ||
|
|
d3472966d3 | ||
|
|
576ab24609 | ||
|
|
621453b068 | ||
|
|
6638546247 | ||
|
|
f99edbae3c | ||
|
|
150bf1a5b0 | ||
|
|
91a17e3f0c | ||
|
|
7160027c14 | ||
|
|
736d6cf7e6 | ||
|
|
82de6a1c56 | ||
|
|
1fbb4f763e | ||
|
|
20582431d6 | ||
|
|
7d9cf0b064 | ||
|
|
a834f221f1 | ||
|
|
c95dca4023 | ||
|
|
c05c0353fd | ||
|
|
e944e2ecae | ||
|
|
47b3c37bf0 | ||
|
|
190666dc8a | ||
|
|
8c6e7ce61a | ||
|
|
04ead526b9 | ||
|
|
135fb7e6e4 | ||
|
|
ce1e22ac95 | ||
|
|
a216f6bd46 | ||
|
|
c47e5372b5 | ||
|
|
daa673eff1 | ||
|
|
5099dd6aa3 | ||
|
|
f982e754e4 | ||
|
|
4a1ad7755a | ||
|
|
946589d1f1 | ||
|
|
7168ceab94 | ||
|
|
868d57b818 | ||
|
|
6057c57ea4 | ||
|
|
e8437fd435 | ||
|
|
9e317971b4 | ||
|
|
bdb55a02fa | ||
|
|
02e062af65 | ||
|
|
54c5faa12d | ||
|
|
9b0bd81661 | ||
|
|
52da68876d | ||
|
|
2780630a18 | ||
|
|
8f77cc65e2 | ||
|
|
7224363639 | ||
|
|
43851fc1c4 | ||
|
|
e33bf11012 | ||
|
|
0e21376538 | ||
|
|
7a463c1240 | ||
|
|
8e03da855f | ||
|
|
942881d1b7 | ||
|
|
2a50203fcd | ||
|
|
08ce8ffa54 | ||
|
|
90561682cf | ||
|
|
71027e74e0 | ||
|
|
be8bfbe370 | ||
|
|
f7e4447b66 | ||
|
|
df4e58bc39 | ||
|
|
441ea7d696 | ||
|
|
2b06ca5e1a | ||
|
|
434ef97952 | ||
|
|
e1504c4775 | ||
|
|
74f0057ec8 | ||
|
|
7cd74cb852 | ||
|
|
b2e74e4830 | ||
|
|
f54a3e8036 | ||
|
|
f3088c5646 | ||
|
|
db00adaf69 | ||
|
|
ea74add814 | ||
|
|
ba1b3c3c2d | ||
|
|
d5c48ddb94 | ||
|
|
9687318b22 | ||
|
|
f6fb36c58a | ||
|
|
c8a258f36b | ||
|
|
14e0b01b40 | ||
|
|
76de99a5a8 | ||
|
|
8613c6e1cd | ||
|
|
5bb6b29ca3 | ||
|
|
61558d518e | ||
|
|
3b724c8415 | ||
|
|
910565aa7c | ||
|
|
c7a2e060e3 | ||
|
|
740e787f54 | ||
|
|
bd87d50948 | ||
|
|
d3c0c422a5 | ||
|
|
72718cbab7 | ||
|
|
ea28951e0e | ||
|
|
39a78f6be3 | ||
|
|
fe8aaca320 | ||
|
|
f5cc58ea9d | ||
|
|
44992452cf | ||
|
|
f080871911 | ||
|
|
6789eb88e2 | ||
|
|
1a97be4a25 | ||
|
|
c9ef5c0715 | ||
|
|
0a9f8d4f6a | ||
|
|
cac2c153c2 | ||
|
|
f2d6ba8775 | ||
|
|
92abaad22c | ||
|
|
3df0194f6e | ||
|
|
f774841ee0 | ||
|
|
a9de91087a | ||
|
|
76a536ece0 | ||
|
|
719339e116 | ||
|
|
1c3910de5c | ||
|
|
39d3ef6fe8 | ||
|
|
064d0fa6f1 | ||
|
|
99a5843595 | ||
|
|
39e75e39e8 | ||
|
|
322a80e76e | ||
|
|
d4b048735d | ||
|
|
c23e04fd03 | ||
|
|
f67009f554 | ||
|
|
4013544efb | ||
|
|
31d9db7e74 | ||
|
|
384c56f834 | ||
|
|
3282c8200d | ||
|
|
ffbc9174f8 | ||
|
|
dceb00f669 | ||
|
|
920086ae88 | ||
|
|
a1311a2332 | ||
|
|
4c392426f6 | ||
|
|
4528173ff5 | ||
|
|
d3b63a9c45 | ||
|
|
c30a249cf6 | ||
|
|
20962d989f | ||
|
|
a1e4b19486 | ||
|
|
fd0262413e | ||
|
|
aa4d36cc57 | ||
|
|
8d119f06b3 | ||
|
|
708c5126b9 | ||
|
|
577de9f746 | ||
|
|
38a70f5b51 | ||
|
|
118306a9ff | ||
|
|
704717ff1d | ||
|
|
405c472d13 | ||
|
|
17c994db57 | ||
|
|
e227264bad | ||
|
|
8751ea79e7 | ||
|
|
3b3ffa7455 | ||
|
|
4322825ac7 | ||
|
|
d29fa7b382 | ||
|
|
08bb63fa92 | ||
|
|
5dde31f80c | ||
|
|
3bb869f725 | ||
|
|
11686b90f7 | ||
|
|
6df78fa67c | ||
|
|
53b1607c4d | ||
|
|
2341a27f63 | ||
|
|
0661caf9da | ||
|
|
5b5bd77c53 | ||
|
|
d36f6b39c7 | ||
|
|
b2a5b07f92 | ||
|
|
46889a3a32 | ||
|
|
958ebb72a6 | ||
|
|
febc5c53a6 | ||
|
|
6e921300e2 | ||
|
|
e167dec8a9 | ||
|
|
0019f07cc0 | ||
|
|
20fc1e6b0c | ||
|
|
908946a067 | ||
|
|
f1df7b1c1f | ||
|
|
65b9c69bd8 | ||
|
|
b91e47c3b6 | ||
|
|
0e3a11c53b | ||
|
|
53d8d2aaad | ||
|
|
84303cbf2e | ||
|
|
5e457da30b | ||
|
|
855718b179 | ||
|
|
d037c0b0fc | ||
|
|
90f5937413 | ||
|
|
cbbea37b0c | ||
|
|
4d74b23f20 | ||
|
|
79dd26a43a | ||
|
|
259e3076fa | ||
|
|
f2cb2422f0 | ||
|
|
4aea64261e | ||
|
|
cf0cd9acd9 | ||
|
|
b4b0bdc853 | ||
|
|
0b2837fcfa | ||
|
|
f20ad7df01 | ||
|
|
c9071a66ca | ||
|
|
b0ea960159 | ||
|
|
0e7f1d60d6 | ||
|
|
03c79502f9 | ||
|
|
d4d4e00f9d | ||
|
|
b52e5e795e | ||
|
|
68df13d3fe | ||
|
|
5f3d90e411 | ||
|
|
1b68e41db4 | ||
|
|
817ac82968 | ||
|
|
43fd8cb13c | ||
|
|
397fbe7ad3 | ||
|
|
3095e7a256 | ||
|
|
df9e893cbe | ||
|
|
6ff69c88df | ||
|
|
6e4d3c98da | ||
|
|
2404eba11f | ||
|
|
e5c57a679d | ||
|
|
33de55540b | ||
|
|
99e1605bba | ||
|
|
95c4e26b24 | ||
|
|
795953a341 | ||
|
|
75d5e386d3 | ||
|
|
00d2a8527f | ||
|
|
a1ade8fc42 | ||
|
|
7f9fec061a | ||
|
|
ec782d3296 | ||
|
|
2444864508 | ||
|
|
1ccc50b21b | ||
|
|
4552a474b7 | ||
|
|
85c6d8f16e | ||
|
|
14d391cc2e | ||
|
|
8fe87a0df5 | ||
|
|
ff01df3b16 | ||
|
|
c713dc0ca8 | ||
|
|
31ea5d74a7 | ||
|
|
31b01d42c0 | ||
|
|
482b853908 | ||
|
|
675411df85 | ||
|
|
f94d33fa6a | ||
|
|
7fe110dff1 | ||
|
|
591f711886 | ||
|
|
1dc7fc8ac7 | ||
|
|
ffa79d731c | ||
|
|
01dd0416ac | ||
|
|
0bd1676978 | ||
|
|
f3407959a6 | ||
|
|
30e635a934 | ||
|
|
f6706a55ec | ||
|
|
f0b79f88b2 | ||
|
|
262ba0197d | ||
|
|
31c2447fb8 | ||
|
|
54a4ebfe48 | ||
|
|
106c7a1706 | ||
|
|
e469b46ce5 | ||
|
|
0c79561977 | ||
|
|
9e82397de9 | ||
|
|
7c166e2194 | ||
|
|
0a2b09da40 | ||
|
|
c02d578468 | ||
|
|
e990d233e5 | ||
|
|
a7cdcfcc16 | ||
|
|
75224e1661 | ||
|
|
d236b34fd4 | ||
|
|
c8313409ff | ||
|
|
6cba8e0166 | ||
|
|
0067726bbe | ||
|
|
ce620cec0a | ||
|
|
e797ba4800 | ||
|
|
fe27ee2d79 | ||
|
|
95efeee7de | ||
|
|
da30536709 | ||
|
|
108ccffcd8 | ||
|
|
c3ce0c26e7 | ||
|
|
e0c984046c | ||
|
|
64d6337612 | ||
|
|
603684211f | ||
|
|
499b30d175 | ||
|
|
3f3efab221 | ||
|
|
ef9adc92c8 | ||
|
|
ad7c251f56 | ||
|
|
1c0d254ec6 | ||
|
|
f1ce8c0e8a | ||
|
|
1c558be524 | ||
|
|
7d653cb7bf | ||
|
|
b4cc38a1d9 | ||
|
|
8867317b6a | ||
|
|
4b6bfaffba | ||
|
|
fadae42715 | ||
|
|
77c9affc00 | ||
|
|
e5ba15949b | ||
|
|
a3e59697a1 | ||
|
|
0e2125cda3 | ||
|
|
95da414511 | ||
|
|
c0d5ace640 | ||
|
|
ae48407b20 | ||
|
|
e06bd68379 | ||
|
|
ae6665493a | ||
|
|
c6f37afeff | ||
|
|
59f656e3ca | ||
|
|
12658c4756 | ||
|
|
37c185462a | ||
|
|
53c8485a7a | ||
|
|
846021296f | ||
|
|
5c8651c7cd | ||
|
|
ad91f5af53 | ||
|
|
02a68e7541 | ||
|
|
6e5dbc5285 | ||
|
|
fda9415873 | ||
|
|
4054163447 | ||
|
|
e1f5ef59c8 | ||
|
|
44baeb6924 | ||
|
|
c00e8b1709 | ||
|
|
3fb7ce7100 | ||
|
|
86b15ad693 | ||
|
|
53c00ac631 | ||
|
|
99d0c2d846 | ||
|
|
55ede897b9 | ||
|
|
a71e7d7eb1 | ||
|
|
3c28a4f723 | ||
|
|
6b681df473 | ||
|
|
22c1ee7dc8 | ||
|
|
c24e16e8cd | ||
|
|
d452a5cdce | ||
|
|
198dd52700 | ||
|
|
2dfea0f213 | ||
|
|
5064b5c2b1 | ||
|
|
78b553e55e | ||
|
|
8ca3a511cc | ||
|
|
bf26a3f569 | ||
|
|
ddb8753548 | ||
|
|
e73525d0a2 | ||
|
|
f37d947dd5 | ||
|
|
4279f20daf | ||
|
|
2dd7c3b939 | ||
|
|
cf01480ec8 | ||
|
|
d8ed42ae8e | ||
|
|
297592242c | ||
|
|
1c0a8a49fc | ||
|
|
16298e49c7 | ||
|
|
abb0b95006 | ||
|
|
32f5ea7b63 | ||
|
|
90ed5959de | ||
|
|
d999cc4143 | ||
|
|
6c1d8d5c63 | ||
|
|
a316242a4b | ||
|
|
a4ae090c37 | ||
|
|
abdcc64053 | ||
|
|
ac407d42de | ||
|
|
19c4a3f0a8 | ||
|
|
3afd96d421 | ||
|
|
f421666a27 | ||
|
|
af6baa33be | ||
|
|
1096cbe236 | ||
|
|
998be9b90c | ||
|
|
854cb8f27e | ||
|
|
e359094cff | ||
|
|
db57230987 | ||
|
|
4de3cecbbe | ||
|
|
c37a8bd3b1 | ||
|
|
e103b280fd | ||
|
|
2235ea67e2 | ||
|
|
1f6a4f7bd4 | ||
|
|
8c7ffc90f3 | ||
|
|
29d85833f1 | ||
|
|
74c56d69ac | ||
|
|
cf520646a9 | ||
|
|
da5213346a | ||
|
|
bdcb98eafc | ||
|
|
ef637cd7d3 | ||
|
|
dbbabd73c5 | ||
|
|
436ef0074a | ||
|
|
0904dec810 | ||
|
|
0aa07ead7e | ||
|
|
d87cac87ff | ||
|
|
7e8c96896f | ||
|
|
2619db0e58 | ||
|
|
ee3632b85d | ||
|
|
1c88c13fc2 | ||
|
|
7f476eb924 | ||
|
|
2d4f7e3446 | ||
|
|
165c1240a9 | ||
|
|
ef5c66ee17 | ||
|
|
33abd31d1a | ||
|
|
f91463a494 | ||
|
|
30146007d3 | ||
|
|
d53f8f24c4 | ||
|
|
f9621b1e11 | ||
|
|
e2f16ff624 | ||
|
|
121778e5c6 | ||
|
|
ef32488890 | ||
|
|
9d349d618a | ||
|
|
9f0a4f43b8 | ||
|
|
a8826f0d02 | ||
|
|
6689d312a3 | ||
|
|
2d7cb25b20 | ||
|
|
da10eaca00 | ||
|
|
6d3995d4ac | ||
|
|
e3adf8c6bf | ||
|
|
b8e522484d | ||
|
|
ef4240e196 | ||
|
|
8fa4a48ae5 | ||
|
|
4f3da2bc00 | ||
|
|
7b8c2c846f | ||
|
|
b212fd5bcd | ||
|
|
0a297f4656 | ||
|
|
fa397a1d24 | ||
|
|
0610daaec7 | ||
|
|
8278e2b88d | ||
|
|
cb4a9d89cf | ||
|
|
7800c598f6 | ||
|
|
fc651b72ab | ||
|
|
a9a7912372 | ||
|
|
43f435c334 | ||
|
|
693fc0f0c7 | ||
|
|
12e011c481 | ||
|
|
668783ba1c | ||
|
|
c18ebef702 | ||
|
|
03f9f29ce4 | ||
|
|
f62acab715 | ||
|
|
28ab9bfa2a | ||
|
|
9c05481f14 | ||
|
|
8ca92e0536 | ||
|
|
c0f6201ac3 | ||
|
|
27f3a5e16a | ||
|
|
de10dfe91b | ||
|
|
444a690a86 | ||
|
|
3d7b3f7d72 | ||
|
|
741ef7f115 | ||
|
|
e0c84c7176 | ||
|
|
393425e3d1 | ||
|
|
fc9645832d | ||
|
|
c47f41cfc0 | ||
|
|
08221489fd | ||
|
|
3c6766ff06 | ||
|
|
e28aae620a | ||
|
|
c240e7deaf | ||
|
|
31a5965b01 | ||
|
|
7a2a849a97 | ||
|
|
2c96e49566 | ||
|
|
e7846f916a | ||
|
|
d4f4d56f94 | ||
|
|
0108037076 | ||
|
|
88091b4ab3 | ||
|
|
4487462b0d | ||
|
|
dac9b07f16 | ||
|
|
b59212696a | ||
|
|
7fe1376e34 | ||
|
|
e25b90c1b6 | ||
|
|
cb32a0b8fc | ||
|
|
05617f7dca | ||
|
|
2d2f4b9ba9 | ||
|
|
65c7ecbc30 | ||
|
|
da5e37620e | ||
|
|
261057396c | ||
|
|
a43f610555 | ||
|
|
45ae52e5d0 | ||
|
|
a565443d4a | ||
|
|
081d6a3f16 | ||
|
|
c9cc832875 | ||
|
|
595698d8e9 | ||
|
|
b28d94ac97 | ||
|
|
b9684879e7 | ||
|
|
63f974531b | ||
|
|
a559210923 | ||
|
|
8d36fb9edc | ||
|
|
f652b23169 | ||
|
|
d50a2ea288 | ||
|
|
80fc3a1faa | ||
|
|
9a04405ad2 | ||
|
|
fdc9835587 | ||
|
|
702bb2cd06 | ||
|
|
58e8609c1a | ||
|
|
69c5c49094 | ||
|
|
ff8de7607a | ||
|
|
cfd0872cea | ||
|
|
7007f2e7fc | ||
|
|
6d0721341b | ||
|
|
7a156d5ef3 | ||
|
|
185c3320a4 | ||
|
|
1c9cf32735 | ||
|
|
6bfb6da0ad | ||
|
|
56a5738018 | ||
|
|
919667129f | ||
|
|
e9f49a3292 | ||
|
|
711f8b024e | ||
|
|
53716c084b | ||
|
|
2b495bcf9d | ||
|
|
d10812c6b0 | ||
|
|
80202d2679 | ||
|
|
5d120a2f36 | ||
|
|
cc11326baa | ||
|
|
f63dbc0ebd | ||
|
|
88630731dc | ||
|
|
3e2a8d80d7 | ||
|
|
55626e5a41 | ||
|
|
7ff07f7620 | ||
|
|
d2f522277c | ||
|
|
0248edbdb9 | ||
|
|
d24248e93b | ||
|
|
dcd7890a0c | ||
|
|
f219b10c30 | ||
|
|
cc039afc1e | ||
|
|
d2288d8f83 | ||
|
|
4b4f904aac | ||
|
|
e06b774174 | ||
|
|
8d4772f804 | ||
|
|
3be00bd712 | ||
|
|
a0e4449fb2 | ||
|
|
ebfc19def5 | ||
|
|
a4abe26d32 | ||
|
|
5f8cb22d02 | ||
|
|
cafc3973ec | ||
|
|
436a57a61e | ||
|
|
da5111a267 | ||
|
|
ee5cf90684 | ||
|
|
108974f4f7 | ||
|
|
79fa9f1c95 | ||
|
|
40bf12af7a | ||
|
|
263d121f3e | ||
|
|
9222d002f7 | ||
|
|
9a6aab034d | ||
|
|
b931b06941 | ||
|
|
90dc9b6fe6 | ||
|
|
275ede6f0a | ||
|
|
9fd4f8e5c2 | ||
|
|
01b4654461 | ||
|
|
d40e6a9ece | ||
|
|
b2317e0f12 | ||
|
|
444d9bd341 | ||
|
|
8fbe96744b | ||
|
|
1c48ec7196 | ||
|
|
c4b3bbd057 | ||
|
|
502ed94f31 | ||
|
|
18ce091bfa | ||
|
|
60f675758f | ||
|
|
9bfa4553a8 | ||
|
|
358f30ca7a | ||
|
|
e32785831b | ||
|
|
728eceb620 | ||
|
|
57f591d1ce | ||
|
|
e4397a1c73 | ||
|
|
1170cf474f | ||
|
|
ce8b0cf748 | ||
|
|
696f371aa4 | ||
|
|
8ebd514251 | ||
|
|
d3bc6001d8 | ||
|
|
f5415ca824 | ||
|
|
0dbe27438d | ||
|
|
7448789951 | ||
|
|
dc6138d286 | ||
|
|
7457f637a2 | ||
|
|
c9da734c0e | ||
|
|
141c814d72 | ||
|
|
bdb98cf210 | ||
|
|
7cc3d5907f | ||
|
|
3c709a28f7 | ||
|
|
e9ea7b13b6 | ||
|
|
ac16880eec | ||
|
|
c54bdd921a | ||
|
|
fc17528337 | ||
|
|
9b2db6ec53 | ||
|
|
0fcfaa2a98 | ||
|
|
ba97a728f2 | ||
|
|
a3b046fdcd | ||
|
|
ddfda3efde | ||
|
|
a2269cb66e | ||
|
|
1358f00969 | ||
|
|
eedc849c45 | ||
|
|
89679d07ae | ||
|
|
01d866f482 | ||
|
|
73b0312dcb | ||
|
|
f9d9fe6db4 | ||
|
|
e31ca68875 | ||
|
|
8ae273ee3a | ||
|
|
73a197fcde | ||
|
|
648bff02b5 | ||
|
|
7740bc138b | ||
|
|
f5337096d5 | ||
|
|
dddff91fad | ||
|
|
f047678711 | ||
|
|
aa8b23395f | ||
|
|
7c36c19de0 | ||
|
|
49183ced3b | ||
|
|
007e9697c4 | ||
|
|
9989edbe42 | ||
|
|
9742e2ec48 | ||
|
|
b47711e23a | ||
|
|
3dfdb3ac89 | ||
|
|
4d3e2a07d1 | ||
|
|
a42eb3e643 | ||
|
|
39c77a9486 | ||
|
|
e22546c56a | ||
|
|
ced61096df | ||
|
|
c0ea127810 | ||
|
|
29b1e3fa46 | ||
|
|
b2faa0d9f7 | ||
|
|
ddb9b67de3 | ||
|
|
ffbc533565 | ||
|
|
206e490491 | ||
|
|
135066f38c | ||
|
|
51d591e177 | ||
|
|
80a62c8d71 | ||
|
|
90b54c5f7f | ||
|
|
99c81e6858 | ||
|
|
8fcb6278d7 | ||
|
|
89e0abd41d | ||
|
|
a1a0c9ab80 | ||
|
|
9ab2a38d5c | ||
|
|
b5676a53ee | ||
|
|
94898a8758 | ||
|
|
97a6a11066 | ||
|
|
9fe22a5c27 | ||
|
|
f9b35c71d7 | ||
|
|
6b47f6f769 | ||
|
|
37d8483728 | ||
|
|
4776a9e785 | ||
|
|
50bc5b0302 | ||
|
|
7d496f20a1 | ||
|
|
42fbd0a1af | ||
|
|
8960bfebc0 | ||
|
|
b1209c1e7a | ||
|
|
365583efd7 | ||
|
|
926c2d71ee | ||
|
|
ce57c747cc | ||
|
|
3e9129655a | ||
|
|
12f41f30b3 | ||
|
|
1e7edd0218 | ||
|
|
2fd8ef389e | ||
|
|
53589c3954 | ||
|
|
34a0218f35 |
12
.github/workflows/ci.py
vendored
12
.github/workflows/ci.py
vendored
@@ -30,13 +30,17 @@ def install_deps():
|
|||||||
print('Installing kitty dependencies...')
|
print('Installing kitty dependencies...')
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
if is_macos:
|
if is_macos:
|
||||||
items = (x.strip() for x in open('Brewfile').readlines() if not x.startswith('#'))
|
items = (x.split()[1].strip('"') for x in open('Brewfile').readlines() if x.strip().startswith('brew '))
|
||||||
run('brew', 'install', *items)
|
run('brew', 'install', 'fish', *items)
|
||||||
else:
|
else:
|
||||||
run('sudo apt-get update')
|
run('sudo apt-get update')
|
||||||
run('sudo apt-get install -y libgl1-mesa-dev libxi-dev libxrandr-dev libxinerama-dev ca-certificates'
|
run('sudo apt-get install -y libgl1-mesa-dev libxi-dev libxrandr-dev libxinerama-dev ca-certificates'
|
||||||
' libxcursor-dev libxcb-xkb-dev libdbus-1-dev libxkbcommon-dev libharfbuzz-dev libx11-xcb-dev'
|
' libxcursor-dev libxcb-xkb-dev libdbus-1-dev libxkbcommon-dev libharfbuzz-dev libx11-xcb-dev zsh'
|
||||||
' libpng-dev liblcms2-dev libfontconfig-dev libxkbcommon-x11-dev libcanberra-dev librsync-dev uuid-dev')
|
' libpng-dev liblcms2-dev libfontconfig-dev libxkbcommon-x11-dev libcanberra-dev librsync-dev uuid-dev'
|
||||||
|
' zsh bash dash')
|
||||||
|
# for some reason these directories are world writable which causes zsh
|
||||||
|
# compinit to break
|
||||||
|
run('sudo chmod -R og-w /usr/share/zsh')
|
||||||
if is_bundle:
|
if is_bundle:
|
||||||
install_bundle()
|
install_bundle()
|
||||||
else:
|
else:
|
||||||
|
|||||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -42,11 +42,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@master
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 10
|
fetch-depth: 10
|
||||||
- name: Set up Python ${{ matrix.pyver }}
|
- name: Set up Python ${{ matrix.pyver }}
|
||||||
uses: actions/setup-python@master
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.pyver }}
|
python-version: ${{ matrix.pyver }}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
CFLAGS: -funsigned-char
|
CFLAGS: -funsigned-char
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@master
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 10
|
fetch-depth: 10
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
run: if grep -Inr '\s$' kitty kitty_tests kittens docs *.py *.asciidoc *.rst .gitattributes .gitignore; then echo Trailing whitespace found, aborting.; exit 1; fi
|
run: if grep -Inr '\s$' kitty kitty_tests kittens docs *.py *.asciidoc *.rst .gitattributes .gitignore; then echo Trailing whitespace found, aborting.; exit 1; fi
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@master
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.8
|
||||||
|
|
||||||
|
|||||||
3
.github/workflows/codeql-analysis.yml
vendored
3
.github/workflows/codeql-analysis.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
# We must fetch at least the immediate parents so that if this is
|
# We must fetch at least the immediate parents so that if this is
|
||||||
# a pull request then we can checkout the head.
|
# a pull request then we can checkout the head.
|
||||||
@@ -27,6 +27,7 @@ jobs:
|
|||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v1
|
||||||
with:
|
with:
|
||||||
languages: python, c
|
languages: python, c
|
||||||
|
setup-python-dependencies: false
|
||||||
|
|
||||||
- name: Build kitty
|
- name: Build kitty
|
||||||
run: python3 .github/workflows/ci.py build
|
run: python3 .github/workflows/ci.py build
|
||||||
|
|||||||
14
Brewfile
14
Brewfile
@@ -1,7 +1,7 @@
|
|||||||
pkg-config
|
brew "pkg-config"
|
||||||
zlib
|
brew "zlib"
|
||||||
librsync
|
brew "librsync"
|
||||||
python
|
brew "python"
|
||||||
imagemagick
|
brew "imagemagick"
|
||||||
harfbuzz
|
brew "harfbuzz"
|
||||||
sphinx-doc
|
brew "sphinx-doc"
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ https://www.reddit.com/r/KittyTerminal[Reddit community]
|
|||||||
|
|
||||||
Packaging status in various repositories:
|
Packaging status in various repositories:
|
||||||
|
|
||||||
image:https://repology.org/badge/vertical-allrepos/kitty.svg[https://repology.org/project/kitty/versions]
|
image:https://repology.org/badge/vertical-allrepos/kitty.svg["Packaging status", link="https://repology.org/project/kitty/versions"]
|
||||||
|
|||||||
18
__main__.py
18
__main__.py
@@ -31,7 +31,15 @@ def runpy(args: List[str]) -> None:
|
|||||||
|
|
||||||
def hold(args: List[str]) -> None:
|
def hold(args: List[str]) -> None:
|
||||||
import subprocess
|
import subprocess
|
||||||
ret = subprocess.Popen(args[1:]).wait()
|
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
|
from kitty.utils import hold_till_enter
|
||||||
hold_till_enter()
|
hold_till_enter()
|
||||||
raise SystemExit(ret)
|
raise SystemExit(ret)
|
||||||
@@ -42,6 +50,13 @@ def complete(args: List[str]) -> None:
|
|||||||
complete_main(args[1:], entry_points, namespaced_entry_points)
|
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:
|
def launch(args: List[str]) -> None:
|
||||||
import runpy
|
import runpy
|
||||||
sys.argv = args[1:]
|
sys.argv = args[1:]
|
||||||
@@ -129,6 +144,7 @@ namespaced_entry_points['hold'] = hold
|
|||||||
namespaced_entry_points['complete'] = complete
|
namespaced_entry_points['complete'] = complete
|
||||||
namespaced_entry_points['runpy'] = runpy
|
namespaced_entry_points['runpy'] = runpy
|
||||||
namespaced_entry_points['launch'] = launch
|
namespaced_entry_points['launch'] = launch
|
||||||
|
namespaced_entry_points['open'] = open_urls
|
||||||
namespaced_entry_points['kitten'] = run_kitten
|
namespaced_entry_points['kitten'] = run_kitten
|
||||||
namespaced_entry_points['edit-config'] = edit_config_file
|
namespaced_entry_points['edit-config'] = edit_config_file
|
||||||
namespaced_entry_points['shebang'] = shebang
|
namespaced_entry_points['shebang'] = shebang
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ def binary_includes():
|
|||||||
'expat', 'sqlite3', 'ffi', 'z', 'lzma', 'png16', 'lcms2', 'crypt',
|
'expat', 'sqlite3', 'ffi', 'z', 'lzma', 'png16', 'lcms2', 'crypt',
|
||||||
'iconv', 'pcre', 'graphite2', 'glib-2.0', 'freetype', 'rsync',
|
'iconv', 'pcre', 'graphite2', 'glib-2.0', 'freetype', 'rsync',
|
||||||
'harfbuzz', 'xkbcommon', 'xkbcommon-x11',
|
'harfbuzz', 'xkbcommon', 'xkbcommon-x11',
|
||||||
'ncursesw', 'readline', 'brotlicommon', 'brotlienc', 'brotlidec'
|
'ncursesw', 'readline', 'brotlicommon', 'brotlienc', 'brotlidec',
|
||||||
|
'wayland-client', 'wayland-cursor',
|
||||||
))) + (
|
))) + (
|
||||||
get_dll_path('bz2', 2), get_dll_path('ssl', 2), get_dll_path('crypto', 2),
|
get_dll_path('bz2', 2), get_dll_path('ssl', 2), get_dll_path('crypto', 2),
|
||||||
get_dll_path(f'python{py_ver}', 2),
|
get_dll_path(f'python{py_ver}', 2),
|
||||||
|
|||||||
@@ -185,7 +185,16 @@ class Freeze(object):
|
|||||||
def add_ca_certs(self):
|
def add_ca_certs(self):
|
||||||
print('\nDownloading CA certs...')
|
print('\nDownloading CA certs...')
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
cdata = urlopen(kitty_constants['cacerts_url']).read()
|
cdata = None
|
||||||
|
for i in range(5):
|
||||||
|
try:
|
||||||
|
cdata = urlopen(kitty_constants['cacerts_url']).read()
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Downloading CA certs failed with error: {e}, retrying...')
|
||||||
|
|
||||||
|
if cdata is None:
|
||||||
|
raise SystemExit('Downloading C certs failed, giving up')
|
||||||
dest = join(self.contents_dir, 'Resources', 'cacert.pem')
|
dest = join(self.contents_dir, 'Resources', 'cacert.pem')
|
||||||
with open(dest, 'wb') as f:
|
with open(dest, 'wb') as f:
|
||||||
f.write(cdata)
|
f.write(cdata)
|
||||||
|
|||||||
@@ -166,8 +166,8 @@
|
|||||||
{
|
{
|
||||||
"name": "pygments",
|
"name": "pygments",
|
||||||
"unix": {
|
"unix": {
|
||||||
"filename": "Pygments-2.7.4.tar.gz",
|
"filename": "Pygments-2.11.2.tar.gz",
|
||||||
"hash": "sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337",
|
"hash": "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a",
|
||||||
"urls": ["pypi"]
|
"urls": ["pypi"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -273,8 +273,8 @@
|
|||||||
"name": "wayland",
|
"name": "wayland",
|
||||||
"os": "linux",
|
"os": "linux",
|
||||||
"unix": {
|
"unix": {
|
||||||
"filename": "wayland-1.19.0.tar.xz",
|
"filename": "wayland-1.20.0.tar.xz",
|
||||||
"hash": "sha256:baccd902300d354581cd5ad3cc49daa4921d55fb416a5883e218750fef166d15",
|
"hash": "sha256:b8a034154c7059772e0fdbd27dbfcda6c732df29cae56a82274f6ec5d7cd8725",
|
||||||
"urls": ["https://wayland.freedesktop.org/releases/{filename}"]
|
"urls": ["https://wayland.freedesktop.org/releases/{filename}"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -283,8 +283,8 @@
|
|||||||
"name": "wayland-protocols",
|
"name": "wayland-protocols",
|
||||||
"os": "linux",
|
"os": "linux",
|
||||||
"unix": {
|
"unix": {
|
||||||
"filename": "wayland-protocols-1.20.tar.xz",
|
"filename": "wayland-protocols-1.25.tar.xz",
|
||||||
"hash": "sha256:9782b7a1a863d82d7c92478497d13c758f52e7da4f197aa16443f73de77e4de7",
|
"hash": "sha256:f1ff0f7199d0a0da337217dd8c99979967808dc37731a1e759e822b75b571460",
|
||||||
"urls": ["https://wayland.freedesktop.org/releases/{filename}"]
|
"urls": ["https://wayland.freedesktop.org/releases/{filename}"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,12 +49,15 @@ particular desktop, but it should work for most major desktop environments.
|
|||||||
.. code-block:: sh
|
.. code-block:: sh
|
||||||
|
|
||||||
# Create a symbolic link to add kitty to PATH (assuming ~/.local/bin is in
|
# Create a symbolic link to add kitty to PATH (assuming ~/.local/bin is in
|
||||||
# your PATH)
|
# your system-wide PATH)
|
||||||
ln -s ~/.local/kitty.app/bin/kitty ~/.local/bin/
|
ln -s ~/.local/kitty.app/bin/kitty ~/.local/bin/
|
||||||
# Place the kitty.desktop file somewhere it can be found by the OS
|
# Place the kitty.desktop file somewhere it can be found by the OS
|
||||||
cp ~/.local/kitty.app/share/applications/kitty.desktop ~/.local/share/applications/
|
cp ~/.local/kitty.app/share/applications/kitty.desktop ~/.local/share/applications/
|
||||||
# Update the path to the kitty icon in the kitty.desktop file
|
# If you want to open text files and images in kitty via your file manager also add the kitty-open.desktop file
|
||||||
sed -i "s|Icon=kitty|Icon=/home/$USER/.local/kitty.app/share/icons/hicolor/256x256/apps/kitty.png|g" ~/.local/share/applications/kitty.desktop
|
cp ~/.local/kitty.app/share/applications/kitty-open.desktop ~/.local/share/applications/
|
||||||
|
# Update the paths to the kitty and its icon in the kitty.desktop file(s)
|
||||||
|
sed -i "s|Icon=kitty|Icon=/home/$USER/.local/kitty.app/share/icons/hicolor/256x256/apps/kitty.png|g" ~/.local/share/applications/kitty*.desktop
|
||||||
|
sed -i "s|Exec=kitty|Exec=/home/$USER/.local/kitty.app/bin/kitty|g" ~/.local/share/applications/kitty*.desktop
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
If you use the venerable `stow <https://www.gnu.org/software/stow/>`_
|
If you use the venerable `stow <https://www.gnu.org/software/stow/>`_
|
||||||
|
|||||||
@@ -125,6 +125,19 @@ make them available in the newly spawned shell.
|
|||||||
Then proceed with ``make`` or ``make app`` according to the platform specific instructions above.
|
Then proceed with ``make`` or ``make app`` according to the platform specific instructions above.
|
||||||
|
|
||||||
|
|
||||||
|
Debug builds
|
||||||
|
--------------
|
||||||
|
|
||||||
|
A basic debug build can be done with::
|
||||||
|
|
||||||
|
make debug
|
||||||
|
|
||||||
|
This includes debug info in the binary for better traces. To build with address sanitizer, use::
|
||||||
|
|
||||||
|
make asan
|
||||||
|
|
||||||
|
Which will result in a debug binary that uses the address sanitizer as well.
|
||||||
|
|
||||||
.. _packagers:
|
.. _packagers:
|
||||||
|
|
||||||
Notes for Linux/macOS packagers
|
Notes for Linux/macOS packagers
|
||||||
@@ -173,32 +186,4 @@ servers into which they ssh, without needing to install all of |kitty|.
|
|||||||
``python -m pip -r docs/requirements.txt``.
|
``python -m pip -r docs/requirements.txt``.
|
||||||
|
|
||||||
This applies to creating packages for |kitty| for macOS package managers such as
|
This applies to creating packages for |kitty| for macOS package managers such as
|
||||||
brew or MacPorts as well.
|
Homebrew or MacPorts as well.
|
||||||
|
|
||||||
|
|
||||||
Changing defaults for packages
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
|kitty| has its defaults chosen to be suitable for a standalone distributable.
|
|
||||||
If you are packaging it a few of these might need to be changed.
|
|
||||||
|
|
||||||
update-checking
|
|
||||||
|kitty| has its own update check mechanism, if you would like to turn
|
|
||||||
it off for your package, use::
|
|
||||||
|
|
||||||
./setup.py linux-package --update-check-interval=0
|
|
||||||
|
|
||||||
shell-integration
|
|
||||||
|kitty| by default injects its :ref:`shell_integration` code into the user's
|
|
||||||
shell using environment variables or (for bash only) modifying
|
|
||||||
the user's :file:`~/.bashrc` file.
|
|
||||||
For a package, it might make more sense to distribute the shell
|
|
||||||
integration scripts into the system-wide shell vendor locations. The
|
|
||||||
shell integration files are found in the :file:`shell-integration`
|
|
||||||
directory. Copy them to the system wide shell vendor locations for each
|
|
||||||
shell, and use::
|
|
||||||
|
|
||||||
./setup.py linux-package --shell-integration=enabled\ no-rc
|
|
||||||
|
|
||||||
This will prevent kitty from modifying the user's shell environment to load
|
|
||||||
the integration scripts.
|
|
||||||
|
|||||||
@@ -9,23 +9,14 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
|
|||||||
Recent major new features
|
Recent major new features
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
.. only:: dirhtml
|
Truly convenient SSH
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
A demo video
|
The :doc:`ssh kitten <kittens/ssh>` is redesigned with powerful new features:
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
A new video showcasing some of kitty's many productivity enhancing features.
|
* Automatic :ref:`shell_integration` on remote machines
|
||||||
|
* Easily :ref:`clone local shell/editor config <real_world_ssh_kitten_config>` on remote machines
|
||||||
.. raw:: html
|
* Automatic :opt:`re-use of existing connections <kitten-ssh.share_connections>` to avoid connection setup latency
|
||||||
|
|
||||||
<video controls width="640" height="360" poster="../_static/poster.png">
|
|
||||||
<source src="https://download.calibre-ebook.com/videos/kitty.mp4" type="video/mp4">
|
|
||||||
<source src="https://download.calibre-ebook.com/videos/kitty.webm" type="video/webm">
|
|
||||||
</video>
|
|
||||||
|
|
||||||
.. rst-class:: caption caption-text
|
|
||||||
|
|
||||||
Watch kitty in action!
|
|
||||||
|
|
||||||
|
|
||||||
Shell integration
|
Shell integration
|
||||||
@@ -38,43 +29,161 @@ scrollback by pressing :sc:`scroll_to_previous_prompt` and clicking with the
|
|||||||
mouse anywhere in the current command to move the cursor there. See
|
mouse anywhere in the current command to move the cursor there. See
|
||||||
:doc:`shell-integration` for details.
|
:doc:`shell-integration` for details.
|
||||||
|
|
||||||
Logos for windows
|
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
kitty has the ability to display arbitrary ``PNG`` images at the corner
|
|
||||||
of any kitty window to serve as a *logo* for quick window identification.
|
|
||||||
Can be controlled via :opt:`window_logo_path` and also and changed via
|
|
||||||
the :ref:`at_set-window-logo` remote control command.
|
|
||||||
Useful to quickly identify special windows or just for some *bling*.
|
|
||||||
|
|
||||||
.. figure:: screenshots/window-logo.png
|
|
||||||
:alt: A screenshot of the kitty shell window showing the kitty logo
|
|
||||||
:align: center
|
|
||||||
:width: 100%
|
|
||||||
|
|
||||||
A screenshot of the kitty shell window showing the kitty logo
|
|
||||||
|
|
||||||
|
|
||||||
Visual keyboard based window select
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Select a kitty window visually using the keyboard, like the
|
|
||||||
:doc:`kittens/hints`, but for kitty windows. Can also be used from shell
|
|
||||||
scripts/third party integrations via the :ref:`at_select-window` remote control
|
|
||||||
command.
|
|
||||||
|
|
||||||
.. figure:: screenshots/select-window.png
|
|
||||||
:alt: A screenshot of the kitty select window function
|
|
||||||
:align: center
|
|
||||||
:width: 100%
|
|
||||||
|
|
||||||
Hints for selecting kitty windows visually
|
|
||||||
|
|
||||||
.. }}}
|
.. }}}
|
||||||
|
|
||||||
Detailed list of changes
|
Detailed list of changes
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
||||||
|
0.25.0 [2022-04-11]
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
- :doc:`kittens/ssh`: automatic shell integration when using SSH. Easily
|
||||||
|
clone local shell and editor configuration on remote machines, and automatic
|
||||||
|
re-use of existing connections to avoid connection setup latency.
|
||||||
|
|
||||||
|
- When pasting URLs at shell prompts automatically quote them. Also allow filtering pasted text and confirm pastes. See :opt:`paste_actions` for details. (:iss:`4873`)
|
||||||
|
|
||||||
|
- macOS: When using Apple's less as the pager for viewing scrollback strip out OSC codes as it cant parse them (:iss:`4788`)
|
||||||
|
|
||||||
|
- diff kitten: Fix incorrect rendering in rare circumstances when scrolling after changing the context size (:iss:`4831`)
|
||||||
|
|
||||||
|
- icat kitten: Fix a regression that broke :option:`kitty +kitten icat --print-window-size` (:pull:`4818`)
|
||||||
|
|
||||||
|
- Wayland: Fix :opt:`hide_window_decorations` causing docked windows to be resized on blur (:iss:`4797`)
|
||||||
|
|
||||||
|
- Bash integration: Prevent shell integration code from running twice if user enables both automatic and manual integration
|
||||||
|
|
||||||
|
- Bash integration: Handle existing PROMPT_COMMAND ending with a literal newline
|
||||||
|
|
||||||
|
- Fix continued lines not having their continued status reset on line feed (:iss:`4837`)
|
||||||
|
|
||||||
|
- macOS: Allow the New kitty Tab/Window Here services to open multiple selected folders. (:pull:`4848`)
|
||||||
|
|
||||||
|
- Wayland: Fix a regression that broke IME when changing windows/tabs (:iss:`4853`)
|
||||||
|
|
||||||
|
- macOS: Fix Unicode paths not decoded correctly when dropping files (:pull:`4879`)
|
||||||
|
|
||||||
|
- Avoid flicker when starting kittens such as the hints kitten (:iss:`4674`)
|
||||||
|
|
||||||
|
- A new action :ac:`scroll_prompt_to_top` to move the current prompt to the top (:pull:`4891`)
|
||||||
|
|
||||||
|
- :ac:`select_tab`: Use stable numbers when selecting the tab (:iss:`4792`)
|
||||||
|
|
||||||
|
- Only check for updates in the official binary builds. Distro packages or source builds will no longer check for updates, regardless of the
|
||||||
|
value of :opt:`update_check_interval`.
|
||||||
|
|
||||||
|
- Fix :opt:`inactive_text_alpha` still being applied to the cursor hidden window after focus (:iss:`4928`)
|
||||||
|
|
||||||
|
- Fix resizing window that is extra tall/wide because of left-over cells not
|
||||||
|
working reliably (:iss:`4913`)
|
||||||
|
|
||||||
|
- A new action :ac:`close_other_tabs_in_os_window` to close other tabs in the active OS window (:pull:`4944`)
|
||||||
|
|
||||||
|
|
||||||
|
0.24.4 [2022-03-03]
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
- Shell integration: Fix the default Bash :code:`$HISTFILE` changing to :file:`~/.sh_history` instead of :file:`~/.bash_history` (:iss:`4765`)
|
||||||
|
|
||||||
|
- Linux binaries: Fix binaries not working on systems with older Wayland client libraries (:iss:`4760`)
|
||||||
|
|
||||||
|
- Fix a regression in the previous release that broke kittens launched with :code:`STDIN` not connected to a terminal (:iss:`4763`)
|
||||||
|
|
||||||
|
- Wayland: Fix surface configure events not being acknowledged before commit
|
||||||
|
the resized buffer (:pull:`4768`)
|
||||||
|
|
||||||
|
|
||||||
|
0.24.3 [2022-02-28]
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
- Bash integration: No longer modify :file:`~/.bashrc` to load :ref:`shell integration <shell_integration>`.
|
||||||
|
It is recommended to remove the lines used to load the shell integration from :file:`~/.bashrc` as they are no-ops.
|
||||||
|
|
||||||
|
- macOS: Allow kitty to handle various URL types. Can be configured via
|
||||||
|
:ref:`launch_actions` (:pull:`4618`)
|
||||||
|
|
||||||
|
- macOS: Add a new service ``Open with kitty`` to open file types that are not
|
||||||
|
recognized by the system (:pull:`4641`)
|
||||||
|
|
||||||
|
- Splits layout: A new value for :option:`launch --location` to auto-select the split axis when splitting existing windows.
|
||||||
|
Wide windows are split side-by-side and tall windows are split one-above-the-other
|
||||||
|
|
||||||
|
- hints kitten: Fix a regression that broke recognition of path:linenumber:colnumber (:iss:`4675`)
|
||||||
|
|
||||||
|
- Fix a regression in the previous release that broke :opt:`active_tab_foreground` (:iss:`4620`)
|
||||||
|
|
||||||
|
- Fix :ac:`show_last_command_output` not working when the output is stored
|
||||||
|
partially in the scrollback pager history buffer (:iss:`4435`)
|
||||||
|
|
||||||
|
- When dropping URLs/files onto kitty at a shell prompt insert them appropriately quoted and space
|
||||||
|
separated (:iss:`4734`)
|
||||||
|
|
||||||
|
- Improve CWD detection when there are multiple foreground processes in the TTY process group
|
||||||
|
|
||||||
|
- A new option :opt:`narrow_symbols` to turn off opportunistic wide rendering of private use codepoints
|
||||||
|
|
||||||
|
- ssh kitten: Fix location of generated terminfo files on NetBSD (:iss:`4622`)
|
||||||
|
|
||||||
|
- A new action to clear the screen up to the line containing the cursor, see
|
||||||
|
:ac:`clear_terminal`
|
||||||
|
|
||||||
|
- A new action :ac:`copy_ansi_to_clipboard` to copy the current selection with ANSI formatting codes
|
||||||
|
(:iss:`4665`)
|
||||||
|
|
||||||
|
- Linux: Do not rescale fallback fonts to match the main font cell height, instead just
|
||||||
|
set the font size and let FreeType take care of it. This matches
|
||||||
|
rendering on macOS (:iss:`4707`)
|
||||||
|
|
||||||
|
- macOS: Fix a regression in the previous release that broke switching input
|
||||||
|
sources by keyboard (:iss:`4621`)
|
||||||
|
|
||||||
|
- macOS: Add the default shortcut :kbd:`cmd+k` to clear the terminal screen and
|
||||||
|
scrollback up to the cursor (:iss:`4625`)
|
||||||
|
|
||||||
|
- Fix a regression in the previous release that broke strikethrough (:disc:`4632`)
|
||||||
|
|
||||||
|
- A new action :ac:`scroll_prompt_to_bottom` to move the current prompt
|
||||||
|
to the bottom, filling in the window from the scrollback (:pull:`4634`)
|
||||||
|
|
||||||
|
- Add two special arguments ``@first-line-on-screen`` and ``@last-line-on-screen``
|
||||||
|
for the :doc:`launch <launch>` command to be used for pager positioning.
|
||||||
|
(:iss:`4462`)
|
||||||
|
|
||||||
|
- Linux: Fix rendering of emoji when using scalable fonts such as Segoe UI Emoji
|
||||||
|
|
||||||
|
- Shell integration: bash: Dont fail if an existing PROMPT_COMMAND ends with a semi-colon (:iss:`4645`)
|
||||||
|
|
||||||
|
- Shell integration: bash: Fix rendering of multiline prompts with more than two lines (:iss:`4681`)
|
||||||
|
|
||||||
|
- Shell integration: fish: Check fish version 3.3.0+ and exit on outdated versions (:pull:`4745`)
|
||||||
|
|
||||||
|
- Shell integration: fish: Fix pipestatus being overwritten (:pull:`4756`)
|
||||||
|
|
||||||
|
- Linux: Fix fontconfig alias not being used if the aliased font is dual spaced instead of monospaced (:iss:`4649`)
|
||||||
|
|
||||||
|
- macOS: Add an option :opt:`macos_menubar_title_max_length` to control the max length of the window title displayed in the global menubar (:iss:`2132`)
|
||||||
|
|
||||||
|
- Fix :opt:`touch_scroll_multiplier` also taking effect in terminal programs such as vim that handle mouse events themselves (:iss:`4680`)
|
||||||
|
|
||||||
|
- Fix symbol/PUA glyphs loaded via :opt:`symbol_map` instead of as fallbacks not using following spaces to render larger versions (:iss:`4670`)
|
||||||
|
|
||||||
|
- macOS: Fix regression in previous release that caused Apple's global shortcuts to not work if they had never been configured on a particular machine (:iss:`4657`)
|
||||||
|
|
||||||
|
- Fix a fast *click, move mouse, click* sequence causing the first click event to be discarded (:iss:`4603`)
|
||||||
|
|
||||||
|
- Wayland: Fix wheel mice with line based scrolling being incorrectly handled as high precision devices (:iss:`4694`)
|
||||||
|
|
||||||
|
- Wayland: Fix touchpads and high resolution wheels not scrolling at the same speed on monitors with different scales (:iss:`4703`)
|
||||||
|
|
||||||
|
- Add an option :opt:`wheel_scroll_min_lines` to set the minimum number of lines for mouse wheel scrolling when using a mouse with a wheel that generates very small offsets when slow scrolling (:pull:`4710`)
|
||||||
|
|
||||||
|
- macOS: Make the shortcut to toggle full screen configurable (:pull:`4714`)
|
||||||
|
|
||||||
|
- macOS: Fix the mouse cursor being set to arrow after switching desktops or toggling full screen (:pull:`4716`)
|
||||||
|
|
||||||
|
- Fix copying of selection after selection has been scrolled off history buffer raising an error (:iss:`4713`)
|
||||||
|
|
||||||
|
|
||||||
0.24.2 [2022-02-03]
|
0.24.2 [2022-02-03]
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
@@ -420,7 +529,7 @@ Detailed list of changes
|
|||||||
0.22.0 [2021-07-26]
|
0.22.0 [2021-07-26]
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
- Add a new :ref:`action-toggle_layout` action to easily zoom/unzoom a window
|
- Add a new :ac:`toggle_layout` action to easily zoom/unzoom a window
|
||||||
|
|
||||||
- When right clicking to extend a selection, move the nearest selection
|
- When right clicking to extend a selection, move the nearest selection
|
||||||
boundary rather than the end of the selection. To restore previous behavior
|
boundary rather than the end of the selection. To restore previous behavior
|
||||||
|
|||||||
26
docs/conf.py
26
docs/conf.py
@@ -200,6 +200,13 @@ def commit_role(
|
|||||||
def write_cli_docs(all_kitten_names: Iterable[str]) -> None:
|
def write_cli_docs(all_kitten_names: Iterable[str]) -> None:
|
||||||
from kitty.cli import option_spec_as_rst
|
from kitty.cli import option_spec_as_rst
|
||||||
from kitty.launch import options_spec as launch_options_spec
|
from kitty.launch import options_spec as launch_options_spec
|
||||||
|
from kittens.ssh.copy import option_text
|
||||||
|
from kittens.ssh.options.definition import copy_message
|
||||||
|
with open('generated/ssh-copy.rst', 'w') as f:
|
||||||
|
f.write(option_spec_as_rst(
|
||||||
|
appname='copy', ospec=option_text, heading_char='^',
|
||||||
|
usage='file-or-dir-to-copy ...', message=copy_message
|
||||||
|
))
|
||||||
with open('generated/launch.rst', 'w') as f:
|
with open('generated/launch.rst', 'w') as f:
|
||||||
f.write(option_spec_as_rst(
|
f.write(option_spec_as_rst(
|
||||||
appname='launch', ospec=launch_options_spec, heading_char='_',
|
appname='launch', ospec=launch_options_spec, heading_char='_',
|
||||||
@@ -399,6 +406,12 @@ def parse_shortcut_node(env: Any, sig: str, signode: Any) -> str:
|
|||||||
return sig
|
return sig
|
||||||
|
|
||||||
|
|
||||||
|
def parse_action_node(env: Any, sig: str, signode: Any) -> str:
|
||||||
|
"""Transform an action description into RST nodes."""
|
||||||
|
signode += addnodes.desc_name(sig, sig)
|
||||||
|
return sig
|
||||||
|
|
||||||
|
|
||||||
def process_opt_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
|
def process_opt_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
|
||||||
conf_name, opt = target.partition('.')[::2]
|
conf_name, opt = target.partition('.')[::2]
|
||||||
if not opt:
|
if not opt:
|
||||||
@@ -407,6 +420,10 @@ def process_opt_link(env: Any, refnode: Any, has_explicit_title: bool, title: st
|
|||||||
return title, opt_aliases.get(full_name, full_name)
|
return title, opt_aliases.get(full_name, full_name)
|
||||||
|
|
||||||
|
|
||||||
|
def process_action_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
|
||||||
|
return title, target
|
||||||
|
|
||||||
|
|
||||||
def process_shortcut_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
|
def process_shortcut_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
|
||||||
conf_name, slug = target.partition('.')[::2]
|
conf_name, slug = target.partition('.')[::2]
|
||||||
if not slug:
|
if not slug:
|
||||||
@@ -444,6 +461,15 @@ def write_conf_docs(app: Any, all_kitten_names: Iterable[str]) -> None:
|
|||||||
sc_role.process_link = process_shortcut_link
|
sc_role.process_link = process_shortcut_link
|
||||||
shortcut_slugs.clear()
|
shortcut_slugs.clear()
|
||||||
|
|
||||||
|
app.add_object_type(
|
||||||
|
'action', 'ac',
|
||||||
|
indextemplate="pair: %s; Action",
|
||||||
|
parse_node=parse_action_node,
|
||||||
|
)
|
||||||
|
ac_role = app.registry.domain_roles['std']['ac']
|
||||||
|
ac_role.warn_dangling = True
|
||||||
|
ac_role.process_link = process_action_link
|
||||||
|
|
||||||
def generate_default_config(definition: Definition, name: str) -> None:
|
def generate_default_config(definition: Definition, name: str) -> None:
|
||||||
with open(f'generated/conf-{name}.rst', 'w', encoding='utf-8') as f:
|
with open(f'generated/conf-{name}.rst', 'w', encoding='utf-8') as f:
|
||||||
print('.. highlight:: conf\n', file=f)
|
print('.. highlight:: conf\n', file=f)
|
||||||
|
|||||||
@@ -24,15 +24,21 @@ Comments can be added to the config file as lines starting with the ``#``
|
|||||||
character. This works only if the ``#`` character is the first character
|
character. This works only if the ``#`` character is the first character
|
||||||
in the line.
|
in the line.
|
||||||
|
|
||||||
|
.. _include:
|
||||||
|
|
||||||
You can include secondary config files via the :code:`include` directive. If
|
You can include secondary config files via the :code:`include` directive. If
|
||||||
you use a relative path for :code:`include`, it is resolved with respect to the
|
you use a relative path for :code:`include`, it is resolved with respect to the
|
||||||
location of the current config file. Note that environment variables are
|
location of the current config file. Note that environment variables are
|
||||||
expanded, so :code:`${USER}.conf` becomes :file:`name.conf` if
|
expanded, so :code:`${USER}.conf` becomes :file:`name.conf` if
|
||||||
:code:`USER=name`. Also, you can use :code:`globinclude` to include files
|
:code:`USER=name`. Also, you can use :code:`globinclude` to include files
|
||||||
matching a shell glob pattern. For example::
|
matching a shell glob pattern and :code:`envinclude` to include configuration
|
||||||
|
from environment variables. For example::
|
||||||
|
|
||||||
include other.conf
|
include other.conf
|
||||||
|
# Include *.conf files from all subdirs of kitty.d inside the kitty config dir
|
||||||
globinclude kitty.d/**/*.conf
|
globinclude kitty.d/**/*.conf
|
||||||
|
# Include the *contents* of all env vars starting with KITTY_CONF_
|
||||||
|
envinclude KITTY_CONF_*
|
||||||
|
|
||||||
|
|
||||||
.. note:: Syntax highlighting for :file:`kitty.conf` in vim is available via
|
.. note:: Syntax highlighting for :file:`kitty.conf` in vim is available via
|
||||||
|
|||||||
63
docs/faq.rst
63
docs/faq.rst
@@ -6,21 +6,22 @@ Frequently Asked Questions
|
|||||||
Some special symbols are rendered small/truncated in kitty?
|
Some special symbols are rendered small/truncated in kitty?
|
||||||
-----------------------------------------------------------
|
-----------------------------------------------------------
|
||||||
|
|
||||||
The number of cells a unicode character takes up are controlled by the unicode
|
The number of cells a Unicode character takes up are controlled by the Unicode
|
||||||
standard. All characters are rendered in a single cell unless the unicode
|
standard. All characters are rendered in a single cell unless the Unicode
|
||||||
standard says they should be rendered in two cells. When a symbol does not fit,
|
standard says they should be rendered in two cells. When a symbol does not fit,
|
||||||
it will either be rescaled to be smaller or truncated (depending on how much
|
it will either be rescaled to be smaller or truncated (depending on how much
|
||||||
extra space it needs). This is often different from other terminals which just
|
extra space it needs). This is often different from other terminals which just
|
||||||
let the character overflow into neighboring cells, which is fine if the
|
let the character overflow into neighboring cells, which is fine if the
|
||||||
neighboring cell is empty, but looks terrible if it is not.
|
neighboring cell is empty, but looks terrible if it is not.
|
||||||
|
|
||||||
Some programs, like powerline, vim with fancy gutter symbols/status-bar, etc.
|
Some programs, like Powerline, vim with fancy gutter symbols/status-bar, etc.
|
||||||
misuse unicode characters from the private use area to represent symbols. Often
|
use Unicode characters from the private use area to represent symbols. Often
|
||||||
these symbols are square and should be rendered in two cells. However, since
|
these symbols are wide and should be rendered in two cells. However, since
|
||||||
private use area symbols all have their width set to one in the unicode
|
private use area symbols all have their width set to one in the Unicode
|
||||||
standard, |kitty| renders them either smaller or truncated. The exception is if
|
standard, |kitty| renders them either smaller or truncated. The exception is if
|
||||||
these characters are followed by a space or empty cell in which case kitty
|
these characters are followed by a space or empty cell in which case kitty
|
||||||
makes use of the extra cell to render them in two cells.
|
makes use of the extra cell to render them in two cells. This behavior can be
|
||||||
|
turned off for specific symbols using :opt:`narrow_symbols`.
|
||||||
|
|
||||||
|
|
||||||
Using a color theme with a background color does not work well in vim?
|
Using a color theme with a background color does not work well in vim?
|
||||||
@@ -45,30 +46,21 @@ terminfo files to the server::
|
|||||||
|
|
||||||
kitty +kitten ssh myserver
|
kitty +kitten ssh myserver
|
||||||
|
|
||||||
This ssh kitten takes all the same command line arguments
|
This :doc:`ssh kitten <kittens/ssh>` takes all the same command line arguments
|
||||||
as ssh, you can alias it to ssh in your shell's rc files to avoid having to
|
as ssh, you can alias it to something small in your shell's rc files to avoid
|
||||||
type it each time::
|
having to type it each time::
|
||||||
|
|
||||||
alias ssh="kitty +kitten ssh"
|
alias s="kitty +kitten ssh"
|
||||||
|
|
||||||
Remember to also setup :ref:`shell_integration` for completion and other
|
If the ssh kitten fails, use the following one-liner instead (it
|
||||||
niceties.
|
|
||||||
|
|
||||||
If for some reason that does not work (typically because the server is using a
|
|
||||||
non POSIX compliant shell as ``/bin/sh``), you can try using it with ``python``
|
|
||||||
instead::
|
|
||||||
|
|
||||||
kitty +kitten ssh use-python myserver
|
|
||||||
|
|
||||||
If that also fails, perhaps because python is not installed on the remote
|
|
||||||
server, use the following one-liner instead (it
|
|
||||||
is slower as it needs to ssh into the server twice, but will work with most
|
is slower as it needs to ssh into the server twice, but will work with most
|
||||||
servers)::
|
servers)::
|
||||||
|
|
||||||
infocmp -a xterm-kitty | ssh myserver tic -x -o \~/.terminfo /dev/stdin
|
infocmp -a xterm-kitty | ssh myserver tic -x -o \~/.terminfo /dev/stdin
|
||||||
|
|
||||||
If you are behind a proxy (like Balabit) that prevents this, you must redirect the
|
If you are behind a proxy (like Balabit) that prevents this, or ``tic`` comes
|
||||||
1st command to a file, copy that to the server and run ``tic`` manually. If you
|
with macOS that does not support reading from STDIN, you must redirect the 1st
|
||||||
|
command to a file, copy that to the server and run ``tic`` manually. If you
|
||||||
connect to a server, embedded or Android system that doesn't have ``tic``, copy over
|
connect to a server, embedded or Android system that doesn't have ``tic``, copy over
|
||||||
your local file terminfo to the other system as :file:`~/.terminfo/x/xterm-kitty`.
|
your local file terminfo to the other system as :file:`~/.terminfo/x/xterm-kitty`.
|
||||||
|
|
||||||
@@ -96,6 +88,12 @@ Make sure the TERM environment variable, is ``xterm-kitty``. And either the
|
|||||||
TERMINFO environment variable points to a directory containing :file:`x/xterm-kitty`
|
TERMINFO environment variable points to a directory containing :file:`x/xterm-kitty`
|
||||||
or that file is under :file:`~/.terminfo/x/`.
|
or that file is under :file:`~/.terminfo/x/`.
|
||||||
|
|
||||||
|
For macOS, you may also need to put that file under :file:`~/.terminfo/78/`::
|
||||||
|
|
||||||
|
mkdir -p ~/.terminfo/{78,x}
|
||||||
|
ln -snf ../x/xterm-kitty ~/.terminfo/78/xterm-kitty
|
||||||
|
tic -x -o ~/.terminfo "$KITTY_INSTALLATION_DIR/terminfo/kitty.terminfo"
|
||||||
|
|
||||||
Note that ``sudo`` might remove TERMINFO. Then setting it at the shell prompt can
|
Note that ``sudo`` might remove TERMINFO. Then setting it at the shell prompt can
|
||||||
be too late, because command line editing may not be reinitialized. In that case
|
be too late, because command line editing may not be reinitialized. In that case
|
||||||
you can either ask ``sudo`` to set it or if that is not supported, insert an ``env``
|
you can either ask ``sudo`` to set it or if that is not supported, insert an ``env``
|
||||||
@@ -202,13 +200,14 @@ bitmapped fonts.
|
|||||||
.. note::
|
.. note::
|
||||||
If you are trying to use a font patched with NERD font symbols, dont do that
|
If you are trying to use a font patched with NERD font symbols, dont do that
|
||||||
as patching destroys fonts. There is no need, simply install the standalone
|
as patching destroys fonts. There is no need, simply install the standalone
|
||||||
NERD font, kitty should pick up symbols from it automatically, and you can
|
NERD font (the file :file:`NerdFontsSymbolsOnly.zip` from the `NERD font
|
||||||
tell it to do so explicitly in case it doesnt with the :opt:`symbol_map`
|
releases page <https://github.com/ryanoasis/nerd-fonts/releases>`__). kitty
|
||||||
directive::
|
should pick up symbols from it automatically, and you can tell it to do so
|
||||||
|
explicitly in case it doesnt with the :opt:`symbol_map` directive::
|
||||||
|
|
||||||
symbol_map U+23FB-U+23FE,U+2B58,U+E200-U+E2A9,U+E0A0-U+E0A3,U+E0B0-U+E0BF,U+E0C0-U+E0C8,U+E0CC-U+E0CF,U+E0D0-U+E0D2,U+E0D4,U+E700-U+E7C5,U+F000-U+F2E0,U+2665,U+26A1,U+F400-U+F4A8,U+F67C,U+E000-U+E00A,U+F300-U+F313,U+E5FA-U+E62B Symbols Nerd Font
|
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
|
||||||
|
|
||||||
If your font is not listed in ``kitty list-fonts`` it means that it is not
|
If your font is not listed in ``kitty +list-fonts`` it means that it is not
|
||||||
monospace or is a bitmapped font. On Linux you can list all monospace fonts with::
|
monospace or is a bitmapped font. On Linux you can list all monospace fonts with::
|
||||||
|
|
||||||
fc-list : family spacing outline scalable | grep -e spacing=100 -e spacing=90 | grep -e outline=True | grep -e scalable=True
|
fc-list : family spacing outline scalable | grep -e spacing=100 -e spacing=90 | grep -e outline=True | grep -e scalable=True
|
||||||
@@ -236,7 +235,7 @@ command to rebuild your fontconfig cache::
|
|||||||
|
|
||||||
fc-cache -r
|
fc-cache -r
|
||||||
|
|
||||||
Then, the font will be available in ``kitty list-fonts``.
|
Then, the font will be available in ``kitty +list-fonts``.
|
||||||
|
|
||||||
|
|
||||||
How can I assign a single global shortcut to bring up the kitty terminal?
|
How can I assign a single global shortcut to bring up the kitty terminal?
|
||||||
@@ -270,6 +269,10 @@ homepage:
|
|||||||
:target: https://github.com/hristost/kitty-alternative-icon
|
:target: https://github.com/hristost/kitty-alternative-icon
|
||||||
:width: 256
|
:width: 256
|
||||||
|
|
||||||
|
.. image:: https://github.com/igrmk/whiskers/raw/main/whiskers.svg
|
||||||
|
:target: https://github.com/igrmk/whiskers
|
||||||
|
:width: 256
|
||||||
|
|
||||||
On macOS you can change the icon by following the steps:
|
On macOS you can change the icon by following the steps:
|
||||||
|
|
||||||
#. Find :file:`kitty.app` in the Applications folder, select it and press :kbd:`⌘+i`
|
#. Find :file:`kitty.app` in the Applications folder, select it and press :kbd:`⌘+i`
|
||||||
|
|||||||
@@ -57,6 +57,12 @@ Variables that influence kitty behavior
|
|||||||
Controls where kitty stores cache files. Defaults to :file:`~/.cache/kitty`
|
Controls where kitty stores cache files. Defaults to :file:`~/.cache/kitty`
|
||||||
or :file:`~/Library/Caches/kitty` on macOS.
|
or :file:`~/Library/Caches/kitty` on macOS.
|
||||||
|
|
||||||
|
.. envvar:: KITTY_RUNTIME_DIRECTORY
|
||||||
|
|
||||||
|
Controls where kitty stores runtime files like sockets. Defaults to
|
||||||
|
the :code:`XDG_RUNTIME_DIR` environment variable if that is defined
|
||||||
|
otherwise the run directory inside the kitty cache directory is used.
|
||||||
|
|
||||||
.. envvar:: VISUAL
|
.. envvar:: VISUAL
|
||||||
|
|
||||||
The terminal editor (such as ``vi`` or ``nano``) kitty uses, when, for
|
The terminal editor (such as ``vi`` or ``nano``) kitty uses, when, for
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ Some programs and libraries that use the kitty graphics protocol:
|
|||||||
Other terminals that have implemented the graphics protocol:
|
Other terminals that have implemented the graphics protocol:
|
||||||
|
|
||||||
* `WezTerm <https://github.com/wez/wezterm/issues/986>`_
|
* `WezTerm <https://github.com/wez/wezterm/issues/986>`_
|
||||||
|
* `Konsole <https://invent.kde.org/utilities/konsole/-/merge_requests/594>`_
|
||||||
|
* `wayst <https://github.com/91861/wayst>`_
|
||||||
|
|
||||||
|
|
||||||
Getting the window size
|
Getting the window size
|
||||||
@@ -325,14 +327,14 @@ use the *query action*, set ``a=q``. Then the terminal emulator will try to load
|
|||||||
the image and respond with either OK or an error, as above, but it will not
|
the image and respond with either OK or an error, as above, but it will not
|
||||||
replace an existing image with the same id, nor will it store the image.
|
replace an existing image with the same id, nor will it store the image.
|
||||||
|
|
||||||
As of September 2021, kitty and WezTerm are the only terminal emulators to support this
|
As of April 2022, kitty and WezTerm are the only terminal emulators to
|
||||||
graphics protocol. We intend that any terminal emulator that wishes to support
|
support this graphics protocol completely, with Konsole and wayst having partial support.
|
||||||
it can do so. To check if a terminal emulator supports the graphics protocol the best way
|
We intend that any terminal emulator that wishes to support it can do so. To
|
||||||
is to send the above *query action* followed by a request for the
|
check if a terminal emulator supports the graphics protocol the best way is to
|
||||||
`primary device attributes <https://vt100.net/docs/vt510-rm/DA1.html>`_. If you
|
send the above *query action* followed by a request for the `primary device
|
||||||
get back an answer for the device attributes without getting back an answer for
|
attributes <https://vt100.net/docs/vt510-rm/DA1.html>`_. If you get back an
|
||||||
the *query action* the terminal emulator does not support the graphics
|
answer for the device attributes without getting back an answer for the *query
|
||||||
protocol.
|
action* the terminal emulator does not support the graphics protocol.
|
||||||
|
|
||||||
This means that terminal emulators that support the graphics protocol, **must**
|
This means that terminal emulators that support the graphics protocol, **must**
|
||||||
reply to *query actions* immediately without processing other input. Most
|
reply to *query actions* immediately without processing other input. Most
|
||||||
|
|||||||
@@ -1,261 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# vim:fileencoding=utf-8
|
|
||||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
|
||||||
|
|
||||||
from __future__ import (
|
|
||||||
absolute_import, division, print_function, unicode_literals
|
|
||||||
)
|
|
||||||
|
|
||||||
import atexit
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import platform
|
|
||||||
import re
|
|
||||||
import shlex
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
py3 = sys.version_info[0] > 2
|
|
||||||
is64bit = platform.architecture()[0] == '64bit'
|
|
||||||
is_macos = 'darwin' in sys.platform.lower()
|
|
||||||
is_linux_arm = is_linux_arm64 = False
|
|
||||||
if is_macos:
|
|
||||||
mac_ver = tuple(map(int, platform.mac_ver()[0].split('.')))
|
|
||||||
if mac_ver[:2] < (10, 12):
|
|
||||||
raise SystemExit('Your version of macOS is too old, at least 10.12 is required')
|
|
||||||
else:
|
|
||||||
machine = (os.uname()[4] or '').lower()
|
|
||||||
if machine.startswith('arm') or machine.startswith('aarch64'):
|
|
||||||
is_linux_arm = True
|
|
||||||
is_linux_arm64 = machine.startswith('arm64') or machine.startswith('aarch64')
|
|
||||||
|
|
||||||
try:
|
|
||||||
__file__
|
|
||||||
from_file = True
|
|
||||||
except NameError:
|
|
||||||
from_file = False
|
|
||||||
|
|
||||||
if py3:
|
|
||||||
unicode = str
|
|
||||||
raw_input = input
|
|
||||||
import urllib.request as urllib
|
|
||||||
|
|
||||||
def encode_for_subprocess(x):
|
|
||||||
return x
|
|
||||||
else:
|
|
||||||
from future_builtins import map
|
|
||||||
import urllib2 as urllib
|
|
||||||
|
|
||||||
def encode_for_subprocess(x):
|
|
||||||
if isinstance(x, unicode):
|
|
||||||
x = x.encode('utf-8')
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
def run(*args):
|
|
||||||
if len(args) == 1:
|
|
||||||
args = shlex.split(args[0])
|
|
||||||
args = list(map(encode_for_subprocess, args))
|
|
||||||
ret = subprocess.Popen(args).wait()
|
|
||||||
if ret != 0:
|
|
||||||
raise SystemExit(ret)
|
|
||||||
|
|
||||||
|
|
||||||
class Reporter: # {{{
|
|
||||||
|
|
||||||
def __init__(self, fname):
|
|
||||||
self.fname = fname
|
|
||||||
self.last_percent = 0
|
|
||||||
|
|
||||||
def __call__(self, blocks, block_size, total_size):
|
|
||||||
percent = (blocks*block_size)/float(total_size)
|
|
||||||
report = '\rDownloaded {:.1%} '.format(percent)
|
|
||||||
if percent - self.last_percent > 0.05:
|
|
||||||
self.last_percent = percent
|
|
||||||
print(report, end='')
|
|
||||||
sys.stdout.flush()
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
|
|
||||||
def get_release_data(relname='latest'):
|
|
||||||
print('Checking for latest release on GitHub...')
|
|
||||||
req = urllib.Request(
|
|
||||||
'https://api.github.com/repos/kovidgoyal/kitty/releases/' + relname,
|
|
||||||
headers={'Accept': 'application/vnd.github.v3+json'})
|
|
||||||
try:
|
|
||||||
res = urllib.urlopen(req).read().decode('utf-8')
|
|
||||||
except Exception as err:
|
|
||||||
raise SystemExit('Failed to contact {} with error: {}'.format(req.get_full_url(), err))
|
|
||||||
data = json.loads(res)
|
|
||||||
html_url = data['html_url'].replace('/tag/', '/download/').rstrip('/')
|
|
||||||
for asset in data.get('assets', ()):
|
|
||||||
name = asset['name']
|
|
||||||
if is_macos:
|
|
||||||
if name.endswith('.dmg'):
|
|
||||||
return html_url + '/' + name, asset['size']
|
|
||||||
else:
|
|
||||||
if name.endswith('.txz'):
|
|
||||||
if is64bit:
|
|
||||||
q = '-arm64.txz' if is_linux_arm64 else '-x86_64.txz'
|
|
||||||
if name.endswith(q):
|
|
||||||
return html_url + '/' + name, asset['size']
|
|
||||||
else:
|
|
||||||
if name.endswith('-i686.txz'):
|
|
||||||
return html_url + '/' + name, asset['size']
|
|
||||||
raise SystemExit('Failed to find the installer package on github')
|
|
||||||
|
|
||||||
|
|
||||||
def do_download(url, size, dest):
|
|
||||||
print('Will download and install', os.path.basename(dest))
|
|
||||||
reporter = Reporter(os.path.basename(dest))
|
|
||||||
|
|
||||||
# Get content length and check if range is supported
|
|
||||||
rq = urllib.urlopen(url)
|
|
||||||
headers = rq.info()
|
|
||||||
sent_size = int(headers['content-length'])
|
|
||||||
if sent_size != size:
|
|
||||||
raise SystemExit('Failed to download from {} Content-Length ({}) != {}'.format(url, sent_size, size))
|
|
||||||
with open(dest, 'wb') as f:
|
|
||||||
while f.tell() < size:
|
|
||||||
raw = rq.read(8192)
|
|
||||||
if not raw:
|
|
||||||
break
|
|
||||||
f.write(raw)
|
|
||||||
reporter(f.tell(), 1, size)
|
|
||||||
rq.close()
|
|
||||||
if os.path.getsize(dest) < size:
|
|
||||||
raise SystemExit('Download failed, try again later')
|
|
||||||
print('\rDownloaded {} bytes'.format(os.path.getsize(dest)))
|
|
||||||
|
|
||||||
|
|
||||||
def clean_cache(cache, fname):
|
|
||||||
for x in os.listdir(cache):
|
|
||||||
if fname not in x:
|
|
||||||
os.remove(os.path.join(cache, x))
|
|
||||||
|
|
||||||
|
|
||||||
def download_installer(url, size):
|
|
||||||
fname = url.rpartition('/')[-1]
|
|
||||||
tdir = tempfile.gettempdir()
|
|
||||||
cache = os.path.join(tdir, 'kitty-installer-cache')
|
|
||||||
if not os.path.exists(cache):
|
|
||||||
os.makedirs(cache)
|
|
||||||
clean_cache(cache, fname)
|
|
||||||
dest = os.path.join(cache, fname)
|
|
||||||
if os.path.exists(dest) and os.path.getsize(dest) == size:
|
|
||||||
print('Using previously downloaded', fname)
|
|
||||||
return dest
|
|
||||||
if os.path.exists(dest):
|
|
||||||
os.remove(dest)
|
|
||||||
do_download(url, size, dest)
|
|
||||||
return dest
|
|
||||||
|
|
||||||
|
|
||||||
def macos_install(dmg, dest='/Applications', launch=True):
|
|
||||||
mp = tempfile.mkdtemp()
|
|
||||||
atexit.register(shutil.rmtree, mp)
|
|
||||||
run('hdiutil', 'attach', dmg, '-mountpoint', mp)
|
|
||||||
try:
|
|
||||||
os.chdir(mp)
|
|
||||||
app = 'kitty.app'
|
|
||||||
d = os.path.join(dest, app)
|
|
||||||
if os.path.exists(d):
|
|
||||||
shutil.rmtree(d)
|
|
||||||
dest = os.path.join(dest, app)
|
|
||||||
run('ditto', '-v', app, dest)
|
|
||||||
print('Successfully installed kitty into', dest)
|
|
||||||
if launch:
|
|
||||||
run('open', dest)
|
|
||||||
finally:
|
|
||||||
os.chdir('/')
|
|
||||||
run('hdiutil', 'detach', mp)
|
|
||||||
|
|
||||||
|
|
||||||
def linux_install(installer, dest=os.path.expanduser('~/.local'), launch=True):
|
|
||||||
dest = os.path.join(dest, 'kitty.app')
|
|
||||||
if os.path.exists(dest):
|
|
||||||
shutil.rmtree(dest)
|
|
||||||
os.makedirs(dest)
|
|
||||||
print('Extracting tarball...')
|
|
||||||
run('tar', '-C', dest, '-xJof', installer)
|
|
||||||
print('kitty successfully installed to', dest)
|
|
||||||
kitty = os.path.join(dest, 'bin', 'kitty')
|
|
||||||
print('Use', kitty, 'to run kitty')
|
|
||||||
if launch:
|
|
||||||
run(kitty, '--detach')
|
|
||||||
|
|
||||||
|
|
||||||
def main(dest=None, launch=True, installer=None):
|
|
||||||
if not dest:
|
|
||||||
if is_macos:
|
|
||||||
dest = '/Applications'
|
|
||||||
else:
|
|
||||||
dest = os.path.expanduser('~/.local')
|
|
||||||
if is_linux_arm and not is_linux_arm64:
|
|
||||||
raise SystemExit(
|
|
||||||
'You are running on a 32-bit ARM system. The kitty binaries are only'
|
|
||||||
' available for 64 bit ARM systems. You will have to build from'
|
|
||||||
' source.')
|
|
||||||
if not installer:
|
|
||||||
url, size = get_release_data()
|
|
||||||
installer = download_installer(url, size)
|
|
||||||
else:
|
|
||||||
if installer == 'nightly':
|
|
||||||
url, size = get_release_data('tags/nightly')
|
|
||||||
installer = download_installer(url, size)
|
|
||||||
else:
|
|
||||||
installer = os.path.abspath(installer)
|
|
||||||
if not os.access(installer, os.R_OK):
|
|
||||||
raise SystemExit('Could not read from: {}'.format(installer))
|
|
||||||
if is_macos:
|
|
||||||
macos_install(installer, dest=dest, launch=launch)
|
|
||||||
else:
|
|
||||||
linux_install(installer, dest=dest, launch=launch)
|
|
||||||
|
|
||||||
|
|
||||||
def script_launch():
|
|
||||||
# To test: python3 -c "import runpy; runpy.run_path('installer.py', run_name='script_launch')"
|
|
||||||
def path(x):
|
|
||||||
return os.path.expandvars(os.path.expanduser(x))
|
|
||||||
|
|
||||||
def to_bool(x):
|
|
||||||
return x.lower() in {'y', 'yes', '1', 'true'}
|
|
||||||
|
|
||||||
type_map = {x: path for x in 'dest installer'.split()}
|
|
||||||
type_map['launch'] = to_bool
|
|
||||||
kwargs = {}
|
|
||||||
|
|
||||||
for arg in sys.argv[1:]:
|
|
||||||
if arg:
|
|
||||||
m = re.match('([a-z_]+)=(.+)', arg)
|
|
||||||
if m is None:
|
|
||||||
raise SystemExit('Unrecognized command line argument: ' + arg)
|
|
||||||
k = m.group(1)
|
|
||||||
if k not in type_map:
|
|
||||||
raise SystemExit('Unrecognized command line argument: ' + arg)
|
|
||||||
kwargs[k] = type_map[k](m.group(2))
|
|
||||||
main(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def update_installer_wrapper():
|
|
||||||
# To run: python3 -c "import runpy; runpy.run_path('installer.py', run_name='update_wrapper')" installer.sh
|
|
||||||
with open(__file__, 'rb') as f:
|
|
||||||
src = f.read().decode('utf-8')
|
|
||||||
wrapper = sys.argv[-1]
|
|
||||||
with open(wrapper, 'r+b') as f:
|
|
||||||
raw = f.read().decode('utf-8')
|
|
||||||
nraw = re.sub(r'^# HEREDOC_START.+^# HEREDOC_END', lambda m: '# HEREDOC_START\n{}\n# HEREDOC_END'.format(src), raw, flags=re.MULTILINE | re.DOTALL)
|
|
||||||
if 'update_intaller_wrapper()' not in nraw:
|
|
||||||
raise SystemExit('regex substitute of HEREDOC failed')
|
|
||||||
f.seek(0), f.truncate()
|
|
||||||
f.write(nraw.encode('utf-8'))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__' and from_file:
|
|
||||||
main()
|
|
||||||
elif __name__ == 'update_wrapper':
|
|
||||||
update_installer_wrapper()
|
|
||||||
elif __name__ == 'script_launch':
|
|
||||||
script_launch()
|
|
||||||
@@ -1,292 +1,174 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
#
|
|
||||||
# installer.sh
|
|
||||||
# Copyright (C) 2018 Kovid Goyal <kovid at kovidgoyal.net>
|
# Copyright (C) 2018 Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
#
|
#
|
||||||
# Distributed under terms of the GPLv3 license.
|
# Distributed under terms of the GPLv3 license.
|
||||||
#
|
|
||||||
|
|
||||||
python=$(command -v python3)
|
{ \unalias command; \unset -f command; } >/dev/null 2>&1
|
||||||
if [ -z "$python" ]; then
|
tdir=''
|
||||||
python=$(command -v python2)
|
cleanup() {
|
||||||
fi
|
[ -n "$tdir" ] && {
|
||||||
if [ -z "$python" ]; then
|
command rm -rf "$tdir"
|
||||||
python=$(command -v python2.7)
|
tdir=''
|
||||||
fi
|
}
|
||||||
if [ -z "$python" ]; then
|
}
|
||||||
python=$(command -v python)
|
|
||||||
fi
|
|
||||||
if [ -z "$python" ]; then
|
|
||||||
python=python
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo Using python executable: $python
|
die() {
|
||||||
|
cleanup
|
||||||
|
printf "\033[31m%s\033[m\n\r" "$*" > /dev/stderr;
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
|
||||||
$python -c "import sys; script_launch=lambda:sys.exit('Download of installer failed!'); exec(sys.stdin.read()); script_launch()" "$@" <<'INSTALLER_HEREDOC'
|
detect_network_tool() {
|
||||||
# {{{
|
if command -v curl 2> /dev/null > /dev/null; then
|
||||||
# HEREDOC_START
|
fetch() {
|
||||||
#!/usr/bin/env python3
|
command curl -fL "$1"
|
||||||
# vim:fileencoding=utf-8
|
}
|
||||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
fetch_quiet() {
|
||||||
|
command curl -fsSL "$1"
|
||||||
from __future__ import (
|
}
|
||||||
absolute_import, division, print_function, unicode_literals
|
elif command -v wget 2> /dev/null > /dev/null; then
|
||||||
)
|
fetch() {
|
||||||
|
command wget -O- "$1"
|
||||||
import atexit
|
}
|
||||||
import json
|
fetch_quiet() {
|
||||||
import os
|
command wget --quiet -O- "$1"
|
||||||
import platform
|
}
|
||||||
import re
|
else
|
||||||
import shlex
|
die "Neither curl nor wget available, cannot download kitty"
|
||||||
import shutil
|
fi
|
||||||
import subprocess
|
}
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
py3 = sys.version_info[0] > 2
|
|
||||||
is64bit = platform.architecture()[0] == '64bit'
|
|
||||||
is_macos = 'darwin' in sys.platform.lower()
|
|
||||||
is_linux_arm = is_linux_arm64 = False
|
|
||||||
if is_macos:
|
|
||||||
mac_ver = tuple(map(int, platform.mac_ver()[0].split('.')))
|
|
||||||
if mac_ver[:2] < (10, 12):
|
|
||||||
raise SystemExit('Your version of macOS is too old, at least 10.12 is required')
|
|
||||||
else:
|
|
||||||
machine = (os.uname()[4] or '').lower()
|
|
||||||
if machine.startswith('arm') or machine.startswith('aarch64'):
|
|
||||||
is_linux_arm = True
|
|
||||||
is_linux_arm64 = machine.startswith('arm64') or machine.startswith('aarch64')
|
|
||||||
|
|
||||||
try:
|
|
||||||
__file__
|
|
||||||
from_file = True
|
|
||||||
except NameError:
|
|
||||||
from_file = False
|
|
||||||
|
|
||||||
if py3:
|
|
||||||
unicode = str
|
|
||||||
raw_input = input
|
|
||||||
import urllib.request as urllib
|
|
||||||
|
|
||||||
def encode_for_subprocess(x):
|
|
||||||
return x
|
|
||||||
else:
|
|
||||||
from future_builtins import map
|
|
||||||
import urllib2 as urllib
|
|
||||||
|
|
||||||
def encode_for_subprocess(x):
|
|
||||||
if isinstance(x, unicode):
|
|
||||||
x = x.encode('utf-8')
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
def run(*args):
|
detect_os() {
|
||||||
if len(args) == 1:
|
arch=""
|
||||||
args = shlex.split(args[0])
|
case "$(command uname)" in
|
||||||
args = list(map(encode_for_subprocess, args))
|
'Darwin') OS="macos";;
|
||||||
ret = subprocess.Popen(args).wait()
|
'Linux')
|
||||||
if ret != 0:
|
OS="linux"
|
||||||
raise SystemExit(ret)
|
case "$(command uname -m)" in
|
||||||
|
x86_64) arch="x86_64";;
|
||||||
|
aarch64*) arch="arm64";;
|
||||||
|
armv8*) arch="arm64";;
|
||||||
|
i386) arch="i686";;
|
||||||
|
i686) arch="i686";;
|
||||||
|
*) die "Unknown CPU architecture $(command uname -m)";;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
*) die "kitty binaries are not available for $(command uname)"
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
expand_tilde() {
|
||||||
|
tilde_less="${1#\~/}"
|
||||||
|
[ "$1" != "$tilde_less" ] && tilde_less="$HOME/$tilde_less"
|
||||||
|
printf '%s' "$tilde_less"
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
dest='~/.local'
|
||||||
|
[ "$OS" = "macos" ] && dest="/Applications"
|
||||||
|
launch='y'
|
||||||
|
installer=''
|
||||||
|
while :; do
|
||||||
|
case "$1" in
|
||||||
|
dest=*) dest="${1#*=}";;
|
||||||
|
launch=*) launch="${1#*=}";;
|
||||||
|
installer=*) installer="${1#*=}";;
|
||||||
|
"") break;;
|
||||||
|
*) die "Unrecognized command line option: $1";;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
dest=$(expand_tilde "${dest}")
|
||||||
|
[ "$launch" != "y" -a "$launch" != "n" ] && die "Unrecognized command line option: launch=$launch"
|
||||||
|
dest="$dest/kitty.app"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Reporter: # {{{
|
get_file_url() {
|
||||||
|
url="https://github.com/kovidgoyal/kitty/releases/download/$1/kitty-$2"
|
||||||
|
if [ "$OS" = "macos" ]; then
|
||||||
|
url="$url.dmg"
|
||||||
|
else
|
||||||
|
url="$url-$arch.txz"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, fname):
|
get_release_url() {
|
||||||
self.fname = fname
|
release_version=$(fetch_quiet "https://sw.kovidgoyal.net/kitty/current-version.txt")
|
||||||
self.last_percent = 0
|
[ $? -ne 0 -o -z "$release_version" ] && die "Could not get kitty latest release version"
|
||||||
|
get_file_url "v$release_version" "$release_version"
|
||||||
def __call__(self, blocks, block_size, total_size):
|
}
|
||||||
percent = (blocks*block_size)/float(total_size)
|
|
||||||
report = '\rDownloaded {:.1%} '.format(percent)
|
|
||||||
if percent - self.last_percent > 0.05:
|
|
||||||
self.last_percent = percent
|
|
||||||
print(report, end='')
|
|
||||||
sys.stdout.flush()
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
|
|
||||||
def get_release_data(relname='latest'):
|
get_nightly_url() {
|
||||||
print('Checking for latest release on GitHub...')
|
get_file_url "nightly" "nightly"
|
||||||
req = urllib.Request(
|
}
|
||||||
'https://api.github.com/repos/kovidgoyal/kitty/releases/' + relname,
|
|
||||||
headers={'Accept': 'application/vnd.github.v3+json'})
|
|
||||||
try:
|
|
||||||
res = urllib.urlopen(req).read().decode('utf-8')
|
|
||||||
except Exception as err:
|
|
||||||
raise SystemExit('Failed to contact {} with error: {}'.format(req.get_full_url(), err))
|
|
||||||
data = json.loads(res)
|
|
||||||
html_url = data['html_url'].replace('/tag/', '/download/').rstrip('/')
|
|
||||||
for asset in data.get('assets', ()):
|
|
||||||
name = asset['name']
|
|
||||||
if is_macos:
|
|
||||||
if name.endswith('.dmg'):
|
|
||||||
return html_url + '/' + name, asset['size']
|
|
||||||
else:
|
|
||||||
if name.endswith('.txz'):
|
|
||||||
if is64bit:
|
|
||||||
q = '-arm64.txz' if is_linux_arm64 else '-x86_64.txz'
|
|
||||||
if name.endswith(q):
|
|
||||||
return html_url + '/' + name, asset['size']
|
|
||||||
else:
|
|
||||||
if name.endswith('-i686.txz'):
|
|
||||||
return html_url + '/' + name, asset['size']
|
|
||||||
raise SystemExit('Failed to find the installer package on github')
|
|
||||||
|
|
||||||
|
get_download_url() {
|
||||||
|
installer_is_file="n"
|
||||||
|
case "$installer" in
|
||||||
|
"nightly") get_nightly_url ;;
|
||||||
|
"") get_release_url ;;
|
||||||
|
*) installer_is_file="y" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
def do_download(url, size, dest):
|
linux_install() {
|
||||||
print('Will download and install', os.path.basename(dest))
|
if [ "$installer_is_file" = "y" ]; then
|
||||||
reporter = Reporter(os.path.basename(dest))
|
command tar -C "$dest" "-xJof" "$installer"
|
||||||
|
else
|
||||||
|
printf '%s\n\n' "Downloading from: $url"
|
||||||
|
fetch "$url" | command tar -C "$dest" "-xJof" "-"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Get content length and check if range is supported
|
macos_install() {
|
||||||
rq = urllib.urlopen(url)
|
tdir=$(command mktemp -d "/tmp/kitty-install-XXXXXXXXXXXX")
|
||||||
headers = rq.info()
|
[ "$installer_is_file" != "y" ] && {
|
||||||
sent_size = int(headers['content-length'])
|
installer="$tdir/kitty.dmg"
|
||||||
if sent_size != size:
|
printf '%s\n\n' "Downloading from: $url"
|
||||||
raise SystemExit('Failed to download from {} Content-Length ({}) != {}'.format(url, sent_size, size))
|
fetch "$url" > "$installer" || die "Failed to download: $url"
|
||||||
with open(dest, 'wb') as f:
|
}
|
||||||
while f.tell() < size:
|
command mkdir "$tdir/mp"
|
||||||
raw = rq.read(8192)
|
command hdiutil attach "$installer" "-mountpoint" "$tdir/mp" || die "Failed to mount kitty.dmg"
|
||||||
if not raw:
|
command ditto -v "$tdir/mp/kitty.app" "$dest"
|
||||||
break
|
rc="$?"
|
||||||
f.write(raw)
|
command hdiutil detach "$tdir/mp"
|
||||||
reporter(f.tell(), 1, size)
|
command rm -rf "$tdir"
|
||||||
rq.close()
|
tdir=''
|
||||||
if os.path.getsize(dest) < size:
|
[ "$rc" != "0" ] && die "Failed to copy kitty.app from mounted dmg"
|
||||||
raise SystemExit('Download failed, try again later')
|
}
|
||||||
print('\rDownloaded {} bytes'.format(os.path.getsize(dest)))
|
|
||||||
|
|
||||||
|
prepare_install_dest() {
|
||||||
|
printf "%s\n" "Installing to $dest"
|
||||||
|
command rm -rf "$dest"
|
||||||
|
command mkdir -p "$dest" || die "Failed to create the directory: $dest"
|
||||||
|
}
|
||||||
|
|
||||||
def clean_cache(cache, fname):
|
exec_kitty() {
|
||||||
for x in os.listdir(cache):
|
if [ "$OS" = "macos" ]; then
|
||||||
if fname not in x:
|
exec "open" "$dest"
|
||||||
os.remove(os.path.join(cache, x))
|
else
|
||||||
|
exec "$dest/bin/kitty" "--detach"
|
||||||
|
fi
|
||||||
|
die "Failed to launch kitty"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
[ -n "$KITTY_WINDOW_ID" ] && die "You should not try to update kitty from within kitty itself. For best results, quit all kitty instances and run this script from another terminal"
|
||||||
|
detect_os
|
||||||
|
parse_args "$@"
|
||||||
|
detect_network_tool
|
||||||
|
get_download_url
|
||||||
|
prepare_install_dest
|
||||||
|
if [ "$OS" = "macos" ]; then
|
||||||
|
macos_install
|
||||||
|
else
|
||||||
|
linux_install
|
||||||
|
fi
|
||||||
|
[ "$launch" = "y" ] && exec_kitty
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
def download_installer(url, size):
|
main "$@"
|
||||||
fname = url.rpartition('/')[-1]
|
|
||||||
tdir = tempfile.gettempdir()
|
|
||||||
cache = os.path.join(tdir, 'kitty-installer-cache')
|
|
||||||
if not os.path.exists(cache):
|
|
||||||
os.makedirs(cache)
|
|
||||||
clean_cache(cache, fname)
|
|
||||||
dest = os.path.join(cache, fname)
|
|
||||||
if os.path.exists(dest) and os.path.getsize(dest) == size:
|
|
||||||
print('Using previously downloaded', fname)
|
|
||||||
return dest
|
|
||||||
if os.path.exists(dest):
|
|
||||||
os.remove(dest)
|
|
||||||
do_download(url, size, dest)
|
|
||||||
return dest
|
|
||||||
|
|
||||||
|
|
||||||
def macos_install(dmg, dest='/Applications', launch=True):
|
|
||||||
mp = tempfile.mkdtemp()
|
|
||||||
atexit.register(shutil.rmtree, mp)
|
|
||||||
run('hdiutil', 'attach', dmg, '-mountpoint', mp)
|
|
||||||
try:
|
|
||||||
os.chdir(mp)
|
|
||||||
app = 'kitty.app'
|
|
||||||
d = os.path.join(dest, app)
|
|
||||||
if os.path.exists(d):
|
|
||||||
shutil.rmtree(d)
|
|
||||||
dest = os.path.join(dest, app)
|
|
||||||
run('ditto', '-v', app, dest)
|
|
||||||
print('Successfully installed kitty into', dest)
|
|
||||||
if launch:
|
|
||||||
run('open', dest)
|
|
||||||
finally:
|
|
||||||
os.chdir('/')
|
|
||||||
run('hdiutil', 'detach', mp)
|
|
||||||
|
|
||||||
|
|
||||||
def linux_install(installer, dest=os.path.expanduser('~/.local'), launch=True):
|
|
||||||
dest = os.path.join(dest, 'kitty.app')
|
|
||||||
if os.path.exists(dest):
|
|
||||||
shutil.rmtree(dest)
|
|
||||||
os.makedirs(dest)
|
|
||||||
print('Extracting tarball...')
|
|
||||||
run('tar', '-C', dest, '-xJof', installer)
|
|
||||||
print('kitty successfully installed to', dest)
|
|
||||||
kitty = os.path.join(dest, 'bin', 'kitty')
|
|
||||||
print('Use', kitty, 'to run kitty')
|
|
||||||
if launch:
|
|
||||||
run(kitty, '--detach')
|
|
||||||
|
|
||||||
|
|
||||||
def main(dest=None, launch=True, installer=None):
|
|
||||||
if not dest:
|
|
||||||
if is_macos:
|
|
||||||
dest = '/Applications'
|
|
||||||
else:
|
|
||||||
dest = os.path.expanduser('~/.local')
|
|
||||||
if is_linux_arm and not is_linux_arm64:
|
|
||||||
raise SystemExit(
|
|
||||||
'You are running on a 32-bit ARM system. The kitty binaries are only'
|
|
||||||
' available for 64 bit ARM systems. You will have to build from'
|
|
||||||
' source.')
|
|
||||||
if not installer:
|
|
||||||
url, size = get_release_data()
|
|
||||||
installer = download_installer(url, size)
|
|
||||||
else:
|
|
||||||
if installer == 'nightly':
|
|
||||||
url, size = get_release_data('tags/nightly')
|
|
||||||
installer = download_installer(url, size)
|
|
||||||
else:
|
|
||||||
installer = os.path.abspath(installer)
|
|
||||||
if not os.access(installer, os.R_OK):
|
|
||||||
raise SystemExit('Could not read from: {}'.format(installer))
|
|
||||||
if is_macos:
|
|
||||||
macos_install(installer, dest=dest, launch=launch)
|
|
||||||
else:
|
|
||||||
linux_install(installer, dest=dest, launch=launch)
|
|
||||||
|
|
||||||
|
|
||||||
def script_launch():
|
|
||||||
# To test: python3 -c "import runpy; runpy.run_path('installer.py', run_name='script_launch')"
|
|
||||||
def path(x):
|
|
||||||
return os.path.expandvars(os.path.expanduser(x))
|
|
||||||
|
|
||||||
def to_bool(x):
|
|
||||||
return x.lower() in {'y', 'yes', '1', 'true'}
|
|
||||||
|
|
||||||
type_map = {x: path for x in 'dest installer'.split()}
|
|
||||||
type_map['launch'] = to_bool
|
|
||||||
kwargs = {}
|
|
||||||
|
|
||||||
for arg in sys.argv[1:]:
|
|
||||||
if arg:
|
|
||||||
m = re.match('([a-z_]+)=(.+)', arg)
|
|
||||||
if m is None:
|
|
||||||
raise SystemExit('Unrecognized command line argument: ' + arg)
|
|
||||||
k = m.group(1)
|
|
||||||
if k not in type_map:
|
|
||||||
raise SystemExit('Unrecognized command line argument: ' + arg)
|
|
||||||
kwargs[k] = type_map[k](m.group(2))
|
|
||||||
main(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def update_installer_wrapper():
|
|
||||||
# To run: python3 -c "import runpy; runpy.run_path('installer.py', run_name='update_wrapper')" installer.sh
|
|
||||||
with open(__file__, 'rb') as f:
|
|
||||||
src = f.read().decode('utf-8')
|
|
||||||
wrapper = sys.argv[-1]
|
|
||||||
with open(wrapper, 'r+b') as f:
|
|
||||||
raw = f.read().decode('utf-8')
|
|
||||||
nraw = re.sub(r'^# HEREDOC_START.+^# HEREDOC_END', lambda m: '# HEREDOC_START\n{}\n# HEREDOC_END'.format(src), raw, flags=re.MULTILINE | re.DOTALL)
|
|
||||||
if 'update_intaller_wrapper()' not in nraw:
|
|
||||||
raise SystemExit('regex substitute of HEREDOC failed')
|
|
||||||
f.seek(0), f.truncate()
|
|
||||||
f.write(nraw.encode('utf-8'))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__' and from_file:
|
|
||||||
main()
|
|
||||||
elif __name__ == 'update_wrapper':
|
|
||||||
update_installer_wrapper()
|
|
||||||
elif __name__ == 'script_launch':
|
|
||||||
script_launch()
|
|
||||||
|
|
||||||
# HEREDOC_END
|
|
||||||
# }}}
|
|
||||||
INSTALLER_HEREDOC
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ In addition to kitty, this protocol is also implemented in:
|
|||||||
* The `notcurses library
|
* The `notcurses library
|
||||||
<https://github.com/dankamongmen/notcurses/issues/2131>`__
|
<https://github.com/dankamongmen/notcurses/issues/2131>`__
|
||||||
* The `kakoune text editor <https://github.com/mawww/kakoune/issues/4103>`__
|
* The `kakoune text editor <https://github.com/mawww/kakoune/issues/4103>`__
|
||||||
|
* The `dte text editor <https://gitlab.com/craigbarnes/dte/-/issues/138>`__
|
||||||
|
|
||||||
.. versionadded:: 0.20.0
|
.. versionadded:: 0.20.0
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ your machine).
|
|||||||
# get the kitty window into which to paste answer
|
# get the kitty window into which to paste answer
|
||||||
w = boss.window_id_map.get(target_window_id)
|
w = boss.window_id_map.get(target_window_id)
|
||||||
if w is not None:
|
if w is not None:
|
||||||
w.paste(answer)
|
w.paste_text(answer)
|
||||||
|
|
||||||
|
|
||||||
Now in :file:`kitty.conf` add the lines::
|
Now in :file:`kitty.conf` add the lines::
|
||||||
@@ -134,7 +134,7 @@ terminal program, you can tell the kittens system to run the
|
|||||||
|
|
||||||
For example, here is a kitten that "zooms/unzooms" the current terminal window
|
For example, here is a kitten that "zooms/unzooms" the current terminal window
|
||||||
by switching to the stack layout or back to the previous layout. This is
|
by switching to the stack layout or back to the previous layout. This is
|
||||||
equivalent to the builtin :ref:`action-toggle_layout` action.
|
equivalent to the builtin :ac:`toggle_layout` action.
|
||||||
|
|
||||||
Create a file in the kitty config folder, :file:`~/.config/kitty/zoom_toggle.py`
|
Create a file in the kitty config folder, :file:`~/.config/kitty/zoom_toggle.py`
|
||||||
|
|
||||||
|
|||||||
154
docs/kittens/ssh.rst
Normal file
154
docs/kittens/ssh.rst
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
Truly convenient SSH
|
||||||
|
=========================================
|
||||||
|
|
||||||
|
* Automatic :ref:`shell_integration` on remote hosts
|
||||||
|
|
||||||
|
* Easily :ref:`clone local shell/editor config <real_world_ssh_kitten_config>` on remote hosts
|
||||||
|
|
||||||
|
* Automatic :opt:`re-use of existing connections <kitten-ssh.share_connections>` to avoid connection setup latency
|
||||||
|
|
||||||
|
* Make kitty itself available in the remote host :opt:`on demand <kitten-ssh.remote_kitty>`
|
||||||
|
|
||||||
|
* Easily :opt:`change terminal colors <kitten-ssh.color_scheme>` when connecting to remote hosts
|
||||||
|
|
||||||
|
.. versionadded:: 0.25.0
|
||||||
|
Automatic shell integration, file transfer and reuse of connections
|
||||||
|
|
||||||
|
The ssh kitten allows you to login easily to remote hosts, and automatically
|
||||||
|
setup the environment there to be as comfortable as your local shell. You
|
||||||
|
can specify environment variables to set on the remote host and
|
||||||
|
files to copy there, making your remote experience just like your
|
||||||
|
local shell. Additionally, it automatically sets up :ref:`shell_integration` on
|
||||||
|
the remote host and copies the kitty terminfo database there.
|
||||||
|
|
||||||
|
The ssh kitten is a thin wrapper around the traditional `ssh <https://man.openbsd.org/ssh>`__
|
||||||
|
command line program and supports all the same options and arguments and configuration.
|
||||||
|
In interactive usage scenarios it is a drop in replacement for ``ssh``. To try it
|
||||||
|
out, simply run:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
kitty +kitten ssh some-hostname-to-connect-to
|
||||||
|
|
||||||
|
You should end up at a shell prompt on the remote host, with shell
|
||||||
|
integration enabled. If you like it you can add an alias to it in your shell's
|
||||||
|
rc files:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
alias s=kitty +kitten ssh
|
||||||
|
|
||||||
|
So now you can just type ``s hostname`` to connect.
|
||||||
|
|
||||||
|
If you define a mapping in :file:`kitty.conf` such as::
|
||||||
|
|
||||||
|
map f1 new_window_with_cwd
|
||||||
|
|
||||||
|
Then, pressing :kbd:`F1` will open a new window automatically logged
|
||||||
|
into the same host using the ssh kitten, at the same directory.
|
||||||
|
|
||||||
|
The ssh kitten can be configured using the :file:`~/.config/kitty/ssh.conf`
|
||||||
|
file where you can specify environment variables to set on the remote host
|
||||||
|
and files to copy from the local to the remote host. Let's see a
|
||||||
|
quick example:
|
||||||
|
|
||||||
|
.. code-block:: conf
|
||||||
|
|
||||||
|
# Copy the files and directories needed to setup some common tools
|
||||||
|
copy .zshrc .vimrc .vim
|
||||||
|
# Setup some environment variables
|
||||||
|
env SOME_VAR=x
|
||||||
|
# COPIED_VAR will have the same value on the remote host as it does locally
|
||||||
|
env COPIED_VAR=_kitty_copy_env_var_
|
||||||
|
|
||||||
|
# Create some per hostname settings
|
||||||
|
hostname someserver-*
|
||||||
|
copy env-files
|
||||||
|
env SOMETHING=else
|
||||||
|
|
||||||
|
hostname someuser@somehost
|
||||||
|
copy --dest=foo/bar some-file
|
||||||
|
copy --glob some/files.*
|
||||||
|
|
||||||
|
|
||||||
|
See below for full details on the syntax and options of :file:`ssh.conf`.
|
||||||
|
Additionally, you can pass config options on the command line:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
kitty +kitten ssh --kitten interpreter=python servername
|
||||||
|
|
||||||
|
The :code:`--kitten` argument can be specified multiple times, with directives
|
||||||
|
from :file:`ssh.conf`. These are merged with :file:`ssh.conf` as if they were
|
||||||
|
appended to the end of that file. They apply only to the host being SSHed to
|
||||||
|
by this invocation, so any :opt:`hostname <kitten-ssh.hostname>` directives are ignored.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Due to limitations in the design of SSH, any typing you do before the
|
||||||
|
shell prompt appears may be lost. So ideally don't start typing till you see
|
||||||
|
the shell prompt. 😇
|
||||||
|
|
||||||
|
|
||||||
|
.. _real_world_ssh_kitten_config:
|
||||||
|
|
||||||
|
A real world example
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Suppose you often SSH into a production server, and you would like to setup
|
||||||
|
your shell and editor there using your custom settings. However, other people
|
||||||
|
could SSH in as well and you don't want to clobber their settings. Here is how
|
||||||
|
this could be achieved using the ssh kitten with zsh and vim as the shell and
|
||||||
|
editor, respectively:
|
||||||
|
|
||||||
|
.. code-block:: conf
|
||||||
|
|
||||||
|
# Have these settings apply to servers in my organization
|
||||||
|
hostname myserver-*
|
||||||
|
|
||||||
|
# Setup zsh to read its files from my-conf/zsh
|
||||||
|
env ZDOTDIR $HOME/my-conf/zsh
|
||||||
|
copy --dest my-conf/zsh/.zshrc .zshrc
|
||||||
|
copy --dest my-conf/zsh/.zshenv .zshenv
|
||||||
|
# If you use other zsh init files add them in a similar manner
|
||||||
|
|
||||||
|
# Setup vim to read its config from my-conf/vim
|
||||||
|
env VIMINIT $HOME/my-conf/vim/vimrc
|
||||||
|
env VIMRUNTIME $HOME/my-conf/vim
|
||||||
|
copy --dest my-conf/vim .vim
|
||||||
|
copy --dest my-conf/vim/vimrc .vimrc
|
||||||
|
|
||||||
|
|
||||||
|
How it works
|
||||||
|
----------------
|
||||||
|
|
||||||
|
The ssh kitten works by having SSH transmit and execute a POSIX sh (or
|
||||||
|
:opt:`optionally <kitten-ssh.interpreter>` Python) bootstrap script on the
|
||||||
|
remote host using an :opt:`interpreter <kitten-ssh.interpreter>`. This script
|
||||||
|
reads setup data over the tty device, which kitty sends as a base64 encoded
|
||||||
|
compressed tarball. The script extracts it and places the :opt:`files <kitten-ssh.copy>`
|
||||||
|
and sets the :opt:`environment variables <kitten-ssh.env>` before finally
|
||||||
|
launching the :opt:`login shell <kitten-ssh.login_shell>` with :opt:`shell
|
||||||
|
integration <kitten-ssh.shell_integration>` enabled. The data is requested by
|
||||||
|
the kitten over the TTY with a random one time password. kitty reads the request
|
||||||
|
and if the password matches a password pre-stored in shared memory on the
|
||||||
|
localhost by the kitten, the transmission is allowed. If your OpenSSH version is
|
||||||
|
>= 8.4 then the data is transmitted instantly without any roundtrip delay.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
When connecting to BSD hosts, it is possible the bootstrap script will
|
||||||
|
fail or run slowly, because the default shells are crippled in various ways.
|
||||||
|
Your best bet is to install Python on the remote, make sure the login shell
|
||||||
|
is something POSIX sh compliant, and use :code:`python` as the :opt:`interpreter
|
||||||
|
<kitten-ssh.interpreter>` in :file:`ssh.conf`.
|
||||||
|
|
||||||
|
.. include:: /generated/conf-kitten-ssh.rst
|
||||||
|
|
||||||
|
|
||||||
|
.. _ssh_copy_command:
|
||||||
|
|
||||||
|
The copy command
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
.. include:: /generated/ssh-copy.rst
|
||||||
@@ -15,6 +15,7 @@ Extend with kittens
|
|||||||
kittens/remote_file
|
kittens/remote_file
|
||||||
kittens/hyperlinked_grep
|
kittens/hyperlinked_grep
|
||||||
kittens/transfer
|
kittens/transfer
|
||||||
|
kittens/ssh
|
||||||
kittens/custom
|
kittens/custom
|
||||||
kittens/*
|
kittens/*
|
||||||
|
|
||||||
@@ -62,6 +63,11 @@ Some prominent kittens:
|
|||||||
other :term:`kitty windows <window>`.
|
other :term:`kitty windows <window>`.
|
||||||
|
|
||||||
|
|
||||||
|
:doc:`SSH <kittens/ssh>`
|
||||||
|
SSH with automatic shell integration, connection re-use for low latency and easy cloning
|
||||||
|
of local shell and editor configuration to the remote machine.
|
||||||
|
|
||||||
|
|
||||||
:doc:`Panel <kittens/panel>`
|
:doc:`Panel <kittens/panel>`
|
||||||
Draw a GPU accelerated dock panel on your desktop showing the output
|
Draw a GPU accelerated dock panel on your desktop showing the output
|
||||||
from an arbitrary terminal program.
|
from an arbitrary terminal program.
|
||||||
|
|||||||
@@ -95,6 +95,12 @@ the command line:
|
|||||||
``@cursor-y``
|
``@cursor-y``
|
||||||
replaced by the current cursor y position with 1 being the topmost cell
|
replaced by the current cursor y position with 1 being the topmost cell
|
||||||
|
|
||||||
|
``@first-line-on-screen``
|
||||||
|
replaced by the first line on screen. Can be used for pager positioning.
|
||||||
|
|
||||||
|
``@last-line-on-screen``
|
||||||
|
replaced by the last line on screen. Can be used for pager positioning.
|
||||||
|
|
||||||
|
|
||||||
For example::
|
For example::
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,11 @@ define a few extra key bindings in :file:`kitty.conf`::
|
|||||||
# the two windows are placed side by side
|
# the two windows are placed side by side
|
||||||
map F6 launch --location=vsplit
|
map F6 launch --location=vsplit
|
||||||
|
|
||||||
|
# Create a new window splitting the space used by the existing one so that
|
||||||
|
# the two windows are placed side by side if the existing window is wide or
|
||||||
|
# one above the other if the existing window is tall.
|
||||||
|
map F4 launch --location=split
|
||||||
|
|
||||||
# Rotate the current split, chaging its split axis from vertical to
|
# Rotate the current split, chaging its split axis from vertical to
|
||||||
# horizontal or vice versa
|
# horizontal or vice versa
|
||||||
map F7 layout_action rotate
|
map F7 layout_action rotate
|
||||||
@@ -248,6 +253,8 @@ action, for example::
|
|||||||
map ctrl+right resize_window wider
|
map ctrl+right resize_window wider
|
||||||
map ctrl+up resize_window taller
|
map ctrl+up resize_window taller
|
||||||
map ctrl+down resize_window shorter 3
|
map ctrl+down resize_window shorter 3
|
||||||
|
# reset all windows in the tab to default sizes
|
||||||
|
map ctrl+home resize_window reset
|
||||||
|
|
||||||
The ``resize_window`` action has a second, optional argument to control
|
The ``resize_window`` action has a second, optional argument to control
|
||||||
the resizing increment (a positive integer that defaults to 1).
|
the resizing increment (a positive integer that defaults to 1).
|
||||||
|
|||||||
@@ -102,14 +102,38 @@ lines. The various available criteria are:
|
|||||||
``image-??.png``
|
``image-??.png``
|
||||||
|
|
||||||
|
|
||||||
|
.. _launch_actions:
|
||||||
|
|
||||||
Scripting the opening of files with kitty on macOS
|
Scripting the opening of files with kitty on macOS
|
||||||
-------------------------------------------------------
|
-------------------------------------------------------
|
||||||
|
|
||||||
On macOS you can use :guilabel:`Open With` in Finder or drag and drop files
|
On macOS you can use :guilabel:`Open With` in Finder or drag and drop files and
|
||||||
onto the kitty dock icon to open them with kitty. The default action
|
URLs onto the kitty dock icon to open them with kitty. The default actions are:
|
||||||
is to open text files in your editor and images using the icat kitten.
|
|
||||||
Shell scripts are run in a shell. You can customize these actions by creating
|
* Open text files in your editor and images using the icat kitten.
|
||||||
a :file:`launch-actions.conf` file in the kitty config directory, just like
|
* Run shell scripts in a shell
|
||||||
|
* Open SSH urls using the ssh command
|
||||||
|
|
||||||
|
These actions can also be executed from the command line by running::
|
||||||
|
|
||||||
|
open -a kitty.app file_or_url ... (on macOS only)
|
||||||
|
or
|
||||||
|
kitty +open file_or_url ...
|
||||||
|
|
||||||
|
Since macOS lacks an official interface to set default URL scheme handlers,
|
||||||
|
kitty has a command you can use for it. The first argument for is the URL
|
||||||
|
scheme, and the second optional argument is the bundle id of the app, which
|
||||||
|
defaults to kitty, if not specified. For example:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
# Set kitty as the handler for ssh:// URLs
|
||||||
|
kitty +runpy 'from kitty.fast_data_types import cocoa_set_url_handler; import sys; cocoa_set_url_handler(*sys.argv[1:]); print("OK")' ssh
|
||||||
|
# Set someapp as the handler for xyz:// URLs
|
||||||
|
kitty +runpy 'from kitty.fast_data_types import cocoa_set_url_handler; import sys; cocoa_set_url_handler(*sys.argv[1:]); print("OK")' xyz someapp.bundle.identifier
|
||||||
|
|
||||||
|
You can customize these actions by creating a :file:`launch-actions.conf` file
|
||||||
|
in the kitty config directory, just like
|
||||||
the :file:`open-actions.conf` file above. For example:
|
the :file:`open-actions.conf` file above. For example:
|
||||||
|
|
||||||
.. code:: conf
|
.. code:: conf
|
||||||
@@ -138,3 +162,7 @@ the :file:`open-actions.conf` file above. For example:
|
|||||||
protocol file
|
protocol file
|
||||||
mime image/*
|
mime image/*
|
||||||
action launch --type=os-window kitty +kitten icat --hold $FILE_PATH
|
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
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
Press mentions of kitty
|
Press mentions of kitty
|
||||||
========================
|
========================
|
||||||
|
|
||||||
`Console #88 <https://console.substack.com/p/console-88>`__
|
`Python Bytes 272 <https://youtu.be/8HKliSbA-gQ?t=815>`__ (Feb 2022)
|
||||||
|
A podcast demoing some of kitty's coolness
|
||||||
|
|
||||||
|
`Console #88 <https://console.substack.com/p/console-88>`__ (Jan 2022)
|
||||||
An interview with Kovid about kitty
|
An interview with Kovid about kitty
|
||||||
|
|
||||||
|
|
||||||
|
Video reviews
|
||||||
|
--------------
|
||||||
|
|
||||||
|
`Review (Jan 2021) <https://www.youtube.com/watch?v=TTzP2zYJn2k>`__
|
||||||
|
A kitty review by distrotube
|
||||||
|
|
||||||
|
`Review (Dec 2020) <https://www.youtube.com/watch?v=KUMkLhFeBrI>`__
|
||||||
|
A kitty review/intro by TechHut
|
||||||
|
|||||||
@@ -48,11 +48,16 @@ off or it can be disabled entirely as well. The :opt:`shell_integration` option
|
|||||||
takes a space separated list of keywords:
|
takes a space separated list of keywords:
|
||||||
|
|
||||||
disabled
|
disabled
|
||||||
Turn off all shell integration
|
Turn off all shell integration. The shell's launch environment is not
|
||||||
|
modified and :envvar:`KITTY_SHELL_INTEGRATION` is not set. Useful for
|
||||||
|
:ref:`manual integration <manual_shell_integration>`.
|
||||||
|
|
||||||
no-rc
|
no-rc
|
||||||
Do not modify the shell's launch environment to enable integration. Useful if you prefer
|
Do not modify the shell's launch environment to enable integration. Useful if you prefer
|
||||||
to :ref:`manually enable integration <manual_shell_integration>`.
|
to load the kitty shell integration code yourself, either as part of
|
||||||
|
:ref:`manually integration <manual_shell_integration>` or because you have
|
||||||
|
some other software that sets up shell integration.
|
||||||
|
This will still set the :envvar:`KITTY_SHELL_INTEGRATION` environment variable when kitty runs the shell.
|
||||||
|
|
||||||
no-cursor
|
no-cursor
|
||||||
Turn off changing of the text cursor to a bar when editing text
|
Turn off changing of the text cursor to a bar when editing text
|
||||||
@@ -62,6 +67,14 @@ no-title
|
|||||||
Note that for the ``fish`` shell kitty relies on fish's native title
|
Note that for the ``fish`` shell kitty relies on fish's native title
|
||||||
setting functionality instead.
|
setting functionality instead.
|
||||||
|
|
||||||
|
no-cwd
|
||||||
|
Turn off reporting the current working directory. This is used to allow
|
||||||
|
:ac:`new_window_with_cwd` and similar to open windows logged
|
||||||
|
into remote machines using the :doc:`ssh kitten <kittens/ssh>`
|
||||||
|
automatically with the same working directory as the current window.
|
||||||
|
Note that for the ``fish`` shell this will not disable its built-in current
|
||||||
|
working directory reporting.
|
||||||
|
|
||||||
no-prompt-mark
|
no-prompt-mark
|
||||||
Turn off marking of prompts. This disables jumping to prompt, browsing
|
Turn off marking of prompts. This disables jumping to prompt, browsing
|
||||||
output of last command and click to move cursor functionality.
|
output of last command and click to move cursor functionality.
|
||||||
@@ -88,7 +101,7 @@ to be copied.
|
|||||||
|
|
||||||
The feature to jump to previous prompts (
|
The feature to jump to previous prompts (
|
||||||
:sc:`scroll_to_previous_prompt` and :sc:`scroll_to_next_prompt`) and mouse
|
:sc:`scroll_to_previous_prompt` and :sc:`scroll_to_next_prompt`) and mouse
|
||||||
actions (:ref:`action-mouse_select_command_output` and :ref:`action-mouse_show_command_output`) can
|
actions (:ac:`mouse_select_command_output` and :ac:`mouse_show_command_output`) can
|
||||||
be integrated with browsing command output as well. For example, define the
|
be integrated with browsing command output as well. For example, define the
|
||||||
following mapping in :file:`kitty.conf`:
|
following mapping in :file:`kitty.conf`:
|
||||||
|
|
||||||
@@ -137,22 +150,25 @@ different shells.
|
|||||||
|
|
||||||
For fish, to make it automatically load the integration code provided by
|
For fish, to make it automatically load the integration code provided by
|
||||||
kitty, the integration script directory path is prepended to the
|
kitty, the integration script directory path is prepended to the
|
||||||
:code:`XDG_DATA_DIRS` environment variable. This is only applied to the fish
|
``XDG_DATA_DIRS`` environment variable. This is only applied to the fish
|
||||||
process and will be cleaned up by the integration script after startup. No files
|
process and will be cleaned up by the integration script after startup. No files
|
||||||
are added or modified.
|
are added or modified.
|
||||||
|
|
||||||
.. tab:: bash
|
.. tab:: bash
|
||||||
|
|
||||||
For bash, kitty adds a couple of lines to the bottom of :file:`~/.bashrc`
|
For bash, kitty starts bash in POSIX mode, using the environment variable
|
||||||
(in an atomic manner) to load the shell integration code.
|
``ENV`` to load the shell integration script. This prevents bash from
|
||||||
|
loading any startup files itself. The loading of the startup files is done
|
||||||
|
by the integration script, after disabling POSIX mode. From the
|
||||||
|
perspective of those scripts there should be no difference to running
|
||||||
|
vanilla bash.
|
||||||
|
|
||||||
|
|
||||||
Then, when launching the shell, kitty sets the environment variable
|
Then, when launching the shell, kitty sets the environment variable
|
||||||
:envvar:`KITTY_SHELL_INTEGRATION` to the value of the :opt:`shell_integration`
|
:envvar:`KITTY_SHELL_INTEGRATION` to the value of the :opt:`shell_integration`
|
||||||
option. The shell integration code reads the environment variable, turns on the
|
option. The shell integration code reads the environment variable, turns on the
|
||||||
specified integration functionality and then unsets the variable so as to not
|
specified integration functionality and then unsets the variable so as to not
|
||||||
pollute the system. This has the nice effect that the changes to the shell's rc
|
pollute the system.
|
||||||
files become no-ops when running the shell in anything other than kitty itself.
|
|
||||||
|
|
||||||
The actual shell integration code uses hooks provided by each shell to send
|
The actual shell integration code uses hooks provided by each shell to send
|
||||||
special escape codes to kitty, to perform the various tasks. You can see the
|
special escape codes to kitty, to perform the various tasks. You can see the
|
||||||
@@ -185,13 +201,27 @@ code used for each shell below:
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
Shell integration over SSH
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
The easiest way to have shell integration work when SSHing into remote systems
|
||||||
|
is to use the :doc:`ssh kitten <kittens/ssh>`. Simply run::
|
||||||
|
|
||||||
|
kitty +kitten ssh hostname
|
||||||
|
|
||||||
|
And, by magic, you will be logged into the remote system with fully functional
|
||||||
|
shell integration. Alternately, you can :ref:`setup shell integration manually
|
||||||
|
<manual_shell_integration>`, by copying the kitty shell integration scripts to
|
||||||
|
the remote server and editing the shell rc files there, as described below.
|
||||||
|
|
||||||
|
|
||||||
.. _manual_shell_integration:
|
.. _manual_shell_integration:
|
||||||
|
|
||||||
Manual shell integration
|
Manual shell integration
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
The automatic shell integration is designed to be minimally intrusive, as such
|
The automatic shell integration is designed to be minimally intrusive, as such
|
||||||
it wont work for sub-shells, terminal multiplexers, containers, remote systems, etc.
|
it wont work for sub-shells, terminal multiplexers, containers, etc.
|
||||||
For such systems, you should setup manual shell integration by adding some code
|
For such systems, you should setup manual shell integration by adding some code
|
||||||
to your shells startup files to load the shell integration script.
|
to your shells startup files to load the shell integration script.
|
||||||
|
|
||||||
@@ -239,11 +269,9 @@ The value of :envvar:`KITTY_SHELL_INTEGRATION` is the same as that for
|
|||||||
completely, in which case simply do not set the
|
completely, in which case simply do not set the
|
||||||
:envvar:`KITTY_SHELL_INTEGRATION` variable at all.
|
:envvar:`KITTY_SHELL_INTEGRATION` variable at all.
|
||||||
|
|
||||||
If you want this to work while SSHing into a remote system, then you will
|
In a container, you will need to install the kitty shell integration scripts
|
||||||
need to add some code to the snippets above to check if :envvar:`KITTY_INSTALLATION_DIR`
|
and make sure the :envvar:`KITTY_INSTALLATION_DIR` environment variable is set
|
||||||
is empty and if so to set it to some hard coded location with the shell
|
to point to the location of the scripts.
|
||||||
integration scripts that need to be copied onto the remote system.
|
|
||||||
|
|
||||||
|
|
||||||
Integration with other shells
|
Integration with other shells
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ def main() -> None:
|
|||||||
|
|
||||||
from kittens.diff.options.definition import definition as kd
|
from kittens.diff.options.definition import definition as kd
|
||||||
write_output('kittens.diff', kd)
|
write_output('kittens.diff', kd)
|
||||||
|
from kittens.ssh.options.definition import definition as sd
|
||||||
|
write_output('kittens.ssh', sd)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -358,32 +358,49 @@ static GLFWapplicationwillfinishlaunchingfun finish_launching_callback = NULL;
|
|||||||
finish_launching_callback();
|
finish_launching_callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename {
|
- (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename {
|
||||||
(void)theApplication;
|
(void)sender;
|
||||||
if (!filename || !_glfw.ns.file_open_callback) return NO;
|
if (!filename || !_glfw.ns.url_open_callback) return NO;
|
||||||
const char *path = NULL;
|
const char *url = NULL;
|
||||||
@try {
|
@try {
|
||||||
path = [[NSFileManager defaultManager] fileSystemRepresentationWithPath: filename];
|
url = [[[NSURL fileURLWithPath:filename] absoluteString] UTF8String];
|
||||||
} @catch(NSException *exc) {
|
} @catch(NSException *exc) {
|
||||||
NSLog(@"Converting openFile filename: %@ failed with error: %@", filename, exc.reason);
|
NSLog(@"Converting openFile filename: %@ failed with error: %@", filename, exc.reason);
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
if (!path) return NO;
|
if (!url) return NO;
|
||||||
return _glfw.ns.file_open_callback(path);
|
return _glfw.ns.url_open_callback(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames {
|
- (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames {
|
||||||
(void)sender;
|
(void)sender;
|
||||||
if (!_glfw.ns.file_open_callback || !filenames) return;
|
if (!_glfw.ns.url_open_callback || !filenames) return;
|
||||||
for (id x in filenames) {
|
for (id x in filenames) {
|
||||||
NSString *filename = x;
|
NSString *filename = x;
|
||||||
const char *path = NULL;
|
const char *url = NULL;
|
||||||
@try {
|
@try {
|
||||||
path = [[NSFileManager defaultManager] fileSystemRepresentationWithPath: filename];
|
url = [[[NSURL fileURLWithPath:filename] absoluteString] UTF8String];
|
||||||
} @catch(NSException *exc) {
|
} @catch(NSException *exc) {
|
||||||
NSLog(@"Converting openFiles filename: %@ failed with error: %@", filename, exc.reason);
|
NSLog(@"Converting openFiles filename: %@ failed with error: %@", filename, exc.reason);
|
||||||
}
|
}
|
||||||
if (path) _glfw.ns.file_open_callback(path);
|
if (url) _glfw.ns.url_open_callback(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove openFile and openFiles when the minimum supported macOS version is 10.13
|
||||||
|
- (void)application:(NSApplication *)sender openURLs:(NSArray<NSURL *> *)urls
|
||||||
|
{
|
||||||
|
(void)sender;
|
||||||
|
if (!_glfw.ns.url_open_callback || !urls) return;
|
||||||
|
for (id x in urls) {
|
||||||
|
NSURL *ns_url = x;
|
||||||
|
const char *url = NULL;
|
||||||
|
@try {
|
||||||
|
url = [[ns_url absoluteString] UTF8String];
|
||||||
|
} @catch(NSException *exc) {
|
||||||
|
NSLog(@"Converting openURLs url: %@ failed with error: %@", ns_url, exc.reason);
|
||||||
|
}
|
||||||
|
if (url) _glfw.ns.url_open_callback(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,7 +408,6 @@ static GLFWapplicationwillfinishlaunchingfun finish_launching_callback = NULL;
|
|||||||
{
|
{
|
||||||
(void)notification;
|
(void)notification;
|
||||||
[NSApp stop:nil];
|
[NSApp stop:nil];
|
||||||
if (_glfw.ns.file_open_callback) _glfw.ns.file_open_callback(":cocoa::application launched::");
|
|
||||||
|
|
||||||
CGDisplayRegisterReconfigurationCallback(display_reconfigured, NULL);
|
CGDisplayRegisterReconfigurationCallback(display_reconfigured, NULL);
|
||||||
_glfwCocoaPostEmptyEvent();
|
_glfwCocoaPostEmptyEvent();
|
||||||
@@ -563,7 +579,10 @@ is_shiftable_shortcut(int scv) {
|
|||||||
|
|
||||||
static void
|
static void
|
||||||
build_global_shortcuts_lookup(void) {
|
build_global_shortcuts_lookup(void) {
|
||||||
|
// dump these in a terminal with: defaults read com.apple.symbolichotkeys
|
||||||
NSMutableDictionary<NSString*, NSNumber*> *temp = [NSMutableDictionary dictionaryWithCapacity:128]; // will be autoreleased
|
NSMutableDictionary<NSString*, NSNumber*> *temp = [NSMutableDictionary dictionaryWithCapacity:128]; // will be autoreleased
|
||||||
|
NSMutableSet<NSNumber*> *temp_configured = [NSMutableSet setWithCapacity:128]; // will be autoreleased
|
||||||
|
NSMutableSet<NSNumber*> *temp_missing_value = [NSMutableSet setWithCapacity:128]; // will be autoreleased
|
||||||
NSDictionary *apple_settings = [[NSUserDefaults standardUserDefaults] persistentDomainForName:@"com.apple.symbolichotkeys"];
|
NSDictionary *apple_settings = [[NSUserDefaults standardUserDefaults] persistentDomainForName:@"com.apple.symbolichotkeys"];
|
||||||
if (apple_settings) {
|
if (apple_settings) {
|
||||||
NSDictionary<NSString*, id> *symbolic_hotkeys = [apple_settings objectForKey:@"AppleSymbolicHotKeys"];
|
NSDictionary<NSString*, id> *symbolic_hotkeys = [apple_settings objectForKey:@"AppleSymbolicHotKeys"];
|
||||||
@@ -574,9 +593,14 @@ build_global_shortcuts_lookup(void) {
|
|||||||
NSInteger sc = [key integerValue];
|
NSInteger sc = [key integerValue];
|
||||||
NSDictionary *sc_value = obj;
|
NSDictionary *sc_value = obj;
|
||||||
id enabled = [sc_value objectForKey:@"enabled"];
|
id enabled = [sc_value objectForKey:@"enabled"];
|
||||||
if (!enabled || ![enabled isKindOfClass:[NSNumber class]] || ![(NSNumber*)enabled boolValue]) continue;
|
if (!enabled || ![enabled isKindOfClass:[NSNumber class]]) continue;
|
||||||
|
[temp_configured addObject:@(sc)];
|
||||||
|
if (![enabled boolValue]) continue;
|
||||||
id v = [sc_value objectForKey:@"value"];
|
id v = [sc_value objectForKey:@"value"];
|
||||||
if (!v || ![v isKindOfClass:[NSDictionary class]]) continue;
|
if (!v || ![v isKindOfClass:[NSDictionary class]]) {
|
||||||
|
if ([enabled boolValue]) [temp_missing_value addObject:@(sc)];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
NSDictionary *value = v;
|
NSDictionary *value = v;
|
||||||
id t = [value objectForKey:@"type"];
|
id t = [value objectForKey:@"type"];
|
||||||
if (!t || ![t isKindOfClass:[NSString class]] || ![t isEqualToString:@"standard"]) continue;
|
if (!t || ![t isKindOfClass:[NSString class]] || ![t isEqualToString:@"standard"]) continue;
|
||||||
@@ -601,6 +625,39 @@ build_global_shortcuts_lookup(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add global shortcut definitions when the default enabled shortcut is not defined,
|
||||||
|
// or when the default enabled shortcut is not disabled and is missing a value.
|
||||||
|
// Here are the shortcuts that are enabled by default in the standard ANSI (US) layout.
|
||||||
|
// macOS provides separate configurations for some languages or keyboards.
|
||||||
|
// In general, the rules here will not take effect.
|
||||||
|
static char buf[64];
|
||||||
|
#define S(i, t, m, k) if ([temp_configured member:@(i)] == nil || [temp_missing_value member:@(i)] != nil) { \
|
||||||
|
snprintf(buf, sizeof(buf) - 1, #t":%lx:%ld", (unsigned long)m, (long)k); \
|
||||||
|
temp[@(buf)] = @(i); \
|
||||||
|
}
|
||||||
|
|
||||||
|
// launchpad & dock
|
||||||
|
S(kSHKTurnDockHidingOnOrOff, c, (NSEventModifierFlagOption | NSEventModifierFlagCommand), 'd'); // Opt, Cmd, D
|
||||||
|
// mission control
|
||||||
|
S(kSHKMissionControl, v, NSEventModifierFlagControl, 126); // Ctrl, Arrow Up
|
||||||
|
S(kSHKApplicationWindows, v, NSEventModifierFlagControl, 125); // Ctrl, Arrow Down
|
||||||
|
// keyboard
|
||||||
|
S(kSHKMoveFocusToTheMenuBar, v, NSEventModifierFlagControl, 120); // Ctrl, F2
|
||||||
|
S(kSHKMoveFocusToTheDock, v, NSEventModifierFlagControl, 99); // Ctrl, F3
|
||||||
|
S(kSHKMoveFocusToActiveOrNextWindow, v, NSEventModifierFlagControl, 118); // Ctrl, F4
|
||||||
|
S(kSHKMoveFocusToActiveOrNextWindow, v, (NSEventModifierFlagShift | NSEventModifierFlagControl), 118); // Shift, Ctrl, F4
|
||||||
|
S(kSHKMoveFocusToNextWindow, c, NSEventModifierFlagCommand, 96); // Cmd, `
|
||||||
|
S(kSHKMoveFocusToNextWindow, c, (NSEventModifierFlagShift | NSEventModifierFlagCommand), 96); // Shift, Cmd, `
|
||||||
|
S(kSHKMoveFocusToStatusMenus, v, NSEventModifierFlagControl, 100); // Ctrl, F8
|
||||||
|
// input sources
|
||||||
|
S(kSHKSelectThePreviousInputSource, c, NSEventModifierFlagControl, 32); // Ctrl, Space bar
|
||||||
|
S(kSHKSelectNextSourceInInputMenu, c, (NSEventModifierFlagControl | NSEventModifierFlagOption), 32); // Ctrl, Opt, Space bar
|
||||||
|
// spotlight
|
||||||
|
S(kSHKShowSpotlightSearch, c, NSEventModifierFlagCommand, 32); // Cmd, Space bar
|
||||||
|
S(kSHKShowFinderSearchWindow, c, (NSEventModifierFlagOption | NSEventModifierFlagCommand), 32); // Opt, Cmd, Space bar
|
||||||
|
|
||||||
|
#undef S
|
||||||
global_shortcuts = [[NSDictionary dictionaryWithDictionary:temp] retain];
|
global_shortcuts = [[NSDictionary dictionaryWithDictionary:temp] retain];
|
||||||
/* NSLog(@"global_shortcuts: %@", global_shortcuts); */
|
/* NSLog(@"global_shortcuts: %@", global_shortcuts); */
|
||||||
}
|
}
|
||||||
@@ -610,11 +667,10 @@ is_active_apple_global_shortcut(NSEvent *event) {
|
|||||||
if (global_shortcuts == nil) build_global_shortcuts_lookup();
|
if (global_shortcuts == nil) build_global_shortcuts_lookup();
|
||||||
NSEventModifierFlags modifierFlags = USEFUL_MODS([event modifierFlags]);
|
NSEventModifierFlags modifierFlags = USEFUL_MODS([event modifierFlags]);
|
||||||
static char lookup_key[64];
|
static char lookup_key[64];
|
||||||
|
|
||||||
#define LOOKUP(t, k) \
|
#define LOOKUP(t, k) \
|
||||||
snprintf(lookup_key, sizeof(lookup_key) - 1, #t":%lx:%ld", (unsigned long)modifierFlags, (long)k); \
|
snprintf(lookup_key, sizeof(lookup_key) - 1, #t":%lx:%ld", (unsigned long)modifierFlags, (long)k); \
|
||||||
NSNumber *sc = global_shortcuts[@(lookup_key)]; \
|
NSNumber *sc = global_shortcuts[@(lookup_key)]; \
|
||||||
if (sc != nil) return [sc intValue];
|
if (sc != nil) return [sc intValue]; \
|
||||||
|
|
||||||
if ([event.charactersIgnoringModifiers length] == 1) {
|
if ([event.charactersIgnoringModifiers length] == 1) {
|
||||||
if (modifierFlags & NSEventModifierFlagShift) {
|
if (modifierFlags & NSEventModifierFlagShift) {
|
||||||
@@ -752,9 +808,11 @@ int _glfwPlatformInit(void)
|
|||||||
[NSApp setDelegate:_glfw.ns.delegate];
|
[NSApp setDelegate:_glfw.ns.delegate];
|
||||||
static struct {
|
static struct {
|
||||||
unsigned short virtual_key_code;
|
unsigned short virtual_key_code;
|
||||||
|
NSEventModifierFlags input_source_switch_modifiers;
|
||||||
NSTimeInterval timestamp;
|
NSTimeInterval timestamp;
|
||||||
} last_keydown_shortcut_event;
|
} last_keydown_shortcut_event;
|
||||||
last_keydown_shortcut_event.virtual_key_code = 0xffff;
|
last_keydown_shortcut_event.virtual_key_code = 0xffff;
|
||||||
|
last_keydown_shortcut_event.input_source_switch_modifiers = 0;
|
||||||
|
|
||||||
NSEvent* (^keydown_block)(NSEvent*) = ^ NSEvent* (NSEvent* event)
|
NSEvent* (^keydown_block)(NSEvent*) = ^ NSEvent* (NSEvent* event)
|
||||||
{
|
{
|
||||||
@@ -765,6 +823,7 @@ int _glfwPlatformInit(void)
|
|||||||
if ([[NSApp mainMenu] performKeyEquivalent:event]) {
|
if ([[NSApp mainMenu] performKeyEquivalent:event]) {
|
||||||
debug_key("keyDown triggerred global menu bar action ignoring\n");
|
debug_key("keyDown triggerred global menu bar action ignoring\n");
|
||||||
last_keydown_shortcut_event.virtual_key_code = [event keyCode];
|
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];
|
last_keydown_shortcut_event.timestamp = [event timestamp];
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
@@ -773,6 +832,8 @@ int _glfwPlatformInit(void)
|
|||||||
if (is_useful_apple_global_shortcut(global_shortcut)) {
|
if (is_useful_apple_global_shortcut(global_shortcut)) {
|
||||||
debug_key("keyDown triggerred global macOS shortcut ignoring\n");
|
debug_key("keyDown triggerred global macOS shortcut ignoring\n");
|
||||||
last_keydown_shortcut_event.virtual_key_code = [event keyCode];
|
last_keydown_shortcut_event.virtual_key_code = [event keyCode];
|
||||||
|
// record the modifier keys if switching to the next input source
|
||||||
|
last_keydown_shortcut_event.input_source_switch_modifiers = (global_shortcut == kSHKSelectNextSourceInInputMenu) ? USEFUL_MODS([event modifierFlags]) : 0;
|
||||||
last_keydown_shortcut_event.timestamp = [event timestamp];
|
last_keydown_shortcut_event.timestamp = [event timestamp];
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
@@ -805,6 +866,12 @@ int _glfwPlatformInit(void)
|
|||||||
debug_key("-------------- flags changed -----------------\n");
|
debug_key("-------------- flags changed -----------------\n");
|
||||||
debug_key("%s\n", [[event description] UTF8String]);
|
debug_key("%s\n", [[event description] UTF8String]);
|
||||||
last_keydown_shortcut_event.virtual_key_code = 0xffff;
|
last_keydown_shortcut_event.virtual_key_code = 0xffff;
|
||||||
|
// switching to the next input source is only confirmed when all modifier keys are released
|
||||||
|
if (last_keydown_shortcut_event.input_source_switch_modifiers) {
|
||||||
|
if (!([event modifierFlags] & last_keydown_shortcut_event.input_source_switch_modifiers))
|
||||||
|
last_keydown_shortcut_event.input_source_switch_modifiers = 0;
|
||||||
|
return event;
|
||||||
|
}
|
||||||
NSWindow *kw = [NSApp keyWindow];
|
NSWindow *kw = [NSApp keyWindow];
|
||||||
if (kw && kw.contentView) [kw.contentView flagsChanged:event];
|
if (kw && kw.contentView) [kw.contentView flagsChanged:event];
|
||||||
else debug_key("flagsChanged ignored as no keyWindow present\n");
|
else debug_key("flagsChanged ignored as no keyWindow present\n");
|
||||||
|
|||||||
9
glfw/cocoa_platform.h
vendored
9
glfw/cocoa_platform.h
vendored
@@ -68,7 +68,7 @@ typedef void* CVDisplayLinkRef;
|
|||||||
|
|
||||||
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int, unsigned long);
|
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int, unsigned long);
|
||||||
typedef bool (* GLFWapplicationshouldhandlereopenfun)(int);
|
typedef bool (* GLFWapplicationshouldhandlereopenfun)(int);
|
||||||
typedef bool (* GLFWhandlefileopen)(const char*);
|
typedef bool (* GLFWhandleurlopen)(const char*);
|
||||||
typedef void (* GLFWapplicationwillfinishlaunchingfun)(void);
|
typedef void (* GLFWapplicationwillfinishlaunchingfun)(void);
|
||||||
typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
|
typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
|
||||||
typedef void (* GLFWcocoarenderframefun)(GLFWwindow*);
|
typedef void (* GLFWcocoarenderframefun)(GLFWwindow*);
|
||||||
@@ -131,6 +131,7 @@ typedef struct _GLFWwindowNS
|
|||||||
bool maximized;
|
bool maximized;
|
||||||
bool retina;
|
bool retina;
|
||||||
bool in_traditional_fullscreen;
|
bool in_traditional_fullscreen;
|
||||||
|
bool in_fullscreen_transition;
|
||||||
bool titlebar_hidden;
|
bool titlebar_hidden;
|
||||||
unsigned long pre_full_screen_style_mask;
|
unsigned long pre_full_screen_style_mask;
|
||||||
|
|
||||||
@@ -153,6 +154,8 @@ typedef struct _GLFWwindowNS
|
|||||||
// Whether a render frame has been requested for this window
|
// Whether a render frame has been requested for this window
|
||||||
bool renderFrameRequested;
|
bool renderFrameRequested;
|
||||||
GLFWcocoarenderframefun renderFrameCallback;
|
GLFWcocoarenderframefun renderFrameCallback;
|
||||||
|
// update cursor after switching desktops with Mission Control
|
||||||
|
bool delayed_cursor_update_requested;
|
||||||
} _GLFWwindowNS;
|
} _GLFWwindowNS;
|
||||||
|
|
||||||
typedef struct _GLFWDisplayLinkNS
|
typedef struct _GLFWDisplayLinkNS
|
||||||
@@ -199,8 +202,8 @@ typedef struct _GLFWlibraryNS
|
|||||||
_GLFWDisplayLinkNS entries[256];
|
_GLFWDisplayLinkNS entries[256];
|
||||||
size_t count;
|
size_t count;
|
||||||
} displayLinks;
|
} displayLinks;
|
||||||
// the callback to handle file open events
|
// the callback to handle url open events
|
||||||
GLFWhandlefileopen file_open_callback;
|
GLFWhandleurlopen url_open_callback;
|
||||||
|
|
||||||
} _GLFWlibraryNS;
|
} _GLFWlibraryNS;
|
||||||
|
|
||||||
|
|||||||
@@ -582,6 +582,7 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow;
|
- (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow;
|
||||||
|
- (void)request_delayed_cursor_update:(id)sender;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@@ -692,6 +693,9 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
|
|||||||
_glfwPlatformGetCursorPos(window, &x, &y);
|
_glfwPlatformGetCursorPos(window, &x, &y);
|
||||||
_glfwInputCursorPos(window, x, y);
|
_glfwInputCursorPos(window, x, y);
|
||||||
}
|
}
|
||||||
|
// macOS will send a delayed event to update the cursor to arrow after switching desktops.
|
||||||
|
// So we need to delay and update the cursor once after that.
|
||||||
|
[self performSelector:@selector(request_delayed_cursor_update:) withObject:nil afterDelay:0.3];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)windowDidResignKey:(NSNotification *)notification
|
- (void)windowDidResignKey:(NSNotification *)notification
|
||||||
@@ -722,6 +726,38 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)request_delayed_cursor_update:(id)sender
|
||||||
|
{
|
||||||
|
(void)sender;
|
||||||
|
if (window) window->ns.delayed_cursor_update_requested = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)windowWillEnterFullScreen:(NSNotification *)notification
|
||||||
|
{
|
||||||
|
(void)notification;
|
||||||
|
if (window) window->ns.in_fullscreen_transition = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)windowDidEnterFullScreen:(NSNotification *)notification
|
||||||
|
{
|
||||||
|
(void)notification;
|
||||||
|
if (window) window->ns.in_fullscreen_transition = false;
|
||||||
|
[self performSelector:@selector(request_delayed_cursor_update:) withObject:nil afterDelay:0.3];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)windowWillExitFullScreen:(NSNotification *)notification
|
||||||
|
{
|
||||||
|
(void)notification;
|
||||||
|
if (window) window->ns.in_fullscreen_transition = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)windowDidExitFullScreen:(NSNotification *)notification
|
||||||
|
{
|
||||||
|
(void)notification;
|
||||||
|
if (window) window->ns.in_fullscreen_transition = false;
|
||||||
|
[self performSelector:@selector(request_delayed_cursor_update:) withObject:nil afterDelay:0.3];
|
||||||
|
}
|
||||||
|
|
||||||
@end // }}}
|
@end // }}}
|
||||||
|
|
||||||
// Text input context class for the GLFW content view {{{
|
// Text input context class for the GLFW content view {{{
|
||||||
@@ -904,6 +940,11 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
|
|||||||
|
|
||||||
window->ns.cursorWarpDeltaX = 0;
|
window->ns.cursorWarpDeltaX = 0;
|
||||||
window->ns.cursorWarpDeltaY = 0;
|
window->ns.cursorWarpDeltaY = 0;
|
||||||
|
|
||||||
|
if (window->ns.delayed_cursor_update_requested) {
|
||||||
|
window->ns.delayed_cursor_update_requested = false;
|
||||||
|
if (cursorInContentArea(window)) updateCursorImage(window);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)rightMouseDown:(NSEvent *)event
|
- (void)rightMouseDown:(NSEvent *)event
|
||||||
@@ -1003,6 +1044,8 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
|
|||||||
|
|
||||||
- (void)updateTrackingAreas
|
- (void)updateTrackingAreas
|
||||||
{
|
{
|
||||||
|
if (window && [window->ns.object areCursorRectsEnabled])
|
||||||
|
[window->ns.object disableCursorRects];
|
||||||
if (trackingArea != nil)
|
if (trackingArea != nil)
|
||||||
{
|
{
|
||||||
[self removeTrackingArea:trackingArea];
|
[self removeTrackingArea:trackingArea];
|
||||||
@@ -1325,14 +1368,17 @@ is_ascii_control_char(char x) {
|
|||||||
options:options];
|
options:options];
|
||||||
if (!objs) return NO;
|
if (!objs) return NO;
|
||||||
const NSUInteger count = [objs count];
|
const NSUInteger count = [objs count];
|
||||||
|
NSMutableString *uri_list = [NSMutableString stringWithCapacity:4096]; // auto-released
|
||||||
if (count)
|
if (count)
|
||||||
{
|
{
|
||||||
for (NSUInteger i = 0; i < count; i++)
|
for (NSUInteger i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
id obj = objs[i];
|
id obj = objs[i];
|
||||||
if ([obj isKindOfClass:[NSURL class]]) {
|
if ([obj isKindOfClass:[NSURL class]]) {
|
||||||
const char *path = [obj fileSystemRepresentation];
|
NSURL *url = (NSURL*)obj;
|
||||||
_glfwInputDrop(window, "text/plain;charset=utf-8", path, strlen(path));
|
if ([uri_list length] > 0) [uri_list appendString:@("\n")];
|
||||||
|
if (url.fileURL) [uri_list appendString:url.filePathURL.absoluteString];
|
||||||
|
else [uri_list appendString:url.absoluteString];
|
||||||
} else if ([obj isKindOfClass:[NSString class]]) {
|
} else if ([obj isKindOfClass:[NSString class]]) {
|
||||||
const char *text = [obj UTF8String];
|
const char *text = [obj UTF8String];
|
||||||
_glfwInputDrop(window, "text/plain;charset=utf-8", text, strlen(text));
|
_glfwInputDrop(window, "text/plain;charset=utf-8", text, strlen(text));
|
||||||
@@ -1342,6 +1388,7 @@ is_ascii_control_char(char x) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ([uri_list length] > 0) _glfwInputDrop(window, "text/uri-list", uri_list.UTF8String, strlen(uri_list.UTF8String));
|
||||||
|
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
@@ -1554,8 +1601,11 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
|
|||||||
|
|
||||||
- (void)toggleFullScreen:(nullable id)sender
|
- (void)toggleFullScreen:(nullable id)sender
|
||||||
{
|
{
|
||||||
if (glfw_window && glfw_window->ns.toggleFullscreenCallback && glfw_window->ns.toggleFullscreenCallback((GLFWwindow*)glfw_window) == 1)
|
if (glfw_window) {
|
||||||
return;
|
if (glfw_window->ns.in_fullscreen_transition) return;
|
||||||
|
if (glfw_window->ns.toggleFullscreenCallback && glfw_window->ns.toggleFullscreenCallback((GLFWwindow*)glfw_window) == 1) return;
|
||||||
|
glfw_window->ns.in_fullscreen_transition = true;
|
||||||
|
}
|
||||||
// When resizeIncrements is set, Cocoa cannot restore the original window size after returning from fullscreen.
|
// When resizeIncrements is set, Cocoa cannot restore the original window size after returning from fullscreen.
|
||||||
const NSSize original = [self resizeIncrements];
|
const NSSize original = [self resizeIncrements];
|
||||||
[self setResizeIncrements:NSMakeSize(1.0, 1.0)];
|
[self setResizeIncrements:NSMakeSize(1.0, 1.0)];
|
||||||
@@ -2642,10 +2692,10 @@ GLFWAPI GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow *hand
|
|||||||
return previous;
|
return previous;
|
||||||
}
|
}
|
||||||
|
|
||||||
GLFWAPI GLFWhandlefileopen glfwSetCocoaFileOpenCallback(GLFWhandlefileopen callback) {
|
GLFWAPI GLFWhandleurlopen glfwSetCocoaURLOpenCallback(GLFWhandleurlopen callback) {
|
||||||
_GLFW_REQUIRE_INIT_OR_RETURN(nil);
|
_GLFW_REQUIRE_INIT_OR_RETURN(nil);
|
||||||
GLFWhandlefileopen prev = _glfw.ns.file_open_callback;
|
GLFWhandleurlopen prev = _glfw.ns.url_open_callback;
|
||||||
_glfw.ns.file_open_callback = callback;
|
_glfw.ns.url_open_callback = callback;
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
glfw/dbus_glfw.c
vendored
2
glfw/dbus_glfw.c
vendored
@@ -263,7 +263,7 @@ call_method_with_msg(DBusConnection *conn, DBusMessage *msg, int timeout, dbus_p
|
|||||||
|
|
||||||
static bool
|
static bool
|
||||||
call_method(DBusConnection *conn, const char *node, const char *path, const char *interface, const char *method, int timeout, dbus_pending_callback callback, void *user_data, va_list ap) {
|
call_method(DBusConnection *conn, const char *node, const char *path, const char *interface, const char *method, int timeout, dbus_pending_callback callback, void *user_data, va_list ap) {
|
||||||
if (!conn) return false;
|
if (!conn || !path) return false;
|
||||||
DBusMessage *msg = dbus_message_new_method_call(node, path, interface, method);
|
DBusMessage *msg = dbus_message_new_method_call(node, path, interface, method);
|
||||||
if (!msg) return false;
|
if (!msg) return false;
|
||||||
bool retval = false;
|
bool retval = false;
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ def generate_wrappers(glfw_header: str) -> None:
|
|||||||
void* glfwGetNSGLContext(GLFWwindow *window)
|
void* glfwGetNSGLContext(GLFWwindow *window)
|
||||||
uint32_t glfwGetCocoaMonitor(GLFWmonitor* monitor)
|
uint32_t glfwGetCocoaMonitor(GLFWmonitor* monitor)
|
||||||
GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow* window, GLFWcocoatextinputfilterfun callback)
|
GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow* window, GLFWcocoatextinputfilterfun callback)
|
||||||
GLFWhandlefileopen glfwSetCocoaFileOpenCallback(GLFWhandlefileopen callback)
|
GLFWhandleurlopen glfwSetCocoaURLOpenCallback(GLFWhandleurlopen callback)
|
||||||
GLFWcocoatogglefullscreenfun glfwSetCocoaToggleFullscreenIntercept(GLFWwindow *window, GLFWcocoatogglefullscreenfun callback)
|
GLFWcocoatogglefullscreenfun glfwSetCocoaToggleFullscreenIntercept(GLFWwindow *window, GLFWcocoatogglefullscreenfun callback)
|
||||||
GLFWapplicationshouldhandlereopenfun glfwSetApplicationShouldHandleReopen(GLFWapplicationshouldhandlereopenfun callback)
|
GLFWapplicationshouldhandlereopenfun glfwSetApplicationShouldHandleReopen(GLFWapplicationshouldhandlereopenfun callback)
|
||||||
GLFWapplicationwillfinishlaunchingfun glfwSetApplicationWillFinishLaunching(GLFWapplicationwillfinishlaunchingfun callback)
|
GLFWapplicationwillfinishlaunchingfun glfwSetApplicationWillFinishLaunching(GLFWapplicationwillfinishlaunchingfun callback)
|
||||||
@@ -277,7 +277,7 @@ const char *action_text, int32_t timeout, GLFWDBusnotificationcreatedfun callbac
|
|||||||
|
|
||||||
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int,unsigned long);
|
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int,unsigned long);
|
||||||
typedef bool (* GLFWapplicationshouldhandlereopenfun)(int);
|
typedef bool (* GLFWapplicationshouldhandlereopenfun)(int);
|
||||||
typedef bool (* GLFWhandlefileopen)(const char*);
|
typedef bool (* GLFWhandleurlopen)(const char*);
|
||||||
typedef void (* GLFWapplicationwillfinishlaunchingfun)(void);
|
typedef void (* GLFWapplicationwillfinishlaunchingfun)(void);
|
||||||
typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
|
typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
|
||||||
typedef void (* GLFWcocoarenderframefun)(GLFWwindow*);
|
typedef void (* GLFWcocoarenderframefun)(GLFWwindow*);
|
||||||
@@ -292,6 +292,8 @@ const char* load_glfw(const char* path);
|
|||||||
f.write(header)
|
f.write(header)
|
||||||
|
|
||||||
code = '''
|
code = '''
|
||||||
|
// generated by glfw.py DO NOT edit
|
||||||
|
|
||||||
#define GFW_EXTERN
|
#define GFW_EXTERN
|
||||||
#include "data-types.h"
|
#include "data-types.h"
|
||||||
#include "glfw-wrapper.h"
|
#include "glfw-wrapper.h"
|
||||||
|
|||||||
198
glfw/linux_desktop_settings.c
vendored
198
glfw/linux_desktop_settings.c
vendored
@@ -10,77 +10,118 @@
|
|||||||
#include <strings.h>
|
#include <strings.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
static const char *DESKTOP_SERVICE = "org.freedesktop.portal.Desktop";
|
#define DESKTOP_SERVICE "org.freedesktop.portal.Desktop"
|
||||||
static const char *DESKTOP_PATH = "/org/freedesktop/portal/desktop";
|
#define DESKTOP_PATH "/org/freedesktop/portal/desktop"
|
||||||
static const char *DESKTOP_INTERFACE = "org.freedesktop.portal.Settings";
|
#define DESKTOP_INTERFACE "org.freedesktop.portal.Settings"
|
||||||
static const char *GNOME_DESKTOP_NAMESPACE = "org.gnome.desktop.interface";
|
#define GNOME_DESKTOP_NAMESPACE "org.gnome.desktop.interface"
|
||||||
|
#define FDO_DESKTOP_NAMESPACE "org.freedesktop.appearance"
|
||||||
|
#define FDO_APPEARANCE_KEY "color-scheme"
|
||||||
|
|
||||||
|
|
||||||
static char theme_name[64] = {0};
|
static char theme_name[128] = {0};
|
||||||
static int theme_size = -1;
|
static int theme_size = -1;
|
||||||
static bool gnome_cursor_theme_read = false, gnome_cursor_size_read = false;
|
static uint32_t appearance = 0;
|
||||||
|
static bool is_gnome = false;
|
||||||
|
static bool cursor_theme_changed = false;
|
||||||
|
|
||||||
static bool
|
#define HANDLER(name) static void name(DBusMessage *msg, const char* errmsg, void *data) { \
|
||||||
parse_dbus_message_for_type(DBusMessage *const reply, const char *errmsg, const int type, void *value) {
|
|
||||||
DBusMessageIter iter[3];
|
|
||||||
dbus_message_iter_init(reply, &iter[0]);
|
|
||||||
#define FAIL { _glfwInputError(GLFW_PLATFORM_ERROR, "%s", errmsg); return false; }
|
|
||||||
if (dbus_message_iter_get_arg_type(&iter[0]) != DBUS_TYPE_VARIANT) FAIL;
|
|
||||||
dbus_message_iter_recurse(&iter[0], &iter[1]);
|
|
||||||
if (dbus_message_iter_get_arg_type(&iter[1]) != DBUS_TYPE_VARIANT) FAIL;
|
|
||||||
dbus_message_iter_recurse(&iter[1], &iter[2]);
|
|
||||||
if (dbus_message_iter_get_arg_type(&iter[2]) != type) FAIL;
|
|
||||||
dbus_message_iter_get_basic(&iter[2], value);
|
|
||||||
return true;
|
|
||||||
#undef FAIL
|
|
||||||
}
|
|
||||||
|
|
||||||
#define HANDLER(name) void name(DBusMessage *msg, const char* errmsg, void *data) { \
|
|
||||||
(void)data; \
|
(void)data; \
|
||||||
if (errmsg) { \
|
if (errmsg) { \
|
||||||
_glfwInputError(GLFW_PLATFORM_ERROR, "%s: failed with error: %s", #name, errmsg); \
|
_glfwInputError(GLFW_PLATFORM_ERROR, "%s: failed with error: %s", #name, errmsg); \
|
||||||
return; \
|
return; \
|
||||||
}
|
}
|
||||||
|
|
||||||
HANDLER(on_gnome_cursor_theme_read)
|
static void
|
||||||
const char *name;
|
process_fdo_setting(const char *key, DBusMessageIter *value) {
|
||||||
if (!parse_dbus_message_for_type(msg, "Failed to get cursor theme name from reply", DBUS_TYPE_STRING, &name)) return;
|
if (strcmp(key, FDO_APPEARANCE_KEY) == 0) {
|
||||||
if (name && name[0]) {
|
if (dbus_message_iter_get_arg_type(value) == DBUS_TYPE_UINT32) {
|
||||||
gnome_cursor_theme_read = true;
|
dbus_message_iter_get_basic(value, &appearance);
|
||||||
strncpy(theme_name, name, sizeof(theme_name) - 1);
|
if (appearance > 2) appearance = 0;
|
||||||
if (gnome_cursor_size_read) _glfwPlatformChangeCursorTheme();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HANDLER(on_gnome_cursor_size_read)
|
|
||||||
int32_t sz;
|
|
||||||
if (!parse_dbus_message_for_type(msg, "Failed to get cursor theme size from reply", DBUS_TYPE_INT32, &sz)) return;
|
|
||||||
gnome_cursor_size_read = true;
|
|
||||||
theme_size = sz;
|
|
||||||
if (gnome_cursor_theme_read) _glfwPlatformChangeCursorTheme();
|
|
||||||
}
|
|
||||||
#undef HANDLER
|
|
||||||
|
|
||||||
|
|
||||||
static bool
|
|
||||||
call_read(DBusConnection *session_bus, dbus_pending_callback callback, const char *namespace, const char *key) {
|
|
||||||
return glfw_dbus_call_method_with_reply(
|
|
||||||
session_bus, DESKTOP_SERVICE, DESKTOP_PATH, DESKTOP_INTERFACE, "Read", DBUS_TIMEOUT_USE_DEFAULT,
|
|
||||||
callback, NULL, DBUS_TYPE_STRING, &namespace, DBUS_TYPE_STRING, &key, DBUS_TYPE_INVALID);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
get_from_gnome(void) {
|
process_gnome_setting(const char *key, DBusMessageIter *value) {
|
||||||
theme_size = 32;
|
if (strcmp(key, "cursor-size") == 0) {
|
||||||
DBusConnection *session_bus = glfw_dbus_session_bus();
|
if (dbus_message_iter_get_arg_type(value) == DBUS_TYPE_INT32) {
|
||||||
if (session_bus) {
|
int32_t sz;
|
||||||
const char *theme_key = "cursor-theme";
|
dbus_message_iter_get_basic(value, &sz);
|
||||||
call_read(session_bus, on_gnome_cursor_theme_read, GNOME_DESKTOP_NAMESPACE, theme_key);
|
if (sz > 0 && sz != theme_size) {
|
||||||
const char *size_key = "cursor-size";
|
theme_size = sz;
|
||||||
call_read(session_bus, on_gnome_cursor_size_read, GNOME_DESKTOP_NAMESPACE, size_key);
|
cursor_theme_changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (strcmp(key, "cursor-theme") == 0) {
|
||||||
|
if (dbus_message_iter_get_arg_type(value) == DBUS_TYPE_STRING) {
|
||||||
|
const char *name;
|
||||||
|
dbus_message_iter_get_basic(value, &name);
|
||||||
|
if (name) {
|
||||||
|
strncpy(theme_name, name, sizeof(theme_name) - 1);
|
||||||
|
cursor_theme_changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
process_settings_dict(DBusMessageIter *array_iter, void(process_setting)(const char *, DBusMessageIter*)) {
|
||||||
|
DBusMessageIter item_iter, value_iter;
|
||||||
|
while (dbus_message_iter_get_arg_type(array_iter) == DBUS_TYPE_DICT_ENTRY) {
|
||||||
|
dbus_message_iter_recurse(array_iter, &item_iter);
|
||||||
|
if (dbus_message_iter_get_arg_type(&item_iter) == DBUS_TYPE_STRING) {
|
||||||
|
const char *key;
|
||||||
|
dbus_message_iter_get_basic(&item_iter, &key);
|
||||||
|
if (dbus_message_iter_next(&item_iter) && dbus_message_iter_get_arg_type(&item_iter) == DBUS_TYPE_VARIANT) {
|
||||||
|
dbus_message_iter_recurse(&item_iter, &value_iter);
|
||||||
|
process_setting(key, &value_iter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!dbus_message_iter_next(array_iter)) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HANDLER(process_desktop_settings)
|
||||||
|
cursor_theme_changed = false;
|
||||||
|
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);
|
||||||
|
while (dbus_message_iter_get_arg_type(&array) == DBUS_TYPE_DICT_ENTRY) {
|
||||||
|
dbus_message_iter_recurse(&array, &item);
|
||||||
|
if (dbus_message_iter_get_arg_type(&item) == DBUS_TYPE_STRING) {
|
||||||
|
const char *namespace;
|
||||||
|
dbus_message_iter_get_basic(&item, &namespace);
|
||||||
|
if (dbus_message_iter_next(&item) && dbus_message_iter_get_arg_type(&item) == DBUS_TYPE_ARRAY) {
|
||||||
|
dbus_message_iter_recurse(&item, &settings);
|
||||||
|
if (strcmp(namespace, FDO_DESKTOP_NAMESPACE) == 0) {
|
||||||
|
process_settings_dict(&settings, process_fdo_setting);
|
||||||
|
} else if (is_gnome && strcmp(namespace, GNOME_DESKTOP_NAMESPACE) == 0) {
|
||||||
|
process_settings_dict(&settings, process_gnome_setting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!dbus_message_iter_next(&array)) break;
|
||||||
|
}
|
||||||
|
#undef die
|
||||||
|
if (cursor_theme_changed) _glfwPlatformChangeCursorTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef HANDLER
|
||||||
|
|
||||||
|
static bool
|
||||||
|
read_desktop_settings(DBusConnection *session_bus) {
|
||||||
|
DBusMessage *msg = dbus_message_new_method_call(DESKTOP_SERVICE, DESKTOP_PATH, DESKTOP_INTERFACE, "ReadAll");
|
||||||
|
if (!msg) return false;
|
||||||
|
DBusMessageIter iter, array_iter;
|
||||||
|
dbus_message_iter_init_append(msg, &iter);
|
||||||
|
if (!dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "s", &array_iter)) { dbus_message_unref(msg); return false; }
|
||||||
|
if (!dbus_message_iter_close_container(&iter, &array_iter)) { dbus_message_unref(msg); return false; }
|
||||||
|
bool ok = call_method_with_msg(session_bus, msg, DBUS_TIMEOUT_USE_DEFAULT, process_desktop_settings, NULL);
|
||||||
|
dbus_message_unref(msg);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
glfw_current_cursor_theme(const char **theme, int *size) {
|
glfw_current_cursor_theme(const char **theme, int *size) {
|
||||||
@@ -100,10 +141,55 @@ get_cursor_theme_from_env(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
on_color_scheme_change(DBusMessage *message) {
|
||||||
|
DBusMessageIter iter[2];
|
||||||
|
dbus_message_iter_init (message, &iter[0]);
|
||||||
|
int current_type;
|
||||||
|
while ((current_type = dbus_message_iter_get_arg_type (&iter[0])) != DBUS_TYPE_INVALID) {
|
||||||
|
if (current_type == DBUS_TYPE_VARIANT) {
|
||||||
|
dbus_message_iter_recurse(&iter[0], &iter[1]);
|
||||||
|
if (dbus_message_iter_get_arg_type(&iter[1]) == DBUS_TYPE_UINT32) {
|
||||||
|
uint32_t val = 0;
|
||||||
|
dbus_message_iter_get_basic(&iter[1], &val);
|
||||||
|
if (val > 2) val = 0;
|
||||||
|
if (val != appearance) {
|
||||||
|
appearance = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
dbus_message_iter_next(&iter[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static DBusHandlerResult
|
||||||
|
setting_changed(DBusConnection *conn UNUSED, DBusMessage *msg, void *user_data UNUSED) {
|
||||||
|
/* printf("session_bus settings_changed invoked interface: %s member: %s\n", dbus_message_get_interface(msg), dbus_message_get_member(msg)); */
|
||||||
|
if (dbus_message_is_signal(msg, DESKTOP_INTERFACE, "SettingChanged")) {
|
||||||
|
const char *namespace = NULL, *key = NULL;
|
||||||
|
if (glfw_dbus_get_args(msg, "Failed to get namespace and key from SettingChanged notification signal", DBUS_TYPE_STRING, &namespace, DBUS_TYPE_STRING, &key, DBUS_TYPE_INVALID)) {
|
||||||
|
if (strcmp(namespace, FDO_DESKTOP_NAMESPACE) == 0) {
|
||||||
|
if (strcmp(key, FDO_APPEARANCE_KEY) == 0) {
|
||||||
|
on_color_scheme_change(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
glfw_initialize_desktop_settings(void) {
|
glfw_initialize_desktop_settings(void) {
|
||||||
get_cursor_theme_from_env();
|
get_cursor_theme_from_env();
|
||||||
const char *desktop = getenv("XDG_CURRENT_DESKTOP");
|
const char *desktop = getenv("XDG_CURRENT_DESKTOP");
|
||||||
bool is_gnome = desktop && strncasecmp(desktop, "GNOME", sizeof("GNOME") - 1) == 0;
|
is_gnome = desktop && strstr(desktop, "GNOME");
|
||||||
if (is_gnome) get_from_gnome();
|
DBusConnection *session_bus = glfw_dbus_session_bus();
|
||||||
|
if (session_bus) {
|
||||||
|
if (!read_desktop_settings(session_bus)) _glfwInputError(GLFW_PLATFORM_ERROR, "Failed to read desktop settings, make sure you have the desktop portal running.");
|
||||||
|
dbus_bus_add_match(session_bus, "type='signal',interface='" DESKTOP_INTERFACE "',member='SettingChanged'", NULL);
|
||||||
|
dbus_connection_add_filter(session_bus, setting_changed, NULL, NULL);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
glfw/wl_client_side_decorations.c
vendored
3
glfw/wl_client_side_decorations.c
vendored
@@ -353,6 +353,7 @@ ensure_csd_resources(_GLFWwindow *window) {
|
|||||||
!decs.mapping.data
|
!decs.mapping.data
|
||||||
);
|
);
|
||||||
const bool needs_update = focus_changed || size_changed || !decs.left.surface;
|
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);
|
||||||
if (!needs_update) return false;
|
if (!needs_update) return false;
|
||||||
if (size_changed) {
|
if (size_changed) {
|
||||||
free_csd_buffers(window);
|
free_csd_buffers(window);
|
||||||
@@ -409,7 +410,7 @@ change_csd_title(_GLFWwindow *window) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
set_csd_window_geometry(_GLFWwindow *window, int32_t *width, int32_t *height) {
|
set_csd_window_geometry(_GLFWwindow *window, int32_t *width, int32_t *height) {
|
||||||
bool has_csd = window->decorated && !window->wl.decorations.serverSide && window->wl.decorations.left.surface && !(window->wl.toplevel_states & TOPLEVEL_STATE_FULLSCREEN);
|
bool has_csd = window->decorated && !window->wl.decorations.serverSide && window->wl.decorations.left.surface && !(window->wl.current.toplevel_states & TOPLEVEL_STATE_FULLSCREEN);
|
||||||
bool size_specified_by_compositor = *width > 0 && *height > 0;
|
bool size_specified_by_compositor = *width > 0 && *height > 0;
|
||||||
if (!size_specified_by_compositor) {
|
if (!size_specified_by_compositor) {
|
||||||
*width = window->wl.user_requested_content_size.width;
|
*width = window->wl.user_requested_content_size.width;
|
||||||
|
|||||||
78
glfw/wl_init.c
vendored
78
glfw/wl_init.c
vendored
@@ -54,6 +54,8 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#define debug(...) if (_glfw.hints.init.debugRendering) fprintf(stderr, __VA_ARGS__);
|
||||||
|
|
||||||
static int min(int n1, int n2)
|
static int min(int n1, int n2)
|
||||||
{
|
{
|
||||||
return n1 < n2 ? n1 : n2;
|
return n1 < n2 ? n1 : n2;
|
||||||
@@ -96,7 +98,6 @@ static void pointerHandleEnter(void* data UNUSED,
|
|||||||
if (!window)
|
if (!window)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window->wl.decorations.focus = focus;
|
window->wl.decorations.focus = focus;
|
||||||
_glfw.wl.serial = serial;
|
_glfw.wl.serial = serial;
|
||||||
_glfw.wl.pointerFocus = window;
|
_glfw.wl.pointerFocus = window;
|
||||||
@@ -243,7 +244,7 @@ static void pointerHandleButton(void* data UNUSED,
|
|||||||
window->wl.decorations.last_click_on_top_decoration_at = monotonic();
|
window->wl.decorations.last_click_on_top_decoration_at = monotonic();
|
||||||
if (window->wl.decorations.last_click_on_top_decoration_at - last_click_at <= _glfwPlatformGetDoubleClickInterval(window)) {
|
if (window->wl.decorations.last_click_on_top_decoration_at - last_click_at <= _glfwPlatformGetDoubleClickInterval(window)) {
|
||||||
window->wl.decorations.last_click_on_top_decoration_at = 0;
|
window->wl.decorations.last_click_on_top_decoration_at = 0;
|
||||||
if (window->wl.toplevel_states & TOPLEVEL_STATE_MAXIMIZED)
|
if (window->wl.current.toplevel_states & TOPLEVEL_STATE_MAXIMIZED)
|
||||||
xdg_toplevel_unset_maximized(window->wl.xdg.toplevel);
|
xdg_toplevel_unset_maximized(window->wl.xdg.toplevel);
|
||||||
else
|
else
|
||||||
xdg_toplevel_set_maximized(window->wl.xdg.toplevel);
|
xdg_toplevel_set_maximized(window->wl.xdg.toplevel);
|
||||||
@@ -331,20 +332,83 @@ static void pointerHandleAxis(void* data UNUSED,
|
|||||||
assert(axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL ||
|
assert(axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL ||
|
||||||
axis == WL_POINTER_AXIS_VERTICAL_SCROLL);
|
axis == WL_POINTER_AXIS_VERTICAL_SCROLL);
|
||||||
|
|
||||||
if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL)
|
if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) {
|
||||||
x = -wl_fixed_to_double(value);
|
if (window->wl.axis_discrete_count.x) {
|
||||||
else if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL)
|
window->wl.axis_discrete_count.x--;
|
||||||
y = -wl_fixed_to_double(value);
|
return;
|
||||||
|
}
|
||||||
|
x = -wl_fixed_to_double(value) * (window->wl.scale);
|
||||||
|
}
|
||||||
|
else if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) {
|
||||||
|
if (window->wl.axis_discrete_count.y) {
|
||||||
|
window->wl.axis_discrete_count.y--;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
y = -wl_fixed_to_double(value) * (window->wl.scale);
|
||||||
|
}
|
||||||
|
|
||||||
_glfwInputScroll(window, x, y, 1, _glfw.wl.xkb.states.modifiers);
|
_glfwInputScroll(window, x, y, 1, _glfw.wl.xkb.states.modifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void pointerHandleFrame(void* data UNUSED,
|
||||||
|
struct wl_pointer* pointer UNUSED)
|
||||||
|
{
|
||||||
|
_GLFWwindow* window = _glfw.wl.pointerFocus;
|
||||||
|
if (window) {
|
||||||
|
window->wl.axis_discrete_count.x = 0;
|
||||||
|
window->wl.axis_discrete_count.y = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void pointerHandleAxisSource(void* data UNUSED,
|
||||||
|
struct wl_pointer* pointer UNUSED,
|
||||||
|
uint32_t source UNUSED)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
static void pointerHandleAxisStop(void *data UNUSED,
|
||||||
|
struct wl_pointer *wl_pointer UNUSED,
|
||||||
|
uint32_t time UNUSED,
|
||||||
|
uint32_t axis UNUSED)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void pointerHandleAxisDiscrete(void *data UNUSED,
|
||||||
|
struct wl_pointer *wl_pointer UNUSED,
|
||||||
|
uint32_t axis,
|
||||||
|
int32_t discrete)
|
||||||
|
{
|
||||||
|
_GLFWwindow* window = _glfw.wl.pointerFocus;
|
||||||
|
double x = 0.0, y = 0.0;
|
||||||
|
if (!window)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert(axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL ||
|
||||||
|
axis == WL_POINTER_AXIS_VERTICAL_SCROLL);
|
||||||
|
|
||||||
|
if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) {
|
||||||
|
x = -discrete;
|
||||||
|
window->wl.axis_discrete_count.x++;
|
||||||
|
}
|
||||||
|
else if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) {
|
||||||
|
y = -discrete;
|
||||||
|
window->wl.axis_discrete_count.y++;
|
||||||
|
}
|
||||||
|
|
||||||
|
_glfwInputScroll(window, x, y, 0, _glfw.wl.xkb.states.modifiers);
|
||||||
|
}
|
||||||
|
|
||||||
static const struct wl_pointer_listener pointerListener = {
|
static const struct wl_pointer_listener pointerListener = {
|
||||||
pointerHandleEnter,
|
pointerHandleEnter,
|
||||||
pointerHandleLeave,
|
pointerHandleLeave,
|
||||||
pointerHandleMotion,
|
pointerHandleMotion,
|
||||||
pointerHandleButton,
|
pointerHandleButton,
|
||||||
pointerHandleAxis,
|
pointerHandleAxis,
|
||||||
|
pointerHandleFrame,
|
||||||
|
pointerHandleAxisSource,
|
||||||
|
pointerHandleAxisStop,
|
||||||
|
pointerHandleAxisDiscrete,
|
||||||
};
|
};
|
||||||
|
|
||||||
static void keyboardHandleKeymap(void* data UNUSED,
|
static void keyboardHandleKeymap(void* data UNUSED,
|
||||||
@@ -571,7 +635,7 @@ static void registryHandleGlobal(void* data UNUSED,
|
|||||||
{
|
{
|
||||||
if (!_glfw.wl.seat)
|
if (!_glfw.wl.seat)
|
||||||
{
|
{
|
||||||
_glfw.wl.seatVersion = min(4, version);
|
_glfw.wl.seatVersion = min(5, version);
|
||||||
_glfw.wl.seat =
|
_glfw.wl.seat =
|
||||||
wl_registry_bind(registry, name, &wl_seat_interface,
|
wl_registry_bind(registry, name, &wl_seat_interface,
|
||||||
_glfw.wl.seatVersion);
|
_glfw.wl.seatVersion);
|
||||||
|
|||||||
17
glfw/wl_platform.h
vendored
17
glfw/wl_platform.h
vendored
@@ -127,6 +127,10 @@ typedef enum WaylandWindowState {
|
|||||||
|
|
||||||
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;
|
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;
|
||||||
|
|
||||||
|
enum WaylandWindowPendingState {
|
||||||
|
PENDING_STATE_TOPLEVEL = 1,
|
||||||
|
PENDING_STATE_DECORATION = 2
|
||||||
|
};
|
||||||
|
|
||||||
// Wayland-specific per-window data
|
// Wayland-specific per-window data
|
||||||
//
|
//
|
||||||
@@ -210,9 +214,20 @@ typedef struct _GLFWwindowWayland
|
|||||||
int32_t width, height;
|
int32_t width, height;
|
||||||
} user_requested_content_size;
|
} user_requested_content_size;
|
||||||
|
|
||||||
uint32_t toplevel_states;
|
|
||||||
bool maximize_on_first_show;
|
bool maximize_on_first_show;
|
||||||
|
// counters for ignoring axis events following axis_discrete events in the
|
||||||
|
// same frame along the same axis
|
||||||
|
struct {
|
||||||
|
unsigned int x, y;
|
||||||
|
} axis_discrete_count;
|
||||||
|
bool surface_configured_once;
|
||||||
|
|
||||||
|
uint32_t pending_state;
|
||||||
|
struct {
|
||||||
|
int width, height;
|
||||||
|
uint32_t toplevel_states;
|
||||||
|
uint32_t decoration_mode;
|
||||||
|
} current, pending;
|
||||||
} _GLFWwindowWayland;
|
} _GLFWwindowWayland;
|
||||||
|
|
||||||
typedef enum _GLFWWaylandOfferType
|
typedef enum _GLFWWaylandOfferType
|
||||||
|
|||||||
20
glfw/wl_text_input.c
vendored
20
glfw/wl_text_input.c
vendored
@@ -47,7 +47,7 @@ static void
|
|||||||
send_text(const char *text, GLFWIMEState ime_state) {
|
send_text(const char *text, GLFWIMEState ime_state) {
|
||||||
_GLFWwindow *w = _glfwFocusedWindow();
|
_GLFWwindow *w = _glfwFocusedWindow();
|
||||||
if (w && w->callbacks.keyboard) {
|
if (w && w->callbacks.keyboard) {
|
||||||
GLFWkeyevent fake_ev = {.action = GLFW_PRESS};
|
GLFWkeyevent fake_ev = {.action = text ? GLFW_PRESS : GLFW_RELEASE};
|
||||||
fake_ev.text = text;
|
fake_ev.text = text;
|
||||||
fake_ev.ime_state = ime_state;
|
fake_ev.ime_state = ime_state;
|
||||||
w->callbacks.keyboard((GLFWwindow*) w, &fake_ev);
|
w->callbacks.keyboard((GLFWwindow*) w, &fake_ev);
|
||||||
@@ -93,6 +93,9 @@ text_input_done(void *data UNUSED, struct zwp_text_input_v3 *txt_input UNUSED, u
|
|||||||
if (pending_pre_edit) {
|
if (pending_pre_edit) {
|
||||||
send_text(pending_pre_edit, GLFW_IME_PREEDIT_CHANGED);
|
send_text(pending_pre_edit, GLFW_IME_PREEDIT_CHANGED);
|
||||||
free(pending_pre_edit); pending_pre_edit = NULL;
|
free(pending_pre_edit); pending_pre_edit = NULL;
|
||||||
|
} else {
|
||||||
|
// Clear pre-edit text
|
||||||
|
send_text(NULL, GLFW_IME_PREEDIT_CHANGED);
|
||||||
}
|
}
|
||||||
if (pending_commit) {
|
if (pending_commit) {
|
||||||
send_text(pending_commit, GLFW_IME_COMMIT_TEXT);
|
send_text(pending_commit, GLFW_IME_COMMIT_TEXT);
|
||||||
@@ -139,7 +142,20 @@ _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
|
|||||||
switch(ev->type) {
|
switch(ev->type) {
|
||||||
case GLFW_IME_UPDATE_FOCUS:
|
case GLFW_IME_UPDATE_FOCUS:
|
||||||
debug("\ntext-input: updating IME focus state, focused: %d\n", ev->focused);
|
debug("\ntext-input: updating IME focus state, focused: %d\n", ev->focused);
|
||||||
if (ev->focused) zwp_text_input_v3_enable(text_input); else zwp_text_input_v3_disable(text_input);
|
if (ev->focused) {
|
||||||
|
zwp_text_input_v3_enable(text_input);
|
||||||
|
zwp_text_input_v3_set_content_type(text_input, ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE, ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL);
|
||||||
|
} else {
|
||||||
|
if (pending_pre_edit) {
|
||||||
|
// Clear pre-edit text
|
||||||
|
send_text(NULL, GLFW_IME_PREEDIT_CHANGED);
|
||||||
|
free(pending_pre_edit); pending_pre_edit = NULL;
|
||||||
|
}
|
||||||
|
if (pending_commit) {
|
||||||
|
free(pending_commit); pending_commit = NULL;
|
||||||
|
}
|
||||||
|
zwp_text_input_v3_disable(text_input);
|
||||||
|
}
|
||||||
commit();
|
commit();
|
||||||
break;
|
break;
|
||||||
case GLFW_IME_UPDATE_CURSOR_POSITION: {
|
case GLFW_IME_UPDATE_CURSOR_POSITION: {
|
||||||
|
|||||||
109
glfw/wl_window.c
vendored
109
glfw/wl_window.c
vendored
@@ -234,7 +234,7 @@ clipboard_mime(void) {
|
|||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static bool
|
||||||
dispatchChangesAfterConfigure(_GLFWwindow *window, int32_t width, int32_t height) {
|
dispatchChangesAfterConfigure(_GLFWwindow *window, int32_t width, int32_t height) {
|
||||||
bool size_changed = width != window->wl.width || height != window->wl.height;
|
bool size_changed = width != window->wl.width || height != window->wl.height;
|
||||||
bool scale_changed = checkScaleChange(window);
|
bool scale_changed = checkScaleChange(window);
|
||||||
@@ -246,12 +246,14 @@ dispatchChangesAfterConfigure(_GLFWwindow *window, int32_t width, int32_t height
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (scale_changed) {
|
if (scale_changed) {
|
||||||
if (!size_changed)
|
debug("Scale changed to %d in dispatchChangesAfterConfigure\n", window->wl.scale);
|
||||||
resizeFramebuffer(window);
|
if (!size_changed) resizeFramebuffer(window);
|
||||||
_glfwInputWindowContentScale(window, window->wl.scale, window->wl.scale);
|
_glfwInputWindowContentScale(window, window->wl.scale, window->wl.scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
_glfwInputWindowDamage(window);
|
_glfwInputWindowDamage(window);
|
||||||
|
|
||||||
|
return size_changed || scale_changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@@ -269,24 +271,9 @@ xdgDecorationHandleConfigure(void* data,
|
|||||||
uint32_t mode)
|
uint32_t mode)
|
||||||
{
|
{
|
||||||
_GLFWwindow* window = data;
|
_GLFWwindow* window = data;
|
||||||
|
window->wl.pending.decoration_mode = mode;
|
||||||
bool has_server_side_decorations = (mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
|
window->wl.pending_state |= PENDING_STATE_DECORATION;
|
||||||
debug("XDG decoration configure event received: has_server_side_decorations: %d\n", has_server_side_decorations);
|
debug("XDG decoration configure event received: has_server_side_decorations: %d\n", (mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE));
|
||||||
if (has_server_side_decorations == window->wl.decorations.serverSide) return;
|
|
||||||
window->wl.decorations.serverSide = has_server_side_decorations;
|
|
||||||
int width = window->wl.width, height = window->wl.height;
|
|
||||||
if (window->wl.decorations.serverSide) {
|
|
||||||
free_csd_surfaces(window);
|
|
||||||
height += window->wl.decorations.metrics.visible_titlebar_height;
|
|
||||||
} else {
|
|
||||||
ensure_csd_resources(window);
|
|
||||||
}
|
|
||||||
set_csd_window_geometry(window, &width, &height);
|
|
||||||
dispatchChangesAfterConfigure(window, width, height);
|
|
||||||
ensure_csd_resources(window);
|
|
||||||
wl_surface_commit(window->wl.surface);
|
|
||||||
debug("final window content size: %dx%d\n", window->wl.width, window->wl.height);
|
|
||||||
inform_compositor_of_window_geometry(window, "configure-decorations");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static const struct zxdg_toplevel_decoration_v1_listener xdgDecorationListener = {
|
static const struct zxdg_toplevel_decoration_v1_listener xdgDecorationListener = {
|
||||||
@@ -311,6 +298,7 @@ static void surfaceHandleEnter(void *data,
|
|||||||
window->wl.monitors[window->wl.monitorsCount++] = monitor;
|
window->wl.monitors[window->wl.monitorsCount++] = monitor;
|
||||||
|
|
||||||
if (checkScaleChange(window)) {
|
if (checkScaleChange(window)) {
|
||||||
|
debug("Scale changed to %d in surface enter event\n", window->wl.scale);
|
||||||
resizeFramebuffer(window);
|
resizeFramebuffer(window);
|
||||||
_glfwInputWindowContentScale(window, window->wl.scale, window->wl.scale);
|
_glfwInputWindowContentScale(window, window->wl.scale, window->wl.scale);
|
||||||
ensure_csd_resources(window);
|
ensure_csd_resources(window);
|
||||||
@@ -336,6 +324,7 @@ static void surfaceHandleLeave(void *data,
|
|||||||
window->wl.monitors[--window->wl.monitorsCount] = NULL;
|
window->wl.monitors[--window->wl.monitorsCount] = NULL;
|
||||||
|
|
||||||
if (checkScaleChange(window)) {
|
if (checkScaleChange(window)) {
|
||||||
|
debug("Scale changed to %d in surface leave event\n", window->wl.scale);
|
||||||
resizeFramebuffer(window);
|
resizeFramebuffer(window);
|
||||||
_glfwInputWindowContentScale(window, window->wl.scale, window->wl.scale);
|
_glfwInputWindowContentScale(window, window->wl.scale, window->wl.scale);
|
||||||
ensure_csd_resources(window);
|
ensure_csd_resources(window);
|
||||||
@@ -416,7 +405,7 @@ static void setFullscreen(_GLFWwindow* window, _GLFWmonitor* monitor, bool on)
|
|||||||
|
|
||||||
bool
|
bool
|
||||||
_glfwPlatformIsFullscreen(_GLFWwindow *window, unsigned int flags UNUSED) {
|
_glfwPlatformIsFullscreen(_GLFWwindow *window, unsigned int flags UNUSED) {
|
||||||
return window->wl.toplevel_states & TOPLEVEL_STATE_FULLSCREEN;
|
return window->wl.current.toplevel_states & TOPLEVEL_STATE_FULLSCREEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
@@ -458,7 +447,7 @@ xdgToplevelHandleConfigure(void* data,
|
|||||||
if (new_states & TOPLEVEL_STATE_RESIZING) {
|
if (new_states & TOPLEVEL_STATE_RESIZING) {
|
||||||
if (width) window->wl.user_requested_content_size.width = width;
|
if (width) window->wl.user_requested_content_size.width = width;
|
||||||
if (height) window->wl.user_requested_content_size.height = height;
|
if (height) window->wl.user_requested_content_size.height = height;
|
||||||
if (!(window->wl.toplevel_states & TOPLEVEL_STATE_RESIZING)) _glfwInputLiveResize(window, true);
|
if (!(window->wl.current.toplevel_states & TOPLEVEL_STATE_RESIZING)) _glfwInputLiveResize(window, true);
|
||||||
}
|
}
|
||||||
if (width != 0 && height != 0)
|
if (width != 0 && height != 0)
|
||||||
{
|
{
|
||||||
@@ -475,16 +464,11 @@ xdgToplevelHandleConfigure(void* data,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bool live_resize_done = !(new_states & TOPLEVEL_STATE_RESIZING) && (window->wl.toplevel_states & TOPLEVEL_STATE_RESIZING);
|
|
||||||
window->wl.toplevel_states = new_states;
|
window->wl.pending.toplevel_states = new_states;
|
||||||
set_csd_window_geometry(window, &width, &height);
|
window->wl.pending.width = width;
|
||||||
dispatchChangesAfterConfigure(window, width, height);
|
window->wl.pending.height = height;
|
||||||
debug("final window content size: %dx%d\n", window->wl.width, window->wl.height);
|
window->wl.pending_state |= PENDING_STATE_TOPLEVEL;
|
||||||
_glfwInputWindowFocus(window, window->wl.toplevel_states & TOPLEVEL_STATE_ACTIVATED);
|
|
||||||
ensure_csd_resources(window);
|
|
||||||
wl_surface_commit(window->wl.surface);
|
|
||||||
inform_compositor_of_window_geometry(window, "configure");
|
|
||||||
if (live_resize_done) _glfwInputLiveResize(window, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void xdgToplevelHandleClose(void* data,
|
static void xdgToplevelHandleClose(void* data,
|
||||||
@@ -499,11 +483,64 @@ static const struct xdg_toplevel_listener xdgToplevelListener = {
|
|||||||
xdgToplevelHandleClose
|
xdgToplevelHandleClose
|
||||||
};
|
};
|
||||||
|
|
||||||
static void xdgSurfaceHandleConfigure(void* data UNUSED,
|
static void xdgSurfaceHandleConfigure(void* data,
|
||||||
struct xdg_surface* surface,
|
struct xdg_surface* surface,
|
||||||
uint32_t serial)
|
uint32_t serial)
|
||||||
{
|
{
|
||||||
|
_GLFWwindow* window = data;
|
||||||
xdg_surface_ack_configure(surface, serial);
|
xdg_surface_ack_configure(surface, serial);
|
||||||
|
if (window->wl.pending_state & PENDING_STATE_TOPLEVEL) {
|
||||||
|
uint32_t new_states = window->wl.pending.toplevel_states;
|
||||||
|
int width = window->wl.pending.width;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new_states != window->wl.current.toplevel_states ||
|
||||||
|
width != window->wl.current.width ||
|
||||||
|
height != window->wl.current.height) {
|
||||||
|
|
||||||
|
bool live_resize_done = !(new_states & TOPLEVEL_STATE_RESIZING) && (window->wl.current.toplevel_states & TOPLEVEL_STATE_RESIZING);
|
||||||
|
window->wl.current.toplevel_states = new_states;
|
||||||
|
window->wl.current.width = width;
|
||||||
|
window->wl.current.height = height;
|
||||||
|
_glfwInputWindowFocus(window, window->wl.current.toplevel_states & TOPLEVEL_STATE_ACTIVATED);
|
||||||
|
if (live_resize_done) _glfwInputLiveResize(window, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window->wl.pending_state & PENDING_STATE_DECORATION) {
|
||||||
|
uint32_t mode = window->wl.pending.decoration_mode;
|
||||||
|
bool has_server_side_decorations = (mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
|
||||||
|
window->wl.decorations.serverSide = has_server_side_decorations;
|
||||||
|
window->wl.current.decoration_mode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool resized = 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);
|
||||||
|
if (window->wl.decorations.serverSide) {
|
||||||
|
free_csd_surfaces(window);
|
||||||
|
} else {
|
||||||
|
ensure_csd_resources(window);
|
||||||
|
}
|
||||||
|
debug("final window content size: %dx%d resized: %d\n", width, height, resized);
|
||||||
|
}
|
||||||
|
|
||||||
|
inform_compositor_of_window_geometry(window, "configure");
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
window->wl.pending_state = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static const struct xdg_surface_listener xdgSurfaceListener = {
|
static const struct xdg_surface_listener xdgSurfaceListener = {
|
||||||
@@ -991,7 +1028,7 @@ void _glfwPlatformRestoreWindow(_GLFWwindow* window)
|
|||||||
{
|
{
|
||||||
if (window->monitor)
|
if (window->monitor)
|
||||||
xdg_toplevel_unset_fullscreen(window->wl.xdg.toplevel);
|
xdg_toplevel_unset_fullscreen(window->wl.xdg.toplevel);
|
||||||
if (window->wl.toplevel_states & TOPLEVEL_STATE_MAXIMIZED)
|
if (window->wl.current.toplevel_states & TOPLEVEL_STATE_MAXIMIZED)
|
||||||
xdg_toplevel_unset_maximized(window->wl.xdg.toplevel);
|
xdg_toplevel_unset_maximized(window->wl.xdg.toplevel);
|
||||||
// There is no way to unset minimized, or even to know if we are
|
// There is no way to unset minimized, or even to know if we are
|
||||||
// minimized, so there is nothing to do in this case.
|
// minimized, so there is nothing to do in this case.
|
||||||
@@ -1092,7 +1129,7 @@ int _glfwPlatformWindowVisible(_GLFWwindow* window)
|
|||||||
|
|
||||||
int _glfwPlatformWindowMaximized(_GLFWwindow* window)
|
int _glfwPlatformWindowMaximized(_GLFWwindow* window)
|
||||||
{
|
{
|
||||||
return window->wl.toplevel_states & TOPLEVEL_STATE_MAXIMIZED;
|
return window->wl.current.toplevel_states & TOPLEVEL_STATE_MAXIMIZED;
|
||||||
}
|
}
|
||||||
|
|
||||||
int _glfwPlatformWindowHovered(_GLFWwindow* window)
|
int _glfwPlatformWindowHovered(_GLFWwindow* window)
|
||||||
|
|||||||
2
glfw/xkb_glfw.c
vendored
2
glfw/xkb_glfw.c
vendored
@@ -33,7 +33,7 @@
|
|||||||
#include <X11/XKBlib.h>
|
#include <X11/XKBlib.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define debug(...) if (_glfw.hints.init.debugKeyboard) printf(__VA_ARGS__);
|
#define debug(...) if (_glfw.hints.init.debugKeyboard) fprintf(stderr, __VA_ARGS__);
|
||||||
|
|
||||||
#ifdef XKB_HAS_NO_UTF32
|
#ifdef XKB_HAS_NO_UTF32
|
||||||
#include "xkb-compat-shim.h"
|
#include "xkb-compat-shim.h"
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class HistoryCompleter:
|
|||||||
def option_text() -> str:
|
def option_text() -> str:
|
||||||
return '''\
|
return '''\
|
||||||
--type -t
|
--type -t
|
||||||
choices=line,yesno,choices
|
choices=line,yesno,choices,password
|
||||||
default=line
|
default=line
|
||||||
Type of input. Defaults to asking for a line of text.
|
Type of input. Defaults to asking for a line of text.
|
||||||
|
|
||||||
@@ -103,6 +103,11 @@ For example: y:Yes and n;red:No
|
|||||||
A default choice or text. If unspecified, it is "y" for :code:`yesno`, the first choice
|
A default choice or text. If unspecified, it is "y" for :code:`yesno`, the first choice
|
||||||
for :code:`choices` and empty for others. The default choice is selected when the user
|
for :code:`choices` and empty for others. The default choice is selected when the user
|
||||||
presses the Enter key.
|
presses the Enter key.
|
||||||
|
|
||||||
|
|
||||||
|
--prompt -p
|
||||||
|
default="> "
|
||||||
|
The prompt to use when inputting a line of text or a password.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
@@ -140,6 +145,55 @@ def extra_for(width: int, screen_width: int) -> int:
|
|||||||
return max(0, screen_width - width) // 2 + 1
|
return max(0, screen_width - width) // 2 + 1
|
||||||
|
|
||||||
|
|
||||||
|
class Password(Handler):
|
||||||
|
|
||||||
|
def __init__(self, cli_opts: AskCLIOptions, prompt: str) -> None:
|
||||||
|
self.cli_opts = cli_opts
|
||||||
|
self.prompt = prompt
|
||||||
|
from kittens.tui.line_edit import LineEdit
|
||||||
|
self.line_edit = LineEdit(is_password=True)
|
||||||
|
|
||||||
|
def initialize(self) -> None:
|
||||||
|
self.cmd.set_cursor_shape('beam')
|
||||||
|
self.draw_screen()
|
||||||
|
|
||||||
|
@Handler.atomic_update
|
||||||
|
def draw_screen(self) -> None:
|
||||||
|
self.cmd.clear_screen()
|
||||||
|
if self.cli_opts.message:
|
||||||
|
for line in self.cli_opts.message.splitlines():
|
||||||
|
self.print(line)
|
||||||
|
self.print()
|
||||||
|
self.line_edit.write(self.write, self.prompt)
|
||||||
|
|
||||||
|
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
|
||||||
|
self.line_edit.on_text(text, in_bracketed_paste)
|
||||||
|
self.draw_screen()
|
||||||
|
|
||||||
|
def on_key(self, key_event: KeyEventType) -> None:
|
||||||
|
if self.line_edit.on_key(key_event):
|
||||||
|
self.draw_screen()
|
||||||
|
return
|
||||||
|
if key_event.matches('enter'):
|
||||||
|
self.quit_loop(0)
|
||||||
|
if key_event.matches('esc'):
|
||||||
|
self.quit_loop(1)
|
||||||
|
|
||||||
|
def on_resize(self, screen_size: ScreenSize) -> None:
|
||||||
|
self.screen_size = screen_size
|
||||||
|
self.draw_screen()
|
||||||
|
|
||||||
|
def on_interrupt(self) -> None:
|
||||||
|
self.quit_loop(1)
|
||||||
|
on_eot = on_interrupt
|
||||||
|
|
||||||
|
@property
|
||||||
|
def response(self) -> str:
|
||||||
|
if self._tui_loop.return_code == 0:
|
||||||
|
return self.line_edit.current_input
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
class Choose(Handler):
|
class Choose(Handler):
|
||||||
mouse_tracking = MouseTracking.buttons_only
|
mouse_tracking = MouseTracking.buttons_only
|
||||||
|
|
||||||
@@ -361,6 +415,15 @@ def main(args: List[str]) -> Response:
|
|||||||
loop.loop(handler)
|
loop.loop(handler)
|
||||||
return {'items': items, 'response': handler.response}
|
return {'items': items, 'response': handler.response}
|
||||||
|
|
||||||
|
prompt = cli_opts.prompt
|
||||||
|
if prompt[0] == prompt[-1] and prompt[0] in '\'"':
|
||||||
|
prompt = prompt[1:-1]
|
||||||
|
if cli_opts.type == 'password':
|
||||||
|
loop = Loop()
|
||||||
|
phandler = Password(cli_opts, prompt)
|
||||||
|
loop.loop(phandler)
|
||||||
|
return {'items': items, 'response': phandler.response}
|
||||||
|
|
||||||
import readline as rl
|
import readline as rl
|
||||||
readline = rl
|
readline = rl
|
||||||
from kitty.shell import init_readline
|
from kitty.shell import init_readline
|
||||||
@@ -371,7 +434,6 @@ def main(args: List[str]) -> Response:
|
|||||||
if cli_opts.message:
|
if cli_opts.message:
|
||||||
print(styled(cli_opts.message, bold=True))
|
print(styled(cli_opts.message, bold=True))
|
||||||
|
|
||||||
prompt = '> '
|
|
||||||
with suppress(KeyboardInterrupt, EOFError):
|
with suppress(KeyboardInterrupt, EOFError):
|
||||||
if cli_opts.default:
|
if cli_opts.default:
|
||||||
def prefill_text() -> None:
|
def prefill_text() -> None:
|
||||||
|
|||||||
@@ -167,7 +167,9 @@ def highlight_collection(collection: Collection, aliases: Optional[Dict[str, str
|
|||||||
try:
|
try:
|
||||||
highlights = future.result()
|
highlights = future.result()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f'Running syntax highlighting for {path} generated an exception: {e}'
|
import traceback
|
||||||
|
tb = traceback.format_exc()
|
||||||
|
return f'Running syntax highlighting for {path} generated an exception: {e} with traceback:\n{tb}'
|
||||||
ans[path] = highlights
|
ans[path] = highlights
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|||||||
@@ -293,10 +293,11 @@ class DiffHandler(Handler):
|
|||||||
|
|
||||||
def scroll_lines(self, amt: int = 1) -> None:
|
def scroll_lines(self, amt: int = 1) -> None:
|
||||||
new_pos = max(0, min(self.scroll_pos + amt, self.max_scroll_pos))
|
new_pos = max(0, min(self.scroll_pos + amt, self.max_scroll_pos))
|
||||||
|
amt = new_pos - self.scroll_pos
|
||||||
if new_pos == self.scroll_pos:
|
if new_pos == self.scroll_pos:
|
||||||
self.cmd.bell()
|
self.cmd.bell()
|
||||||
return
|
return
|
||||||
if abs(new_pos - self.scroll_pos) >= self.num_lines - 1:
|
if abs(amt) >= self.num_lines - 1:
|
||||||
self.scroll_pos = new_pos
|
self.scroll_pos = new_pos
|
||||||
self.draw_screen()
|
self.draw_screen()
|
||||||
return
|
return
|
||||||
|
|||||||
2
kittens/diff/options/parse.py
generated
2
kittens/diff/options/parse.py
generated
@@ -93,7 +93,7 @@ def create_result_dict() -> typing.Dict[str, typing.Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
actions = frozenset(('map',))
|
actions: typing.FrozenSet[str] = frozenset(('map',))
|
||||||
|
|
||||||
|
|
||||||
def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:
|
def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:
|
||||||
|
|||||||
@@ -20,12 +20,14 @@ from kitty.fast_data_types import get_options, set_clipboard_string
|
|||||||
from kitty.key_encoding import KeyEvent
|
from kitty.key_encoding import KeyEvent
|
||||||
from kitty.typing import BossType, KittyCommonOpts
|
from kitty.typing import BossType, KittyCommonOpts
|
||||||
from kitty.utils import (
|
from kitty.utils import (
|
||||||
ScreenSize, resolve_custom_file, screen_size_function, set_primary_selection
|
ScreenSize, kitty_ansi_sanitizer_pat, resolve_custom_file,
|
||||||
|
screen_size_function, set_primary_selection
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..tui.handler import Handler, result_handler
|
from ..tui.handler import Handler, result_handler
|
||||||
from ..tui.loop import Loop
|
from ..tui.loop import Loop
|
||||||
from ..tui.operations import faint, styled
|
from ..tui.operations import faint, styled
|
||||||
|
from ..tui.utils import report_error, report_unhandled_error
|
||||||
|
|
||||||
|
|
||||||
@lru_cache()
|
@lru_cache()
|
||||||
@@ -116,6 +118,9 @@ def render(text: str, current_input: str, all_marks: Sequence[Mark], ignore_mark
|
|||||||
|
|
||||||
class Hints(Handler):
|
class Hints(Handler):
|
||||||
|
|
||||||
|
use_alternate_screen = False # disabled to avoid screen being blanked at exit causing flicker
|
||||||
|
overlay_ready_report_needed = True
|
||||||
|
|
||||||
def __init__(self, text: str, all_marks: Sequence[Mark], index_map: Dict[int, Mark], args: HintsCLIOptions):
|
def __init__(self, text: str, all_marks: Sequence[Mark], index_map: Dict[int, Mark], args: HintsCLIOptions):
|
||||||
self.text, self.index_map = text, index_map
|
self.text, self.index_map = text, index_map
|
||||||
self.alphabet = args.alphabet or DEFAULT_HINT_ALPHABET
|
self.alphabet = args.alphabet or DEFAULT_HINT_ALPHABET
|
||||||
@@ -230,7 +235,7 @@ def regex_finditer(pat: 'Pattern[str]', minimum_match_length: int, text: str) ->
|
|||||||
yield s, e, m.groupdict()
|
yield s, e, m.groupdict()
|
||||||
|
|
||||||
|
|
||||||
closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>', '*': '*', '"': '"', "'": "'"}
|
closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>', '*': '*', '"': '"', "'": "'", "“": "”", "‘": "’"}
|
||||||
opening_brackets = ''.join(closing_bracket_map)
|
opening_brackets = ''.join(closing_bracket_map)
|
||||||
PostprocessorFunc = Callable[[str, int, int], Tuple[int, int]]
|
PostprocessorFunc = Callable[[str, int, int], Tuple[int, int]]
|
||||||
postprocessor_map: Dict[str, PostprocessorFunc] = {}
|
postprocessor_map: Dict[str, PostprocessorFunc] = {}
|
||||||
@@ -288,11 +293,12 @@ def quotes(text: str, s: int, e: int) -> Tuple[int, int]:
|
|||||||
# Remove matching quotes
|
# Remove matching quotes
|
||||||
if s < e <= len(text):
|
if s < e <= len(text):
|
||||||
before = text[s]
|
before = text[s]
|
||||||
if before in '\'"':
|
if before in '\'"“‘':
|
||||||
if text[e-1] == before:
|
q = closing_bracket_map[before]
|
||||||
|
if text[e-1] == q:
|
||||||
s += 1
|
s += 1
|
||||||
e -= 1
|
e -= 1
|
||||||
elif text[e:e+1] == before:
|
elif text[e:e+1] == q:
|
||||||
s += 1
|
s += 1
|
||||||
return s, e
|
return s, e
|
||||||
|
|
||||||
@@ -430,11 +436,7 @@ def load_custom_processor(customize_processing: str) -> Any:
|
|||||||
return runpy.run_path(custom_path, run_name='__main__')
|
return runpy.run_path(custom_path, run_name='__main__')
|
||||||
|
|
||||||
|
|
||||||
def remove_sgr(text: str) -> str:
|
def process_escape_codes(text: str) -> Tuple[str, Tuple[Mark, ...]]:
|
||||||
return re.sub(r'\x1b\[.*?m', '', text)
|
|
||||||
|
|
||||||
|
|
||||||
def process_hyperlinks(text: str) -> Tuple[str, Tuple[Mark, ...]]:
|
|
||||||
hyperlinks: List[Mark] = []
|
hyperlinks: List[Mark] = []
|
||||||
removed_size = idx = 0
|
removed_size = idx = 0
|
||||||
active_hyperlink_url: Optional[str] = None
|
active_hyperlink_url: Optional[str] = None
|
||||||
@@ -457,6 +459,9 @@ def process_hyperlinks(text: str) -> Tuple[str, Tuple[Mark, ...]]:
|
|||||||
def process_hyperlink(m: 're.Match[str]') -> str:
|
def process_hyperlink(m: 're.Match[str]') -> str:
|
||||||
nonlocal removed_size, active_hyperlink_url, active_hyperlink_id, active_hyperlink_start_offset
|
nonlocal removed_size, active_hyperlink_url, active_hyperlink_id, active_hyperlink_start_offset
|
||||||
raw = m.group()
|
raw = m.group()
|
||||||
|
if not raw.startswith('\x1b]8'):
|
||||||
|
removed_size += len(raw)
|
||||||
|
return ''
|
||||||
start = m.start() - removed_size
|
start = m.start() - removed_size
|
||||||
removed_size += len(raw)
|
removed_size += len(raw)
|
||||||
if active_hyperlink_url is not None:
|
if active_hyperlink_url is not None:
|
||||||
@@ -474,7 +479,7 @@ def process_hyperlinks(text: str) -> Tuple[str, Tuple[Mark, ...]]:
|
|||||||
|
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
text = re.sub(r'\x1b\]8.+?\x1b\\', process_hyperlink, text)
|
text = kitty_ansi_sanitizer_pat().sub(process_hyperlink, text)
|
||||||
if active_hyperlink_url is not None:
|
if active_hyperlink_url is not None:
|
||||||
add_hyperlink(len(text))
|
add_hyperlink(len(text))
|
||||||
return text, tuple(hyperlinks)
|
return text, tuple(hyperlinks)
|
||||||
@@ -482,8 +487,8 @@ def process_hyperlinks(text: str) -> Tuple[str, Tuple[Mark, ...]]:
|
|||||||
|
|
||||||
def run(args: HintsCLIOptions, text: str, extra_cli_args: Sequence[str] = ()) -> Optional[Dict[str, Any]]:
|
def run(args: HintsCLIOptions, text: str, extra_cli_args: Sequence[str] = ()) -> Optional[Dict[str, Any]]:
|
||||||
try:
|
try:
|
||||||
text = parse_input(remove_sgr(text))
|
text = parse_input(text)
|
||||||
text, hyperlinks = process_hyperlinks(text)
|
text, hyperlinks = process_escape_codes(text)
|
||||||
pattern, post_processors = functions_for(args)
|
pattern, post_processors = functions_for(args)
|
||||||
if args.type == 'linenum':
|
if args.type == 'linenum':
|
||||||
args.customize_processing = '::linenum::'
|
args.customize_processing = '::linenum::'
|
||||||
@@ -499,7 +504,7 @@ def run(args: HintsCLIOptions, text: str, extra_cli_args: Sequence[str] = ()) ->
|
|||||||
all_marks = tuple(mark(pattern, post_processors, text, args))
|
all_marks = tuple(mark(pattern, post_processors, text, args))
|
||||||
if not all_marks:
|
if not all_marks:
|
||||||
none_of = {'url': 'URLs', 'hyperlink': 'hyperlinks'}.get(args.type, 'matches')
|
none_of = {'url': 'URLs', 'hyperlink': 'hyperlinks'}.get(args.type, 'matches')
|
||||||
input(_('No {} found, press Enter to quit.').format(none_of))
|
report_error(_('No {} found.').format(none_of))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
largest_index = all_marks[-1].index
|
largest_index = all_marks[-1].index
|
||||||
@@ -511,11 +516,7 @@ def run(args: HintsCLIOptions, text: str, extra_cli_args: Sequence[str] = ()) ->
|
|||||||
m.index = largest_index - m.index + offset
|
m.index = largest_index - m.index + offset
|
||||||
index_map = {m.index: m for m in all_marks}
|
index_map = {m.index: m for m in all_marks}
|
||||||
except Exception:
|
except Exception:
|
||||||
import traceback
|
report_unhandled_error()
|
||||||
traceback.print_exc()
|
|
||||||
input('Press Enter to quit.')
|
|
||||||
raise SystemExit(1)
|
|
||||||
|
|
||||||
return run_loop(args, text, all_marks, index_map, extra_cli_args)
|
return run_loop(args, text, all_marks, index_map, extra_cli_args)
|
||||||
|
|
||||||
|
|
||||||
@@ -676,9 +677,7 @@ def main(args: List[str]) -> Optional[Dict[str, Any]]:
|
|||||||
text = ''
|
text = ''
|
||||||
if sys.stdin.isatty():
|
if sys.stdin.isatty():
|
||||||
if '--help' not in args and '-h' not in args:
|
if '--help' not in args and '-h' not in args:
|
||||||
print('You must pass the text to be hinted on STDIN', file=sys.stderr)
|
report_unhandled_error('You must pass the text to be hinted on STDIN')
|
||||||
input(_('Press Enter to quit'))
|
|
||||||
return None
|
|
||||||
else:
|
else:
|
||||||
text = sys.stdin.buffer.read().decode('utf-8')
|
text = sys.stdin.buffer.read().decode('utf-8')
|
||||||
sys.stdin = open(os.ctermid())
|
sys.stdin = open(os.ctermid())
|
||||||
@@ -686,29 +685,37 @@ def main(args: List[str]) -> Optional[Dict[str, Any]]:
|
|||||||
opts, items = parse_hints_args(args[1:])
|
opts, items = parse_hints_args(args[1:])
|
||||||
except SystemExit as e:
|
except SystemExit as e:
|
||||||
if e.code != 0:
|
if e.code != 0:
|
||||||
print(e.args[0], file=sys.stderr)
|
report_unhandled_error(e.args[0])
|
||||||
input(_('Press Enter to quit'))
|
|
||||||
return None
|
return None
|
||||||
if items and not (opts.customize_processing or opts.type == 'linenum'):
|
if items and not (opts.customize_processing or opts.type == 'linenum'):
|
||||||
print('Extra command line arguments present: {}'.format(' '.join(items)), file=sys.stderr)
|
report_unhandled_error('Extra command line arguments present: {}'.format(' '.join(items)))
|
||||||
input(_('Press Enter to quit'))
|
|
||||||
try:
|
try:
|
||||||
return run(opts, text, items)
|
return run(opts, text, items)
|
||||||
except Exception:
|
except Exception:
|
||||||
import traceback
|
report_unhandled_error()
|
||||||
traceback.print_exc()
|
|
||||||
input(_('Press Enter to quit'))
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def linenum_handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: BossType, extra_cli_args: Sequence[str], *a: Any) -> None:
|
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']):
|
for m, g in zip(data['match'], data['groupdicts']):
|
||||||
if m:
|
if m:
|
||||||
path, line = g['path'], g['line']
|
path, line = g['path'], g['line']
|
||||||
path = os.path.expanduser(path.split(':')[-1])
|
# look for trailers on path of the for :number
|
||||||
line = int(line)
|
while True:
|
||||||
break
|
m = pat.search(path)
|
||||||
else:
|
if m is None:
|
||||||
|
break
|
||||||
|
line = m.group(1)
|
||||||
|
path = path[:-len(m.group())]
|
||||||
|
|
||||||
|
return os.path.expanduser(path), int(line)
|
||||||
|
return '', -1
|
||||||
|
|
||||||
|
|
||||||
|
def linenum_handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: BossType, extra_cli_args: Sequence[str], *a: Any) -> None:
|
||||||
|
path, line = linenum_process_result(data)
|
||||||
|
if not path:
|
||||||
return
|
return
|
||||||
|
|
||||||
cmd = [x.format(path=path, line=line) for x in extra_cli_args or ('vim', '+{line}', '{path}')]
|
cmd = [x.format(path=path, line=line) for x in extra_cli_args or ('vim', '+{line}', '{path}')]
|
||||||
@@ -739,7 +746,7 @@ def linenum_handle_result(args: List[str], data: Dict[str, Any], target_window_i
|
|||||||
}[action])(*cmd)
|
}[action])(*cmd)
|
||||||
|
|
||||||
|
|
||||||
@result_handler(type_of_input='screen-ansi')
|
@result_handler(type_of_input='screen-ansi', has_ready_notification=Hints.overlay_ready_report_needed)
|
||||||
def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: BossType) -> None:
|
def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: BossType) -> None:
|
||||||
if data['customize_processing']:
|
if data['customize_processing']:
|
||||||
m = load_custom_processor(data['customize_processing'])
|
m = load_custom_processor(data['customize_processing'])
|
||||||
@@ -781,7 +788,7 @@ def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int,
|
|||||||
if program == '-':
|
if program == '-':
|
||||||
w = boss.window_id_map.get(target_window_id)
|
w = boss.window_id_map.get(target_window_id)
|
||||||
if w is not None:
|
if w is not None:
|
||||||
w.paste(joined_text())
|
w.paste_text(joined_text())
|
||||||
elif program == '@':
|
elif program == '@':
|
||||||
set_clipboard_string(joined_text())
|
set_clipboard_string(joined_text())
|
||||||
elif program == '*':
|
elif program == '*':
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ def main() -> None:
|
|||||||
if not sys.stdout.isatty() and '--pretty' not in sys.argv:
|
if not sys.stdout.isatty() and '--pretty' not in sys.argv:
|
||||||
os.execlp('rg', 'rg', *sys.argv[1:])
|
os.execlp('rg', 'rg', *sys.argv[1:])
|
||||||
cmdline = ['rg', '--pretty', '--with-filename'] + sys.argv[1:]
|
cmdline = ['rg', '--pretty', '--with-filename'] + sys.argv[1:]
|
||||||
p = subprocess.Popen(cmdline, stdout=subprocess.PIPE)
|
try:
|
||||||
|
p = subprocess.Popen(cmdline, stdout=subprocess.PIPE)
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise SystemExit('Could not find the rg executable in your PATH. Is ripgrep installed?')
|
||||||
assert p.stdout is not None
|
assert p.stdout is not None
|
||||||
write: Callable[[bytes], None] = cast(Callable[[bytes], None], sys.stdout.buffer.write)
|
write: Callable[[bytes], None] = cast(Callable[[bytes], None], sys.stdout.buffer.write)
|
||||||
sgr_pat = re.compile(br'\x1b\[.*?m')
|
sgr_pat = re.compile(br'\x1b\[.*?m')
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ detecting image display support.
|
|||||||
--print-window-size
|
--print-window-size
|
||||||
type=bool-set
|
type=bool-set
|
||||||
Print out the window size as :italic:`widthxheight` (in pixels) and quit. This is a
|
Print out the window size as :italic:`widthxheight` (in pixels) and quit. This is a
|
||||||
convenience method to query the window size if using kitty icat from a
|
convenience method to query the window size if using :code:`kitty +kitten icat` from a
|
||||||
scripting language that cannot make termios calls.
|
scripting language that cannot make termios calls.
|
||||||
|
|
||||||
|
|
||||||
@@ -515,7 +515,12 @@ def main(args: List[str] = sys.argv) -> None:
|
|||||||
if cli_opts.print_window_size:
|
if cli_opts.print_window_size:
|
||||||
screen_size_function.cache_clear()
|
screen_size_function.cache_clear()
|
||||||
with open(os.ctermid()) as tty:
|
with open(os.ctermid()) as tty:
|
||||||
ss = screen_size_function(tty)()
|
try:
|
||||||
|
fd = tty.fileno()
|
||||||
|
except AttributeError:
|
||||||
|
# use default value for fd if ctermid is not available
|
||||||
|
fd = None
|
||||||
|
ss = screen_size_function(fd)()
|
||||||
print(f'{ss.width}x{ss.height}', end='')
|
print(f'{ss.width}x{ss.height}', end='')
|
||||||
raise SystemExit(0)
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
|||||||
@@ -122,10 +122,17 @@ class ControlMaster:
|
|||||||
cmd.extend(['-i', conn_data.identity_file])
|
cmd.extend(['-i', conn_data.identity_file])
|
||||||
self.batch_cmd_prefix = cmd + ['-o', 'BatchMode=yes']
|
self.batch_cmd_prefix = cmd + ['-o', 'BatchMode=yes']
|
||||||
|
|
||||||
|
def check_call(self, cmd: List[str]) -> None:
|
||||||
|
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL)
|
||||||
|
stdout = p.communicate()[0]
|
||||||
|
if p.wait() != 0:
|
||||||
|
out = stdout.decode('utf-8', 'replace')
|
||||||
|
raise Exception(f'The ssh command: {shlex.join(cmd)} failed with exit code {p.returncode} and output: {out}')
|
||||||
|
|
||||||
def __enter__(self) -> 'ControlMaster':
|
def __enter__(self) -> 'ControlMaster':
|
||||||
subprocess.check_call(
|
self.check_call(
|
||||||
self.cmd_prefix + ['-o', 'ControlMaster=auto', '-fN', self.conn_data.hostname])
|
self.cmd_prefix + ['-o', 'ControlMaster=auto', '-fN', self.conn_data.hostname])
|
||||||
subprocess.check_call(
|
self.check_call(
|
||||||
self.batch_cmd_prefix + ['-O', 'check', self.conn_data.hostname])
|
self.batch_cmd_prefix + ['-O', 'check', self.conn_data.hostname])
|
||||||
if not self.dest:
|
if not self.dest:
|
||||||
self.tdir = tempfile.mkdtemp()
|
self.tdir = tempfile.mkdtemp()
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ from contextlib import contextmanager
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import TYPE_CHECKING, Any, Dict, FrozenSet, Generator, List, cast
|
from typing import TYPE_CHECKING, Any, Dict, FrozenSet, Generator, List, cast
|
||||||
|
|
||||||
|
from kitty.constants import list_kitty_resources
|
||||||
from kitty.types import run_once
|
from kitty.types import run_once
|
||||||
from kitty.utils import resolve_abs_or_config_path
|
from kitty.utils import resolve_abs_or_config_path
|
||||||
|
|
||||||
|
|
||||||
aliases = {'url_hints': 'hints'}
|
aliases = {'url_hints': 'hints'}
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from kitty.conf.types import Definition
|
from kitty.conf.types import Definition
|
||||||
@@ -69,6 +69,7 @@ def create_kitten_handler(kitten: str, orig_args: List[str]) -> Any:
|
|||||||
ans = partial(m['end'], [kitten] + orig_args)
|
ans = partial(m['end'], [kitten] + orig_args)
|
||||||
setattr(ans, 'type_of_input', getattr(m['end'], 'type_of_input', None))
|
setattr(ans, 'type_of_input', getattr(m['end'], 'type_of_input', None))
|
||||||
setattr(ans, 'no_ui', getattr(m['end'], 'no_ui', False))
|
setattr(ans, 'no_ui', getattr(m['end'], 'no_ui', False))
|
||||||
|
setattr(ans, 'has_ready_notification', getattr(m['end'], 'has_ready_notification', False))
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
@@ -85,42 +86,31 @@ def launch(args: List[str]) -> None:
|
|||||||
del args[:2]
|
del args[:2]
|
||||||
args = [kitten] + args
|
args = [kitten] + args
|
||||||
os.environ['KITTY_CONFIG_DIRECTORY'] = config_dir
|
os.environ['KITTY_CONFIG_DIRECTORY'] = config_dir
|
||||||
from kittens.tui.operations import clear_screen, reset_mode, Mode
|
|
||||||
set_debug(kitten)
|
set_debug(kitten)
|
||||||
m = import_kitten_main_module(config_dir, kitten)
|
m = import_kitten_main_module(config_dir, kitten)
|
||||||
try:
|
try:
|
||||||
result = m['start'](args)
|
result = m['start'](args)
|
||||||
finally:
|
finally:
|
||||||
sys.stdin = sys.__stdin__
|
sys.stdin = sys.__stdin__
|
||||||
print(reset_mode(Mode.ALTERNATE_SCREEN) + clear_screen(), end='')
|
|
||||||
if result is not None:
|
if result is not None:
|
||||||
import json
|
import json
|
||||||
data = json.dumps(result)
|
import base64
|
||||||
print('OK:', len(data), data)
|
data = base64.b85encode(json.dumps(result).encode('utf-8'))
|
||||||
|
sys.stdout.buffer.write(b'\x1bP@kitty-kitten-result|')
|
||||||
|
sys.stdout.buffer.write(data)
|
||||||
|
sys.stdout.buffer.write(b'\x1b\\')
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
def deserialize(output: str) -> Any:
|
|
||||||
import json
|
|
||||||
if output.startswith('OK: '):
|
|
||||||
try:
|
|
||||||
prefix, sz, rest = output.split(' ', 2)
|
|
||||||
return json.loads(rest[:int(sz)])
|
|
||||||
except Exception:
|
|
||||||
raise ValueError(f'Failed to parse kitten output: {output!r}')
|
|
||||||
|
|
||||||
|
|
||||||
def run_kitten(kitten: str, run_name: str = '__main__') -> None:
|
def run_kitten(kitten: str, run_name: str = '__main__') -> None:
|
||||||
import runpy
|
import runpy
|
||||||
original_kitten_name = kitten
|
original_kitten_name = kitten
|
||||||
kitten = resolved_kitten(kitten)
|
kitten = resolved_kitten(kitten)
|
||||||
set_debug(kitten)
|
set_debug(kitten)
|
||||||
try:
|
if kitten in all_kitten_names():
|
||||||
runpy.run_module(f'kittens.{kitten}.main', run_name=run_name)
|
runpy.run_module(f'kittens.{kitten}.main', run_name=run_name)
|
||||||
return
|
return
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
# Look for a custom kitten
|
# Look for a custom kitten
|
||||||
if not kitten.endswith('.py'):
|
if not kitten.endswith('.py'):
|
||||||
kitten += '.py'
|
kitten += '.py'
|
||||||
@@ -137,7 +127,6 @@ def run_kitten(kitten: str, run_name: str = '__main__') -> None:
|
|||||||
|
|
||||||
@run_once
|
@run_once
|
||||||
def all_kitten_names() -> FrozenSet[str]:
|
def all_kitten_names() -> FrozenSet[str]:
|
||||||
from kitty.constants import list_kitty_resources
|
|
||||||
ans = []
|
ans = []
|
||||||
for name in list_kitty_resources('kittens'):
|
for name in list_kitty_resources('kittens'):
|
||||||
if '__' not in name and '.' not in name and name != 'tui':
|
if '__' not in name and '.' not in name and name != 'tui':
|
||||||
|
|||||||
@@ -23,21 +23,22 @@ def real_main(args: List[str]) -> None:
|
|||||||
cli_opts, items = parse_args(args[1:], OPTIONS, '', msg, 'hints', result_class=ErrorCLIOptions)
|
cli_opts, items = parse_args(args[1:], OPTIONS, '', msg, 'hints', result_class=ErrorCLIOptions)
|
||||||
error_message = sys.stdin.buffer.read().decode('utf-8')
|
error_message = sys.stdin.buffer.read().decode('utf-8')
|
||||||
sys.stdin = open(os.ctermid())
|
sys.stdin = open(os.ctermid())
|
||||||
print(styled(cli_opts.title, fg_intense=True, fg='red', bold=True))
|
if cli_opts.title:
|
||||||
print()
|
print(styled(cli_opts.title, fg_intense=True, fg='red', bold=True))
|
||||||
|
print()
|
||||||
print(error_message)
|
print(error_message)
|
||||||
print()
|
print()
|
||||||
input('Press Enter to close.')
|
input('\x1b[1;32mPress Enter to close\x1b[m')
|
||||||
|
|
||||||
|
|
||||||
def main(args: List[str]) -> None:
|
def main(args: List[str]) -> None:
|
||||||
try:
|
try:
|
||||||
with suppress(KeyboardInterrupt):
|
with suppress(KeyboardInterrupt, EOFError):
|
||||||
real_main(args)
|
real_main(args)
|
||||||
except Exception:
|
except Exception:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
input('Press Enter to close.')
|
input('Press Enter to close')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
74
kittens/ssh/config.py
Normal file
74
kittens/ssh/config.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
|
||||||
|
import fnmatch
|
||||||
|
import os
|
||||||
|
from typing import Any, Dict, Iterable, Optional
|
||||||
|
|
||||||
|
from kitty.conf.utils import (
|
||||||
|
load_config as _load_config, parse_config_base, resolve_config
|
||||||
|
)
|
||||||
|
from kitty.constants import config_dir
|
||||||
|
|
||||||
|
from .options.types import Options as SSHOptions, defaults
|
||||||
|
|
||||||
|
SYSTEM_CONF = '/etc/xdg/kitty/ssh.conf'
|
||||||
|
defconf = os.path.join(config_dir, 'ssh.conf')
|
||||||
|
|
||||||
|
|
||||||
|
def host_matches(mpat: str, hostname: str, username: str) -> bool:
|
||||||
|
for pat in mpat.split():
|
||||||
|
upat = '*'
|
||||||
|
if '@' in pat:
|
||||||
|
upat, pat = pat.split('@', 1)
|
||||||
|
if fnmatch.fnmatchcase(hostname, pat) and fnmatch.fnmatchcase(username, upat):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(*paths: str, overrides: Optional[Iterable[str]] = None, hostname: str = '!', username: str = '') -> SSHOptions:
|
||||||
|
from .options.parse import (
|
||||||
|
create_result_dict, merge_result_dicts, parse_conf_item
|
||||||
|
)
|
||||||
|
from .options.utils import (
|
||||||
|
first_seen_positions, get_per_hosts_dict, init_results_dict
|
||||||
|
)
|
||||||
|
|
||||||
|
def merge_dicts(base: Dict[str, Any], vals: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
base_phd = get_per_hosts_dict(base)
|
||||||
|
vals_phd = get_per_hosts_dict(vals)
|
||||||
|
for hostname in base_phd:
|
||||||
|
vals_phd[hostname] = merge_result_dicts(base_phd[hostname], vals_phd.get(hostname, {}))
|
||||||
|
ans: Dict[str, Any] = vals_phd.pop(vals['hostname'])
|
||||||
|
ans['per_host_dicts'] = vals_phd
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def parse_config(lines: Iterable[str]) -> Dict[str, Any]:
|
||||||
|
ans: Dict[str, Any] = init_results_dict(create_result_dict())
|
||||||
|
parse_config_base(lines, parse_conf_item, ans)
|
||||||
|
return ans
|
||||||
|
|
||||||
|
overrides = tuple(overrides) if overrides is not None else ()
|
||||||
|
first_seen_positions.clear()
|
||||||
|
first_seen_positions['*'] = 0
|
||||||
|
opts_dict, paths = _load_config(
|
||||||
|
defaults, parse_config, merge_dicts, *paths, overrides=overrides, initialize_defaults=init_results_dict)
|
||||||
|
phd = get_per_hosts_dict(opts_dict)
|
||||||
|
final_dict: Dict[str, Any] = {}
|
||||||
|
for hostname_pat in sorted(phd, key=first_seen_positions.__getitem__):
|
||||||
|
if host_matches(hostname_pat, hostname, username):
|
||||||
|
od = phd[hostname_pat]
|
||||||
|
for k, v in od.items():
|
||||||
|
if isinstance(v, dict):
|
||||||
|
bv = final_dict.setdefault(k, {})
|
||||||
|
bv.update(v)
|
||||||
|
else:
|
||||||
|
final_dict[k] = v
|
||||||
|
first_seen_positions.clear()
|
||||||
|
return SSHOptions(final_dict)
|
||||||
|
|
||||||
|
|
||||||
|
def init_config(hostname: str, username: str, overrides: Optional[Iterable[str]] = None) -> SSHOptions:
|
||||||
|
config = tuple(resolve_config(SYSTEM_CONF, defconf))
|
||||||
|
return load_config(*config, overrides=overrides, hostname=hostname, username=username)
|
||||||
104
kittens/ssh/copy.py
Normal file
104
kittens/ssh/copy.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import uuid
|
||||||
|
from typing import (
|
||||||
|
Dict, Iterable, Iterator, List, NamedTuple, Optional, Sequence, Tuple
|
||||||
|
)
|
||||||
|
|
||||||
|
from kitty.cli import parse_args
|
||||||
|
from kitty.cli_stub import CopyCLIOptions
|
||||||
|
from kitty.types import run_once
|
||||||
|
|
||||||
|
from ..transfer.utils import expand_home, home_path
|
||||||
|
|
||||||
|
|
||||||
|
@run_once
|
||||||
|
def option_text() -> str:
|
||||||
|
return '''
|
||||||
|
--glob
|
||||||
|
type=bool-set
|
||||||
|
Interpret file arguments as glob patterns.
|
||||||
|
|
||||||
|
|
||||||
|
--dest
|
||||||
|
The destination on the remote host to copy to. Relative paths are resolved
|
||||||
|
relative to HOME on the remote host. When this option is not specified, the
|
||||||
|
local file path is used as the remote destination (with the HOME directory
|
||||||
|
getting automatically replaced by the remote HOME). Note that environment
|
||||||
|
variables and ~ are not expanded.
|
||||||
|
|
||||||
|
|
||||||
|
--exclude
|
||||||
|
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.
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def parse_copy_args(args: Optional[Sequence[str]] = None) -> Tuple[CopyCLIOptions, List[str]]:
|
||||||
|
args = list(args or ())
|
||||||
|
try:
|
||||||
|
opts, args = parse_args(result_class=CopyCLIOptions, args=args, ospec=option_text)
|
||||||
|
except SystemExit as e:
|
||||||
|
raise CopyCLIError from e
|
||||||
|
return opts, args
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_file_spec(spec: str, is_glob: bool) -> Iterator[str]:
|
||||||
|
ans = os.path.expandvars(expand_home(spec))
|
||||||
|
if not os.path.isabs(ans):
|
||||||
|
ans = expand_home(f'~/{ans}')
|
||||||
|
if is_glob:
|
||||||
|
files = glob.glob(ans)
|
||||||
|
if not files:
|
||||||
|
raise CopyCLIError(f'{spec} does not exist')
|
||||||
|
else:
|
||||||
|
if not os.path.exists(ans):
|
||||||
|
raise CopyCLIError(f'{spec} does not exist')
|
||||||
|
files = [ans]
|
||||||
|
for x in files:
|
||||||
|
yield os.path.normpath(x).replace(os.sep, '/')
|
||||||
|
|
||||||
|
|
||||||
|
class CopyCLIError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_arcname(loc: str, dest: Optional[str], home: str) -> str:
|
||||||
|
if dest:
|
||||||
|
arcname = dest
|
||||||
|
else:
|
||||||
|
arcname = os.path.normpath(loc)
|
||||||
|
if arcname.startswith(home):
|
||||||
|
arcname = os.path.relpath(arcname, home)
|
||||||
|
arcname = os.path.normpath(arcname).replace(os.sep, '/')
|
||||||
|
prefix = 'root' if arcname.startswith('/') else 'home/'
|
||||||
|
return prefix + arcname
|
||||||
|
|
||||||
|
|
||||||
|
class CopyInstruction(NamedTuple):
|
||||||
|
local_path: str
|
||||||
|
arcname: str
|
||||||
|
exclude_patterns: Tuple[str, ...]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_copy_instructions(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, CopyInstruction]]:
|
||||||
|
opts, args = parse_copy_args(shlex.split(val))
|
||||||
|
locations: List[str] = []
|
||||||
|
for a in args:
|
||||||
|
locations.extend(resolve_file_spec(a, opts.glob))
|
||||||
|
if not locations:
|
||||||
|
raise CopyCLIError('No files to copy specified')
|
||||||
|
if len(locations) > 1 and opts.dest:
|
||||||
|
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))
|
||||||
@@ -1,130 +1,300 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
import atexit
|
||||||
|
import fnmatch
|
||||||
|
import glob
|
||||||
|
import io
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
import secrets
|
||||||
import shlex
|
import shlex
|
||||||
|
import shutil
|
||||||
|
import stat
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from contextlib import suppress
|
import tarfile
|
||||||
from typing import List, NoReturn, Optional, Set, Tuple
|
import tempfile
|
||||||
from .completion import ssh_options, complete
|
import termios
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from base64 import standard_b64decode, standard_b64encode
|
||||||
|
from contextlib import contextmanager, suppress
|
||||||
|
from getpass import getuser
|
||||||
|
from select import select
|
||||||
|
from typing import (
|
||||||
|
Callable, Dict, Iterator, List, NoReturn, Optional, Sequence, Set, Tuple,
|
||||||
|
Union
|
||||||
|
)
|
||||||
|
|
||||||
from kitty.utils import SSHConnectionData
|
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.shm import SharedMemory
|
||||||
|
from kitty.types import run_once
|
||||||
|
from kitty.utils import SSHConnectionData, set_echo as turn_off_echo, expandvars, resolve_abs_or_config_path
|
||||||
|
|
||||||
SHELL_SCRIPT = '''\
|
from .completion import complete, ssh_options
|
||||||
#!/bin/sh
|
from .config import init_config
|
||||||
# macOS ships with an ancient version of tic that cannot read from stdin, so we
|
from .copy import CopyInstruction
|
||||||
# create a temp file for it
|
from .options.types import Options as SSHOptions
|
||||||
tmp=$(mktemp)
|
from .options.utils import DELETE_ENV_VAR
|
||||||
cat >$tmp << 'TERMEOF'
|
|
||||||
TERMINFO
|
|
||||||
TERMEOF
|
|
||||||
|
|
||||||
tic_out=$(tic -x -o $HOME/.terminfo $tmp 2>&1)
|
|
||||||
rc=$?
|
|
||||||
rm $tmp
|
|
||||||
if [ "$rc" != "0" ]; then echo "$tic_out"; exit 1; fi
|
|
||||||
if [ -z "$USER" ]; then export USER=$(whoami); fi
|
|
||||||
export TERMINFO="$HOME/.terminfo"
|
|
||||||
login_shell=""
|
|
||||||
python=""
|
|
||||||
|
|
||||||
login_shell_is_ok() {
|
|
||||||
if [ -z "$login_shell" ] || [ ! -x "$login_shell" ]; then return 1; fi
|
|
||||||
case "$login_shell" in
|
|
||||||
*sh) return 0;
|
|
||||||
esac
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_python() {
|
|
||||||
python=$(command -v python3)
|
|
||||||
if [ -z "$python" ]; then python=$(command -v python2); fi
|
|
||||||
if [ -z "$python" ]; then python=python; fi
|
|
||||||
}
|
|
||||||
|
|
||||||
using_getent() {
|
|
||||||
cmd=$(command -v getent)
|
|
||||||
if [ -z "$cmd" ]; then return; fi
|
|
||||||
output=$($cmd passwd $USER 2>/dev/null)
|
|
||||||
if [ $? = 0 ]; then login_shell=$(echo $output | cut -d: -f7); fi
|
|
||||||
}
|
|
||||||
|
|
||||||
using_id() {
|
|
||||||
cmd=$(command -v id)
|
|
||||||
if [ -z "$cmd" ]; then return; fi
|
|
||||||
output=$($cmd -P $USER 2>/dev/null)
|
|
||||||
if [ $? = 0 ]; then login_shell=$(echo $output | cut -d: -f7); fi
|
|
||||||
}
|
|
||||||
|
|
||||||
using_passwd() {
|
|
||||||
cmd=$(command -v grep)
|
|
||||||
if [ -z "$cmd" ]; then return; fi
|
|
||||||
output=$($cmd "^$USER:" /etc/passwd 2>/dev/null)
|
|
||||||
if [ $? = 0 ]; then login_shell=$(echo $output | cut -d: -f7); fi
|
|
||||||
}
|
|
||||||
|
|
||||||
using_python() {
|
|
||||||
detect_python
|
|
||||||
if [ ! -x "$python" ]; then return; fi
|
|
||||||
output=$($python -c "import pwd, os; print(pwd.getpwuid(os.geteuid()).pw_shell)")
|
|
||||||
if [ $? = 0 ]; then login_shell=$output; fi
|
|
||||||
}
|
|
||||||
|
|
||||||
execute_with_python() {
|
|
||||||
detect_python
|
|
||||||
exec $python -c "import os; os.execl('$login_shell', '-' '$shell_name')"
|
|
||||||
}
|
|
||||||
|
|
||||||
die() { echo "$*" 1>&2 ; exit 1; }
|
|
||||||
|
|
||||||
using_getent
|
|
||||||
if ! login_shell_is_ok; then using_id; fi
|
|
||||||
if ! login_shell_is_ok; then using_python; fi
|
|
||||||
if ! login_shell_is_ok; then using_passwd; fi
|
|
||||||
if ! login_shell_is_ok; then die "Could not detect login shell"; fi
|
|
||||||
|
|
||||||
|
|
||||||
# If a command was passed to SSH execute it here
|
@run_once
|
||||||
EXEC_CMD
|
def ssh_exe() -> str:
|
||||||
|
return shutil.which('ssh') or 'ssh'
|
||||||
# We need to pass the first argument to the executed program with a leading -
|
|
||||||
# to make sure the shell executes as a login shell. Note that not all shells
|
|
||||||
# support exec -a so we use the below to try to detect such shells
|
|
||||||
shell_name=$(basename $login_shell)
|
|
||||||
if [ -z "$PIPESTATUS" ]; then
|
|
||||||
# the dash shell does not support exec -a and also does not define PIPESTATUS
|
|
||||||
execute_with_python
|
|
||||||
fi
|
|
||||||
exec -a "-$shell_name" $login_shell
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
PYTHON_SCRIPT = '''\
|
def is_kitten_cmdline(q: List[str]) -> bool:
|
||||||
#!/usr/bin/env python
|
if len(q) < 4:
|
||||||
from __future__ import print_function
|
return False
|
||||||
from tempfile import NamedTemporaryFile
|
if os.path.basename(q[0]).lower() != 'kitty':
|
||||||
import subprocess, os, sys, pwd, binascii, json
|
return False
|
||||||
|
return q[1:3] == ['+kitten', 'ssh'] or q[1:4] == ['+', 'kitten', 'ssh']
|
||||||
|
|
||||||
# macOS ships with an ancient version of tic that cannot read from stdin, so we
|
|
||||||
# create a temp file for it
|
def set_cwd_in_cmdline(cwd: str, argv: List[str]) -> None:
|
||||||
with NamedTemporaryFile() as tmp:
|
for i, arg in enumerate(tuple(argv)):
|
||||||
tmp.write(binascii.unhexlify('{terminfo}'))
|
if arg.startswith('--kitten=cwd'):
|
||||||
p = subprocess.Popen(['tic', '-x', '-o', os.path.expanduser('~/.terminfo'), tmp.name], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
argv[i] = f'--kitten=cwd={cwd}'
|
||||||
stdout, stderr = p.communicate()
|
return
|
||||||
if p.wait() != 0:
|
elif i > 0 and argv[i-1] == '--kitten' and (arg.startswith('cwd=') or arg.startswith('cwd ')):
|
||||||
getattr(sys.stderr, 'buffer', sys.stderr).write(stdout + stderr)
|
argv[i] = cwd
|
||||||
raise SystemExit('Failed to compile terminfo using tic')
|
return
|
||||||
command_to_execute = json.loads(binascii.unhexlify('{command_to_execute}'))
|
idx = argv.index('ssh')
|
||||||
try:
|
argv.insert(idx + 1, f'--kitten=cwd={cwd}')
|
||||||
shell_path = pwd.getpwuid(os.geteuid()).pw_shell or '/bin/sh'
|
|
||||||
except KeyError:
|
|
||||||
shell_path = '/bin/sh'
|
# See https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html
|
||||||
shell_name = '-' + os.path.basename(shell_path)
|
quote_pat = re.compile('([\\`"\n])')
|
||||||
if command_to_execute:
|
|
||||||
os.execlp(shell_path, shell_path, '-c', command_to_execute)
|
|
||||||
os.execlp(shell_path, shell_name)
|
def quote_env_val(x: str) -> str:
|
||||||
'''
|
x = quote_pat.sub(r'\\\1', x)
|
||||||
|
x = x.replace('$(', r'\$(') # prevent execution with $()
|
||||||
|
return f'"{x}"'
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_env(env: Dict[str, str], base_env: Dict[str, str]) -> bytes:
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
def a(k: str, val: str) -> None:
|
||||||
|
lines.append(f'export {shlex.quote(k)}={quote_env_val(val)}')
|
||||||
|
|
||||||
|
for k in sorted(env):
|
||||||
|
v = env[k]
|
||||||
|
if v == DELETE_ENV_VAR:
|
||||||
|
lines.append(f'unset {shlex.quote(k)}')
|
||||||
|
elif v == '_kitty_copy_env_var_':
|
||||||
|
q = base_env.get(k)
|
||||||
|
if q is not None:
|
||||||
|
a(k, q)
|
||||||
|
else:
|
||||||
|
a(k, v)
|
||||||
|
return '\n'.join(lines).encode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
@run_once
|
||||||
|
def kitty_opts() -> Options:
|
||||||
|
from kitty.cli import create_default_opts
|
||||||
|
return create_default_opts()
|
||||||
|
|
||||||
|
|
||||||
|
def make_tarfile(ssh_opts: SSHOptions, base_env: Dict[str, str], compression: str = 'gz') -> bytes:
|
||||||
|
|
||||||
|
def normalize_tarinfo(tarinfo: tarfile.TarInfo) -> tarfile.TarInfo:
|
||||||
|
tarinfo.uname = tarinfo.gname = ''
|
||||||
|
tarinfo.uid = tarinfo.gid = 0
|
||||||
|
return tarinfo
|
||||||
|
|
||||||
|
def add_data_as_file(tf: tarfile.TarFile, arcname: str, data: Union[str, bytes]) -> tarfile.TarInfo:
|
||||||
|
ans = tarfile.TarInfo(arcname)
|
||||||
|
ans.mtime = 0
|
||||||
|
ans.type = tarfile.REGTYPE
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = data.encode('utf-8')
|
||||||
|
ans.size = len(data)
|
||||||
|
normalize_tarinfo(ans)
|
||||||
|
tf.addfile(ans, io.BytesIO(data))
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def filter_from_globs(*pats: str) -> Callable[[tarfile.TarInfo], Optional[tarfile.TarInfo]]:
|
||||||
|
def filter(tarinfo: tarfile.TarInfo) -> Optional[tarfile.TarInfo]:
|
||||||
|
for junk_dir in ('.DS_Store', '__pycache__'):
|
||||||
|
for pat in (f'*/{junk_dir}', f'*/{junk_dir}/*'):
|
||||||
|
if fnmatch.fnmatch(tarinfo.name, pat):
|
||||||
|
return None
|
||||||
|
for pat in pats:
|
||||||
|
if fnmatch.fnmatch(tarinfo.name, pat):
|
||||||
|
return None
|
||||||
|
return normalize_tarinfo(tarinfo)
|
||||||
|
return filter
|
||||||
|
|
||||||
|
from kitty.shell_integration import get_effective_ksi_env_var
|
||||||
|
if ssh_opts.shell_integration == 'inherited':
|
||||||
|
ksi = get_effective_ksi_env_var(kitty_opts())
|
||||||
|
else:
|
||||||
|
from kitty.options.utils import shell_integration
|
||||||
|
ksi = get_effective_ksi_env_var(Options({'shell_integration': shell_integration(ssh_opts.shell_integration)}))
|
||||||
|
|
||||||
|
env = {
|
||||||
|
'TERM': os.environ.get('TERM') or kitty_opts().term,
|
||||||
|
'COLORTERM': 'truecolor',
|
||||||
|
}
|
||||||
|
for q in ('KITTY_WINDOW_ID', 'WINDOWID'):
|
||||||
|
val = os.environ.get(q)
|
||||||
|
if val is not None:
|
||||||
|
env[q] = val
|
||||||
|
env.update(ssh_opts.env)
|
||||||
|
env['KITTY_SHELL_INTEGRATION'] = ksi or DELETE_ENV_VAR
|
||||||
|
env['KITTY_SSH_KITTEN_DATA_DIR'] = ssh_opts.remote_dir
|
||||||
|
if ssh_opts.login_shell:
|
||||||
|
env['KITTY_LOGIN_SHELL'] = ssh_opts.login_shell
|
||||||
|
if ssh_opts.cwd:
|
||||||
|
env['KITTY_LOGIN_CWD'] = ssh_opts.cwd
|
||||||
|
if ssh_opts.remote_kitty != 'no':
|
||||||
|
env['KITTY_REMOTE'] = ssh_opts.remote_kitty
|
||||||
|
env_script = serialize_env(env, base_env)
|
||||||
|
buf = io.BytesIO()
|
||||||
|
with tarfile.open(mode=f'w:{compression}', fileobj=buf, encoding='utf-8') as tf:
|
||||||
|
rd = ssh_opts.remote_dir.rstrip('/')
|
||||||
|
for ci in ssh_opts.copy.values():
|
||||||
|
tf.add(ci.local_path, arcname=ci.arcname, filter=filter_from_globs(*ci.exclude_patterns))
|
||||||
|
add_data_as_file(tf, 'data.sh', env_script)
|
||||||
|
if compression == 'gz':
|
||||||
|
tf.add(f'{shell_integration_dir}/ssh/bootstrap-utils.sh', arcname='bootstrap-utils.sh', filter=normalize_tarinfo)
|
||||||
|
if ksi:
|
||||||
|
arcname = 'home/' + rd + '/shell-integration'
|
||||||
|
tf.add(shell_integration_dir, arcname=arcname, filter=filter_from_globs(
|
||||||
|
f'{arcname}/ssh/*', # bootstrap files are sent as command line args
|
||||||
|
f'{arcname}/zsh/kitty.zsh', # present for legacy compat not needed by ssh kitten
|
||||||
|
))
|
||||||
|
if ssh_opts.remote_kitty != 'no':
|
||||||
|
arcname = 'home/' + rd + '/kitty'
|
||||||
|
add_data_as_file(tf, arcname + '/version', str_version.encode('ascii'))
|
||||||
|
tf.add(shell_integration_dir + '/ssh/kitty', arcname=arcname + '/bin/kitty', filter=normalize_tarinfo)
|
||||||
|
tf.add(f'{terminfo_dir}/kitty.terminfo', arcname='home/.terminfo/kitty.terminfo', filter=normalize_tarinfo)
|
||||||
|
tf.add(glob.glob(f'{terminfo_dir}/*/xterm-kitty')[0], arcname='home/.terminfo/x/xterm-kitty', filter=normalize_tarinfo)
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def get_ssh_data(msg: str, request_id: str) -> Iterator[bytes]:
|
||||||
|
yield b'\nKITTY_DATA_START\n' # to discard leading data
|
||||||
|
try:
|
||||||
|
msg = standard_b64decode(msg).decode('utf-8')
|
||||||
|
md = dict(x.split('=', 1) for x in msg.split(':'))
|
||||||
|
pw = md['pw']
|
||||||
|
pwfilename = md['pwfile']
|
||||||
|
rq_id = md['id']
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
|
yield b'invalid ssh data request message\n'
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
with SharedMemory(pwfilename, readonly=True) as shm:
|
||||||
|
shm.unlink()
|
||||||
|
if shm.stats.st_uid != os.geteuid() or shm.stats.st_gid != os.getegid():
|
||||||
|
raise ValueError('Incorrect owner on pwfile')
|
||||||
|
mode = stat.S_IMODE(shm.stats.st_mode)
|
||||||
|
if mode != stat.S_IREAD:
|
||||||
|
raise ValueError('Incorrect permissions on pwfile')
|
||||||
|
env_data = json.loads(shm.read_data_with_size())
|
||||||
|
if pw != env_data['pw']:
|
||||||
|
raise ValueError('Incorrect password')
|
||||||
|
if rq_id != request_id:
|
||||||
|
raise ValueError('Incorrect request id')
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
yield f'{e}\n'.encode('utf-8')
|
||||||
|
else:
|
||||||
|
yield b'OK\n'
|
||||||
|
ssh_opts = SSHOptions(env_data['opts'])
|
||||||
|
ssh_opts.copy = {k: CopyInstruction(*v) for k, v in ssh_opts.copy.items()}
|
||||||
|
encoded_data = memoryview(env_data['tarfile'].encode('ascii'))
|
||||||
|
# macOS has a 255 byte limit on its input queue as per man stty.
|
||||||
|
# Not clear if that applies to canonical mode input as well, but
|
||||||
|
# better to be safe.
|
||||||
|
line_sz = 254
|
||||||
|
while encoded_data:
|
||||||
|
yield encoded_data[:line_sz]
|
||||||
|
yield b'\n'
|
||||||
|
encoded_data = encoded_data[line_sz:]
|
||||||
|
yield b'KITTY_DATA_END\n'
|
||||||
|
|
||||||
|
|
||||||
|
def safe_remove(x: str) -> None:
|
||||||
|
with suppress(OSError):
|
||||||
|
os.remove(x)
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_script(ans: str, replacements: Dict[str, str], script_type: str) -> str:
|
||||||
|
for k in ('EXEC_CMD', 'EXPORT_HOME_CMD'):
|
||||||
|
replacements[k] = replacements.get(k, '')
|
||||||
|
|
||||||
|
def sub(m: 're.Match[str]') -> str:
|
||||||
|
return replacements[m.group()]
|
||||||
|
|
||||||
|
return re.sub('|'.join(fr'\b{k}\b' for k in replacements), sub, ans)
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_exec_cmd(remote_args: Sequence[str], is_python: bool) -> str:
|
||||||
|
# ssh simply concatenates multiple commands using a space see
|
||||||
|
# line 1129 of ssh.c and on the remote side sshd.c runs the
|
||||||
|
# concatenated command as shell -c cmd
|
||||||
|
if is_python:
|
||||||
|
return standard_b64encode(' '.join(remote_args).encode('utf-8')).decode('ascii')
|
||||||
|
args = ' '.join(c.replace("'", """'"'"'""") for c in remote_args)
|
||||||
|
return f"""unset KITTY_SHELL_INTEGRATION; exec "$login_shell" -c '{args}'"""
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_export_home_cmd(ssh_opts: SSHOptions, is_python: bool) -> str:
|
||||||
|
home = ssh_opts.env.get('HOME')
|
||||||
|
if home == '_kitty_copy_env_var_':
|
||||||
|
home = os.environ.get('HOME')
|
||||||
|
if home:
|
||||||
|
if is_python:
|
||||||
|
return standard_b64encode(home.encode('utf-8')).decode('ascii')
|
||||||
|
else:
|
||||||
|
return f'export HOME={quote_env_val(home)}; cd "$HOME"'
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def bootstrap_script(
|
||||||
|
ssh_opts: SSHOptions, script_type: str = 'sh', remote_args: Sequence[str] = (),
|
||||||
|
test_script: str = '', request_id: Optional[str] = None, cli_hostname: str = '', cli_uname: str = '',
|
||||||
|
request_data: bool = False, echo_on: bool = True
|
||||||
|
) -> Tuple[str, Dict[str, str], SharedMemory]:
|
||||||
|
if request_id is None:
|
||||||
|
request_id = os.environ['KITTY_PID'] + '-' + os.environ['KITTY_WINDOW_ID']
|
||||||
|
is_python = script_type == 'py'
|
||||||
|
export_home_cmd = prepare_export_home_cmd(ssh_opts, is_python) if 'HOME' in ssh_opts.env else ''
|
||||||
|
exec_cmd = prepare_exec_cmd(remote_args, is_python) if remote_args else ''
|
||||||
|
with open(os.path.join(shell_integration_dir, 'ssh', f'bootstrap.{script_type}')) as f:
|
||||||
|
ans = f.read()
|
||||||
|
pw = secrets.token_hex()
|
||||||
|
tfd = standard_b64encode(make_tarfile(ssh_opts, dict(os.environ), 'gz' if script_type == 'sh' else 'bz2')).decode('ascii')
|
||||||
|
data = {'pw': pw, 'opts': ssh_opts._asdict(), 'hostname': cli_hostname, 'uname': cli_uname, 'tarfile': tfd}
|
||||||
|
db = json.dumps(data)
|
||||||
|
with SharedMemory(size=len(db) + SharedMemory.num_bytes_for_size, mode=stat.S_IREAD, prefix=f'kssh-{os.getpid()}-') as shm:
|
||||||
|
shm.write_data_with_size(db)
|
||||||
|
shm.flush()
|
||||||
|
atexit.register(shm.unlink)
|
||||||
|
sensitive_data = {'REQUEST_ID': request_id, 'DATA_PASSWORD': pw, 'PASSWORD_FILENAME': shm.name}
|
||||||
|
replacements = {
|
||||||
|
'EXPORT_HOME_CMD': export_home_cmd,
|
||||||
|
'EXEC_CMD': exec_cmd, 'TEST_SCRIPT': test_script,
|
||||||
|
'REQUEST_DATA': '1' if request_data else '0', 'ECHO_ON': '1' if echo_on else '0',
|
||||||
|
}
|
||||||
|
sd = replacements.copy()
|
||||||
|
if request_data:
|
||||||
|
sd.update(sensitive_data)
|
||||||
|
replacements.update(sensitive_data)
|
||||||
|
return prepare_script(ans, sd, script_type), replacements, shm
|
||||||
|
|
||||||
|
|
||||||
def get_ssh_cli() -> Tuple[Set[str], Set[str]]:
|
def get_ssh_cli() -> Tuple[Set[str], Set[str]]:
|
||||||
@@ -139,13 +309,22 @@ def get_ssh_cli() -> Tuple[Set[str], Set[str]]:
|
|||||||
return boolean_ssh_args, other_ssh_args
|
return boolean_ssh_args, other_ssh_args
|
||||||
|
|
||||||
|
|
||||||
def get_connection_data(args: List[str], cwd: str = '') -> Optional[SSHConnectionData]:
|
def is_extra_arg(arg: str, extra_args: Tuple[str, ...]) -> str:
|
||||||
|
for x in extra_args:
|
||||||
|
if arg == x or arg.startswith(f'{x}='):
|
||||||
|
return x
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def get_connection_data(args: List[str], cwd: str = '', extra_args: Tuple[str, ...] = ()) -> Optional[SSHConnectionData]:
|
||||||
boolean_ssh_args, other_ssh_args = get_ssh_cli()
|
boolean_ssh_args, other_ssh_args = get_ssh_cli()
|
||||||
port: Optional[int] = None
|
port: Optional[int] = None
|
||||||
expecting_port = expecting_identity = False
|
expecting_port = expecting_identity = False
|
||||||
expecting_option_val = False
|
expecting_option_val = False
|
||||||
expecting_hostname = False
|
expecting_hostname = False
|
||||||
|
expecting_extra_val = ''
|
||||||
host_name = identity_file = found_ssh = ''
|
host_name = identity_file = found_ssh = ''
|
||||||
|
found_extra_args: List[Tuple[str, str]] = []
|
||||||
|
|
||||||
for i, arg in enumerate(args):
|
for i, arg in enumerate(args):
|
||||||
if not found_ssh:
|
if not found_ssh:
|
||||||
@@ -173,6 +352,15 @@ def get_connection_data(args: List[str], cwd: str = '') -> Optional[SSHConnectio
|
|||||||
else:
|
else:
|
||||||
identity_file = arg[2:]
|
identity_file = arg[2:]
|
||||||
continue
|
continue
|
||||||
|
if arg.startswith('--') and extra_args:
|
||||||
|
matching_ex = is_extra_arg(arg, extra_args)
|
||||||
|
if matching_ex:
|
||||||
|
if '=' in arg:
|
||||||
|
exval = arg.partition('=')[-1]
|
||||||
|
found_extra_args.append((matching_ex, exval))
|
||||||
|
continue
|
||||||
|
expecting_extra_val = matching_ex
|
||||||
|
|
||||||
expecting_option_val = True
|
expecting_option_val = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -183,6 +371,9 @@ def get_connection_data(args: List[str], cwd: str = '') -> Optional[SSHConnectio
|
|||||||
expecting_port = False
|
expecting_port = False
|
||||||
elif expecting_identity:
|
elif expecting_identity:
|
||||||
identity_file = arg
|
identity_file = arg
|
||||||
|
elif expecting_extra_val:
|
||||||
|
found_extra_args.append((expecting_extra_val, arg))
|
||||||
|
expecting_extra_val = ''
|
||||||
expecting_option_val = False
|
expecting_option_val = False
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -190,13 +381,22 @@ def get_connection_data(args: List[str], cwd: str = '') -> Optional[SSHConnectio
|
|||||||
host_name = arg
|
host_name = arg
|
||||||
if not host_name:
|
if not host_name:
|
||||||
return None
|
return None
|
||||||
|
if host_name.startswith('ssh://'):
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
purl = urlparse(host_name)
|
||||||
|
if purl.hostname:
|
||||||
|
host_name = purl.hostname
|
||||||
|
if purl.username:
|
||||||
|
host_name = f'{purl.username}@{host_name}'
|
||||||
|
if port is None and purl.port:
|
||||||
|
port = purl.port
|
||||||
if identity_file:
|
if identity_file:
|
||||||
if not os.path.isabs(identity_file):
|
if not os.path.isabs(identity_file):
|
||||||
identity_file = os.path.expanduser(identity_file)
|
identity_file = os.path.expanduser(identity_file)
|
||||||
if not os.path.isabs(identity_file):
|
if not os.path.isabs(identity_file):
|
||||||
identity_file = os.path.normpath(os.path.join(cwd or os.getcwd(), identity_file))
|
identity_file = os.path.normpath(os.path.join(cwd or os.getcwd(), identity_file))
|
||||||
|
|
||||||
return SSHConnectionData(found_ssh, host_name, port, identity_file)
|
return SSHConnectionData(found_ssh, host_name, port, identity_file, tuple(found_extra_args))
|
||||||
|
|
||||||
|
|
||||||
class InvalidSSHArgs(ValueError):
|
class InvalidSSHArgs(ValueError):
|
||||||
@@ -208,17 +408,19 @@ class InvalidSSHArgs(ValueError):
|
|||||||
def system_exit(self) -> None:
|
def system_exit(self) -> None:
|
||||||
if self.err_msg:
|
if self.err_msg:
|
||||||
print(self.err_msg, file=sys.stderr)
|
print(self.err_msg, file=sys.stderr)
|
||||||
os.execlp('ssh', 'ssh')
|
os.execlp(ssh_exe(), 'ssh')
|
||||||
|
|
||||||
|
|
||||||
def parse_ssh_args(args: List[str]) -> Tuple[List[str], List[str], bool]:
|
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()
|
boolean_ssh_args, other_ssh_args = get_ssh_cli()
|
||||||
passthrough_args = {f'-{x}' for x in 'Nnf'}
|
passthrough_args = {f'-{x}' for x in 'NnfG'}
|
||||||
ssh_args = []
|
ssh_args = []
|
||||||
server_args: List[str] = []
|
server_args: List[str] = []
|
||||||
expecting_option_val = False
|
expecting_option_val = False
|
||||||
passthrough = False
|
passthrough = False
|
||||||
stop_option_processing = False
|
stop_option_processing = False
|
||||||
|
found_extra_args: List[str] = []
|
||||||
|
expecting_extra_val = ''
|
||||||
for argument in args:
|
for argument in args:
|
||||||
if len(server_args) > 1 or stop_option_processing:
|
if len(server_args) > 1 or stop_option_processing:
|
||||||
server_args.append(argument)
|
server_args.append(argument)
|
||||||
@@ -227,6 +429,16 @@ def parse_ssh_args(args: List[str]) -> Tuple[List[str], List[str], bool]:
|
|||||||
if argument == '--':
|
if argument == '--':
|
||||||
stop_option_processing = True
|
stop_option_processing = True
|
||||||
continue
|
continue
|
||||||
|
if extra_args:
|
||||||
|
matching_ex = is_extra_arg(argument, extra_args)
|
||||||
|
if matching_ex:
|
||||||
|
if '=' in argument:
|
||||||
|
exval = argument.partition('=')[-1]
|
||||||
|
found_extra_args.extend((matching_ex, exval))
|
||||||
|
else:
|
||||||
|
expecting_extra_val = matching_ex
|
||||||
|
expecting_option_val = True
|
||||||
|
continue
|
||||||
# could be a multi-character option
|
# could be a multi-character option
|
||||||
all_args = argument[1:]
|
all_args = argument[1:]
|
||||||
for i, arg in enumerate(all_args):
|
for i, arg in enumerate(all_args):
|
||||||
@@ -247,75 +459,264 @@ def parse_ssh_args(args: List[str]) -> Tuple[List[str], List[str], bool]:
|
|||||||
raise InvalidSSHArgs(f'unknown option -- {arg[1:]}')
|
raise InvalidSSHArgs(f'unknown option -- {arg[1:]}')
|
||||||
continue
|
continue
|
||||||
if expecting_option_val:
|
if expecting_option_val:
|
||||||
ssh_args.append(argument)
|
if expecting_extra_val:
|
||||||
|
found_extra_args.extend((expecting_extra_val, argument))
|
||||||
|
expecting_extra_val = ''
|
||||||
|
else:
|
||||||
|
ssh_args.append(argument)
|
||||||
expecting_option_val = False
|
expecting_option_val = False
|
||||||
continue
|
continue
|
||||||
server_args.append(argument)
|
server_args.append(argument)
|
||||||
if not server_args:
|
if not server_args:
|
||||||
raise InvalidSSHArgs()
|
raise InvalidSSHArgs()
|
||||||
return ssh_args, server_args, passthrough
|
return ssh_args, server_args, passthrough, tuple(found_extra_args)
|
||||||
|
|
||||||
|
|
||||||
def quote(x: str) -> str:
|
def wrap_bootstrap_script(sh_script: str, interpreter: str) -> List[str]:
|
||||||
# we have to escape unbalanced quotes and other unparsable
|
# sshd will execute the command we pass it by join all command line
|
||||||
# args as they will break the shell script
|
# arguments with a space and passing it as a single argument to the users
|
||||||
# But we do not want to quote things like * or 'echo hello'
|
# login shell with -c. If the user has a non POSIX login shell it might
|
||||||
# See https://github.com/kovidgoyal/kitty/issues/1787
|
# have different escaping semantics and syntax, so the command it should
|
||||||
try:
|
# execute has to be as simple as possible, basically of the form
|
||||||
shlex.split(x)
|
# interpreter -c unwrap_script escaped_bootstrap_script
|
||||||
except ValueError:
|
# The unwrap_script is responsible for unescaping the bootstrap script and
|
||||||
x = shlex.quote(x)
|
# executing it.
|
||||||
return x
|
q = os.path.basename(interpreter).lower()
|
||||||
|
is_python = 'python' in q
|
||||||
|
if is_python:
|
||||||
|
es = standard_b64encode(sh_script.encode('utf-8')).decode('ascii')
|
||||||
|
unwrap_script = '''"import base64, sys; eval(compile(base64.standard_b64decode(sys.argv[-1]), 'bootstrap.py', 'exec'))"'''
|
||||||
|
else:
|
||||||
|
# We cant rely on base64 being available on the remote system, so instead
|
||||||
|
# we quote the bootstrap script by replacing ' and \ with \v and \f
|
||||||
|
# also replacing \n and ! with \r and \b for tcsh
|
||||||
|
# finally surrounding with '
|
||||||
|
es = "'" + sh_script.replace("'", '\v').replace('\\', '\f').replace('\n', '\r').replace('!', '\b') + "'"
|
||||||
|
unwrap_script = r"""'eval "$(echo "$0" | tr \\\v\\\f\\\r\\\b \\\047\\\134\\\n\\\041)"' """
|
||||||
|
# exec is supported by all sh like shells, and fish and csh
|
||||||
|
return ['exec', interpreter, '-c', unwrap_script, es]
|
||||||
|
|
||||||
|
|
||||||
def get_posix_cmd(terminfo: str, remote_args: List[str]) -> List[str]:
|
def get_remote_command(
|
||||||
sh_script = SHELL_SCRIPT.replace('TERMINFO', terminfo, 1)
|
remote_args: List[str], ssh_opts: SSHOptions, cli_hostname: str = '', cli_uname: str = '',
|
||||||
command_to_execute = ''
|
echo_on: bool = True, request_data: bool = False
|
||||||
if remote_args:
|
) -> Tuple[List[str], Dict[str, str], str]:
|
||||||
# ssh simply concatenates multiple commands using a space see
|
interpreter = ssh_opts.interpreter
|
||||||
# line 1129 of ssh.c and on the remote side sshd.c runs the
|
q = os.path.basename(interpreter).lower()
|
||||||
# concatenated command as shell -c cmd
|
is_python = 'python' in q
|
||||||
args = [c.replace("'", """'"'"'""") for c in remote_args]
|
sh_script, replacements, shm = bootstrap_script(
|
||||||
command_to_execute = "exec $login_shell -c '{}'".format(' '.join(args))
|
ssh_opts, script_type='py' if is_python else 'sh', remote_args=remote_args,
|
||||||
sh_script = sh_script.replace('EXEC_CMD', command_to_execute)
|
cli_hostname=cli_hostname, cli_uname=cli_uname, echo_on=echo_on, request_data=request_data)
|
||||||
return [f'sh -c {shlex.quote(sh_script)}']
|
return wrap_bootstrap_script(sh_script, interpreter), replacements, shm.name
|
||||||
|
|
||||||
|
|
||||||
def get_python_cmd(terminfo: str, command_to_execute: List[str]) -> List[str]:
|
def connection_sharing_args(opts: SSHOptions, kitty_pid: int) -> List[str]:
|
||||||
import json
|
rd = runtime_dir()
|
||||||
script = PYTHON_SCRIPT.format(
|
# Bloody OpenSSH generates a 40 char hash and in creating the socket
|
||||||
terminfo=terminfo.encode('utf-8').hex(),
|
# appends a 27 char temp suffix to it. Socket max path length is approx
|
||||||
command_to_execute=json.dumps(' '.join(command_to_execute)).encode('utf-8').hex()
|
# ~104 chars. macOS has no system runtime dir so we use a cache dir in
|
||||||
)
|
# /Users/WHY_DOES_ANYONE_USE_MACOS/Library/Caches/APPLE_ARE_IDIOTIC
|
||||||
return [f'python -c "{script}"']
|
if len(rd) > 35 and os.path.isdir('/tmp'):
|
||||||
|
idiotic_design = f'/tmp/kssh-rdir-{os.getuid()}'
|
||||||
|
try:
|
||||||
|
os.symlink(rd, idiotic_design)
|
||||||
|
except FileExistsError:
|
||||||
|
try:
|
||||||
|
dest = os.readlink(idiotic_design)
|
||||||
|
except OSError as e:
|
||||||
|
raise ValueError(f'The {idiotic_design} symlink could not be created as something with that name exists already') from e
|
||||||
|
else:
|
||||||
|
if dest != rd:
|
||||||
|
with tempfile.TemporaryDirectory(dir='/tmp') as tdir:
|
||||||
|
tlink = os.path.join(tdir, 'sigh')
|
||||||
|
os.symlink(rd, tlink)
|
||||||
|
os.rename(tlink, idiotic_design)
|
||||||
|
rd = idiotic_design
|
||||||
|
|
||||||
|
cp = os.path.join(rd, ssh_control_master_template.format(kitty_pid=kitty_pid, ssh_placeholder='%C'))
|
||||||
|
ans: List[str] = [
|
||||||
|
'-o', 'ControlMaster=auto',
|
||||||
|
'-o', f'ControlPath={cp}',
|
||||||
|
'-o', 'ControlPersist=yes',
|
||||||
|
'-o', 'ServerAliveInterval=60',
|
||||||
|
'-o', 'ServerAliveCountMax=5',
|
||||||
|
'-o', 'TCPKeepAlive=no',
|
||||||
|
]
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def restore_terminal_state() -> Iterator[bool]:
|
||||||
|
with open(os.ctermid()) as f:
|
||||||
|
val = termios.tcgetattr(f.fileno())
|
||||||
|
try:
|
||||||
|
yield bool(val[3] & termios.ECHO)
|
||||||
|
finally:
|
||||||
|
termios.tcsetattr(f.fileno(), termios.TCSAFLUSH, val)
|
||||||
|
|
||||||
|
|
||||||
|
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\\'
|
||||||
|
|
||||||
|
|
||||||
|
@run_once
|
||||||
|
def ssh_version() -> Tuple[int, int]:
|
||||||
|
o = subprocess.check_output([ssh_exe(), '-V'], stderr=subprocess.STDOUT).decode()
|
||||||
|
m = re.match(r'OpenSSH_(\d+).(\d+)', o)
|
||||||
|
if m is None:
|
||||||
|
raise ValueError(f'Invalid version string for OpenSSH: {o}')
|
||||||
|
return int(m.group(1)), int(m.group(2))
|
||||||
|
|
||||||
|
|
||||||
|
@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())
|
||||||
|
tty.write(dcs_to_kitty(data_request))
|
||||||
|
tty.flush()
|
||||||
|
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:
|
||||||
|
rd, wr, err = select([tty_fd], [], [tty_fd], max(0, give_up_at - time.monotonic()))
|
||||||
|
if err or not rd:
|
||||||
|
break
|
||||||
|
q = os.read(tty_fd, io.DEFAULT_BUFFER_SIZE)
|
||||||
|
if not q:
|
||||||
|
break
|
||||||
|
data += q
|
||||||
|
|
||||||
|
|
||||||
|
def change_colors(color_scheme: str) -> bool:
|
||||||
|
if not color_scheme:
|
||||||
|
return False
|
||||||
|
from kittens.themes.collection import load_themes, NoCacheFound, text_as_opts
|
||||||
|
from kittens.themes.main import colors_as_escape_codes
|
||||||
|
if color_scheme.endswith('.conf'):
|
||||||
|
conf_file = resolve_abs_or_config_path(color_scheme)
|
||||||
|
try:
|
||||||
|
with open(conf_file) as f:
|
||||||
|
opts = text_as_opts(f.read())
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise SystemExit(f'Failed to find the color conf file: {expandvars(conf_file)}')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
themes = load_themes(-1)
|
||||||
|
except NoCacheFound:
|
||||||
|
themes = load_themes()
|
||||||
|
cs = expandvars(color_scheme)
|
||||||
|
try:
|
||||||
|
theme = themes[cs]
|
||||||
|
except KeyError:
|
||||||
|
raise SystemExit(f'Failed to find the color theme: {cs}')
|
||||||
|
opts = theme.kitty_opts
|
||||||
|
raw = colors_as_escape_codes(opts)
|
||||||
|
print(save_colors(), sep='', end=raw, flush=True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def run_ssh(ssh_args: List[str], server_args: List[str], found_extra_args: Tuple[str, ...]) -> NoReturn:
|
||||||
|
cmd = [ssh_exe()] + ssh_args
|
||||||
|
hostname, remote_args = server_args[0], server_args[1:]
|
||||||
|
if not remote_args:
|
||||||
|
cmd.append('-t')
|
||||||
|
insertion_point = len(cmd)
|
||||||
|
cmd.append('--')
|
||||||
|
cmd.append(hostname)
|
||||||
|
uname = getuser()
|
||||||
|
if hostname.startswith('ssh://'):
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
purl = urlparse(hostname)
|
||||||
|
hostname_for_match = purl.hostname or hostname[6:].split('/', 1)[0]
|
||||||
|
uname = purl.username or uname
|
||||||
|
elif '@' in hostname and hostname[0] != '@':
|
||||||
|
uname, hostname_for_match = hostname.split('@', 1)
|
||||||
|
else:
|
||||||
|
hostname_for_match = hostname
|
||||||
|
hostname_for_match = hostname_for_match.split('@', 1)[-1].split(':', 1)[0]
|
||||||
|
overrides = []
|
||||||
|
pat = re.compile(r'^([a-zA-Z0-9_]+)[ \t]*=')
|
||||||
|
for i, a in enumerate(found_extra_args):
|
||||||
|
if i % 2 == 1:
|
||||||
|
aq = pat.sub(r'\1 ', a.lstrip())
|
||||||
|
if aq.split(maxsplit=1)[0] != 'hostname':
|
||||||
|
overrides.append(aq)
|
||||||
|
if overrides:
|
||||||
|
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']))
|
||||||
|
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:
|
||||||
|
sentinel = os.path.join(cache_dir(), 'openssh-is-new-enough-for-askpass')
|
||||||
|
sentinel_exists = os.path.exists(sentinel)
|
||||||
|
if sentinel_exists or ssh_version() >= (8, 4):
|
||||||
|
if not sentinel_exists:
|
||||||
|
open(sentinel, 'w').close()
|
||||||
|
# SSH_ASKPASS_REQUIRE was introduced in 8.4 release on 2020-09-27
|
||||||
|
need_to_request_data = False
|
||||||
|
os.environ['SSH_ASKPASS_REQUIRE'] = 'force'
|
||||||
|
os.environ['SSH_ASKPASS'] = os.path.join(shell_integration_dir, 'ssh', 'askpass.py')
|
||||||
|
if need_to_request_data and host_opts.share_connections:
|
||||||
|
cp = subprocess.run(cmd[:1] + ['-O', 'check'] + cmd[1:], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
if cp.returncode == 0:
|
||||||
|
# we will use the master connection so SSH does not need to use the tty
|
||||||
|
need_to_request_data = False
|
||||||
|
with restore_terminal_state() as echo_on:
|
||||||
|
rcmd, replacements, shm_name = get_remote_command(
|
||||||
|
remote_args, host_opts, hostname_for_match, uname, echo_on, request_data=need_to_request_data)
|
||||||
|
cmd += rcmd
|
||||||
|
colors_changed = change_colors(host_opts.color_scheme)
|
||||||
|
try:
|
||||||
|
p = subprocess.Popen(cmd)
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise SystemExit('Could not find the ssh executable, is it in your PATH?')
|
||||||
|
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)
|
||||||
|
finally:
|
||||||
|
if colors_changed:
|
||||||
|
print(end=restore_colors(), flush=True)
|
||||||
|
|
||||||
|
|
||||||
def main(args: List[str]) -> NoReturn:
|
def main(args: List[str]) -> NoReturn:
|
||||||
args = args[1:]
|
args = args[1:]
|
||||||
use_posix = True
|
|
||||||
if args and args[0] == 'use-python':
|
if args and args[0] == 'use-python':
|
||||||
args = args[1:]
|
args = args[1:] # backwards compat from when we had a python implementation
|
||||||
use_posix = False
|
|
||||||
try:
|
try:
|
||||||
ssh_args, server_args, passthrough = parse_ssh_args(args)
|
ssh_args, server_args, passthrough, found_extra_args = parse_ssh_args(args, extra_args=('--kitten',))
|
||||||
except InvalidSSHArgs as e:
|
except InvalidSSHArgs as e:
|
||||||
e.system_exit()
|
e.system_exit()
|
||||||
cmd = ['ssh'] + 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:
|
if passthrough:
|
||||||
cmd += server_args
|
raise SystemExit('The SSH kitten is meant for interactive use via SSH only')
|
||||||
else:
|
if not sys.stdin.isatty():
|
||||||
hostname, remote_args = server_args[0], server_args[1:]
|
raise SystemExit('The SSH kitten is meant for interactive use only, STDIN must be a terminal')
|
||||||
if not remote_args:
|
run_ssh(ssh_args, server_args, found_extra_args)
|
||||||
cmd.append('-t')
|
|
||||||
cmd.append('--')
|
|
||||||
cmd.append(hostname)
|
|
||||||
terminfo = subprocess.check_output(['infocmp', '-a']).decode('utf-8')
|
|
||||||
f = get_posix_cmd if use_posix else get_python_cmd
|
|
||||||
cmd += f(terminfo, remote_args)
|
|
||||||
os.execvp('ssh', cmd)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main(sys.argv)
|
main(sys.argv)
|
||||||
elif __name__ == '__completer__':
|
elif __name__ == '__completer__':
|
||||||
setattr(sys, 'kitten_completer', complete)
|
setattr(sys, 'kitten_completer', complete)
|
||||||
|
elif __name__ == '__conf__':
|
||||||
|
from .options.definition import definition
|
||||||
|
sys.options_definition = definition # type: ignore
|
||||||
|
|||||||
0
kittens/ssh/options/__init__.py
Normal file
0
kittens/ssh/options/__init__.py
Normal file
150
kittens/ssh/options/definition.py
Normal file
150
kittens/ssh/options/definition.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
# After editing this file run ./gen-config.py to apply the changes
|
||||||
|
|
||||||
|
from kitty.conf.types import Definition
|
||||||
|
|
||||||
|
|
||||||
|
copy_message = '''\
|
||||||
|
Copy files and directories from local to remote hosts. The specified files are
|
||||||
|
assumed to be relative to the HOME directory and copied to the HOME on the
|
||||||
|
remote. Directories are copied recursively. If absolute paths are used, they are
|
||||||
|
copied as is.'''
|
||||||
|
|
||||||
|
definition = Definition(
|
||||||
|
'kittens.ssh',
|
||||||
|
)
|
||||||
|
|
||||||
|
agr = definition.add_group
|
||||||
|
egr = definition.end_group
|
||||||
|
opt = definition.add_option
|
||||||
|
|
||||||
|
agr('bootstrap', 'Host bootstrap configuration') # {{{
|
||||||
|
|
||||||
|
opt('hostname', '*', option_type='hostname', long_text='''
|
||||||
|
The hostname that the following options apply to. A glob pattern to match multiple
|
||||||
|
hosts can be used. Multiple hostnames can also be specified separated by spaces.
|
||||||
|
The hostname can include an optional username in the form :code:`user@host`.
|
||||||
|
When not specified options apply to all hosts, until the
|
||||||
|
first hostname specification is found. Note that matching of hostname is done against
|
||||||
|
the name you specify on the command line to connect to the remote host.
|
||||||
|
If you wish to include the same basic configuration for many
|
||||||
|
different hosts, you can do so with the :ref:`include <include>` directive.
|
||||||
|
''')
|
||||||
|
|
||||||
|
opt('interpreter', 'sh', long_text='''
|
||||||
|
The interpreter to use on the remote host. Must be either a POSIX complaint shell
|
||||||
|
or a python executable. If the default sh is not available or broken, using
|
||||||
|
an alternate interpreter can be useful.
|
||||||
|
''')
|
||||||
|
|
||||||
|
opt('remote_dir', '.local/share/kitty-ssh-kitten', option_type='relative_dir', long_text='''
|
||||||
|
The location on the remote host where the files needed for this kitten
|
||||||
|
are installed. The location is relative to the HOME directory. Absolute paths or paths
|
||||||
|
that resolve to a location outside the HOME are not allowed.
|
||||||
|
''')
|
||||||
|
|
||||||
|
opt('+copy', '', option_type='copy', add_to_default=False, long_text=f'''
|
||||||
|
{copy_message} For example::
|
||||||
|
|
||||||
|
copy .vimrc .zshrc .config/some-dir
|
||||||
|
|
||||||
|
If a file should be copied to some other destination on the remote host,
|
||||||
|
use :code:`--dest`::
|
||||||
|
|
||||||
|
copy --dest some-other-name some-file
|
||||||
|
|
||||||
|
Glob patterns can be specified to copy multiple files, with :code:`--glob`::
|
||||||
|
|
||||||
|
copy --glob images/*.png
|
||||||
|
|
||||||
|
Files can be excluded when copying with :code:`--exclude`::
|
||||||
|
|
||||||
|
copy --glob --exclude *.jpg --exclude *.bmp images/*
|
||||||
|
|
||||||
|
Files whose remote name matches the exclude pattern will not be copied.
|
||||||
|
For more details, see :ref:`ssh_copy_command`.
|
||||||
|
''')
|
||||||
|
egr() # }}}
|
||||||
|
|
||||||
|
agr('shell', 'Login shell environment') # {{{
|
||||||
|
|
||||||
|
opt('shell_integration', 'inherited', long_text='''
|
||||||
|
Control the shell integration on the remote host. See :ref:`shell_integration`
|
||||||
|
for details on how this setting works. The special value :code:`inherited` means
|
||||||
|
use the setting from :file:`kitty.conf`. This setting is useful for overriding
|
||||||
|
integration on a per-host basis.
|
||||||
|
''')
|
||||||
|
|
||||||
|
opt('login_shell', '', long_text='''
|
||||||
|
The login shell to execute on the remote host. By default, the remote user account's
|
||||||
|
login shell is used.
|
||||||
|
''')
|
||||||
|
|
||||||
|
opt('+env', '', option_type='env', add_to_default=False, long_text='''
|
||||||
|
Specify environment variables to set on the remote host. Note that
|
||||||
|
environment variables can refer to each other, so if you use::
|
||||||
|
|
||||||
|
env MYVAR1=a
|
||||||
|
env MYVAR2=$MYVAR1/$HOME/b
|
||||||
|
|
||||||
|
The value of MYVAR2 will be :code:`a/<path to home directory>/b`. Using
|
||||||
|
:code:`VAR=` will set it to the empty string and using just :code:`VAR`
|
||||||
|
will delete the variable from the child process' environment. The definitions
|
||||||
|
are processed alphabetically. The special value :code:`_kitty_copy_env_var_`
|
||||||
|
will cause the value of the variable to be copied from the local environment.
|
||||||
|
''')
|
||||||
|
|
||||||
|
opt('cwd', '', long_text='''
|
||||||
|
The working directory on the remote host to change to. Env vars in this
|
||||||
|
value are expanded. The default is empty so no changing is done, which
|
||||||
|
usually means the home directory is used.
|
||||||
|
''')
|
||||||
|
|
||||||
|
opt('color_scheme', '', long_text='''
|
||||||
|
Specify a color scheme to use when connecting to the remote host. If the
|
||||||
|
color_scheme ends with :code:`.conf` it is assumed to be the name of a config
|
||||||
|
file to load from the kitty config directory, otherwise it is assumed to be the
|
||||||
|
name of a color theme to load via the themes kitten. Note that only colors
|
||||||
|
applying to the text/background are changed, other config settings in the .conf
|
||||||
|
files/themes are ignored.
|
||||||
|
''')
|
||||||
|
|
||||||
|
opt('remote_kitty', 'if-needed', choices=('if-needed', 'no', 'yes'), long_text='''
|
||||||
|
Make kitty available on the remote server. Useful to run kittens such as the
|
||||||
|
icat kitten to display images or the transfer file kitten to transfer files.
|
||||||
|
Only works if the remote server has an architecture for which pre-compiled
|
||||||
|
kitty binaries are available. Note that kitty is not actually copied to the
|
||||||
|
remote server, instead a small bootstrap script is copied which will download
|
||||||
|
and run kitty when kitty is first executed on the remote server. A value of
|
||||||
|
:code:`if-needed` means kitty is installed only if not already present in the
|
||||||
|
system-wide PATH. A value of :code:`yes` means that kitty is installed even if
|
||||||
|
already present, and the installed kitty takes precedence. Finally, :code:`no`
|
||||||
|
means no kitty is installed on the remote machine. The installed kitty
|
||||||
|
can be updated by running: :code:`kitty +update-kitty` on the remote machine.
|
||||||
|
''')
|
||||||
|
egr() # }}}
|
||||||
|
|
||||||
|
agr('ssh', 'SSH configuration') # {{{
|
||||||
|
|
||||||
|
opt('share_connections', 'yes', option_type='to_bool', long_text='''
|
||||||
|
Within a single kitty instance, all connections to a particular server can be
|
||||||
|
shared. This reduces startup latency for subsequent connections and means that you have
|
||||||
|
to enter the password only once. Under the hood, it uses SSH ControlMasters and
|
||||||
|
these are automatically cleaned up by kitty when it quits.
|
||||||
|
''')
|
||||||
|
|
||||||
|
opt('askpass', 'unless-set', choices=('unless-set', 'ssh', 'native'), long_text='''
|
||||||
|
Control the program SSH uses to ask for passwords or confirmation of host keys
|
||||||
|
etc. The default is to use kitty's native askpass, unless the SSH_ASKPASS
|
||||||
|
environment variable is set. Set it to :code:`ssh` to not interfere with the
|
||||||
|
normal ssh askpass mechanism at all, which typically means that ssh will prompt
|
||||||
|
at the terminal. Set it to :code:`native` to always use kitty's native,
|
||||||
|
built-in askpass implementation. Note that not using the kitty askpass implementation
|
||||||
|
means that SSH might need to use the terminal before the connection is established
|
||||||
|
so the kitten cannot use the terminal to send data without an extra roundtrip,
|
||||||
|
adding to initial connection latency.
|
||||||
|
''')
|
||||||
|
egr() # }}}
|
||||||
89
kittens/ssh/options/parse.py
Normal file
89
kittens/ssh/options/parse.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# generated by gen-config.py DO NOT edit
|
||||||
|
|
||||||
|
import typing
|
||||||
|
from kittens.ssh.options.utils import copy, env, hostname, relative_dir
|
||||||
|
from kitty.conf.utils import merge_dicts, to_bool
|
||||||
|
|
||||||
|
|
||||||
|
class Parser:
|
||||||
|
|
||||||
|
def askpass(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
val = val.lower()
|
||||||
|
if val not in self.choices_for_askpass:
|
||||||
|
raise ValueError(f"The value {val} is not a valid choice for askpass")
|
||||||
|
ans["askpass"] = val
|
||||||
|
|
||||||
|
choices_for_askpass = frozenset(('unless-set', 'ssh', 'native'))
|
||||||
|
|
||||||
|
def color_scheme(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['color_scheme'] = str(val)
|
||||||
|
|
||||||
|
def copy(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
for k, v in copy(val, ans["copy"]):
|
||||||
|
ans["copy"][k] = v
|
||||||
|
|
||||||
|
def cwd(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['cwd'] = str(val)
|
||||||
|
|
||||||
|
def env(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
for k, v in env(val, ans["env"]):
|
||||||
|
ans["env"][k] = v
|
||||||
|
|
||||||
|
def hostname(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
hostname(val, ans)
|
||||||
|
|
||||||
|
def interpreter(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['interpreter'] = str(val)
|
||||||
|
|
||||||
|
def login_shell(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['login_shell'] = str(val)
|
||||||
|
|
||||||
|
def remote_dir(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['remote_dir'] = relative_dir(val)
|
||||||
|
|
||||||
|
def remote_kitty(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
val = val.lower()
|
||||||
|
if val not in self.choices_for_remote_kitty:
|
||||||
|
raise ValueError(f"The value {val} is not a valid choice for remote_kitty")
|
||||||
|
ans["remote_kitty"] = val
|
||||||
|
|
||||||
|
choices_for_remote_kitty = frozenset(('if-needed', 'no', 'yes'))
|
||||||
|
|
||||||
|
def share_connections(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['share_connections'] = to_bool(val)
|
||||||
|
|
||||||
|
def shell_integration(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['shell_integration'] = str(val)
|
||||||
|
|
||||||
|
|
||||||
|
def create_result_dict() -> typing.Dict[str, typing.Any]:
|
||||||
|
return {
|
||||||
|
'copy': {},
|
||||||
|
'env': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
actions: typing.FrozenSet[str] = frozenset(())
|
||||||
|
|
||||||
|
|
||||||
|
def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:
|
||||||
|
ans = {}
|
||||||
|
for k, v in defaults.items():
|
||||||
|
if isinstance(v, dict):
|
||||||
|
ans[k] = merge_dicts(v, vals.get(k, {}))
|
||||||
|
elif k in actions:
|
||||||
|
ans[k] = v + vals.get(k, [])
|
||||||
|
else:
|
||||||
|
ans[k] = vals.get(k, v)
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
parser = Parser()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_conf_item(key: str, val: str, ans: typing.Dict[str, typing.Any]) -> bool:
|
||||||
|
func = getattr(parser, key, None)
|
||||||
|
if func is not None:
|
||||||
|
func(val, ans)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
92
kittens/ssh/options/types.py
Normal file
92
kittens/ssh/options/types.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# generated by gen-config.py DO NOT edit
|
||||||
|
|
||||||
|
import typing
|
||||||
|
import kittens.ssh.copy
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
choices_for_askpass = typing.Literal['unless-set', 'ssh', 'native']
|
||||||
|
choices_for_remote_kitty = typing.Literal['if-needed', 'no', 'yes']
|
||||||
|
else:
|
||||||
|
choices_for_askpass = str
|
||||||
|
choices_for_remote_kitty = str
|
||||||
|
|
||||||
|
option_names = ( # {{{
|
||||||
|
'askpass',
|
||||||
|
'color_scheme',
|
||||||
|
'copy',
|
||||||
|
'cwd',
|
||||||
|
'env',
|
||||||
|
'hostname',
|
||||||
|
'interpreter',
|
||||||
|
'login_shell',
|
||||||
|
'remote_dir',
|
||||||
|
'remote_kitty',
|
||||||
|
'share_connections',
|
||||||
|
'shell_integration') # }}}
|
||||||
|
|
||||||
|
|
||||||
|
class Options:
|
||||||
|
askpass: choices_for_askpass = 'unless-set'
|
||||||
|
color_scheme: str = ''
|
||||||
|
cwd: str = ''
|
||||||
|
hostname: str = '*'
|
||||||
|
interpreter: str = 'sh'
|
||||||
|
login_shell: str = ''
|
||||||
|
remote_dir: str = '.local/share/kitty-ssh-kitten'
|
||||||
|
remote_kitty: choices_for_remote_kitty = 'if-needed'
|
||||||
|
share_connections: bool = True
|
||||||
|
shell_integration: str = 'inherited'
|
||||||
|
copy: typing.Dict[str, kittens.ssh.copy.CopyInstruction] = {}
|
||||||
|
env: typing.Dict[str, str] = {}
|
||||||
|
config_paths: typing.Tuple[str, ...] = ()
|
||||||
|
config_overrides: typing.Tuple[str, ...] = ()
|
||||||
|
|
||||||
|
def __init__(self, options_dict: typing.Optional[typing.Dict[str, typing.Any]] = None) -> None:
|
||||||
|
if options_dict is not None:
|
||||||
|
null = object()
|
||||||
|
for key in option_names:
|
||||||
|
val = options_dict.get(key, null)
|
||||||
|
if val is not null:
|
||||||
|
setattr(self, key, val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _fields(self) -> typing.Tuple[str, ...]:
|
||||||
|
return option_names
|
||||||
|
|
||||||
|
def __iter__(self) -> typing.Iterator[str]:
|
||||||
|
return iter(self._fields)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self._fields)
|
||||||
|
|
||||||
|
def _copy_of_val(self, name: str) -> typing.Any:
|
||||||
|
ans = getattr(self, name)
|
||||||
|
if isinstance(ans, dict):
|
||||||
|
ans = ans.copy()
|
||||||
|
elif isinstance(ans, list):
|
||||||
|
ans = ans[:]
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def _asdict(self) -> typing.Dict[str, typing.Any]:
|
||||||
|
return {k: self._copy_of_val(k) for k in self}
|
||||||
|
|
||||||
|
def _replace(self, **kw: typing.Any) -> "Options":
|
||||||
|
ans = Options()
|
||||||
|
for name in self:
|
||||||
|
setattr(ans, name, self._copy_of_val(name))
|
||||||
|
for name, val in kw.items():
|
||||||
|
setattr(ans, name, val)
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def __getitem__(self, key: typing.Union[int, str]) -> typing.Any:
|
||||||
|
k = option_names[key] if isinstance(key, int) else key
|
||||||
|
try:
|
||||||
|
return getattr(self, k)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
raise KeyError(f"No option named: {k}")
|
||||||
|
|
||||||
|
|
||||||
|
defaults = Options()
|
||||||
|
defaults.copy = {}
|
||||||
|
defaults.env = {}
|
||||||
67
kittens/ssh/options/utils.py
Normal file
67
kittens/ssh/options/utils.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
import posixpath
|
||||||
|
from typing import Any, Dict, Iterable, Optional, Tuple
|
||||||
|
|
||||||
|
from ..copy import CopyInstruction, parse_copy_instructions
|
||||||
|
|
||||||
|
DELETE_ENV_VAR = '_delete_this_env_var_'
|
||||||
|
|
||||||
|
|
||||||
|
def relative_dir(val: str) -> str:
|
||||||
|
if posixpath.isabs(val):
|
||||||
|
raise ValueError(f'Absolute paths not allowed. {val} is invalid.')
|
||||||
|
base = '/ffjdg'
|
||||||
|
q = posixpath.normpath(posixpath.join(base, val))
|
||||||
|
if q == base or not q.startswith(base):
|
||||||
|
raise ValueError(f'Paths that escape their parent dir are not allowed. {val} is not valid')
|
||||||
|
return posixpath.normpath(val)
|
||||||
|
|
||||||
|
|
||||||
|
def env(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, str]]:
|
||||||
|
val = val.strip()
|
||||||
|
if val:
|
||||||
|
if '=' in val:
|
||||||
|
key, v = val.split('=', 1)
|
||||||
|
key, v = key.strip(), v.strip()
|
||||||
|
if key:
|
||||||
|
yield key, v
|
||||||
|
else:
|
||||||
|
yield val, DELETE_ENV_VAR
|
||||||
|
|
||||||
|
|
||||||
|
def copy(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, CopyInstruction]]:
|
||||||
|
yield from parse_copy_instructions(val, current_val)
|
||||||
|
|
||||||
|
|
||||||
|
def init_results_dict(ans: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
ans['hostname'] = '*'
|
||||||
|
ans['per_host_dicts'] = {}
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
def get_per_hosts_dict(results_dict: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
|
||||||
|
ans: Dict[str, Dict[str, Any]] = results_dict.get('per_host_dicts', {}).copy()
|
||||||
|
h = results_dict['hostname']
|
||||||
|
hd = {k: v for k, v in results_dict.items() if k != 'per_host_dicts'}
|
||||||
|
ans[h] = hd
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
first_seen_positions: Dict[str, int] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def hostname(val: str, dict_with_parse_results: Optional[Dict[str, Any]] = None) -> str:
|
||||||
|
if dict_with_parse_results is not None:
|
||||||
|
ch = dict_with_parse_results['hostname']
|
||||||
|
if val != ch:
|
||||||
|
from .parse import create_result_dict
|
||||||
|
phd = get_per_hosts_dict(dict_with_parse_results)
|
||||||
|
dict_with_parse_results.clear()
|
||||||
|
dict_with_parse_results.update(phd.pop(val, create_result_dict()))
|
||||||
|
dict_with_parse_results['per_host_dicts'] = phd
|
||||||
|
dict_with_parse_results['hostname'] = val
|
||||||
|
if val not in first_seen_positions:
|
||||||
|
first_seen_positions[val] = len(first_seen_positions)
|
||||||
|
return val
|
||||||
@@ -36,7 +36,7 @@ def patch_conf(raw: str, theme_name: str) -> str:
|
|||||||
nraw = raw + addition
|
nraw = raw + addition
|
||||||
# comment out all existing color definitions
|
# comment out all existing color definitions
|
||||||
color_conf_items = ( # {{{
|
color_conf_items = ( # {{{
|
||||||
# generated by gen_config.py do not EDIT
|
# generated by gen-config.py DO NOT edit
|
||||||
# ALL_COLORS_START
|
# ALL_COLORS_START
|
||||||
'active_border_color',
|
'active_border_color',
|
||||||
'active_tab_background',
|
'active_tab_background',
|
||||||
@@ -483,6 +483,10 @@ def update_theme_file(path: str) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def text_as_opts(text: str) -> KittyOptions:
|
||||||
|
return KittyOptions(options_dict=parse_config(text.splitlines()))
|
||||||
|
|
||||||
|
|
||||||
class Theme:
|
class Theme:
|
||||||
name: str = ''
|
name: str = ''
|
||||||
author: str = ''
|
author: str = ''
|
||||||
@@ -516,7 +520,7 @@ class Theme:
|
|||||||
@property
|
@property
|
||||||
def kitty_opts(self) -> KittyOptions:
|
def kitty_opts(self) -> KittyOptions:
|
||||||
if self._opts is None:
|
if self._opts is None:
|
||||||
self._opts = KittyOptions(options_dict=parse_config(self.raw.splitlines()))
|
self._opts = text_as_opts(self.raw)
|
||||||
return self._opts
|
return self._opts
|
||||||
|
|
||||||
def save_in_dir(self, dirpath: str) -> None:
|
def save_in_dir(self, dirpath: str) -> None:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from typing import (
|
|||||||
from kitty.cli import create_default_opts, parse_args
|
from kitty.cli import create_default_opts, parse_args
|
||||||
from kitty.cli_stub import ThemesCLIOptions
|
from kitty.cli_stub import ThemesCLIOptions
|
||||||
from kitty.config import cached_values_for
|
from kitty.config import cached_values_for
|
||||||
|
from kitty.options.types import Options as KittyOptions
|
||||||
from kitty.constants import config_dir
|
from kitty.constants import config_dir
|
||||||
from kitty.fast_data_types import truncate_point_for_length, wcswidth
|
from kitty.fast_data_types import truncate_point_for_length, wcswidth
|
||||||
from kitty.rgb import color_as_sharp, color_from_int
|
from kitty.rgb import color_as_sharp, color_from_int
|
||||||
@@ -23,7 +24,7 @@ from kitty.utils import ScreenSize
|
|||||||
from ..tui.handler import Handler
|
from ..tui.handler import Handler
|
||||||
from ..tui.line_edit import LineEdit
|
from ..tui.line_edit import LineEdit
|
||||||
from ..tui.loop import Loop
|
from ..tui.loop import Loop
|
||||||
from ..tui.operations import color_code, styled
|
from ..tui.operations import color_code, set_default_colors, styled
|
||||||
from .collection import MARK_AFTER, NoCacheFound, Theme, Themes, load_themes
|
from .collection import MARK_AFTER, NoCacheFound, Theme, Themes, load_themes
|
||||||
|
|
||||||
separator = '║'
|
separator = '║'
|
||||||
@@ -132,6 +133,17 @@ class ThemesList:
|
|||||||
return self.themes[self.current_idx]
|
return self.themes[self.current_idx]
|
||||||
|
|
||||||
|
|
||||||
|
def colors_as_escape_codes(o: KittyOptions) -> str:
|
||||||
|
ans = set_default_colors(
|
||||||
|
fg=o.foreground, bg=o.background, cursor=o.cursor, select_bg=o.selection_background, select_fg=o.selection_foreground
|
||||||
|
)
|
||||||
|
cmds = []
|
||||||
|
for i in range(256):
|
||||||
|
col = color_as_sharp(color_from_int(o.color_table[i]))
|
||||||
|
cmds.append(f'{i};{col}')
|
||||||
|
return ans + '\033]4;' + ';'.join(cmds) + '\033\\'
|
||||||
|
|
||||||
|
|
||||||
class ThemesHandler(Handler):
|
class ThemesHandler(Handler):
|
||||||
|
|
||||||
def __init__(self, cached_values: Dict[str, Any], cli_opts: ThemesCLIOptions) -> None:
|
def __init__(self, cached_values: Dict[str, Any], cli_opts: ThemesCLIOptions) -> None:
|
||||||
@@ -195,15 +207,9 @@ class ThemesHandler(Handler):
|
|||||||
o = self.themes_list.current_theme.kitty_opts
|
o = self.themes_list.current_theme.kitty_opts
|
||||||
else:
|
else:
|
||||||
o = create_default_opts()
|
o = create_default_opts()
|
||||||
self.cmd.set_default_colors(
|
|
||||||
fg=o.foreground, bg=o.background, cursor=o.cursor, select_bg=o.selection_background, select_fg=o.selection_foreground
|
|
||||||
)
|
|
||||||
self.current_opts = o
|
self.current_opts = o
|
||||||
cmds = []
|
cmd = colors_as_escape_codes(o)
|
||||||
for i in range(256):
|
self.write(cmd)
|
||||||
col = color_as_sharp(color_from_int(o.color_table[i]))
|
|
||||||
cmds.append(f'{i};{col}')
|
|
||||||
self.print(end='\033]4;' + ';'.join(cmds) + '\033\\')
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def redraw_after_category_change(self) -> None:
|
def redraw_after_category_change(self) -> None:
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ class PatchFile(StreamingJob):
|
|||||||
|
|
||||||
def read_from_src(self, b: memoryview, pos: int) -> int:
|
def read_from_src(self, b: memoryview, pos: int) -> int:
|
||||||
self.src_file.seek(pos)
|
self.src_file.seek(pos)
|
||||||
return self.src_file.readinto(b) # type: ignore
|
return self.src_file.readinto(b)
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
if not self.src_file.closed:
|
if not self.src_file.closed:
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class Handler:
|
|||||||
use_alternate_screen = True
|
use_alternate_screen = True
|
||||||
mouse_tracking = MouseTracking.none
|
mouse_tracking = MouseTracking.none
|
||||||
terminal_io_ended = False
|
terminal_io_ended = False
|
||||||
|
overlay_ready_report_needed = False
|
||||||
|
|
||||||
def _initialize(
|
def _initialize(
|
||||||
self,
|
self,
|
||||||
@@ -206,18 +207,23 @@ class HandleResult:
|
|||||||
type_of_input: Optional[str] = None
|
type_of_input: Optional[str] = None
|
||||||
no_ui: bool = False
|
no_ui: bool = False
|
||||||
|
|
||||||
def __init__(self, impl: Callable[..., Any], type_of_input: Optional[str], no_ui: bool):
|
def __init__(self, impl: Callable[..., Any], type_of_input: Optional[str], no_ui: bool, has_ready_notification: bool):
|
||||||
self.impl = impl
|
self.impl = impl
|
||||||
self.no_ui = no_ui
|
self.no_ui = no_ui
|
||||||
self.type_of_input = type_of_input
|
self.type_of_input = type_of_input
|
||||||
|
self.has_ready_notification = has_ready_notification
|
||||||
|
|
||||||
def __call__(self, args: Sequence[str], data: Any, target_window_id: int, boss: BossType) -> Any:
|
def __call__(self, args: Sequence[str], data: Any, target_window_id: int, boss: BossType) -> Any:
|
||||||
return self.impl(args, data, target_window_id, boss)
|
return self.impl(args, data, target_window_id, boss)
|
||||||
|
|
||||||
|
|
||||||
def result_handler(type_of_input: Optional[str] = None, no_ui: bool = False) -> Callable[[Callable[..., Any]], HandleResult]:
|
def result_handler(
|
||||||
|
type_of_input: Optional[str] = None,
|
||||||
|
no_ui: bool = False,
|
||||||
|
has_ready_notification: bool = Handler.overlay_ready_report_needed
|
||||||
|
) -> Callable[[Callable[..., Any]], HandleResult]:
|
||||||
|
|
||||||
def wrapper(impl: Callable[..., Any]) -> HandleResult:
|
def wrapper(impl: Callable[..., Any]) -> HandleResult:
|
||||||
return HandleResult(impl, type_of_input, no_ui)
|
return HandleResult(impl, type_of_input, no_ui, has_ready_notification)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ from .operations import (
|
|||||||
|
|
||||||
class LineEdit:
|
class LineEdit:
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self, is_password: bool = False) -> None:
|
||||||
self.clear()
|
self.clear()
|
||||||
|
self.is_password = is_password
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
self.current_input = ''
|
self.current_input = ''
|
||||||
@@ -31,7 +32,10 @@ class LineEdit:
|
|||||||
if self.pending_bell:
|
if self.pending_bell:
|
||||||
write('\a')
|
write('\a')
|
||||||
self.pending_bell = False
|
self.pending_bell = False
|
||||||
text = prompt + self.current_input
|
ci = self.current_input
|
||||||
|
if self.is_password:
|
||||||
|
ci = '*' * wcswidth(ci)
|
||||||
|
text = prompt + ci
|
||||||
cursor_pos = self.cursor_pos + wcswidth(prompt)
|
cursor_pos = self.cursor_pos + wcswidth(prompt)
|
||||||
if screen_cols:
|
if screen_cols:
|
||||||
write(SAVE_CURSOR + text + RESTORE_CURSOR)
|
write(SAVE_CURSOR + text + RESTORE_CURSOR)
|
||||||
|
|||||||
@@ -110,14 +110,16 @@ class TermManager:
|
|||||||
|
|
||||||
class MouseButton(IntFlag):
|
class MouseButton(IntFlag):
|
||||||
NONE, LEFT, MIDDLE, RIGHT, FOURTH, FIFTH = 0, 1, 2, 4, 8, 16
|
NONE, LEFT, MIDDLE, RIGHT, FOURTH, FIFTH = 0, 1, 2, 4, 8, 16
|
||||||
|
WHEEL_UP, WHEEL_DOWN = -1, -2
|
||||||
|
|
||||||
|
|
||||||
bmap = {0: MouseButton.LEFT, 1: MouseButton.MIDDLE, 2: MouseButton.RIGHT}
|
bmap = {0: MouseButton.LEFT, 1: MouseButton.MIDDLE, 2: MouseButton.RIGHT}
|
||||||
MOTION_INDICATOR = 1 << 5
|
|
||||||
EXTRA_BUTTON_INDICATOR = 1 << 6
|
|
||||||
SHIFT_INDICATOR = 1 << 2
|
SHIFT_INDICATOR = 1 << 2
|
||||||
ALT_INDICATOR = 1 << 3
|
ALT_INDICATOR = 1 << 3
|
||||||
CTRL_INDICATOR = 1 << 4
|
CTRL_INDICATOR = 1 << 4
|
||||||
|
MOTION_INDICATOR = 1 << 5
|
||||||
|
SCROLL_BUTTON_INDICATOR = 1 << 6
|
||||||
|
EXTRA_BUTTON_INDICATOR = 1 << 7
|
||||||
|
|
||||||
|
|
||||||
class EventType(Enum):
|
class EventType(Enum):
|
||||||
@@ -149,7 +151,9 @@ def decode_sgr_mouse(text: str, screen_size: ScreenSize) -> MouseEvent:
|
|||||||
buttons: MouseButton = MouseButton.NONE
|
buttons: MouseButton = MouseButton.NONE
|
||||||
cb3 = cb & 3
|
cb3 = cb & 3
|
||||||
if cb3 != 3:
|
if cb3 != 3:
|
||||||
if cb & EXTRA_BUTTON_INDICATOR:
|
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
|
buttons |= MouseButton.FIFTH if cb3 & 1 else MouseButton.FOURTH
|
||||||
else:
|
else:
|
||||||
buttons |= bmap[cb3]
|
buttons |= bmap[cb3]
|
||||||
@@ -178,7 +182,7 @@ class UnhandledException(Handler):
|
|||||||
self.cmd.set_default_colors()
|
self.cmd.set_default_colors()
|
||||||
self.write(self.tb.replace('\n', '\r\n'))
|
self.write(self.tb.replace('\n', '\r\n'))
|
||||||
self.write('\r\n')
|
self.write('\r\n')
|
||||||
self.write('Press the Enter key to quit')
|
self.write('Press Enter to quit')
|
||||||
|
|
||||||
def on_key(self, key_event: KeyEventType) -> None:
|
def on_key(self, key_event: KeyEventType) -> None:
|
||||||
if key_event.key == 'ENTER':
|
if key_event.key == 'ENTER':
|
||||||
@@ -232,6 +236,7 @@ class Loop:
|
|||||||
else:
|
else:
|
||||||
self.asyncio_loop = asyncio.get_event_loop()
|
self.asyncio_loop = asyncio.get_event_loop()
|
||||||
self.return_code = 0
|
self.return_code = 0
|
||||||
|
self.overlay_ready_reported = False
|
||||||
self.optional_actions = optional_actions
|
self.optional_actions = optional_actions
|
||||||
self.read_buf = ''
|
self.read_buf = ''
|
||||||
self.decoder = codecs.getincrementaldecoder('utf-8')('ignore')
|
self.decoder = codecs.getincrementaldecoder('utf-8')('ignore')
|
||||||
@@ -431,6 +436,8 @@ class Loop:
|
|||||||
self.asyncio_loop.set_exception_handler(handle_exception)
|
self.asyncio_loop.set_exception_handler(handle_exception)
|
||||||
handler._initialize(self._get_screen_size(), term_manager, schedule_write, self, debug, image_manager)
|
handler._initialize(self._get_screen_size(), term_manager, schedule_write, self, debug, image_manager)
|
||||||
with handler:
|
with handler:
|
||||||
|
if handler.overlay_ready_report_needed:
|
||||||
|
handler.cmd.overlay_ready()
|
||||||
self.asyncio_loop.add_reader(
|
self.asyncio_loop.add_reader(
|
||||||
tty_fd, self._read_ready, handler, tty_fd)
|
tty_fd, self._read_ready, handler, tty_fd)
|
||||||
self.asyncio_loop.add_writer(
|
self.asyncio_loop.add_writer(
|
||||||
@@ -463,9 +470,12 @@ class Loop:
|
|||||||
|
|
||||||
term_manager.extra_finalize = b''.join(self.write_buf).decode('utf-8')
|
term_manager.extra_finalize = b''.join(self.write_buf).decode('utf-8')
|
||||||
if tb is not None:
|
if tb is not None:
|
||||||
|
report_overlay_ready = handler.overlay_ready_report_needed and not self.overlay_ready_reported
|
||||||
self.return_code = 1
|
self.return_code = 1
|
||||||
if not handler.terminal_io_ended:
|
if not handler.terminal_io_ended:
|
||||||
self._report_error_loop(tb, term_manager)
|
self._report_error_loop(tb, term_manager, report_overlay_ready)
|
||||||
|
|
||||||
def _report_error_loop(self, tb: str, term_manager: TermManager) -> None:
|
def _report_error_loop(self, tb: str, term_manager: TermManager, overlay_ready_report_needed: bool) -> None:
|
||||||
self.loop_impl(UnhandledException(tb), term_manager)
|
handler = UnhandledException(tb)
|
||||||
|
handler.overlay_ready_report_needed = overlay_ready_report_needed
|
||||||
|
self.loop_impl(handler, term_manager)
|
||||||
|
|||||||
@@ -413,6 +413,11 @@ def restore_colors() -> str:
|
|||||||
return '\x1b[#Q'
|
return '\x1b[#Q'
|
||||||
|
|
||||||
|
|
||||||
|
@cmd
|
||||||
|
def overlay_ready() -> str:
|
||||||
|
return '\x1bP@kitty-overlay-ready|\x1b\\'
|
||||||
|
|
||||||
|
|
||||||
@cmd
|
@cmd
|
||||||
def write_to_clipboard(data: Union[str, bytes], use_primary: bool = False) -> str:
|
def write_to_clipboard(data: Union[str, bytes], use_primary: bool = False) -> str:
|
||||||
from base64 import standard_b64encode
|
from base64 import standard_b64encode
|
||||||
|
|||||||
@@ -48,3 +48,24 @@ def human_size(
|
|||||||
from math import log
|
from math import log
|
||||||
exponent = min(int(log(size, 1024)), len(unit_list) - 1)
|
exponent = min(int(log(size, 1024)), len(unit_list) - 1)
|
||||||
return format_number(size / 1024**exponent, max_num_of_decimals) + sep + unit_list[exponent]
|
return format_number(size / 1024**exponent, max_num_of_decimals) + sep + unit_list[exponent]
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
print(end=overlay_ready())
|
||||||
|
if msg:
|
||||||
|
print(msg, file=sys.stderr)
|
||||||
|
if print_exc:
|
||||||
|
cls, e, tb = sys.exc_info()
|
||||||
|
if e and not isinstance(e, (SystemExit, KeyboardInterrupt)):
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
with suppress(KeyboardInterrupt, EOFError):
|
||||||
|
input('Press Enter to quit')
|
||||||
|
raise SystemExit(return_code)
|
||||||
|
|
||||||
|
|
||||||
|
def report_unhandled_error(msg: str = '') -> None:
|
||||||
|
' Report an unhandled exception with the overlay ready message '
|
||||||
|
return report_error(msg, print_exc=True)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from ..tui.operations import (
|
|||||||
clear_screen, colored, cursor, faint, set_line_wrapping, set_window_title,
|
clear_screen, colored, cursor, faint, set_line_wrapping, set_window_title,
|
||||||
sgr, styled
|
sgr, styled
|
||||||
)
|
)
|
||||||
|
from ..tui.utils import report_unhandled_error
|
||||||
|
|
||||||
HEX, NAME, EMOTICONS, FAVORITES = 'HEX', 'NAME', 'EMOTICONS', 'FAVORITES'
|
HEX, NAME, EMOTICONS, FAVORITES = 'HEX', 'NAME', 'EMOTICONS', 'FAVORITES'
|
||||||
favorites_path = os.path.join(config_dir, 'unicode-input-favorites.conf')
|
favorites_path = os.path.join(config_dir, 'unicode-input-favorites.conf')
|
||||||
@@ -281,6 +282,8 @@ def is_index(w: str) -> bool:
|
|||||||
|
|
||||||
class UnicodeInput(Handler):
|
class UnicodeInput(Handler):
|
||||||
|
|
||||||
|
overlay_ready_report_needed = True
|
||||||
|
|
||||||
def __init__(self, cached_values: Dict[str, Any], emoji_variation: str = 'none') -> None:
|
def __init__(self, cached_values: Dict[str, Any], emoji_variation: str = 'none') -> None:
|
||||||
self.cached_values = cached_values
|
self.cached_values = cached_values
|
||||||
self.emoji_variation = ''
|
self.emoji_variation = ''
|
||||||
@@ -566,8 +569,7 @@ def main(args: List[str]) -> Optional[str]:
|
|||||||
cli_opts, items = parse_unicode_input_args(args[1:])
|
cli_opts, items = parse_unicode_input_args(args[1:])
|
||||||
except SystemExit as e:
|
except SystemExit as e:
|
||||||
if e.code != 0:
|
if e.code != 0:
|
||||||
print(e.args[0], file=sys.stderr)
|
report_unhandled_error(e.args[0])
|
||||||
input(_('Press Enter to quit'))
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
loop = Loop()
|
loop = Loop()
|
||||||
@@ -585,11 +587,11 @@ def main(args: List[str]) -> Optional[str]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@result_handler()
|
@result_handler(has_ready_notification=UnicodeInput.overlay_ready_report_needed)
|
||||||
def handle_result(args: List[str], current_char: str, target_window_id: int, boss: BossType) -> None:
|
def handle_result(args: List[str], current_char: str, target_window_id: int, boss: BossType) -> None:
|
||||||
w = boss.window_id_map.get(target_window_id)
|
w = boss.window_id_map.get(target_window_id)
|
||||||
if w is not None:
|
if w is not None:
|
||||||
w.paste(current_char)
|
w.paste_text(current_char)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
60870
kittens/unicode_input/names.h
generated
60870
kittens/unicode_input/names.h
generated
File diff suppressed because one or more lines are too long
@@ -99,10 +99,7 @@ def as_rst() -> str:
|
|||||||
|
|
||||||
for action in allg[group]:
|
for action in allg[group]:
|
||||||
a('')
|
a('')
|
||||||
a(f'.. _action-{action.name}:')
|
a(f'.. action:: {action.name}')
|
||||||
a('')
|
|
||||||
a(action.name)
|
|
||||||
a('+' * len(action.name))
|
|
||||||
a('')
|
a('')
|
||||||
a(action.short_help)
|
a(action.short_help)
|
||||||
a('')
|
a('')
|
||||||
|
|||||||
265
kitty/boss.py
265
kitty/boss.py
@@ -5,25 +5,25 @@ import atexit
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
|
from time import monotonic
|
||||||
from typing import (
|
from typing import (
|
||||||
Any, Callable, Container, Dict, Iterable, Iterator, List, Optional, Tuple,
|
Any, Callable, Container, Dict, Iterable, Iterator, List, Optional,
|
||||||
Union
|
Sequence, Tuple, Union
|
||||||
)
|
)
|
||||||
from weakref import WeakValueDictionary
|
from weakref import WeakValueDictionary
|
||||||
|
|
||||||
from .child import (
|
from .child import cached_process_data, default_env, set_default_env
|
||||||
cached_process_data, cwd_of_process, default_env, set_default_env
|
|
||||||
)
|
|
||||||
from .cli import create_opts, parse_args
|
from .cli import create_opts, parse_args
|
||||||
from .cli_stub import CLIOptions
|
from .cli_stub import CLIOptions
|
||||||
from .conf.utils import BadLine, KeyAction, to_cmdline
|
from .conf.utils import BadLine, KeyAction, to_cmdline
|
||||||
from .config import common_opts_as_dict, prepare_config_file_for_editing
|
from .config import common_opts_as_dict, prepare_config_file_for_editing
|
||||||
from .constants import (
|
from .constants import (
|
||||||
appname, config_dir, is_macos, is_wayland, kitty_exe, logo_png_file,
|
appname, cache_dir, config_dir, is_macos, is_wayland, kitty_exe,
|
||||||
supports_primary_selection, website_url
|
logo_png_file, supports_primary_selection, website_url
|
||||||
)
|
)
|
||||||
from .fast_data_types import (
|
from .fast_data_types import (
|
||||||
CLOSE_BEING_CONFIRMED, GLFW_MOD_ALT, GLFW_MOD_CONTROL, GLFW_MOD_SHIFT,
|
CLOSE_BEING_CONFIRMED, GLFW_MOD_ALT, GLFW_MOD_CONTROL, GLFW_MOD_SHIFT,
|
||||||
@@ -56,12 +56,13 @@ from .tabs import (
|
|||||||
from .types import _T, AsyncResponse, SingleKey, WindowSystemMouseEvent, ac
|
from .types import _T, AsyncResponse, SingleKey, WindowSystemMouseEvent, ac
|
||||||
from .typing import PopenType, TypedDict
|
from .typing import PopenType, TypedDict
|
||||||
from .utils import (
|
from .utils import (
|
||||||
func_name, get_editor, get_new_os_window_size, get_primary_selection,
|
cleanup_ssh_control_masters, func_name, get_editor, get_new_os_window_size,
|
||||||
is_path_in_temp_dir, log_error, open_url, parse_address_spec,
|
get_primary_selection, is_path_in_temp_dir, less_version, log_error,
|
||||||
parse_uri_list, platform_window_id, remove_socket_file, safe_print,
|
macos_version, open_url, parse_address_spec, parse_uri_list,
|
||||||
set_primary_selection, single_instance, startup_notification_handler, which
|
platform_window_id, remove_socket_file, safe_print, set_primary_selection,
|
||||||
|
single_instance, startup_notification_handler, which
|
||||||
)
|
)
|
||||||
from .window import CommandOutput, MatchPatternType, Window
|
from .window import CommandOutput, CwdRequest, MatchPatternType, Window
|
||||||
|
|
||||||
|
|
||||||
class OSWindowDict(TypedDict):
|
class OSWindowDict(TypedDict):
|
||||||
@@ -229,7 +230,6 @@ class Boss:
|
|||||||
global_shortcuts: Dict[str, SingleKey]
|
global_shortcuts: Dict[str, SingleKey]
|
||||||
):
|
):
|
||||||
set_layout_options(opts)
|
set_layout_options(opts)
|
||||||
self.cocoa_application_launched = False
|
|
||||||
self.clipboard_buffers: Dict[str, str] = {}
|
self.clipboard_buffers: Dict[str, str] = {}
|
||||||
self.update_check_process: Optional['PopenType[bytes]'] = None
|
self.update_check_process: Optional['PopenType[bytes]'] = None
|
||||||
self.window_id_map: WeakValueDictionary[int, Window] = WeakValueDictionary()
|
self.window_id_map: WeakValueDictionary[int, Window] = WeakValueDictionary()
|
||||||
@@ -273,9 +273,9 @@ class Boss:
|
|||||||
for sc in self.global_shortcuts.values():
|
for sc in self.global_shortcuts.values():
|
||||||
self.keymap.pop(sc, None)
|
self.keymap.pop(sc, None)
|
||||||
|
|
||||||
def startup_first_child(self, os_window_id: Optional[int]) -> None:
|
def startup_first_child(self, os_window_id: Optional[int], startup_sessions: Sequence[Optional[Session]] = ()) -> None:
|
||||||
startup_sessions = create_sessions(get_options(), self.args, default_session=get_options().startup_session)
|
si = startup_sessions or create_sessions(get_options(), self.args, default_session=get_options().startup_session)
|
||||||
for startup_session in startup_sessions:
|
for startup_session in si:
|
||||||
self.add_os_window(startup_session, os_window_id=os_window_id)
|
self.add_os_window(startup_session, os_window_id=os_window_id)
|
||||||
os_window_id = None
|
os_window_id = None
|
||||||
if self.args.start_as != 'normal':
|
if self.args.start_as != 'normal':
|
||||||
@@ -439,7 +439,7 @@ class Boss:
|
|||||||
return os_window_id
|
return os_window_id
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _new_os_window(self, args: Union[SpecialWindowInstance, Iterable[str]], cwd_from: Optional[int] = None) -> int:
|
def _new_os_window(self, args: Union[SpecialWindowInstance, Iterable[str]], cwd_from: Optional[CwdRequest] = None) -> int:
|
||||||
if isinstance(args, SpecialWindowInstance):
|
if isinstance(args, SpecialWindowInstance):
|
||||||
sw: Optional[SpecialWindowInstance] = args
|
sw: Optional[SpecialWindowInstance] = args
|
||||||
else:
|
else:
|
||||||
@@ -461,12 +461,14 @@ class Boss:
|
|||||||
@ac('win', 'New OS Window with the same working directory as the currently active window')
|
@ac('win', 'New OS Window with the same working directory as the currently active window')
|
||||||
def new_os_window_with_cwd(self, *args: str) -> None:
|
def new_os_window_with_cwd(self, *args: str) -> None:
|
||||||
w = self.active_window_for_cwd
|
w = self.active_window_for_cwd
|
||||||
cwd_from = w.child.pid_for_cwd if w is not None else None
|
self._new_os_window(args, CwdRequest(w))
|
||||||
self._new_os_window(args, cwd_from)
|
|
||||||
|
|
||||||
def new_os_window_with_wd(self, wd: str) -> None:
|
def new_os_window_with_wd(self, wd: Union[str, List[str]], str_is_multiple_paths: bool = False) -> None:
|
||||||
special_window = SpecialWindow(None, cwd=wd)
|
if isinstance(wd, str):
|
||||||
self._new_os_window(special_window)
|
wd = wd.split(os.pathsep) if str_is_multiple_paths else [wd]
|
||||||
|
for path in wd:
|
||||||
|
special_window = SpecialWindow(None, cwd=path)
|
||||||
|
self._new_os_window(special_window)
|
||||||
|
|
||||||
def add_child(self, window: Window) -> None:
|
def add_child(self, window: Window) -> None:
|
||||||
assert window.child.pid is not None and window.child.child_fd is not None
|
assert window.child.pid is not None and window.child.child_fd is not None
|
||||||
@@ -546,6 +548,10 @@ class Boss:
|
|||||||
from .cli_stub import CLIOptions
|
from .cli_stub import CLIOptions
|
||||||
startup_id = data.get('startup_id')
|
startup_id = data.get('startup_id')
|
||||||
args, rest = parse_args(data['args'][1:], result_class=CLIOptions)
|
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:
|
||||||
|
self.launch_urls(*cmdline_args_for_open, no_replace_window=True)
|
||||||
|
return None
|
||||||
args.args = rest
|
args.args = rest
|
||||||
opts = create_opts(args)
|
opts = create_opts(args)
|
||||||
if args.session == '-':
|
if args.session == '-':
|
||||||
@@ -667,6 +673,15 @@ class Boss:
|
|||||||
if tab:
|
if tab:
|
||||||
self.confirm_tab_close(tab)
|
self.confirm_tab_close(tab)
|
||||||
|
|
||||||
|
@ac('tab', 'Close all the tabs in the current OS window other than the currently active tab')
|
||||||
|
def close_other_tabs_in_os_window(self) -> None:
|
||||||
|
tm = self.active_tab_manager
|
||||||
|
if tm is not None and len(tm.tabs) > 1:
|
||||||
|
active_tab = self.active_tab
|
||||||
|
for tab in tm:
|
||||||
|
if tab is not active_tab:
|
||||||
|
self.close_tab(tab)
|
||||||
|
|
||||||
def confirm(
|
def confirm(
|
||||||
self, msg: str, # can contain newlines and ANSI formatting
|
self, msg: str, # can contain newlines and ANSI formatting
|
||||||
callback: Callable[..., None], # called with True or False and *args
|
callback: Callable[..., None], # called with True or False and *args
|
||||||
@@ -696,6 +711,18 @@ class Boss:
|
|||||||
cmd += ['-c', c]
|
cmd += ['-c', c]
|
||||||
self._run_kitten('ask', cmd, window=window, custom_callback=callback_, default_data={'response': ''})
|
self._run_kitten('ask', cmd, window=window, custom_callback=callback_, default_data={'response': ''})
|
||||||
|
|
||||||
|
def get_line(
|
||||||
|
self, msg: str, # can contain newlines and ANSI formatting
|
||||||
|
callback: Callable[..., None], # called with the answer or empty string when aborted
|
||||||
|
window: Optional[Window] = None, # the window associated with the confirmation
|
||||||
|
prompt: str = '> ',
|
||||||
|
is_password: bool = False
|
||||||
|
) -> None:
|
||||||
|
def callback_(res: Dict[str, Any], x: int, boss: Boss) -> None:
|
||||||
|
callback(res.get('response') or '')
|
||||||
|
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': ''})
|
||||||
|
|
||||||
def confirm_tab_close(self, tab: Tab) -> None:
|
def confirm_tab_close(self, tab: Tab) -> None:
|
||||||
x = get_options().confirm_os_window_close
|
x = get_options().confirm_os_window_close
|
||||||
num = tab.number_of_windows_with_running_programs if x < 0 else len(tab)
|
num = tab.number_of_windows_with_running_programs if x < 0 else len(tab)
|
||||||
@@ -746,9 +773,16 @@ class Boss:
|
|||||||
if not getattr(self, 'io_thread_started', False):
|
if not getattr(self, 'io_thread_started', False):
|
||||||
self.child_monitor.start()
|
self.child_monitor.start()
|
||||||
self.io_thread_started = True
|
self.io_thread_started = True
|
||||||
self.startup_first_child(first_os_window_id)
|
urls: List[str] = getattr(sys, 'cmdline_args_for_open', [])
|
||||||
|
sess = create_sessions(get_options(), self.args, special_window=SpecialWindow([kitty_exe(), '+runpy', 'input()']))
|
||||||
|
if urls:
|
||||||
|
delattr(sys, 'cmdline_args_for_open')
|
||||||
|
self.startup_first_child(first_os_window_id, startup_sessions=tuple(sess))
|
||||||
|
self.launch_urls(*urls)
|
||||||
|
else:
|
||||||
|
self.startup_first_child(first_os_window_id)
|
||||||
|
|
||||||
if get_options().update_check_interval > 0 and not hasattr(self, 'update_check_started'):
|
if get_options().update_check_interval > 0 and not hasattr(self, 'update_check_started') and getattr(sys, 'frozen', False):
|
||||||
from .update_check import run_update_check
|
from .update_check import run_update_check
|
||||||
run_update_check(get_options().update_check_interval * 60 * 60)
|
run_update_check(get_options().update_check_interval * 60 * 60)
|
||||||
self.update_check_started = True
|
self.update_check_started = True
|
||||||
@@ -772,14 +806,15 @@ class Boss:
|
|||||||
See :sc:`reset_terminal <reset_terminal>` for details. For example::
|
See :sc:`reset_terminal <reset_terminal>` for details. For example::
|
||||||
|
|
||||||
# Reset the terminal
|
# Reset the terminal
|
||||||
map kitty_mod+f9 clear_terminal reset active
|
map f1 clear_terminal reset active
|
||||||
# Clear the terminal screen by erasing all contents
|
# Clear the terminal screen by erasing all contents
|
||||||
map kitty_mod+f10 clear_terminal clear active
|
map f1 clear_terminal clear active
|
||||||
# Clear the terminal scrollback by erasing it
|
# Clear the terminal scrollback by erasing it
|
||||||
map kitty_mod+f11 clear_terminal scrollback active
|
map f1 clear_terminal scrollback active
|
||||||
# Scroll the contents of the screen into the scrollback
|
# Scroll the contents of the screen into the scrollback
|
||||||
map kitty_mod+f12 clear_terminal scroll active
|
map f1 clear_terminal scroll active
|
||||||
|
# Clear everything up to the line with the cursor
|
||||||
|
map f1 clear_terminal to_cursor active
|
||||||
''')
|
''')
|
||||||
def clear_terminal(self, action: str, only_active: bool) -> None:
|
def clear_terminal(self, action: str, only_active: bool) -> None:
|
||||||
if only_active:
|
if only_active:
|
||||||
@@ -789,17 +824,21 @@ class Boss:
|
|||||||
windows.append(w)
|
windows.append(w)
|
||||||
else:
|
else:
|
||||||
windows = list(self.all_windows)
|
windows = list(self.all_windows)
|
||||||
reset = action == 'reset'
|
if action == 'reset':
|
||||||
how = 3 if action == 'scrollback' else 2
|
for w in windows:
|
||||||
for w in windows:
|
w.clear_screen(reset=True, scrollback=True)
|
||||||
if action == 'scroll':
|
elif action == 'scrollback':
|
||||||
w.screen.scroll_until_cursor()
|
for w in windows:
|
||||||
continue
|
w.clear_screen(scrollback=True)
|
||||||
w.screen.cursor.x = w.screen.cursor.y = 0
|
elif action == 'clear':
|
||||||
if reset:
|
for w in windows:
|
||||||
w.screen.reset()
|
w.clear_screen()
|
||||||
else:
|
elif action == 'scroll':
|
||||||
w.screen.erase_in_display(how, False)
|
for w in windows:
|
||||||
|
w.scroll_prompt_to_top()
|
||||||
|
elif action == 'to_cursor':
|
||||||
|
for w in windows:
|
||||||
|
w.scroll_prompt_to_top(clear_scrollback=True)
|
||||||
|
|
||||||
def increase_font_size(self) -> None: # legacy
|
def increase_font_size(self) -> None: # legacy
|
||||||
cfs = global_font_size()
|
cfs = global_font_size()
|
||||||
@@ -1048,10 +1087,11 @@ class Boss:
|
|||||||
self.mouse_handler(ev)
|
self.mouse_handler(ev)
|
||||||
|
|
||||||
def select_window_in_tab_using_overlay(self, tab: Tab, msg: str, only_window_ids: Container[int] = ()) -> Optional[Window]:
|
def select_window_in_tab_using_overlay(self, tab: Tab, msg: str, only_window_ids: Container[int] = ()) -> Optional[Window]:
|
||||||
windows = tuple((w.id, w.title) for i, w in tab.windows.iter_windows_with_number(only_visible=False)
|
windows = tuple((None, f'Current window: {w.title}' if w is self.active_window else w.title)
|
||||||
if not only_window_ids or w.id in only_window_ids)
|
if only_window_ids and w.id not in only_window_ids else (w.id, w.title)
|
||||||
|
for i, w in tab.windows.iter_windows_with_number(only_visible=False))
|
||||||
if len(windows) < 1:
|
if len(windows) < 1:
|
||||||
self.visual_window_select_action_trigger(windows[0][0] if windows else 0)
|
self.visual_window_select_action_trigger(windows[0][0] if windows and windows[0][0] is not None else 0)
|
||||||
if get_options().enable_audio_bell:
|
if get_options().enable_audio_bell:
|
||||||
ring_bell()
|
ring_bell()
|
||||||
return None
|
return None
|
||||||
@@ -1062,7 +1102,8 @@ class Boss:
|
|||||||
self.current_visual_select = None
|
self.current_visual_select = None
|
||||||
if cvs and q is cvs:
|
if cvs and q is cvs:
|
||||||
q.trigger(ans if isinstance(ans, int) else 0)
|
q.trigger(ans if isinstance(ans, int) else 0)
|
||||||
return self.choose_entry(msg, windows, chosen)
|
|
||||||
|
return self.choose_entry(msg, windows, chosen, hints_args=('--hints-offset=0', '--alphabet', get_options().visual_window_select_characters.lower()))
|
||||||
|
|
||||||
@ac('win', '''
|
@ac('win', '''
|
||||||
Resize the active window interactively
|
Resize the active window interactively
|
||||||
@@ -1206,8 +1247,13 @@ class Boss:
|
|||||||
if w is not None:
|
if w is not None:
|
||||||
text = data.decode('utf-8', 'replace')
|
text = data.decode('utf-8', 'replace')
|
||||||
if mime == 'text/uri-list':
|
if mime == 'text/uri-list':
|
||||||
text = '\n'.join(parse_uri_list(text))
|
urls = parse_uri_list(text)
|
||||||
w.paste(text)
|
if w.at_prompt:
|
||||||
|
import shlex
|
||||||
|
text = ' '.join(map(shlex.quote, urls))
|
||||||
|
else:
|
||||||
|
text = '\n'.join(urls)
|
||||||
|
w.paste_text(text)
|
||||||
|
|
||||||
@ac('win', 'Focus the nth OS window')
|
@ac('win', 'Focus the nth OS window')
|
||||||
def nth_os_window(self, num: int = 1) -> None:
|
def nth_os_window(self, num: int = 1) -> None:
|
||||||
@@ -1293,6 +1339,7 @@ class Boss:
|
|||||||
s.close()
|
s.close()
|
||||||
|
|
||||||
def display_scrollback(self, window: Window, data: Union[bytes, str], input_line_number: int = 0, title: str = '', report_cursor: bool = True) -> None:
|
def display_scrollback(self, window: Window, data: Union[bytes, str], input_line_number: int = 0, title: str = '', report_cursor: bool = True) -> None:
|
||||||
|
|
||||||
def prepare_arg(x: str) -> str:
|
def prepare_arg(x: str) -> str:
|
||||||
x = x.replace('INPUT_LINE_NUMBER', str(input_line_number))
|
x = x.replace('INPUT_LINE_NUMBER', str(input_line_number))
|
||||||
x = x.replace('CURSOR_LINE', str(window.screen.cursor.y + 1) if report_cursor else '0')
|
x = x.replace('CURSOR_LINE', str(window.screen.cursor.y + 1) if report_cursor else '0')
|
||||||
@@ -1308,6 +1355,15 @@ class Boss:
|
|||||||
tab = self.active_tab
|
tab = self.active_tab
|
||||||
if tab is not None:
|
if tab is not None:
|
||||||
bdata = data.encode('utf-8') if isinstance(data, str) else data
|
bdata = data.encode('utf-8') if isinstance(data, str) else data
|
||||||
|
if is_macos and cmd[0] == '/usr/bin/less' and macos_version()[:2] < (12, 3):
|
||||||
|
# the system less before macOS 12.3 barfs up OSC codes, so sanitize them ourselves
|
||||||
|
sentinel = os.path.join(cache_dir(), 'less-is-new-enough')
|
||||||
|
if not os.path.exists(sentinel):
|
||||||
|
if less_version(cmd[0]) >= 581:
|
||||||
|
open(sentinel, 'w').close()
|
||||||
|
else:
|
||||||
|
bdata = re.sub(br'\x1b\].*?\x1b\\', b'', bdata)
|
||||||
|
|
||||||
tab.new_special_window(
|
tab.new_special_window(
|
||||||
SpecialWindow(cmd, bdata, title or _('History'), overlay_for=window.id, cwd=window.cwd_of_child),
|
SpecialWindow(cmd, bdata, title or _('History'), overlay_for=window.id, cwd=window.cwd_of_child),
|
||||||
copy_colors_from=self.active_window
|
copy_colors_from=self.active_window
|
||||||
@@ -1321,15 +1377,6 @@ class Boss:
|
|||||||
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(), '+runpy', 'import os, sys, time; time.sleep(0.05); os.execvp(sys.argv[1], sys.argv[1:])'] + get_editor(get_options()) + [confpath]
|
||||||
self.new_os_window(*cmd)
|
self.new_os_window(*cmd)
|
||||||
|
|
||||||
def get_output(self, source_window: Window, num_lines: Optional[int] = 1) -> str:
|
|
||||||
output = ''
|
|
||||||
s = source_window.screen
|
|
||||||
if num_lines is None:
|
|
||||||
num_lines = s.lines
|
|
||||||
for i in range(min(num_lines, s.lines)):
|
|
||||||
output += str(s.linebuf.line(i))
|
|
||||||
return output
|
|
||||||
|
|
||||||
def _run_kitten(
|
def _run_kitten(
|
||||||
self,
|
self,
|
||||||
kitten: str,
|
kitten: str,
|
||||||
@@ -1393,7 +1440,8 @@ class Boss:
|
|||||||
'OVERLAID_WINDOW_COLS': str(w.screen.columns),
|
'OVERLAID_WINDOW_COLS': str(w.screen.columns),
|
||||||
},
|
},
|
||||||
cwd=w.cwd_of_child,
|
cwd=w.cwd_of_child,
|
||||||
overlay_for=w.id
|
overlay_for=w.id,
|
||||||
|
overlay_behind=end_kitten.has_ready_notification,
|
||||||
),
|
),
|
||||||
copy_colors_from=w
|
copy_colors_from=w
|
||||||
)
|
)
|
||||||
@@ -1419,9 +1467,7 @@ class Boss:
|
|||||||
source_window: Window,
|
source_window: Window,
|
||||||
default_data: Optional[Dict[str, Any]] = None
|
default_data: Optional[Dict[str, Any]] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
output = self.get_output(source_window, num_lines=None)
|
data, source_window.kitten_result = source_window.kitten_result, None
|
||||||
from kittens.runner import deserialize
|
|
||||||
data = deserialize(output)
|
|
||||||
if data is None:
|
if data is None:
|
||||||
data = default_data
|
data = default_data
|
||||||
if data is not None:
|
if data is not None:
|
||||||
@@ -1557,7 +1603,7 @@ class Boss:
|
|||||||
if text:
|
if text:
|
||||||
w = self.active_window
|
w = self.active_window
|
||||||
if w is not None:
|
if w is not None:
|
||||||
w.paste(text)
|
w.paste_with_actions(text)
|
||||||
|
|
||||||
@ac('cp', 'Paste from the clipboard to the active window')
|
@ac('cp', 'Paste from the clipboard to the active window')
|
||||||
def paste_from_clipboard(self) -> None:
|
def paste_from_clipboard(self) -> None:
|
||||||
@@ -1682,7 +1728,7 @@ class Boss:
|
|||||||
self, cmd: List[str],
|
self, cmd: List[str],
|
||||||
window: Optional[Window] = None,
|
window: Optional[Window] = None,
|
||||||
stdin: Optional[str] = None,
|
stdin: Optional[str] = None,
|
||||||
cwd_from: Optional[int] = None,
|
cwd_from: Optional[CwdRequest] = None,
|
||||||
as_overlay: bool = False
|
as_overlay: bool = False
|
||||||
) -> SpecialWindowInstance:
|
) -> SpecialWindowInstance:
|
||||||
w = window or self.active_window
|
w = window or self.active_window
|
||||||
@@ -1704,7 +1750,7 @@ class Boss:
|
|||||||
cwd: Optional[str] = None,
|
cwd: Optional[str] = None,
|
||||||
env: Optional[Dict[str, str]] = None,
|
env: Optional[Dict[str, str]] = None,
|
||||||
stdin: Optional[bytes] = None,
|
stdin: Optional[bytes] = None,
|
||||||
cwd_from: Optional[int] = None
|
cwd_from: Optional[CwdRequest] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
import subprocess
|
import subprocess
|
||||||
env = env or None
|
env = env or None
|
||||||
@@ -1714,7 +1760,7 @@ class Boss:
|
|||||||
env = env_
|
env = env_
|
||||||
if cwd_from:
|
if cwd_from:
|
||||||
with suppress(Exception):
|
with suppress(Exception):
|
||||||
cwd = cwd_of_process(cwd_from)
|
cwd = cwd_from.cwd_of_child
|
||||||
|
|
||||||
if stdin:
|
if stdin:
|
||||||
r, w = safe_pipe(False)
|
r, w = safe_pipe(False)
|
||||||
@@ -1732,7 +1778,7 @@ class Boss:
|
|||||||
def pipe(self, source: str, dest: str, exe: str, *args: str) -> Optional[Window]:
|
def pipe(self, source: str, dest: str, exe: str, *args: str) -> Optional[Window]:
|
||||||
cmd = [exe] + list(args)
|
cmd = [exe] + list(args)
|
||||||
window = self.active_window
|
window = self.active_window
|
||||||
cwd_from = window.child.pid_for_cwd if window else None
|
cwd_from = CwdRequest(window) if window else None
|
||||||
|
|
||||||
def create_window() -> SpecialWindowInstance:
|
def create_window() -> SpecialWindowInstance:
|
||||||
return self.special_window_for_cmd(
|
return self.special_window_for_cmd(
|
||||||
@@ -1760,7 +1806,7 @@ class Boss:
|
|||||||
self.run_background_process(cmd, cwd_from=cwd_from, stdin=stdin, env=env)
|
self.run_background_process(cmd, cwd_from=cwd_from, stdin=stdin, env=env)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def args_to_special_window(self, args: Iterable[str], cwd_from: Optional[int] = None) -> SpecialWindowInstance:
|
def args_to_special_window(self, args: Iterable[str], cwd_from: Optional[CwdRequest] = None) -> SpecialWindowInstance:
|
||||||
args = list(args)
|
args = list(args)
|
||||||
stdin = None
|
stdin = None
|
||||||
w = self.active_window
|
w = self.active_window
|
||||||
@@ -1781,7 +1827,7 @@ class Boss:
|
|||||||
cmd.append(arg)
|
cmd.append(arg)
|
||||||
return SpecialWindow(cmd, stdin, cwd_from=cwd_from)
|
return SpecialWindow(cmd, stdin, cwd_from=cwd_from)
|
||||||
|
|
||||||
def _new_tab(self, args: Union[SpecialWindowInstance, Iterable[str]], cwd_from: Optional[int] = None, as_neighbor: bool = False) -> Optional[Tab]:
|
def _new_tab(self, args: Union[SpecialWindowInstance, Iterable[str]], cwd_from: Optional[CwdRequest] = None, as_neighbor: bool = False) -> Optional[Tab]:
|
||||||
special_window = None
|
special_window = None
|
||||||
if args:
|
if args:
|
||||||
if isinstance(args, SpecialWindowInstance):
|
if isinstance(args, SpecialWindowInstance):
|
||||||
@@ -1793,7 +1839,7 @@ class Boss:
|
|||||||
return tm.new_tab(special_window=special_window, cwd_from=cwd_from, as_neighbor=as_neighbor)
|
return tm.new_tab(special_window=special_window, cwd_from=cwd_from, as_neighbor=as_neighbor)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _create_tab(self, args: List[str], cwd_from: Optional[int] = None) -> None:
|
def _create_tab(self, args: List[str], cwd_from: Optional[CwdRequest] = None) -> None:
|
||||||
as_neighbor = False
|
as_neighbor = False
|
||||||
if args and args[0].startswith('!'):
|
if args and args[0].startswith('!'):
|
||||||
as_neighbor = 'neighbor' in args[0][1:].split(',')
|
as_neighbor = 'neighbor' in args[0][1:].split(',')
|
||||||
@@ -1806,15 +1852,18 @@ class Boss:
|
|||||||
|
|
||||||
@ac('tab', 'Create a new tab with working directory for the window in it set to the same as the active window')
|
@ac('tab', 'Create a new tab with working directory for the window in it set to the same as the active window')
|
||||||
def new_tab_with_cwd(self, *args: str) -> None:
|
def new_tab_with_cwd(self, *args: str) -> None:
|
||||||
w = self.active_window_for_cwd
|
self._create_tab(list(args), cwd_from=CwdRequest(self.active_window_for_cwd))
|
||||||
cwd_from = w.child.pid_for_cwd if w is not None else None
|
|
||||||
self._create_tab(list(args), cwd_from=cwd_from)
|
|
||||||
|
|
||||||
def new_tab_with_wd(self, wd: str) -> None:
|
def new_tab_with_wd(self, wd: Union[str, List[str]], str_is_multiple_paths: bool = False) -> None:
|
||||||
special_window = SpecialWindow(None, cwd=wd)
|
if not self.os_window_map:
|
||||||
self._new_tab(special_window)
|
self.add_os_window()
|
||||||
|
if isinstance(wd, str):
|
||||||
|
wd = wd.split(os.pathsep) if str_is_multiple_paths else [wd]
|
||||||
|
for path in wd:
|
||||||
|
special_window = SpecialWindow(None, cwd=path)
|
||||||
|
self._new_tab(special_window)
|
||||||
|
|
||||||
def _new_window(self, args: List[str], cwd_from: Optional[int] = None) -> Optional[Window]:
|
def _new_window(self, args: List[str], cwd_from: Optional[CwdRequest] = None) -> Optional[Window]:
|
||||||
tab = self.active_tab
|
tab = self.active_tab
|
||||||
if tab is None:
|
if tab is None:
|
||||||
return None
|
return None
|
||||||
@@ -1842,8 +1891,7 @@ class Boss:
|
|||||||
w = self.active_window_for_cwd
|
w = self.active_window_for_cwd
|
||||||
if w is None:
|
if w is None:
|
||||||
return self.new_window(*args)
|
return self.new_window(*args)
|
||||||
cwd_from = w.child.pid_for_cwd
|
self._new_window(list(args), cwd_from=CwdRequest(w))
|
||||||
self._new_window(list(args), cwd_from=cwd_from)
|
|
||||||
|
|
||||||
@ac('misc', '''
|
@ac('misc', '''
|
||||||
Launch the specified program in a new window/tab/etc.
|
Launch the specified program in a new window/tab/etc.
|
||||||
@@ -2096,18 +2144,19 @@ class Boss:
|
|||||||
def choose_entry(
|
def choose_entry(
|
||||||
self, title: str, entries: Iterable[Tuple[Union[_T, str, None], str]],
|
self, title: str, entries: Iterable[Tuple[Union[_T, str, None], str]],
|
||||||
callback: Callable[[Union[_T, str, None]], None],
|
callback: Callable[[Union[_T, str, None]], None],
|
||||||
subtitle: str = ''
|
subtitle: str = '',
|
||||||
|
hints_args: Optional[Tuple[str, ...]] = None,
|
||||||
) -> Optional[Window]:
|
) -> Optional[Window]:
|
||||||
lines = [title, subtitle, '']
|
lines = [title, subtitle, ' '] if subtitle else [title, ' ']
|
||||||
idx_map: List[Union[_T, str]] = []
|
idx_map: List[Union[_T, str, None]] = []
|
||||||
ans: Union[str, _T, None] = None
|
ans: Union[str, _T, None] = None
|
||||||
fmt = ': {1}'
|
fmt = ': {1}'
|
||||||
|
|
||||||
for obj, text in entries:
|
for obj, text in entries:
|
||||||
|
idx_map.append(obj)
|
||||||
if obj is None:
|
if obj is None:
|
||||||
lines.append(text)
|
lines.append(text)
|
||||||
else:
|
else:
|
||||||
idx_map.append(obj)
|
|
||||||
lines.append(fmt.format(len(idx_map), text))
|
lines.append(fmt.format(len(idx_map), text))
|
||||||
|
|
||||||
def done(data: Dict[str, Any], target_window_id: int, self: Boss) -> None:
|
def done(data: Dict[str, Any], target_window_id: int, self: Boss) -> None:
|
||||||
@@ -2120,7 +2169,8 @@ class Boss:
|
|||||||
q = self._run_kitten(
|
q = self._run_kitten(
|
||||||
'hints', args=(
|
'hints', args=(
|
||||||
'--ascending', '--customize-processing=::import::kitty.choose_entry',
|
'--ascending', '--customize-processing=::import::kitty.choose_entry',
|
||||||
r'--regex=(?m)^:\s+.+$', '--window-title', title,
|
'--window-title', title,
|
||||||
|
*(hints_args or ())
|
||||||
), input_data='\r\n'.join(lines).encode('utf-8'), custom_callback=done, action_on_removal=done2
|
), input_data='\r\n'.join(lines).encode('utf-8'), custom_callback=done, action_on_removal=done2
|
||||||
)
|
)
|
||||||
return q if isinstance(q, Window) else None
|
return q if isinstance(q, Window) else None
|
||||||
@@ -2136,16 +2186,13 @@ class Boss:
|
|||||||
|
|
||||||
def format_tab_title(tab: Tab) -> str:
|
def format_tab_title(tab: Tab) -> str:
|
||||||
w = 'windows' if tab.num_window_groups > 1 else 'window'
|
w = 'windows' if tab.num_window_groups > 1 else 'window'
|
||||||
return f'{tab.title} [{tab.num_window_groups} {w}]'
|
return f'{tab.name or tab.title} [{tab.num_window_groups} {w}]'
|
||||||
|
|
||||||
ct = self.active_tab
|
ct = self.active_tab
|
||||||
st = ''
|
|
||||||
if ct is not None:
|
|
||||||
st = f'Current tab: {format_tab_title(ct)}'
|
|
||||||
self.choose_entry(
|
self.choose_entry(
|
||||||
'Choose a tab to switch to',
|
'Choose a tab to switch to',
|
||||||
((t.id, format_tab_title(t)) for t in self.all_tabs if t is not ct),
|
((None, f'Current tab: {format_tab_title(t)}') if t is ct else (t.id, format_tab_title(t)) for t in self.all_tabs),
|
||||||
chosen, subtitle=st
|
chosen
|
||||||
)
|
)
|
||||||
|
|
||||||
@ac('win', '''
|
@ac('win', '''
|
||||||
@@ -2208,8 +2255,6 @@ class Boss:
|
|||||||
|
|
||||||
# Can be called with kitty -o "map f1 send_test_notification"
|
# Can be called with kitty -o "map f1 send_test_notification"
|
||||||
def send_test_notification(self) -> None:
|
def send_test_notification(self) -> None:
|
||||||
from time import monotonic
|
|
||||||
|
|
||||||
from .notify import notify
|
from .notify import notify
|
||||||
now = monotonic()
|
now = monotonic()
|
||||||
ident = f'test-notify-{now}'
|
ident = f'test-notify-{now}'
|
||||||
@@ -2231,30 +2276,48 @@ class Boss:
|
|||||||
output = '\n'.join(f'{k}={v}' for k, v in os.environ.items())
|
output = '\n'.join(f'{k}={v}' for k, v in os.environ.items())
|
||||||
self.display_scrollback(w, output, title=_('Current kitty env vars'), report_cursor=False)
|
self.display_scrollback(w, output, title=_('Current kitty env vars'), report_cursor=False)
|
||||||
|
|
||||||
def open_file(self, path: str) -> None:
|
@ac('debug', 'Close all shared SSH connections')
|
||||||
if path == ":cocoa::application launched::":
|
def close_shared_ssh_connections(self) -> None:
|
||||||
self.cocoa_application_launched = True
|
cleanup_ssh_control_masters()
|
||||||
return
|
|
||||||
from .open_actions import actions_for_launch
|
def launch_urls(self, *urls: str, no_replace_window: bool = False) -> None:
|
||||||
from .launch import force_window_launch
|
from .launch import force_window_launch
|
||||||
actions = list(actions_for_launch(path))
|
from .open_actions import actions_for_launch
|
||||||
|
actions: List[KeyAction] = []
|
||||||
|
failures = []
|
||||||
|
for url in urls:
|
||||||
|
uactions = tuple(actions_for_launch(url))
|
||||||
|
if uactions:
|
||||||
|
actions.extend(uactions)
|
||||||
|
else:
|
||||||
|
failures.append(url)
|
||||||
tab = self.active_tab
|
tab = self.active_tab
|
||||||
if tab is not None:
|
if tab is not None:
|
||||||
w = tab.active_window
|
w = tab.active_window
|
||||||
else:
|
else:
|
||||||
w = None
|
w = None
|
||||||
needs_window_replaced = not self.cocoa_application_launched or not self.os_window_map and w is not None and w.id == 1
|
needs_window_replaced = False
|
||||||
|
if not no_replace_window and not get_options().startup_session:
|
||||||
|
if w is not None and w.id == 1 and monotonic() - w.started_at < 2 and len(tuple(self.all_windows)) == 1:
|
||||||
|
# first window, soon after startup replace it
|
||||||
|
needs_window_replaced = True
|
||||||
|
|
||||||
def clear_initial_window() -> None:
|
def clear_initial_window() -> None:
|
||||||
if needs_window_replaced and tab is not None and w is not None:
|
if needs_window_replaced and tab is not None and w is not None:
|
||||||
tab.remove_window(w)
|
tab.remove_window(w)
|
||||||
|
|
||||||
if not actions:
|
if failures:
|
||||||
with force_window_launch(needs_window_replaced):
|
from kittens.tui.operations import styled
|
||||||
self.launch(kitty_exe(), '+runpy', f'print("The file:", {path!r}, "is of unknown type, cannot open it.");'
|
spec = '\n '.join(styled(u, fg='red') for u in failures)
|
||||||
'from kitty.utils import hold_till_enter; hold_till_enter(); raise SystemExit(1)')
|
bdata = f"Unknown URL type, cannot open:\n\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)
|
||||||
|
else:
|
||||||
|
self._new_os_window(special_window)
|
||||||
clear_initial_window()
|
clear_initial_window()
|
||||||
else:
|
needs_window_replaced = False
|
||||||
|
if actions:
|
||||||
with force_window_launch(needs_window_replaced):
|
with force_window_launch(needs_window_replaced):
|
||||||
self.dispatch_action(actions.pop(0))
|
self.dispatch_action(actions.pop(0))
|
||||||
clear_initial_window()
|
clear_initial_window()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#version GLSL_VERSION
|
#version GLSL_VERSION
|
||||||
#define WHICH_PROGRAM
|
#define {WHICH_PROGRAM}
|
||||||
#define NOT_TRANSPARENT
|
#define NOT_TRANSPARENT
|
||||||
|
|
||||||
#if defined(SIMPLE) || defined(BACKGROUND) || defined(SPECIAL)
|
#if defined(SIMPLE) || defined(BACKGROUND) || defined(SPECIAL)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#version GLSL_VERSION
|
#version GLSL_VERSION
|
||||||
#define WHICH_PROGRAM
|
#define {WHICH_PROGRAM}
|
||||||
#define NOT_TRANSPARENT
|
#define NOT_TRANSPARENT
|
||||||
#define DECORATION_SHIFT {DECORATION_SHIFT}
|
#define DECORATION_SHIFT {DECORATION_SHIFT}
|
||||||
#define REVERSE_SHIFT {REVERSE_SHIFT}
|
#define REVERSE_SHIFT {REVERSE_SHIFT}
|
||||||
@@ -79,8 +79,8 @@ const uint COLOR_MASK = uint(0x4000);
|
|||||||
const uint ZERO = uint(0);
|
const uint ZERO = uint(0);
|
||||||
const uint ONE = uint(1);
|
const uint ONE = uint(1);
|
||||||
const uint TWO = uint(2);
|
const uint TWO = uint(2);
|
||||||
const uint FOUR = uint(4);
|
const uint STRIKE_SPRITE_INDEX = uint({STRIKE_SPRITE_INDEX});
|
||||||
const uint SEVEN = uint(7);
|
const uint DECORATION_MASK = uint({DECORATION_MASK});
|
||||||
|
|
||||||
vec3 color_to_vec(uint c) {
|
vec3 color_to_vec(uint c) {
|
||||||
uint r, g, b;
|
uint r, g, b;
|
||||||
@@ -193,8 +193,8 @@ void main() {
|
|||||||
foreground = choose_color(float(is_selected & ONE), selection_color, foreground);
|
foreground = choose_color(float(is_selected & ONE), selection_color, foreground);
|
||||||
decoration_fg = choose_color(float(is_selected & ONE), selection_color, decoration_fg);
|
decoration_fg = choose_color(float(is_selected & ONE), selection_color, decoration_fg);
|
||||||
// Underline and strike through (rendered via sprites)
|
// Underline and strike through (rendered via sprites)
|
||||||
underline_pos = choose_color(in_url, to_sprite_pos(pos, url_style, ZERO, ZERO), to_sprite_pos(pos, (text_attrs >> DECORATION_SHIFT) & SEVEN, ZERO, ZERO));
|
underline_pos = choose_color(in_url, to_sprite_pos(pos, url_style, ZERO, ZERO), to_sprite_pos(pos, (text_attrs >> DECORATION_SHIFT) & DECORATION_MASK, ZERO, ZERO));
|
||||||
strike_pos = to_sprite_pos(pos, ((text_attrs >> STRIKE_SHIFT) & ONE) * FOUR, ZERO, ZERO);
|
strike_pos = to_sprite_pos(pos, ((text_attrs >> STRIKE_SHIFT) & ONE) * STRIKE_SPRITE_INDEX, ZERO, ZERO);
|
||||||
|
|
||||||
// Cursor
|
// Cursor
|
||||||
cursor_color_vec = vec4(color_to_vec(cursor_bg), 1.0);
|
cursor_color_vec = vec4(color_to_vec(cursor_bg), 1.0);
|
||||||
|
|||||||
@@ -319,10 +319,10 @@ schedule_write_to_child_python(unsigned long id, const char *prefix, PyObject *a
|
|||||||
static PyObject *
|
static PyObject *
|
||||||
needs_write(ChildMonitor UNUSED *self, PyObject *args) {
|
needs_write(ChildMonitor UNUSED *self, PyObject *args) {
|
||||||
#define needs_write_doc "needs_write(id, data) -> Queue data to be written to child."
|
#define needs_write_doc "needs_write(id, data) -> Queue data to be written to child."
|
||||||
unsigned long id, sz;
|
unsigned long id;
|
||||||
const char *data;
|
Py_buffer buf;
|
||||||
if (!PyArg_ParseTuple(args, "ks#", &id, &data, &sz)) return NULL;
|
if (!PyArg_ParseTuple(args, "ky*", &id, &buf)) return NULL;
|
||||||
if (schedule_write_to_child(id, 1, data, (size_t)sz)) { Py_RETURN_TRUE; }
|
if (schedule_write_to_child(id, 1, buf.buf, (size_t)buf.len)) { Py_RETURN_TRUE; }
|
||||||
Py_RETURN_FALSE;
|
Py_RETURN_FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,8 +553,6 @@ pyset_iutf8(ChildMonitor *self, PyObject *args) {
|
|||||||
#undef INCREF_CHILD
|
#undef INCREF_CHILD
|
||||||
#undef DECREF_CHILD
|
#undef DECREF_CHILD
|
||||||
|
|
||||||
extern void cocoa_update_menu_bar_title(PyObject*);
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
cursor_needs_render(Window *w) {
|
cursor_needs_render(Window *w) {
|
||||||
return w->cursor_visible_at_last_render != w->render_data.screen->cursor_render_info.is_visible || w->last_cursor_x != w->render_data.screen->cursor_render_info.x || w->last_cursor_y != w->render_data.screen->cursor_render_info.y || w->last_cursor_shape != w->render_data.screen->cursor_render_info.shape;
|
return w->cursor_visible_at_last_render != w->render_data.screen->cursor_render_info.is_visible || w->last_cursor_x != w->render_data.screen->cursor_render_info.x || w->last_cursor_y != w->render_data.screen->cursor_render_info.y || w->last_cursor_shape != w->render_data.screen->cursor_render_info.shape;
|
||||||
@@ -592,7 +590,7 @@ change_menubar_title(PyObject *title UNUSED) {
|
|||||||
static PyObject *current_title = NULL;
|
static PyObject *current_title = NULL;
|
||||||
if (title != current_title) {
|
if (title != current_title) {
|
||||||
current_title = title;
|
current_title = title;
|
||||||
if (title && OPT(macos_show_window_title_in) & MENUBAR) cocoa_update_menu_bar_title(title);
|
if (title && OPT(macos_show_window_title_in) & MENUBAR) update_menu_bar_title(title);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -639,6 +637,7 @@ prepare_to_render_os_window(OSWindow *os_window, monotonic_t now, unsigned int *
|
|||||||
if (is_active_window) {
|
if (is_active_window) {
|
||||||
*active_window_id = w->id;
|
*active_window_id = w->id;
|
||||||
if (collect_cursor_info(&WD.screen->cursor_render_info, w, now, os_window)) needs_render = true;
|
if (collect_cursor_info(&WD.screen->cursor_render_info, w, now, os_window)) needs_render = true;
|
||||||
|
WD.screen->cursor_render_info.is_focused = os_window->is_focused;
|
||||||
set_os_window_title_from_window(w, os_window);
|
set_os_window_title_from_window(w, os_window);
|
||||||
*active_window_bg = window_bg;
|
*active_window_bg = window_bg;
|
||||||
} else {
|
} else {
|
||||||
@@ -1001,21 +1000,21 @@ static bool cocoa_pending_actions[NUM_COCOA_PENDING_ACTIONS] = {0};
|
|||||||
static bool has_cocoa_pending_actions = false;
|
static bool has_cocoa_pending_actions = false;
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char* wd;
|
char* wd;
|
||||||
char **open_files;
|
char **open_urls;
|
||||||
size_t open_files_count;
|
size_t open_urls_count;
|
||||||
size_t open_files_capacity;
|
size_t open_urls_capacity;
|
||||||
} CocoaPendingActionsData;
|
} CocoaPendingActionsData;
|
||||||
static CocoaPendingActionsData cocoa_pending_actions_data = {0};
|
static CocoaPendingActionsData cocoa_pending_actions_data = {0};
|
||||||
|
|
||||||
void
|
void
|
||||||
set_cocoa_pending_action(CocoaPendingAction action, const char *wd) {
|
set_cocoa_pending_action(CocoaPendingAction action, const char *data) {
|
||||||
if (wd) {
|
if (data) {
|
||||||
if (action == OPEN_FILE) {
|
if (action == LAUNCH_URLS) {
|
||||||
ensure_space_for(&cocoa_pending_actions_data, open_files, char*, cocoa_pending_actions_data.open_files_count + 8, open_files_capacity, 8, true);
|
ensure_space_for(&cocoa_pending_actions_data, open_urls, char*, cocoa_pending_actions_data.open_urls_count + 8, open_urls_capacity, 8, true);
|
||||||
cocoa_pending_actions_data.open_files[cocoa_pending_actions_data.open_files_count++] = strdup(wd);
|
cocoa_pending_actions_data.open_urls[cocoa_pending_actions_data.open_urls_count++] = strdup(data);
|
||||||
} else {
|
} else {
|
||||||
if (cocoa_pending_actions_data.wd) free(cocoa_pending_actions_data.wd);
|
if (cocoa_pending_actions_data.wd) free(cocoa_pending_actions_data.wd);
|
||||||
cocoa_pending_actions_data.wd = strdup(wd);
|
cocoa_pending_actions_data.wd = strdup(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cocoa_pending_actions[action] = true;
|
cocoa_pending_actions[action] = true;
|
||||||
@@ -1038,23 +1037,25 @@ process_cocoa_pending_actions(void) {
|
|||||||
if (cocoa_pending_actions[NEW_WINDOW]) { call_boss(new_window, NULL); }
|
if (cocoa_pending_actions[NEW_WINDOW]) { call_boss(new_window, NULL); }
|
||||||
if (cocoa_pending_actions[CLOSE_WINDOW]) { call_boss(close_window, NULL); }
|
if (cocoa_pending_actions[CLOSE_WINDOW]) { call_boss(close_window, NULL); }
|
||||||
if (cocoa_pending_actions[RESET_TERMINAL]) { call_boss(clear_terminal, "sO", "reset", Py_True ); }
|
if (cocoa_pending_actions[RESET_TERMINAL]) { call_boss(clear_terminal, "sO", "reset", Py_True ); }
|
||||||
|
if (cocoa_pending_actions[CLEAR_TERMINAL_AND_SCROLLBACK]) { call_boss(clear_terminal, "sO", "to_cursor", Py_True ); }
|
||||||
if (cocoa_pending_actions[RELOAD_CONFIG]) { call_boss(load_config_file, NULL); }
|
if (cocoa_pending_actions[RELOAD_CONFIG]) { call_boss(load_config_file, NULL); }
|
||||||
if (cocoa_pending_actions[TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY]) { call_boss(toggle_macos_secure_keyboard_entry, NULL); }
|
if (cocoa_pending_actions[TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY]) { call_boss(toggle_macos_secure_keyboard_entry, NULL); }
|
||||||
|
if (cocoa_pending_actions[TOGGLE_FULLSCREEN]) { call_boss(toggle_fullscreen, NULL); }
|
||||||
if (cocoa_pending_actions_data.wd) {
|
if (cocoa_pending_actions_data.wd) {
|
||||||
if (cocoa_pending_actions[NEW_OS_WINDOW_WITH_WD]) { call_boss(new_os_window_with_wd, "s", cocoa_pending_actions_data.wd); }
|
if (cocoa_pending_actions[NEW_OS_WINDOW_WITH_WD]) { call_boss(new_os_window_with_wd, "sO", cocoa_pending_actions_data.wd, Py_True); }
|
||||||
if (cocoa_pending_actions[NEW_TAB_WITH_WD]) { call_boss(new_tab_with_wd, "s", cocoa_pending_actions_data.wd); }
|
if (cocoa_pending_actions[NEW_TAB_WITH_WD]) { call_boss(new_tab_with_wd, "sO", cocoa_pending_actions_data.wd, Py_True); }
|
||||||
free(cocoa_pending_actions_data.wd);
|
free(cocoa_pending_actions_data.wd);
|
||||||
cocoa_pending_actions_data.wd = NULL;
|
cocoa_pending_actions_data.wd = NULL;
|
||||||
}
|
}
|
||||||
if (cocoa_pending_actions_data.open_files_count) {
|
if (cocoa_pending_actions_data.open_urls_count) {
|
||||||
for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.open_files_count; cpa++) {
|
for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.open_urls_count; cpa++) {
|
||||||
if (cocoa_pending_actions_data.open_files[cpa]) {
|
if (cocoa_pending_actions_data.open_urls[cpa]) {
|
||||||
call_boss(open_file, "s", cocoa_pending_actions_data.open_files[cpa]);
|
call_boss(launch_urls, "s", cocoa_pending_actions_data.open_urls[cpa]);
|
||||||
free(cocoa_pending_actions_data.open_files[cpa]);
|
free(cocoa_pending_actions_data.open_urls[cpa]);
|
||||||
cocoa_pending_actions_data.open_files[cpa] = NULL;
|
cocoa_pending_actions_data.open_urls[cpa] = NULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cocoa_pending_actions_data.open_files_count = 0;
|
cocoa_pending_actions_data.open_urls_count = 0;
|
||||||
}
|
}
|
||||||
memset(cocoa_pending_actions, 0, sizeof(cocoa_pending_actions));
|
memset(cocoa_pending_actions, 0, sizeof(cocoa_pending_actions));
|
||||||
has_cocoa_pending_actions = false;
|
has_cocoa_pending_actions = false;
|
||||||
@@ -1114,11 +1115,11 @@ main_loop(ChildMonitor *self, PyObject *a UNUSED) {
|
|||||||
run_main_loop(process_global_state, self);
|
run_main_loop(process_global_state, self);
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
if (cocoa_pending_actions_data.wd) { free(cocoa_pending_actions_data.wd); cocoa_pending_actions_data.wd = NULL; }
|
if (cocoa_pending_actions_data.wd) { free(cocoa_pending_actions_data.wd); cocoa_pending_actions_data.wd = NULL; }
|
||||||
if (cocoa_pending_actions_data.open_files) {
|
if (cocoa_pending_actions_data.open_urls) {
|
||||||
for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.open_files_count; cpa++) {
|
for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.open_urls_count; cpa++) {
|
||||||
if (cocoa_pending_actions_data.open_files[cpa]) free(cocoa_pending_actions_data.open_files[cpa]);
|
if (cocoa_pending_actions_data.open_urls[cpa]) free(cocoa_pending_actions_data.open_urls[cpa]);
|
||||||
}
|
}
|
||||||
free(cocoa_pending_actions_data.open_files); cocoa_pending_actions_data.open_files = NULL;
|
free(cocoa_pending_actions_data.open_urls); cocoa_pending_actions_data.open_urls = NULL;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (PyErr_Occurred()) return NULL;
|
if (PyErr_Occurred()) return NULL;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import sys
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from contextlib import contextmanager, suppress
|
from contextlib import contextmanager, suppress
|
||||||
from typing import (
|
from typing import (
|
||||||
DefaultDict, Dict, Generator, List, Optional, Sequence, Tuple
|
DefaultDict, Dict, Generator, List, Optional, Sequence, Tuple, TYPE_CHECKING
|
||||||
)
|
)
|
||||||
|
|
||||||
import kitty.fast_data_types as fast_data_types
|
import kitty.fast_data_types as fast_data_types
|
||||||
@@ -20,6 +20,8 @@ try:
|
|||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
except ImportError:
|
except ImportError:
|
||||||
TypedDict = dict
|
TypedDict = dict
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .window import CwdRequest
|
||||||
|
|
||||||
|
|
||||||
if is_macos:
|
if is_macos:
|
||||||
@@ -192,14 +194,14 @@ class Child:
|
|||||||
cwd: str,
|
cwd: str,
|
||||||
stdin: Optional[bytes] = None,
|
stdin: Optional[bytes] = None,
|
||||||
env: Optional[Dict[str, str]] = None,
|
env: Optional[Dict[str, str]] = None,
|
||||||
cwd_from: Optional[int] = None,
|
cwd_from: Optional['CwdRequest'] = None,
|
||||||
allow_remote_control: bool = False
|
allow_remote_control: bool = False
|
||||||
):
|
):
|
||||||
self.allow_remote_control = allow_remote_control
|
self.allow_remote_control = allow_remote_control
|
||||||
self.argv = argv
|
self.argv = list(argv)
|
||||||
if cwd_from is not None:
|
if cwd_from:
|
||||||
try:
|
try:
|
||||||
cwd = cwd_of_process(cwd_from)
|
cwd = cwd_from.modify_argv_for_launch_with_cwd(self.argv) or cwd
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
log_error(f'Failed to read cwd of {cwd_from} with error: {err}')
|
log_error(f'Failed to read cwd of {cwd_from} with error: {err}')
|
||||||
else:
|
else:
|
||||||
@@ -233,7 +235,7 @@ class Child:
|
|||||||
opts = fast_data_types.get_options()
|
opts = fast_data_types.get_options()
|
||||||
if 'disabled' not in opts.shell_integration:
|
if 'disabled' not in opts.shell_integration:
|
||||||
from .shell_integration import modify_shell_environ
|
from .shell_integration import modify_shell_environ
|
||||||
modify_shell_environ(self.argv[0], opts, env)
|
modify_shell_environ(opts, env, self.argv)
|
||||||
env = {k: v for k, v in env.items() if v is not DELETE_ENV_VAR}
|
env = {k: v for k, v in env.items() if v is not DELETE_ENV_VAR}
|
||||||
return env
|
return env
|
||||||
|
|
||||||
@@ -344,8 +346,18 @@ class Child:
|
|||||||
assert self.child_fd is not None
|
assert self.child_fd is not None
|
||||||
pgrp = os.tcgetpgrp(self.child_fd)
|
pgrp = os.tcgetpgrp(self.child_fd)
|
||||||
foreground_processes = processes_in_group(pgrp) if pgrp >= 0 else []
|
foreground_processes = processes_in_group(pgrp) if pgrp >= 0 else []
|
||||||
if len(foreground_processes) == 1:
|
if foreground_processes:
|
||||||
return foreground_processes[0]
|
# there is no easy way that I know of to know which process is the
|
||||||
|
# foreground process in this group from the users perspective,
|
||||||
|
# so we assume the one with the highest PID is as that is most
|
||||||
|
# likely to be the newest process. This situation can happen
|
||||||
|
# for example with a shell script such as:
|
||||||
|
# #!/bin/bash
|
||||||
|
# cd /tmp
|
||||||
|
# vim
|
||||||
|
# With this script , the foreground process group will contain
|
||||||
|
# both the bash instance running the script and vim.
|
||||||
|
return max(foreground_processes)
|
||||||
return self.pid
|
return self.pid
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -10,7 +10,17 @@ from .typing import MarkType
|
|||||||
|
|
||||||
|
|
||||||
def mark(text: str, args: HintsCLIOptions, Mark: Type[MarkType], extra_cli_args: List[str], *a: Any) -> Generator[MarkType, None, None]:
|
def mark(text: str, args: HintsCLIOptions, Mark: Type[MarkType], extra_cli_args: List[str], *a: Any) -> Generator[MarkType, None, None]:
|
||||||
for idx, m in enumerate(re.finditer(args.regex, text)):
|
idx = 0
|
||||||
|
found_start_line = False
|
||||||
|
for m in re.finditer(r'(?m)^.+$', text):
|
||||||
start, end = m.span()
|
start, end = m.span()
|
||||||
mark_text = text[start:end].replace('\n', '').replace('\0', '')
|
line = text[start:end].replace('\0', '').replace('\n', '')
|
||||||
yield Mark(idx, start, end, mark_text, {'index': idx})
|
if line == ' ':
|
||||||
|
found_start_line = True
|
||||||
|
continue
|
||||||
|
if line.startswith(': '):
|
||||||
|
yield Mark(idx, start, end, line, {'index': idx})
|
||||||
|
idx += 1
|
||||||
|
elif found_start_line:
|
||||||
|
# skip this line incrementing the index
|
||||||
|
idx += 1
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ directory is always used and the above searching does not happen.
|
|||||||
|
|
||||||
If :file:`/etc/xdg/{appname}/{conf_name}.conf` exists it is merged before (i.e. with lower
|
If :file:`/etc/xdg/{appname}/{conf_name}.conf` exists it is merged before (i.e. with lower
|
||||||
priority) than any user config files. It can be used to specify system-wide
|
priority) than any user config files. It can be used to specify system-wide
|
||||||
defaults for all users.
|
defaults for all users. You can use either :code:`-` or :code:`/dev/stdin` to read the
|
||||||
|
config from STDIN.
|
||||||
'''.replace(
|
'''.replace(
|
||||||
'{macos_confpath}',
|
'{macos_confpath}',
|
||||||
(':file:`~/Library/Preferences/{appname}/{conf_name}.conf`,' if is_macos else ''), 1
|
(':file:`~/Library/Preferences/{appname}/{conf_name}.conf`,' if is_macos else ''), 1
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ LaunchCLIOptions = AskCLIOptions = ClipboardCLIOptions = DiffCLIOptions = CLIOpt
|
|||||||
HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions
|
HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions
|
||||||
ErrorCLIOptions = UnicodeCLIOptions = RCOptions = RemoteFileCLIOptions = CLIOptions
|
ErrorCLIOptions = UnicodeCLIOptions = RCOptions = RemoteFileCLIOptions = CLIOptions
|
||||||
QueryTerminalCLIOptions = BroadcastCLIOptions = ShowKeyCLIOptions = CLIOptions
|
QueryTerminalCLIOptions = BroadcastCLIOptions = ShowKeyCLIOptions = CLIOptions
|
||||||
ThemesCLIOptions = TransferCLIOptions = CLIOptions
|
ThemesCLIOptions = TransferCLIOptions = CopyCLIOptions = CLIOptions
|
||||||
|
|
||||||
|
|
||||||
def generate_stub() -> None:
|
def generate_stub() -> None:
|
||||||
@@ -78,6 +78,9 @@ def generate_stub() -> None:
|
|||||||
from kittens.transfer.main import option_text as OPTIONS
|
from kittens.transfer.main import option_text as OPTIONS
|
||||||
do(OPTIONS(), 'TransferCLIOptions')
|
do(OPTIONS(), 'TransferCLIOptions')
|
||||||
|
|
||||||
|
from kittens.ssh.copy import option_text as OPTIONS
|
||||||
|
do(OPTIONS(), 'CopyCLIOptions')
|
||||||
|
|
||||||
from kitty.rc.base import all_command_names, command_for_name
|
from kitty.rc.base import all_command_names, command_for_name
|
||||||
for cmd_name in all_command_names():
|
for cmd_name in all_command_names():
|
||||||
cmd = command_for_name(cmd_name)
|
cmd = command_for_name(cmd_name)
|
||||||
|
|||||||
@@ -231,8 +231,10 @@ PENDING(previous_tab, PREVIOUS_TAB)
|
|||||||
PENDING(new_window, NEW_WINDOW)
|
PENDING(new_window, NEW_WINDOW)
|
||||||
PENDING(close_window, CLOSE_WINDOW)
|
PENDING(close_window, CLOSE_WINDOW)
|
||||||
PENDING(reset_terminal, RESET_TERMINAL)
|
PENDING(reset_terminal, RESET_TERMINAL)
|
||||||
|
PENDING(clear_terminal_and_scrollback, CLEAR_TERMINAL_AND_SCROLLBACK)
|
||||||
PENDING(reload_config, RELOAD_CONFIG)
|
PENDING(reload_config, RELOAD_CONFIG)
|
||||||
PENDING(toggle_macos_secure_keyboard_entry, TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY)
|
PENDING(toggle_macos_secure_keyboard_entry, TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY)
|
||||||
|
PENDING(toggle_fullscreen, TOGGLE_FULLSCREEN)
|
||||||
|
|
||||||
- (void)open_kitty_website_url:(id)sender {
|
- (void)open_kitty_website_url:(id)sender {
|
||||||
(void)sender;
|
(void)sender;
|
||||||
@@ -242,6 +244,8 @@ PENDING(toggle_macos_secure_keyboard_entry, TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY)
|
|||||||
- (BOOL)validateMenuItem:(NSMenuItem *)item {
|
- (BOOL)validateMenuItem:(NSMenuItem *)item {
|
||||||
if (item.action == @selector(toggle_macos_secure_keyboard_entry:)) {
|
if (item.action == @selector(toggle_macos_secure_keyboard_entry:)) {
|
||||||
item.state = [SecureKeyboardEntryController sharedInstance].isDesired ? NSControlStateValueOn : NSControlStateValueOff;
|
item.state = [SecureKeyboardEntryController sharedInstance].isDesired ? NSControlStateValueOn : NSControlStateValueOff;
|
||||||
|
} else if (item.action == @selector(toggle_fullscreen:)) {
|
||||||
|
item.title = ([NSApp currentSystemPresentationOptions] & NSApplicationPresentationFullScreen) ? @"Exit Full Screen" : @"Enter Full Screen";
|
||||||
}
|
}
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
@@ -270,8 +274,8 @@ typedef struct {
|
|||||||
} GlobalShortcut;
|
} GlobalShortcut;
|
||||||
typedef struct {
|
typedef struct {
|
||||||
GlobalShortcut new_os_window, close_os_window, close_tab, edit_config_file, reload_config;
|
GlobalShortcut new_os_window, close_os_window, close_tab, edit_config_file, reload_config;
|
||||||
GlobalShortcut previous_tab, next_tab, new_tab, new_window, close_window, reset_terminal;
|
GlobalShortcut previous_tab, next_tab, new_tab, new_window, close_window, reset_terminal, clear_terminal_and_scrollback;
|
||||||
GlobalShortcut toggle_macos_secure_keyboard_entry;
|
GlobalShortcut toggle_macos_secure_keyboard_entry, toggle_fullscreen;
|
||||||
} GlobalShortcuts;
|
} GlobalShortcuts;
|
||||||
static GlobalShortcuts global_shortcuts;
|
static GlobalShortcuts global_shortcuts;
|
||||||
|
|
||||||
@@ -285,8 +289,8 @@ cocoa_set_global_shortcut(PyObject *self UNUSED, PyObject *args) {
|
|||||||
#define Q(x) if (strcmp(name, #x) == 0) gs = &global_shortcuts.x
|
#define Q(x) if (strcmp(name, #x) == 0) gs = &global_shortcuts.x
|
||||||
Q(new_os_window); else Q(close_os_window); else Q(close_tab); else Q(edit_config_file);
|
Q(new_os_window); else Q(close_os_window); else Q(close_tab); else Q(edit_config_file);
|
||||||
else Q(new_tab); else Q(next_tab); else Q(previous_tab);
|
else Q(new_tab); else Q(next_tab); else Q(previous_tab);
|
||||||
else Q(new_window); else Q(close_window); else Q(reset_terminal); else Q(reload_config);
|
else Q(new_window); else Q(close_window); else Q(reset_terminal); else Q(clear_terminal_and_scrollback); else Q(reload_config);
|
||||||
else Q(toggle_macos_secure_keyboard_entry);
|
else Q(toggle_macos_secure_keyboard_entry); else Q(toggle_fullscreen);
|
||||||
#undef Q
|
#undef Q
|
||||||
if (gs == NULL) { PyErr_SetString(PyExc_KeyError, "Unknown shortcut name"); return NULL; }
|
if (gs == NULL) { PyErr_SetString(PyExc_KeyError, "Unknown shortcut name"); return NULL; }
|
||||||
int cocoa_mods;
|
int cocoa_mods;
|
||||||
@@ -495,25 +499,41 @@ cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
|
|||||||
|
|
||||||
- (BOOL)openTab:(NSPasteboard*)pasteboard
|
- (BOOL)openTab:(NSPasteboard*)pasteboard
|
||||||
userData:(NSString *) UNUSED userData error:(NSError **) UNUSED error {
|
userData:(NSString *) UNUSED userData error:(NSError **) UNUSED error {
|
||||||
return [self openFilesFromPasteboard:pasteboard type:NEW_TAB_WITH_WD];
|
return [self openDirsFromPasteboard:pasteboard type:NEW_TAB_WITH_WD];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)openOSWindow:(NSPasteboard*)pasteboard
|
- (BOOL)openOSWindow:(NSPasteboard*)pasteboard
|
||||||
userData:(NSString *) UNUSED userData error:(NSError **) UNUSED error {
|
userData:(NSString *) UNUSED userData error:(NSError **) UNUSED error {
|
||||||
return [self openFilesFromPasteboard:pasteboard type:NEW_OS_WINDOW_WITH_WD];
|
return [self openDirsFromPasteboard:pasteboard type:NEW_OS_WINDOW_WITH_WD];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)openFilesFromPasteboard:(NSPasteboard *)pasteboard type:(int)type {
|
- (BOOL)openDirsFromPasteboard:(NSPasteboard *)pasteboard type:(int)type {
|
||||||
NSDictionary *options = @{ NSPasteboardURLReadingFileURLsOnlyKey: @YES };
|
NSDictionary *options = @{ NSPasteboardURLReadingFileURLsOnlyKey: @YES };
|
||||||
NSArray *filePathArray = [pasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]] options:options];
|
NSArray *filePathArray = [pasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]] options:options];
|
||||||
|
NSMutableArray<NSString*> *dirPathArray = [NSMutableArray arrayWithCapacity:[filePathArray count]];
|
||||||
for (NSURL *url in filePathArray) {
|
for (NSURL *url in filePathArray) {
|
||||||
NSString *path = [url path];
|
NSString *path = [url path];
|
||||||
BOOL isDirectory = NO;
|
BOOL isDirectory = NO;
|
||||||
if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDirectory]) {
|
if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDirectory]) {
|
||||||
if (!isDirectory) {
|
if (!isDirectory) path = [path stringByDeletingLastPathComponent];
|
||||||
path = [path stringByDeletingLastPathComponent];
|
if (![dirPathArray containsObject:path]) [dirPathArray addObject:path];
|
||||||
}
|
}
|
||||||
set_cocoa_pending_action(type, [path UTF8String]);
|
}
|
||||||
|
if ([dirPathArray count] > 0) {
|
||||||
|
// Colons are not valid in paths under macOS.
|
||||||
|
set_cocoa_pending_action(type, [[dirPathArray componentsJoinedByString:@":"] UTF8String]);
|
||||||
|
}
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)openFileURLs:(NSPasteboard*)pasteboard
|
||||||
|
userData:(NSString *) UNUSED userData error:(NSError **) UNUSED error {
|
||||||
|
NSDictionary *options = @{ NSPasteboardURLReadingFileURLsOnlyKey: @YES };
|
||||||
|
NSArray *urlArray = [pasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]] options:options];
|
||||||
|
for (NSURL *url in urlArray) {
|
||||||
|
NSString *path = [url path];
|
||||||
|
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
|
||||||
|
set_cocoa_pending_action(LAUNCH_URLS, [[[NSURL fileURLWithPath:path] absoluteString] UTF8String]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return YES;
|
return YES;
|
||||||
@@ -593,6 +613,7 @@ cocoa_create_global_menu(void) {
|
|||||||
MENU_ITEM(shellMenu, @"Close Window", close_window);
|
MENU_ITEM(shellMenu, @"Close Window", close_window);
|
||||||
[shellMenu addItem:[NSMenuItem separatorItem]];
|
[shellMenu addItem:[NSMenuItem separatorItem]];
|
||||||
MENU_ITEM(shellMenu, @"Reset", reset_terminal);
|
MENU_ITEM(shellMenu, @"Reset", reset_terminal);
|
||||||
|
MENU_ITEM(shellMenu, @"Clear to Cursor Line", clear_terminal_and_scrollback);
|
||||||
[shellMenu release];
|
[shellMenu release];
|
||||||
|
|
||||||
NSMenuItem* windowMenuItem =
|
NSMenuItem* windowMenuItem =
|
||||||
@@ -621,10 +642,7 @@ cocoa_create_global_menu(void) {
|
|||||||
keyEquivalent:@""] setTarget:global_menu_target];
|
keyEquivalent:@""] setTarget:global_menu_target];
|
||||||
|
|
||||||
[windowMenu addItem:[NSMenuItem separatorItem]];
|
[windowMenu addItem:[NSMenuItem separatorItem]];
|
||||||
[[windowMenu addItemWithTitle:@"Enter Full Screen"
|
MENU_ITEM(windowMenu, @"Enter Full Screen", toggle_fullscreen);
|
||||||
action:@selector(toggleFullScreen:)
|
|
||||||
keyEquivalent:@"f"]
|
|
||||||
setKeyEquivalentModifierMask:NSEventModifierFlagControl | NSEventModifierFlagCommand];
|
|
||||||
[NSApp setWindowsMenu:windowMenu];
|
[NSApp setWindowsMenu:windowMenu];
|
||||||
[windowMenu release];
|
[windowMenu release];
|
||||||
|
|
||||||
@@ -656,7 +674,16 @@ cocoa_create_global_menu(void) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
cocoa_update_menu_bar_title(PyObject *pytitle) {
|
cocoa_update_menu_bar_title(PyObject *pytitle) {
|
||||||
NSString *title = @(PyUnicode_AsUTF8(pytitle));
|
NSString *title = nil;
|
||||||
|
if (OPT(macos_menubar_title_max_length) > 0 && PyUnicode_GetLength(pytitle) > OPT(macos_menubar_title_max_length)) {
|
||||||
|
static char fmt[64];
|
||||||
|
snprintf(fmt, sizeof(fmt), "%%%ld.%ldU%%s", OPT(macos_menubar_title_max_length), OPT(macos_menubar_title_max_length));
|
||||||
|
DECREF_AFTER_FUNCTION PyObject *st = PyUnicode_FromFormat(fmt, pytitle, "…");
|
||||||
|
if (st) title = @(PyUnicode_AsUTF8(st));
|
||||||
|
} else {
|
||||||
|
title = @(PyUnicode_AsUTF8(pytitle));
|
||||||
|
}
|
||||||
|
if (!title) return;
|
||||||
NSMenu *bar = [NSApp mainMenu];
|
NSMenu *bar = [NSApp mainMenu];
|
||||||
if (title_menu != NULL) {
|
if (title_menu != NULL) {
|
||||||
[bar removeItem:title_menu];
|
[bar removeItem:title_menu];
|
||||||
@@ -814,6 +841,33 @@ cocoa_set_titlebar_color(void *w, color_type titlebar_color)
|
|||||||
} // autoreleasepool
|
} // autoreleasepool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PyObject*
|
||||||
|
cocoa_set_url_handler(PyObject UNUSED *self, PyObject *args) {
|
||||||
|
@autoreleasepool {
|
||||||
|
|
||||||
|
const char *url_scheme = NULL, *bundle_id = NULL;
|
||||||
|
if (!PyArg_ParseTuple(args, "s|z", &url_scheme, &bundle_id)) return NULL;
|
||||||
|
if (!url_scheme || url_scheme[0] == '\0') {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "Empty url scheme");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *scheme = [NSString stringWithUTF8String:url_scheme];
|
||||||
|
NSString *identifier = @"";
|
||||||
|
if (!bundle_id) {
|
||||||
|
identifier = [[NSBundle mainBundle] bundleIdentifier];
|
||||||
|
if (!identifier || identifier.length == 0) identifier = @"net.kovidgoyal.kitty";
|
||||||
|
} else if (bundle_id[0] != '\0') {
|
||||||
|
identifier = [NSString stringWithUTF8String:bundle_id];
|
||||||
|
}
|
||||||
|
// This API has been marked as deprecated. It will need to be replaced when a new approach is available.
|
||||||
|
OSStatus err = LSSetDefaultHandlerForURLScheme((CFStringRef)scheme, (CFStringRef)identifier);
|
||||||
|
if (err == noErr) Py_RETURN_NONE;
|
||||||
|
PyErr_Format(PyExc_OSError, "Failed to set default handler with error code: %d", err);
|
||||||
|
return NULL;
|
||||||
|
} // autoreleasepool
|
||||||
|
}
|
||||||
|
|
||||||
static NSSound *beep_sound = nil;
|
static NSSound *beep_sound = nil;
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@@ -863,6 +917,7 @@ static PyMethodDef module_methods[] = {
|
|||||||
{"cocoa_set_global_shortcut", (PyCFunction)cocoa_set_global_shortcut, METH_VARARGS, ""},
|
{"cocoa_set_global_shortcut", (PyCFunction)cocoa_set_global_shortcut, METH_VARARGS, ""},
|
||||||
{"cocoa_send_notification", (PyCFunction)cocoa_send_notification, METH_VARARGS, ""},
|
{"cocoa_send_notification", (PyCFunction)cocoa_send_notification, METH_VARARGS, ""},
|
||||||
{"cocoa_set_notification_activated_callback", (PyCFunction)set_notification_activated_callback, METH_O, ""},
|
{"cocoa_set_notification_activated_callback", (PyCFunction)set_notification_activated_callback, METH_O, ""},
|
||||||
|
{"cocoa_set_url_handler", (PyCFunction)cocoa_set_url_handler, METH_VARARGS, ""},
|
||||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -285,7 +285,10 @@ def zsh_output_serializer(ans: Completions) -> str:
|
|||||||
has_descriptions = any(matches.values())
|
has_descriptions = any(matches.values())
|
||||||
if has_descriptions or matches.word_transforms:
|
if has_descriptions or matches.word_transforms:
|
||||||
lines.append('compdescriptions=(')
|
lines.append('compdescriptions=(')
|
||||||
sz = max(map(wcswidth, matches.transformed_words()))
|
try:
|
||||||
|
sz = max(map(wcswidth, matches.transformed_words()))
|
||||||
|
except ValueError:
|
||||||
|
sz = 0
|
||||||
limit = min(16, sz)
|
limit = min(16, sz)
|
||||||
for word, desc in matches.transformed_items():
|
for word, desc in matches.transformed_items():
|
||||||
lines.extend(map(shlex.quote, fmt_desc(word, desc, limit)))
|
lines.extend(map(shlex.quote, fmt_desc(word, desc, limit)))
|
||||||
@@ -695,21 +698,27 @@ def find_completions(words: Sequence[str], new_word: bool, entry_points: Iterabl
|
|||||||
ans.add_match_group('Kittens', (k for k in all_kitten_names() if k.startswith('' if len(words) == 2 else words[2])))
|
ans.add_match_group('Kittens', (k for k in all_kitten_names() if k.startswith('' if len(words) == 2 else words[2])))
|
||||||
else:
|
else:
|
||||||
complete_kitten(ans, words[2], words[3:], new_word)
|
complete_kitten(ans, words[2], words[3:], new_word)
|
||||||
|
elif words[1] == 'open':
|
||||||
|
complete_cli(ans, words[2:], new_word, options_for_completion(), complete_kitty_cli_arg)
|
||||||
return ans
|
return ans
|
||||||
if words[0].startswith('+'):
|
if words[0].startswith('+'):
|
||||||
if len(words) == 1:
|
if len(words) == 1:
|
||||||
if new_word:
|
if new_word:
|
||||||
if words[0] == '+kitten':
|
if words[0] == '+kitten':
|
||||||
ans.add_match_group('Kittens', all_kitten_names())
|
ans.add_match_group('Kittens', all_kitten_names())
|
||||||
|
elif words[0] == '+open':
|
||||||
|
complete_cli(ans, words[1:], new_word, options_for_completion(), complete_kitty_cli_arg)
|
||||||
else:
|
else:
|
||||||
prefix = words[0]
|
prefix = words[0]
|
||||||
ans.add_match_group('Entry points', (c for c in namespaced_entry_points if c.startswith(prefix)))
|
ans.add_match_group('Entry points', (c for c in namespaced_entry_points if c.startswith(prefix)))
|
||||||
else:
|
else:
|
||||||
if len(words) == 2 and not new_word:
|
if words[0] == '+kitten':
|
||||||
ans.add_match_group('Kittens', (k for k in all_kitten_names() if k.startswith(words[1])))
|
if len(words) == 2 and not new_word:
|
||||||
else:
|
ans.add_match_group('Kittens', (k for k in all_kitten_names() if k.startswith(words[1])))
|
||||||
if words[0] == '+kitten':
|
else:
|
||||||
complete_kitten(ans, words[1], words[2:], new_word)
|
complete_kitten(ans, words[1], words[2:], new_word)
|
||||||
|
elif words[0] == '+open':
|
||||||
|
complete_cli(ans, words[1:], new_word, options_for_completion(), complete_kitty_cli_arg)
|
||||||
else:
|
else:
|
||||||
complete_cli(ans, words, new_word, options_for_completion(), complete_kitty_cli_arg)
|
complete_cli(ans, words, new_word, options_for_completion(), complete_kitty_cli_arg)
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
|
|||||||
choices = {}
|
choices = {}
|
||||||
imports: Set[Tuple[str, str]] = set()
|
imports: Set[Tuple[str, str]] = set()
|
||||||
tc_imports: Set[Tuple[str, str]] = set()
|
tc_imports: Set[Tuple[str, str]] = set()
|
||||||
|
ki_imports: 're.Pattern[str]' = re.compile(r'\b((?:kittens|kitty).+?)[,\]]')
|
||||||
|
|
||||||
def option_type_as_str(x: Any) -> str:
|
def option_type_as_str(x: Any) -> str:
|
||||||
needs_import = False
|
needs_import = False
|
||||||
@@ -61,6 +62,11 @@ def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
|
|||||||
if isinstance(option, MultiOption):
|
if isinstance(option, MultiOption):
|
||||||
typ = typ[typ.index('[') + 1:-1]
|
typ = typ[typ.index('[') + 1:-1]
|
||||||
typ = typ.replace('Tuple', 'Dict', 1)
|
typ = typ.replace('Tuple', 'Dict', 1)
|
||||||
|
kq = ki_imports.search(typ)
|
||||||
|
if kq is not None:
|
||||||
|
kqi = kq.group(1)
|
||||||
|
kqim, kqii = kqi.rsplit('.', 1)
|
||||||
|
imports.add((kqim, ''))
|
||||||
return func, typ
|
return func, typ
|
||||||
|
|
||||||
is_mutiple_vars = {}
|
is_mutiple_vars = {}
|
||||||
@@ -120,6 +126,8 @@ def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
|
|||||||
tc_imports.add((func.__module__, func.__name__))
|
tc_imports.add((func.__module__, func.__name__))
|
||||||
|
|
||||||
defval = repr(func(option.defval_as_string))
|
defval = repr(func(option.defval_as_string))
|
||||||
|
if len(defval) > 100:
|
||||||
|
defval += ' # noqa'
|
||||||
if option.macos_defval is not unset:
|
if option.macos_defval is not unset:
|
||||||
md = repr(func(option.macos_defval))
|
md = repr(func(option.macos_defval))
|
||||||
defval = f'{md} if is_macos else {defval}'
|
defval = f'{md} if is_macos else {defval}'
|
||||||
@@ -304,7 +312,7 @@ def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
|
|||||||
|
|
||||||
t('')
|
t('')
|
||||||
t('')
|
t('')
|
||||||
t(f'actions = frozenset({tuple(defn.actions)!r})')
|
t(f'actions: typing.FrozenSet[str] = frozenset({tuple(defn.actions)!r})')
|
||||||
t('')
|
t('')
|
||||||
t('')
|
t('')
|
||||||
t('def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:')
|
t('def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:')
|
||||||
@@ -341,14 +349,17 @@ def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
|
|||||||
for mod, name in imports:
|
for mod, name in imports:
|
||||||
mmap.setdefault(mod, []).append(name)
|
mmap.setdefault(mod, []).append(name)
|
||||||
for mod in sorted(mmap):
|
for mod in sorted(mmap):
|
||||||
names = sorted(mmap[mod])
|
names = list(filter(None, sorted(mmap[mod])))
|
||||||
lines = textwrap.wrap(', '.join(names), 100)
|
if names:
|
||||||
if len(lines) == 1:
|
lines = textwrap.wrap(', '.join(names), 100)
|
||||||
s = lines[0]
|
if len(lines) == 1:
|
||||||
|
s = lines[0]
|
||||||
|
else:
|
||||||
|
s = '\n '.join(lines)
|
||||||
|
s = f'(\n {s}\n)'
|
||||||
|
a(f'from {mod} import {s}')
|
||||||
else:
|
else:
|
||||||
s = '\n '.join(lines)
|
s = ''
|
||||||
s = f'(\n {s}\n)'
|
|
||||||
a(f'from {mod} import {s}')
|
|
||||||
if add_module_imports and mod not in seen_mods and mod != s:
|
if add_module_imports and mod not in seen_mods and mod != s:
|
||||||
a(f'import {mod}')
|
a(f'import {mod}')
|
||||||
seen_mods.add(mod)
|
seen_mods.add(mod)
|
||||||
|
|||||||
@@ -49,10 +49,13 @@ def ref_map() -> Dict[str, str]:
|
|||||||
from kitty.actions import get_all_actions
|
from kitty.actions import get_all_actions
|
||||||
ref_map = {
|
ref_map = {
|
||||||
'layouts': f'{website_url("overview")}#layouts',
|
'layouts': f'{website_url("overview")}#layouts',
|
||||||
|
'include': f'{website_url("conf")}#include',
|
||||||
'watchers': f'{website_url("launch")}#watchers',
|
'watchers': f'{website_url("launch")}#watchers',
|
||||||
'sessions': f'{website_url("overview")}#startup-sessions',
|
'sessions': f'{website_url("overview")}#startup-sessions',
|
||||||
'functional': f'{website_url("keyboard-protocol")}#functional-key-definitions',
|
'functional': f'{website_url("keyboard-protocol")}#functional-key-definitions',
|
||||||
|
'ssh_copy_command': f'{website_url("kittens/ssh")}#ssh-copy-command',
|
||||||
'shell_integration': website_url("shell-integration"),
|
'shell_integration': website_url("shell-integration"),
|
||||||
|
'github_discussions': 'https://github.com/kovidgoyal/kitty/discussions',
|
||||||
}
|
}
|
||||||
for actions in get_all_actions().values():
|
for actions in get_all_actions().values():
|
||||||
for ac in actions:
|
for ac in actions:
|
||||||
@@ -66,6 +69,14 @@ def remove_markup(text: str) -> str:
|
|||||||
if m.group(1) == 'ref':
|
if m.group(1) == 'ref':
|
||||||
q = m.group(2).split('<')[-1].rstrip('>')
|
q = m.group(2).split('<')[-1].rstrip('>')
|
||||||
return ref_map()[q]
|
return ref_map()[q]
|
||||||
|
if m.group(1) == 'ac':
|
||||||
|
q = m.group(2).split('<')[-1].rstrip('>')
|
||||||
|
return ref_map()[f'action-{q}']
|
||||||
|
if m.group(1) == 'disc':
|
||||||
|
parts = m.group(2).split('<')
|
||||||
|
t = parts[0].strip()
|
||||||
|
q = parts[-1].rstrip('>')
|
||||||
|
return f'{t} {ref_map()["github_discussions"]}/{q}'
|
||||||
return str(m.group(2))
|
return str(m.group(2))
|
||||||
|
|
||||||
return re.sub(r':([a-zA-Z0-9]+):`(.+?)`', sub, text, flags=re.DOTALL)
|
return re.sub(r':([a-zA-Z0-9]+):`(.+?)`', sub, text, flags=re.DOTALL)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
|
import sys
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import (
|
from typing import (
|
||||||
Any, Callable, Dict, Generator, Generic, Iterable, Iterator, List,
|
Any, Callable, Dict, Generator, Generic, Iterable, Iterator, List,
|
||||||
@@ -155,6 +156,16 @@ class CurrentlyParsing:
|
|||||||
currently_parsing = CurrentlyParsing()
|
currently_parsing = CurrentlyParsing()
|
||||||
|
|
||||||
|
|
||||||
|
class NamedLineIterator:
|
||||||
|
|
||||||
|
def __init__(self, name: str, lines: Iterator[str]):
|
||||||
|
self.lines = lines
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[str]:
|
||||||
|
return self.lines
|
||||||
|
|
||||||
|
|
||||||
def parse_line(
|
def parse_line(
|
||||||
line: str,
|
line: str,
|
||||||
parse_conf_item: ItemParser,
|
parse_conf_item: ItemParser,
|
||||||
@@ -170,11 +181,18 @@ def parse_line(
|
|||||||
log_error(f'Ignoring invalid config line: {line}')
|
log_error(f'Ignoring invalid config line: {line}')
|
||||||
return
|
return
|
||||||
key, val = m.groups()
|
key, val = m.groups()
|
||||||
if key in ('include', 'globinclude'):
|
if key in ('include', 'globinclude', 'envinclude'):
|
||||||
val = os.path.expandvars(os.path.expanduser(val.strip()))
|
val = os.path.expandvars(os.path.expanduser(val.strip()))
|
||||||
if key == 'globinclude':
|
if key == 'globinclude':
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
vals = tuple(map(lambda x: str(os.fspath(x)), Path(base_path_for_includes).glob(val)))
|
vals = tuple(map(lambda x: str(os.fspath(x)), Path(base_path_for_includes).glob(val)))
|
||||||
|
elif key == 'envinclude':
|
||||||
|
from fnmatch import fnmatchcase
|
||||||
|
for x in os.environ:
|
||||||
|
if fnmatchcase(x, val):
|
||||||
|
with currently_parsing.set_file(f'<env var: {x}>'):
|
||||||
|
_parse(NamedLineIterator(base_path_for_includes, iter(os.environ[x].splitlines())), parse_conf_item, ans, accumulate_bad_lines)
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
if not os.path.isabs(val):
|
if not os.path.isabs(val):
|
||||||
val = os.path.join(base_path_for_includes, val)
|
val = os.path.join(base_path_for_includes, val)
|
||||||
@@ -238,7 +256,7 @@ def merge_dicts(defaults: Dict[str, Any], newvals: Dict[str, Any]) -> Dict[str,
|
|||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
def resolve_config(SYSTEM_CONF: str, defconf: str, config_files_on_cmd_line: Sequence[str]) -> Generator[str, None, None]:
|
def resolve_config(SYSTEM_CONF: str, defconf: str, config_files_on_cmd_line: Sequence[str] = ()) -> Generator[str, None, None]:
|
||||||
if config_files_on_cmd_line:
|
if config_files_on_cmd_line:
|
||||||
if 'NONE' not in config_files_on_cmd_line:
|
if 'NONE' not in config_files_on_cmd_line:
|
||||||
yield SYSTEM_CONF
|
yield SYSTEM_CONF
|
||||||
@@ -253,19 +271,25 @@ def load_config(
|
|||||||
parse_config: Callable[[Iterable[str]], Dict[str, Any]],
|
parse_config: Callable[[Iterable[str]], Dict[str, Any]],
|
||||||
merge_configs: Callable[[Dict[str, Any], Dict[str, Any]], Dict[str, Any]],
|
merge_configs: Callable[[Dict[str, Any], Dict[str, Any]], Dict[str, Any]],
|
||||||
*paths: str,
|
*paths: str,
|
||||||
overrides: Optional[Iterable[str]] = None
|
overrides: Optional[Iterable[str]] = None,
|
||||||
|
initialize_defaults: Callable[[Dict[str, Any]], Dict[str, Any]] = lambda x: x,
|
||||||
) -> Tuple[Dict[str, Any], Tuple[str, ...]]:
|
) -> Tuple[Dict[str, Any], Tuple[str, ...]]:
|
||||||
ans = defaults._asdict()
|
ans = initialize_defaults(defaults._asdict())
|
||||||
found_paths = []
|
found_paths = []
|
||||||
for path in paths:
|
for path in paths:
|
||||||
if not path:
|
if not path:
|
||||||
continue
|
continue
|
||||||
try:
|
if path == '-':
|
||||||
with open(path, encoding='utf-8', errors='replace') as f:
|
path = '/dev/stdin'
|
||||||
with currently_parsing.set_file(path):
|
with currently_parsing.set_file(path):
|
||||||
vals = parse_config(f)
|
vals = parse_config(sys.stdin)
|
||||||
except (FileNotFoundError, PermissionError):
|
else:
|
||||||
continue
|
try:
|
||||||
|
with open(path, encoding='utf-8', errors='replace') as f:
|
||||||
|
with currently_parsing.set_file(path):
|
||||||
|
vals = parse_config(f)
|
||||||
|
except (FileNotFoundError, PermissionError):
|
||||||
|
continue
|
||||||
found_paths.append(path)
|
found_paths.append(path)
|
||||||
ans = merge_configs(ans, vals)
|
ans = merge_configs(ans, vals)
|
||||||
if overrides is not None:
|
if overrides is not None:
|
||||||
|
|||||||
@@ -22,16 +22,37 @@ class Version(NamedTuple):
|
|||||||
|
|
||||||
appname: str = 'kitty'
|
appname: str = 'kitty'
|
||||||
kitty_face = '🐱'
|
kitty_face = '🐱'
|
||||||
version: Version = Version(0, 24, 2)
|
version: Version = Version(0, 25, 0)
|
||||||
str_version: str = '.'.join(map(str, version))
|
str_version: str = '.'.join(map(str, version))
|
||||||
_plat = sys.platform.lower()
|
_plat = sys.platform.lower()
|
||||||
is_macos: bool = 'darwin' in _plat
|
is_macos: bool = 'darwin' in _plat
|
||||||
|
is_running_from_develop: bool = False
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, 'frozen', False):
|
||||||
extensions_dir: str = getattr(sys, 'kitty_extensions_dir')
|
extensions_dir: str = getattr(sys, 'kitty_extensions_dir')
|
||||||
kitty_base_dir = os.path.dirname(extensions_dir)
|
|
||||||
if is_macos:
|
def get_frozen_base() -> str:
|
||||||
kitty_base_dir = os.path.dirname(os.path.dirname(kitty_base_dir))
|
global is_running_from_develop
|
||||||
kitty_base_dir = os.path.join(kitty_base_dir, 'kitty')
|
try:
|
||||||
|
from bypy_importer import running_in_develop_mode # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
is_running_from_develop = running_in_develop_mode()
|
||||||
|
|
||||||
|
if is_running_from_develop:
|
||||||
|
q = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
try:
|
||||||
|
if os.path.isdir(q):
|
||||||
|
return q
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
ans = os.path.dirname(extensions_dir)
|
||||||
|
if is_macos:
|
||||||
|
ans = os.path.dirname(os.path.dirname(ans))
|
||||||
|
ans = os.path.join(ans, 'kitty')
|
||||||
|
return ans
|
||||||
|
kitty_base_dir = get_frozen_base()
|
||||||
|
del get_frozen_base
|
||||||
else:
|
else:
|
||||||
kitty_base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
kitty_base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
extensions_dir = os.path.join(kitty_base_dir, 'kitty')
|
extensions_dir = os.path.join(kitty_base_dir, 'kitty')
|
||||||
@@ -116,6 +137,26 @@ def cache_dir() -> str:
|
|||||||
return candidate
|
return candidate
|
||||||
|
|
||||||
|
|
||||||
|
@run_once
|
||||||
|
def runtime_dir() -> str:
|
||||||
|
if 'KITTY_RUNTIME_DIRECTORY' in os.environ:
|
||||||
|
candidate = os.path.abspath(os.environ['KITTY_RUNTIME_DIRECTORY'])
|
||||||
|
elif is_macos:
|
||||||
|
from .fast_data_types import user_cache_dir
|
||||||
|
candidate = user_cache_dir()
|
||||||
|
elif 'XDG_RUNTIME_DIR' in os.environ:
|
||||||
|
candidate = os.path.abspath(os.environ['XDG_RUNTIME_DIR'])
|
||||||
|
else:
|
||||||
|
candidate = f'/run/user/{os.geteuid()}'
|
||||||
|
if not os.path.isdir(candidate) or not os.access(candidate, os.X_OK | os.W_OK | os.R_OK):
|
||||||
|
candidate = os.path.join(cache_dir(), 'run')
|
||||||
|
os.makedirs(candidate, exist_ok=True)
|
||||||
|
import stat
|
||||||
|
if stat.S_IMODE(os.stat(candidate).st_mode) != 0o700:
|
||||||
|
os.chmod(candidate, 0o700)
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
|
||||||
def wakeup() -> None:
|
def wakeup() -> None:
|
||||||
from .fast_data_types import get_boss
|
from .fast_data_types import get_boss
|
||||||
b = get_boss()
|
b = get_boss()
|
||||||
@@ -133,6 +174,9 @@ except KeyError:
|
|||||||
with suppress(Exception):
|
with suppress(Exception):
|
||||||
print('Failed to read login shell via getpwuid() for current user, falling back to /bin/sh', file=sys.stderr)
|
print('Failed to read login shell via getpwuid() for current user, falling back to /bin/sh', file=sys.stderr)
|
||||||
shell_path = '/bin/sh'
|
shell_path = '/bin/sh'
|
||||||
|
# Keep this short as it is limited to 103 bytes on macOS
|
||||||
|
# https://github.com/ansible/ansible/issues/11536#issuecomment-153030743
|
||||||
|
ssh_control_master_template = 'kssh-{kitty_pid}-{ssh_placeholder}'
|
||||||
|
|
||||||
|
|
||||||
def glfw_path(module: str) -> str:
|
def glfw_path(module: str) -> str:
|
||||||
|
|||||||
@@ -156,6 +156,24 @@ close_tty(PyObject *self UNUSED, PyObject *args) {
|
|||||||
|
|
||||||
#undef TTY_ARGS
|
#undef TTY_ARGS
|
||||||
|
|
||||||
|
static PyObject*
|
||||||
|
py_shm_open(PyObject UNUSED *self, PyObject *args) {
|
||||||
|
char *name;
|
||||||
|
int flags, mode = 0600;
|
||||||
|
if (!PyArg_ParseTuple(args, "si|i", &name, &flags, &mode)) return NULL;
|
||||||
|
long fd = safe_shm_open(name, flags, mode);
|
||||||
|
if (fd < 0) return PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, PyTuple_GET_ITEM(args, 0));
|
||||||
|
return PyLong_FromLong(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject*
|
||||||
|
py_shm_unlink(PyObject UNUSED *self, PyObject *args) {
|
||||||
|
char *name;
|
||||||
|
if (!PyArg_ParseTuple(args, "s", &name)) return NULL;
|
||||||
|
if (shm_unlink(name) != 0) return PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, PyTuple_GET_ITEM(args, 0));
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
static PyObject*
|
static PyObject*
|
||||||
wcwidth_wrap(PyObject UNUSED *self, PyObject *chr) {
|
wcwidth_wrap(PyObject UNUSED *self, PyObject *chr) {
|
||||||
return PyLong_FromLong(wcwidth_std(PyLong_AsLong(chr)));
|
return PyLong_FromLong(wcwidth_std(PyLong_AsLong(chr)));
|
||||||
@@ -184,6 +202,8 @@ static PyMethodDef module_methods[] = {
|
|||||||
{"parse_bytes_dump", (PyCFunction)parse_bytes_dump, METH_VARARGS, ""},
|
{"parse_bytes_dump", (PyCFunction)parse_bytes_dump, METH_VARARGS, ""},
|
||||||
{"redirect_std_streams", (PyCFunction)redirect_std_streams, METH_VARARGS, ""},
|
{"redirect_std_streams", (PyCFunction)redirect_std_streams, METH_VARARGS, ""},
|
||||||
{"locale_is_valid", (PyCFunction)locale_is_valid, METH_VARARGS, ""},
|
{"locale_is_valid", (PyCFunction)locale_is_valid, METH_VARARGS, ""},
|
||||||
|
{"shm_open", (PyCFunction)py_shm_open, METH_VARARGS, ""},
|
||||||
|
{"shm_unlink", (PyCFunction)py_shm_unlink, METH_VARARGS, ""},
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
METHODB(user_cache_dir, METH_NOARGS),
|
METHODB(user_cache_dir, METH_NOARGS),
|
||||||
METHODB(process_group_map, METH_NOARGS),
|
METHODB(process_group_map, METH_NOARGS),
|
||||||
@@ -302,6 +322,8 @@ PyInit_fast_data_types(void) {
|
|||||||
s(STRIKETHROUGH, strike); s(DIM, dim); s(DECORATION, decoration);
|
s(STRIKETHROUGH, strike); s(DIM, dim); s(DECORATION, decoration);
|
||||||
#undef s
|
#undef s
|
||||||
PyModule_AddIntConstant(m, "MARK_MASK", MARK_MASK);
|
PyModule_AddIntConstant(m, "MARK_MASK", MARK_MASK);
|
||||||
|
PyModule_AddIntConstant(m, "DECORATION_MASK", DECORATION_MASK);
|
||||||
|
PyModule_AddIntConstant(m, "NUM_UNDERLINE_STYLES", NUM_UNDERLINE_STYLES);
|
||||||
PyModule_AddStringMacro(m, ERROR_PREFIX);
|
PyModule_AddStringMacro(m, ERROR_PREFIX);
|
||||||
#ifdef KITTY_VCS_REV
|
#ifdef KITTY_VCS_REV
|
||||||
PyModule_AddStringMacro(m, KITTY_VCS_REV);
|
PyModule_AddStringMacro(m, KITTY_VCS_REV);
|
||||||
@@ -319,6 +341,14 @@ PyInit_fast_data_types(void) {
|
|||||||
PyModule_AddIntMacro(m, APC);
|
PyModule_AddIntMacro(m, APC);
|
||||||
PyModule_AddIntMacro(m, OSC);
|
PyModule_AddIntMacro(m, OSC);
|
||||||
PyModule_AddIntMacro(m, FILE_TRANSFER_CODE);
|
PyModule_AddIntMacro(m, FILE_TRANSFER_CODE);
|
||||||
|
#ifdef __APPLE__
|
||||||
|
// Apple says its SHM_NAME_MAX but SHM_NAME_MAX is not actually declared in typical CrApple style.
|
||||||
|
// This value is based on experimentation and from qsharedmemory.cpp in Qt
|
||||||
|
PyModule_AddIntConstant(m, "SHM_NAME_MAX", 30);
|
||||||
|
#else
|
||||||
|
// FreeBSD's man page says this is 1023. Linux says its PATH_MAX.
|
||||||
|
PyModule_AddIntConstant(m, "SHM_NAME_MAX", MIN(1023, PATH_MAX));
|
||||||
|
#endif
|
||||||
|
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ typedef union CellAttrs {
|
|||||||
} CellAttrs;
|
} CellAttrs;
|
||||||
#define MARK_MASK (3u)
|
#define MARK_MASK (3u)
|
||||||
#define WIDTH_MASK (3u)
|
#define WIDTH_MASK (3u)
|
||||||
|
#define DECORATION_MASK (7u)
|
||||||
|
#define NUM_UNDERLINE_STYLES (5u)
|
||||||
#define SGR_MASK (~(((CellAttrs){.width=WIDTH_MASK, .mark=MARK_MASK}).val))
|
#define SGR_MASK (~(((CellAttrs){.width=WIDTH_MASK, .mark=MARK_MASK}).val))
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|||||||
@@ -215,6 +215,19 @@ def debug_config(opts: KittyOpts) -> str:
|
|||||||
p(' ', '\n '.join(opts.config_overrides))
|
p(' ', '\n '.join(opts.config_overrides))
|
||||||
compare_opts(opts, p)
|
compare_opts(opts, p)
|
||||||
p()
|
p()
|
||||||
p(green('Environment variable names seen by the kitty process:'))
|
p(green('Important environment variables seen by the kitty process:'))
|
||||||
p('\t' + '\n\t'.join(sorted(os.environ)))
|
|
||||||
|
def penv(k: str) -> None:
|
||||||
|
v = os.environ.get(k)
|
||||||
|
if v is not None:
|
||||||
|
p('\t' + k.ljust(35), styled(v, dim=True))
|
||||||
|
|
||||||
|
for k in (
|
||||||
|
'PATH LANG KITTY_CONFIG_DIRECTORY KITTY_CACHE_DIRECTORY VISUAL EDITOR SHELL'
|
||||||
|
' GLFW_IM_MODULE KITTY_WAYLAND_DETECT_MODIFIERS DISPLAY WAYLAND_DISPLAY USER XCURSOR_SIZE'
|
||||||
|
).split():
|
||||||
|
penv(k)
|
||||||
|
for k in os.environ:
|
||||||
|
if k.startswith('LC_') or k.startswith('XDG_'):
|
||||||
|
penv(k)
|
||||||
return out.getvalue()
|
return out.getvalue()
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ play_canberra_sound(const char *which_sound, const char *event_id, bool is_path,
|
|||||||
which_type, which_sound,
|
which_type, which_sound,
|
||||||
"event.description", event_id,
|
"event.description", event_id,
|
||||||
"media.role", media_role,
|
"media.role", media_role,
|
||||||
|
"canberra.cache-control", "permanent",
|
||||||
NULL
|
NULL
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from kitty.marks import MarkerFunc
|
|||||||
from kitty.options.types import Options
|
from kitty.options.types import Options
|
||||||
|
|
||||||
# Constants {{{
|
# Constants {{{
|
||||||
|
SHM_NAME_MAX: int
|
||||||
MOUSE_SELECTION_LINE: int
|
MOUSE_SELECTION_LINE: int
|
||||||
MOUSE_SELECTION_EXTEND: int
|
MOUSE_SELECTION_EXTEND: int
|
||||||
MOUSE_SELECTION_NORMAL: int
|
MOUSE_SELECTION_NORMAL: int
|
||||||
@@ -256,6 +257,8 @@ GRAPHICS_PREMULT_PROGRAM: int
|
|||||||
GRAPHICS_PROGRAM: int
|
GRAPHICS_PROGRAM: int
|
||||||
MARK: int
|
MARK: int
|
||||||
MARK_MASK: int
|
MARK_MASK: int
|
||||||
|
DECORATION_MASK: int
|
||||||
|
NUM_UNDERLINE_STYLES: int
|
||||||
OSC: int
|
OSC: int
|
||||||
FILE_TRANSFER_CODE: int
|
FILE_TRANSFER_CODE: int
|
||||||
REVERSE: int
|
REVERSE: int
|
||||||
@@ -736,6 +739,10 @@ def cocoa_get_lang() -> Optional[str]:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def cocoa_set_url_handler(url_scheme: str, bundle_id: Optional[str]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def locale_is_valid(name: str) -> bool:
|
def locale_is_valid(name: str) -> bool:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -961,7 +968,8 @@ def set_font_data(
|
|||||||
descriptor_for_idx: Callable[[int], Tuple[FontObject, bool, bool]],
|
descriptor_for_idx: Callable[[int], Tuple[FontObject, bool, bool]],
|
||||||
bold: int, italic: int, bold_italic: int, num_symbol_fonts: int,
|
bold: int, italic: int, bold_italic: int, num_symbol_fonts: int,
|
||||||
symbol_maps: Tuple[Tuple[int, int, int], ...], font_sz_in_pts: float,
|
symbol_maps: Tuple[Tuple[int, int, int], ...], font_sz_in_pts: float,
|
||||||
font_feature_settings: Dict[str, Tuple[FontFeature, ...]]
|
font_feature_settings: Dict[str, Tuple[FontFeature, ...]],
|
||||||
|
narrow_symbols: Tuple[Tuple[int, int, int], ...],
|
||||||
) -> None:
|
) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -980,7 +988,7 @@ class HistoryBuf:
|
|||||||
def as_text(self, callback: Callable[[str], None], as_ansi: bool, insert_wrap_markers: bool) -> None:
|
def as_text(self, callback: Callable[[str], None], as_ansi: bool, insert_wrap_markers: bool) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def pagerhist_as_text(self) -> str:
|
def pagerhist_as_text(self, upto_output_start: bool = False) -> str:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def pagerhist_as_bytes(self) -> bytes:
|
def pagerhist_as_bytes(self) -> bytes:
|
||||||
@@ -1023,6 +1031,7 @@ class Screen:
|
|||||||
cursor_key_mode: bool
|
cursor_key_mode: bool
|
||||||
auto_repeat_enabled: bool
|
auto_repeat_enabled: bool
|
||||||
render_unfocused_cursor: int
|
render_unfocused_cursor: int
|
||||||
|
last_reported_cwd: Optional[str]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -1049,6 +1058,9 @@ class Screen:
|
|||||||
def line(self, num: int) -> Line:
|
def line(self, num: int) -> Line:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def visual_line(self, num: int) -> Line:
|
||||||
|
pass
|
||||||
|
|
||||||
def draw(self, text: str) -> None:
|
def draw(self, text: str) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -1076,7 +1088,7 @@ class Screen:
|
|||||||
def has_selection(self) -> bool:
|
def has_selection(self) -> bool:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def text_for_selection(self) -> Tuple[str, ...]:
|
def text_for_selection(self, ansi: bool, strip_trailing_spaces: bool) -> Tuple[str, ...]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def is_rectangle_select(self) -> bool:
|
def is_rectangle_select(self) -> bool:
|
||||||
@@ -1103,6 +1115,9 @@ class Screen:
|
|||||||
def reverse_scroll(self, amt: int, fill_from_scrollback: bool = False) -> bool:
|
def reverse_scroll(self, amt: int, fill_from_scrollback: bool = False) -> bool:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def scroll_prompt_to_bottom(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
def clear_selection(self) -> None:
|
def clear_selection(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -1124,10 +1139,10 @@ class Screen:
|
|||||||
as_text_non_visual = as_text
|
as_text_non_visual = as_text
|
||||||
as_text_alternate = as_text
|
as_text_alternate = as_text
|
||||||
|
|
||||||
def cmd_output(self, which: int, callback: Callable[[str], None], as_ansi: bool, insert_wrap_markers: bool) -> None:
|
def cmd_output(self, which: int, callback: Callable[[str], None], as_ansi: bool, insert_wrap_markers: bool) -> bool:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def scroll_until_cursor(self) -> None:
|
def scroll_until_cursor_prompt(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
@@ -1136,6 +1151,9 @@ class Screen:
|
|||||||
def erase_in_display(self, how: int = 0, private: bool = False) -> None:
|
def erase_in_display(self, how: int = 0, private: bool = False) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def clear_scrollback(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
def focus_changed(self, focused: bool) -> bool:
|
def focus_changed(self, focused: bool) -> bool:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -1343,5 +1361,17 @@ def set_os_window_title(os_window_id: int, title: str) -> None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def update_ime_position_for_window(window_id: int, force: bool = False, lost_focus: bool = False) -> bool:
|
def get_os_window_title(os_window_id: int) -> Optional[str]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def update_ime_position_for_window(window_id: int, force: bool = False, update_focus: int = 0) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def shm_open(name: str, flags: int, mode: int = 0o600) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def shm_unlink(name: str) -> None:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
#include "charsets.h"
|
#include "charsets.h"
|
||||||
#include "glyph-cache.h"
|
#include "glyph-cache.h"
|
||||||
|
|
||||||
#define MISSING_GLYPH 4
|
#define MISSING_GLYPH (NUM_UNDERLINE_STYLES + 2)
|
||||||
#define MAX_NUM_EXTRA_GLYPHS_PUA 4u
|
#define MAX_NUM_EXTRA_GLYPHS_PUA 4u
|
||||||
|
|
||||||
typedef void (*send_sprite_to_gpu_func)(FONTS_DATA_HANDLE fg, unsigned int, unsigned int, unsigned int, pixel*);
|
typedef void (*send_sprite_to_gpu_func)(FONTS_DATA_HANDLE fg, unsigned int, unsigned int, unsigned int, pixel*);
|
||||||
@@ -50,8 +50,8 @@ typedef struct {
|
|||||||
size_t font_idx;
|
size_t font_idx;
|
||||||
} SymbolMap;
|
} SymbolMap;
|
||||||
|
|
||||||
static SymbolMap *symbol_maps = NULL;
|
static SymbolMap *symbol_maps = NULL, *narrow_symbols = NULL;
|
||||||
static size_t num_symbol_maps = 0;
|
static size_t num_symbol_maps = 0, num_narrow_symbols = 0;
|
||||||
|
|
||||||
typedef enum { SPACER_STRATEGY_UNKNOWN, SPACERS_BEFORE, SPACERS_AFTER, SPACERS_IOSEVKA } SpacerStrategy;
|
typedef enum { SPACER_STRATEGY_UNKNOWN, SPACERS_BEFORE, SPACERS_AFTER, SPACERS_IOSEVKA } SpacerStrategy;
|
||||||
|
|
||||||
@@ -492,8 +492,8 @@ in_symbol_maps(FontGroup *fg, char_type ch) {
|
|||||||
// - BOX_FONT
|
// - BOX_FONT
|
||||||
// - an index in the fonts list
|
// - an index in the fonts list
|
||||||
static ssize_t
|
static ssize_t
|
||||||
font_for_cell(FontGroup *fg, CPUCell *cpu_cell, GPUCell *gpu_cell, bool *is_fallback_font, bool *is_emoji_presentation) {
|
font_for_cell(FontGroup *fg, CPUCell *cpu_cell, GPUCell *gpu_cell, bool *is_main_font, bool *is_emoji_presentation) {
|
||||||
*is_fallback_font = false;
|
*is_main_font = false;
|
||||||
*is_emoji_presentation = false;
|
*is_emoji_presentation = false;
|
||||||
START_ALLOW_CASE_RANGE
|
START_ALLOW_CASE_RANGE
|
||||||
ssize_t ans;
|
ssize_t ans;
|
||||||
@@ -510,6 +510,7 @@ START_ALLOW_CASE_RANGE
|
|||||||
case 0x1fba0 ... 0x1fbae:
|
case 0x1fba0 ... 0x1fbae:
|
||||||
return BOX_FONT;
|
return BOX_FONT;
|
||||||
default:
|
default:
|
||||||
|
*is_emoji_presentation = has_emoji_presentation(cpu_cell, gpu_cell);
|
||||||
ans = in_symbol_maps(fg, cpu_cell->ch);
|
ans = in_symbol_maps(fg, cpu_cell->ch);
|
||||||
if (ans > -1) return ans;
|
if (ans > -1) return ans;
|
||||||
switch(gpu_cell->attrs.bold | (gpu_cell->attrs.italic << 1)) {
|
switch(gpu_cell->attrs.bold | (gpu_cell->attrs.italic << 1)) {
|
||||||
@@ -523,9 +524,7 @@ START_ALLOW_CASE_RANGE
|
|||||||
ans = fg->bi_font_idx; break;
|
ans = fg->bi_font_idx; break;
|
||||||
}
|
}
|
||||||
if (ans < 0) ans = fg->medium_font_idx;
|
if (ans < 0) ans = fg->medium_font_idx;
|
||||||
*is_emoji_presentation = has_emoji_presentation(cpu_cell, gpu_cell);
|
if (!*is_emoji_presentation && has_cell_text(fg->fonts + ans, cpu_cell)) { *is_main_font = true; return ans; }
|
||||||
if (!*is_emoji_presentation && has_cell_text(fg->fonts + ans, cpu_cell)) return ans;
|
|
||||||
*is_fallback_font = true;
|
|
||||||
return fallback_font(fg, cpu_cell, gpu_cell);
|
return fallback_font(fg, cpu_cell, gpu_cell);
|
||||||
}
|
}
|
||||||
END_ALLOW_CASE_RANGE
|
END_ALLOW_CASE_RANGE
|
||||||
@@ -852,6 +851,19 @@ detect_spacer_strategy(hb_font_t *hbf, Font *font) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If spacer_strategy is still default, check ### glyph to to confirm strategy
|
||||||
|
// https://github.com/kovidgoyal/kitty/issues/4721
|
||||||
|
if (font->spacer_strategy == SPACERS_BEFORE) {
|
||||||
|
cpu_cells[0].ch = '#'; cpu_cells[1].ch = '#'; cpu_cells[2].ch = '#';
|
||||||
|
shape(cpu_cells, gpu_cells, arraysz(cpu_cells), hbf, font, false);
|
||||||
|
if (G(num_glyphs) > 1) {
|
||||||
|
glyph_index glyph_id = G(info)[G(num_glyphs) - 1].codepoint;
|
||||||
|
bool is_special = is_special_glyph(glyph_id, font, &G(current_cell_data));
|
||||||
|
bool is_empty = is_special && is_empty_glyph(glyph_id, font);
|
||||||
|
if (is_empty) font->spacer_strategy = SPACERS_AFTER;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static LigatureType
|
static LigatureType
|
||||||
@@ -963,7 +975,7 @@ group_normal(Font *font, hb_font_t *hbf) {
|
|||||||
*
|
*
|
||||||
* We rely on the cluster numbers from harfbuzz to tell us how many unicode codepoints a glyph corresponds to.
|
* We rely on the cluster numbers from harfbuzz to tell us how many unicode codepoints a glyph corresponds to.
|
||||||
* Then we check if the glyph is a ligature glyph (is_special_glyph) and if it is an empty glyph.
|
* Then we check if the glyph is a ligature glyph (is_special_glyph) and if it is an empty glyph.
|
||||||
* We detect if the font uses EMPTY glyphs before or after ligature glyphs (1. or 3. above) by checking what it does for ===.
|
* We detect if the font uses EMPTY glyphs before or after ligature glyphs (1. or 3. above) by checking what it does for === and ###.
|
||||||
* Finally we look at the glyph name. These five datapoints give us enough information to satisfy the constraint above,
|
* Finally we look at the glyph name. These five datapoints give us enough information to satisfy the constraint above,
|
||||||
* for a wide variety of fonts.
|
* for a wide variety of fonts.
|
||||||
*/
|
*/
|
||||||
@@ -1220,6 +1232,17 @@ is_non_emoji_dingbat(char_type ch) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static unsigned int
|
||||||
|
cell_cap_for_codepoint(const char_type cp) {
|
||||||
|
unsigned int ans = UINT_MAX;
|
||||||
|
for (size_t i = 0; i < num_narrow_symbols; i++) {
|
||||||
|
SymbolMap *sm = narrow_symbols + i;
|
||||||
|
if (sm->left <= cp && cp <= sm->right) ans = sm->font_idx;
|
||||||
|
}
|
||||||
|
return ans;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
render_line(FONTS_DATA_HANDLE fg_, Line *line, index_type lnum, Cursor *cursor, DisableLigature disable_ligature_strategy) {
|
render_line(FONTS_DATA_HANDLE fg_, Line *line, index_type lnum, Cursor *cursor, DisableLigature disable_ligature_strategy) {
|
||||||
#define RENDER if (run_font_idx != NO_FONT && i > first_cell_in_run) { \
|
#define RENDER if (run_font_idx != NO_FONT && i > first_cell_in_run) { \
|
||||||
@@ -1237,12 +1260,12 @@ render_line(FONTS_DATA_HANDLE fg_, Line *line, index_type lnum, Cursor *cursor,
|
|||||||
if (prev_width == 2) { prev_width = 0; continue; }
|
if (prev_width == 2) { prev_width = 0; continue; }
|
||||||
CPUCell *cpu_cell = line->cpu_cells + i;
|
CPUCell *cpu_cell = line->cpu_cells + i;
|
||||||
GPUCell *gpu_cell = line->gpu_cells + i;
|
GPUCell *gpu_cell = line->gpu_cells + i;
|
||||||
bool is_fallback_font, is_emoji_presentation;
|
bool is_main_font, is_emoji_presentation;
|
||||||
ssize_t cell_font_idx = font_for_cell(fg, cpu_cell, gpu_cell, &is_fallback_font, &is_emoji_presentation);
|
ssize_t cell_font_idx = font_for_cell(fg, cpu_cell, gpu_cell, &is_main_font, &is_emoji_presentation);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
cell_font_idx != MISSING_FONT &&
|
cell_font_idx != MISSING_FONT &&
|
||||||
((is_fallback_font && !is_emoji_presentation && is_symbol(cpu_cell->ch)) || (cell_font_idx != BOX_FONT && (is_private_use(cpu_cell->ch))) || is_non_emoji_dingbat(cpu_cell->ch))
|
((!is_main_font && !is_emoji_presentation && is_symbol(cpu_cell->ch)) || (cell_font_idx != BOX_FONT && (is_private_use(cpu_cell->ch))) || is_non_emoji_dingbat(cpu_cell->ch))
|
||||||
) {
|
) {
|
||||||
unsigned int desired_cells = 1;
|
unsigned int desired_cells = 1;
|
||||||
if (cell_font_idx > 0) {
|
if (cell_font_idx > 0) {
|
||||||
@@ -1252,6 +1275,7 @@ render_line(FONTS_DATA_HANDLE fg_, Line *line, index_type lnum, Cursor *cursor,
|
|||||||
int width = get_glyph_width(font->face, glyph_id);
|
int width = get_glyph_width(font->face, glyph_id);
|
||||||
desired_cells = (unsigned int)ceilf((float)width / fg->cell_width);
|
desired_cells = (unsigned int)ceilf((float)width / fg->cell_width);
|
||||||
}
|
}
|
||||||
|
desired_cells = MIN(desired_cells, cell_cap_for_codepoint(cpu_cell->ch));
|
||||||
|
|
||||||
unsigned int num_spaces = 0;
|
unsigned int num_spaces = 0;
|
||||||
while (
|
while (
|
||||||
@@ -1304,6 +1328,7 @@ render_simple_text(FONTS_DATA_HANDLE fg_, const char *text) {
|
|||||||
static void
|
static void
|
||||||
clear_symbol_maps(void) {
|
clear_symbol_maps(void) {
|
||||||
if (symbol_maps) { free(symbol_maps); symbol_maps = NULL; num_symbol_maps = 0; }
|
if (symbol_maps) { free(symbol_maps); symbol_maps = NULL; num_symbol_maps = 0; }
|
||||||
|
if (narrow_symbols) { free(narrow_symbols); narrow_symbols = NULL; num_narrow_symbols = 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -1312,26 +1337,33 @@ typedef struct {
|
|||||||
|
|
||||||
DescriptorIndices descriptor_indices = {0};
|
DescriptorIndices descriptor_indices = {0};
|
||||||
|
|
||||||
static PyObject*
|
static bool
|
||||||
set_font_data(PyObject UNUSED *m, PyObject *args) {
|
set_symbol_maps(SymbolMap **maps, size_t *num, const PyObject *sm) {
|
||||||
PyObject *sm;
|
*num = PyTuple_GET_SIZE(sm);
|
||||||
Py_CLEAR(box_drawing_function); Py_CLEAR(prerender_function); Py_CLEAR(descriptor_for_idx); Py_CLEAR(font_feature_settings);
|
*maps = calloc(*num, sizeof(SymbolMap));
|
||||||
if (!PyArg_ParseTuple(args, "OOOIIIIO!dO",
|
if (*maps == NULL) { PyErr_NoMemory(); return false; }
|
||||||
&box_drawing_function, &prerender_function, &descriptor_for_idx,
|
for (size_t s = 0; s < *num; s++) {
|
||||||
&descriptor_indices.bold, &descriptor_indices.italic, &descriptor_indices.bi, &descriptor_indices.num_symbol_fonts,
|
|
||||||
&PyTuple_Type, &sm, &OPT(font_size), &font_feature_settings)) return NULL;
|
|
||||||
Py_INCREF(box_drawing_function); Py_INCREF(prerender_function); Py_INCREF(descriptor_for_idx); Py_INCREF(font_feature_settings);
|
|
||||||
free_font_groups();
|
|
||||||
clear_symbol_maps();
|
|
||||||
num_symbol_maps = PyTuple_GET_SIZE(sm);
|
|
||||||
symbol_maps = calloc(num_symbol_maps, sizeof(SymbolMap));
|
|
||||||
if (symbol_maps == NULL) return PyErr_NoMemory();
|
|
||||||
for (size_t s = 0; s < num_symbol_maps; s++) {
|
|
||||||
unsigned int left, right, font_idx;
|
unsigned int left, right, font_idx;
|
||||||
SymbolMap *x = symbol_maps + s;
|
SymbolMap *x = *maps + s;
|
||||||
if (!PyArg_ParseTuple(PyTuple_GET_ITEM(sm, s), "III", &left, &right, &font_idx)) return NULL;
|
if (!PyArg_ParseTuple(PyTuple_GET_ITEM(sm, s), "III", &left, &right, &font_idx)) return NULL;
|
||||||
x->left = left; x->right = right; x->font_idx = font_idx;
|
x->left = left; x->right = right; x->font_idx = font_idx;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject*
|
||||||
|
set_font_data(PyObject UNUSED *m, PyObject *args) {
|
||||||
|
PyObject *sm, *ns;
|
||||||
|
Py_CLEAR(box_drawing_function); Py_CLEAR(prerender_function); Py_CLEAR(descriptor_for_idx); Py_CLEAR(font_feature_settings);
|
||||||
|
if (!PyArg_ParseTuple(args, "OOOIIIIO!dOO!",
|
||||||
|
&box_drawing_function, &prerender_function, &descriptor_for_idx,
|
||||||
|
&descriptor_indices.bold, &descriptor_indices.italic, &descriptor_indices.bi, &descriptor_indices.num_symbol_fonts,
|
||||||
|
&PyTuple_Type, &sm, &OPT(font_size), &font_feature_settings, &PyTuple_Type, &ns)) return NULL;
|
||||||
|
Py_INCREF(box_drawing_function); Py_INCREF(prerender_function); Py_INCREF(descriptor_for_idx); Py_INCREF(font_feature_settings);
|
||||||
|
free_font_groups();
|
||||||
|
clear_symbol_maps();
|
||||||
|
set_symbol_maps(&symbol_maps, &num_symbol_maps, sm);
|
||||||
|
set_symbol_maps(&narrow_symbols, &num_narrow_symbols, ns);
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1398,6 +1430,11 @@ initialize_font_group(FontGroup *fg) {
|
|||||||
}
|
}
|
||||||
#undef I
|
#undef I
|
||||||
calc_cell_metrics(fg);
|
calc_cell_metrics(fg);
|
||||||
|
// rescale the symbol_map faces for the desired cell height, this is how fallback fonts are sized as well
|
||||||
|
for (size_t i = 0; i < descriptor_indices.num_symbol_fonts; i++) {
|
||||||
|
Font *font = fg->fonts + i + fg->first_symbol_font_idx;
|
||||||
|
set_size_for_face(font->face, fg->cell_height, true, (FONTS_DATA_HANDLE)fg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -115,8 +115,13 @@ def find_best_match(family: str, bold: bool = False, italic: bool = False, monos
|
|||||||
return candidates[0]
|
return candidates[0]
|
||||||
|
|
||||||
# Use fc-match to see if we can find a monospaced font that matches family
|
# Use fc-match to see if we can find a monospaced font that matches family
|
||||||
for spacing in (FC_MONO, FC_DUAL):
|
# When aliases are defined, spacing can cause the incorrect font to be
|
||||||
possibility = fc_match(family, False, False, spacing)
|
# returned, so check with and without spacing and use the one that matches.
|
||||||
|
mono_possibility = fc_match(family, False, False, FC_MONO)
|
||||||
|
dual_possibility = fc_match(family, False, False, FC_DUAL)
|
||||||
|
any_possibility = fc_match(family, False, False, 0)
|
||||||
|
tries = (dual_possibility, mono_possibility) if any_possibility == dual_possibility else (mono_possibility, dual_possibility)
|
||||||
|
for possibility in tries:
|
||||||
for key, map_key in (('postscript_name', 'ps_map'), ('full_name', 'full_map'), ('family', 'family_map')):
|
for key, map_key in (('postscript_name', 'ps_map'), ('full_name', 'full_map'), ('family', 'family_map')):
|
||||||
val: Optional[str] = cast(Optional[str], possibility.get(key))
|
val: Optional[str] = cast(Optional[str], possibility.get(key))
|
||||||
if val:
|
if val:
|
||||||
|
|||||||
@@ -14,13 +14,14 @@ from kitty.constants import is_macos
|
|||||||
from kitty.fast_data_types import (
|
from kitty.fast_data_types import (
|
||||||
Screen, create_test_font_group, get_fallback_font, set_font_data,
|
Screen, create_test_font_group, get_fallback_font, set_font_data,
|
||||||
set_options, set_send_sprite_to_gpu, sprite_map_set_limits,
|
set_options, set_send_sprite_to_gpu, sprite_map_set_limits,
|
||||||
test_render_line, test_shape
|
test_render_line, test_shape, NUM_UNDERLINE_STYLES
|
||||||
)
|
)
|
||||||
from kitty.fonts.box_drawing import (
|
from kitty.fonts.box_drawing import (
|
||||||
BufType, distribute_dots, render_box_char, render_missing_glyph
|
BufType, distribute_dots, render_box_char, render_missing_glyph
|
||||||
)
|
)
|
||||||
from kitty.options.types import Options, defaults
|
from kitty.options.types import Options, defaults
|
||||||
from kitty.typing import CoreTextFont, FontConfigPattern
|
from kitty.typing import CoreTextFont, FontConfigPattern
|
||||||
|
from kitty.types import _T
|
||||||
from kitty.utils import log_error
|
from kitty.utils import log_error
|
||||||
|
|
||||||
if is_macos:
|
if is_macos:
|
||||||
@@ -50,10 +51,9 @@ def font_for_family(family: str) -> Tuple[FontObject, bool, bool]:
|
|||||||
return font_for_family_fontconfig(family)
|
return font_for_family_fontconfig(family)
|
||||||
|
|
||||||
|
|
||||||
Range = Tuple[Tuple[int, int], str]
|
def merge_ranges(
|
||||||
|
a: Tuple[Tuple[int, int], _T], b: Tuple[Tuple[int, int], _T], priority_map: Dict[Tuple[int, int], int]
|
||||||
|
) -> Generator[Tuple[Tuple[int, int], _T], None, None]:
|
||||||
def merge_ranges(a: Range, b: Range, priority_map: Dict[Tuple[int, int], int]) -> Generator[Range, None, None]:
|
|
||||||
a_start, a_end = a[0]
|
a_start, a_end = a[0]
|
||||||
b_start, b_end = b[0]
|
b_start, b_end = b[0]
|
||||||
a_val, b_val = a[1], b[1]
|
a_val, b_val = a[1], b[1]
|
||||||
@@ -95,7 +95,7 @@ def merge_ranges(a: Range, b: Range, priority_map: Dict[Tuple[int, int], int]) -
|
|||||||
after_range = ((b_end + 1, a_end), a_val)
|
after_range = ((b_end + 1, a_end), a_val)
|
||||||
after_range_prio = a_prio
|
after_range_prio = a_prio
|
||||||
# check if the before, mid and after ranges can be coalesced
|
# check if the before, mid and after ranges can be coalesced
|
||||||
ranges: List[Range] = []
|
ranges: List[Tuple[Tuple[int, int], _T]] = []
|
||||||
priorities: List[int] = []
|
priorities: List[int] = []
|
||||||
for rq, prio in ((before_range, before_range_prio), (mid_range, mid_range_prio), (after_range, after_range_prio)):
|
for rq, prio in ((before_range, before_range_prio), (mid_range, mid_range_prio), (after_range, after_range_prio)):
|
||||||
if rq is None:
|
if rq is None:
|
||||||
@@ -117,7 +117,7 @@ def merge_ranges(a: Range, b: Range, priority_map: Dict[Tuple[int, int], int]) -
|
|||||||
yield from ranges
|
yield from ranges
|
||||||
|
|
||||||
|
|
||||||
def coalesce_symbol_maps(maps: Dict[Tuple[int, int], str]) -> Dict[Tuple[int, int], str]:
|
def coalesce_symbol_maps(maps: Dict[Tuple[int, int], _T]) -> Dict[Tuple[int, int], _T]:
|
||||||
if not maps:
|
if not maps:
|
||||||
return maps
|
return maps
|
||||||
priority_map = {r: i for i, r in enumerate(maps.keys())}
|
priority_map = {r: i for i, r in enumerate(maps.keys())}
|
||||||
@@ -155,6 +155,10 @@ def create_symbol_map(opts: Options) -> Tuple[Tuple[int, int, int], ...]:
|
|||||||
return sm
|
return sm
|
||||||
|
|
||||||
|
|
||||||
|
def create_narrow_symbols(opts: Options) -> Tuple[Tuple[int, int, int], ...]:
|
||||||
|
return tuple((a, b, v) for (a, b), v in coalesce_symbol_maps(opts.narrow_symbols).items())
|
||||||
|
|
||||||
|
|
||||||
def descriptor_for_idx(idx: int) -> Tuple[FontObject, bool, bool]:
|
def descriptor_for_idx(idx: int) -> Tuple[FontObject, bool, bool]:
|
||||||
return current_faces[idx]
|
return current_faces[idx]
|
||||||
|
|
||||||
@@ -193,6 +197,7 @@ def set_font_family(opts: Optional[Options] = None, override_font_size: Optional
|
|||||||
current_faces.append((font_map[k], 'b' in k, 'i' in k))
|
current_faces.append((font_map[k], 'b' in k, 'i' in k))
|
||||||
before = len(current_faces)
|
before = len(current_faces)
|
||||||
sm = create_symbol_map(opts)
|
sm = create_symbol_map(opts)
|
||||||
|
ns = create_narrow_symbols(opts)
|
||||||
num_symbol_fonts = len(current_faces) - before
|
num_symbol_fonts = len(current_faces) - before
|
||||||
font_features = {}
|
font_features = {}
|
||||||
for face, _, _ in current_faces:
|
for face, _, _ in current_faces:
|
||||||
@@ -203,7 +208,7 @@ def set_font_family(opts: Optional[Options] = None, override_font_size: Optional
|
|||||||
set_font_data(
|
set_font_data(
|
||||||
render_box_drawing, prerender_function, descriptor_for_idx,
|
render_box_drawing, prerender_function, descriptor_for_idx,
|
||||||
indices['bold'], indices['italic'], indices['bi'], num_symbol_fonts,
|
indices['bold'], indices['italic'], indices['bi'], num_symbol_fonts,
|
||||||
sm, sz, font_features
|
sm, sz, font_features, ns
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -400,8 +405,15 @@ def prerender_function(
|
|||||||
render_cursor, cursor_beam_thickness=cursor_beam_thickness,
|
render_cursor, cursor_beam_thickness=cursor_beam_thickness,
|
||||||
cursor_underline_thickness=cursor_underline_thickness, cell_width=cell_width,
|
cursor_underline_thickness=cursor_underline_thickness, cell_width=cell_width,
|
||||||
cell_height=cell_height, dpi_x=dpi_x, dpi_y=dpi_y)
|
cell_height=cell_height, dpi_x=dpi_x, dpi_y=dpi_y)
|
||||||
cells = f(1), f(2), f(3), f(4), f(5), f(0, strikethrough=True), f(missing=True), c(1), c(2), c(3)
|
# If you change the mapping of these cells you will need to change
|
||||||
return tuple(map(ctypes.addressof, cells)), cells
|
# NUM_UNDERLINE_STYLES and BEAM_IDX in shader.c and STRIKE_SPRITE_INDEX in
|
||||||
|
# window.py and MISSING_GLYPH in font.c
|
||||||
|
cells = list(map(f, range(1, NUM_UNDERLINE_STYLES + 1))) # underline sprites
|
||||||
|
cells.append(f(0, strikethrough=True)) # strikethrough sprite
|
||||||
|
cells.append(f(missing=True)) # missing glyph
|
||||||
|
cells.extend((c(1), c(2), c(3))) # cursor glyphs
|
||||||
|
tcells = tuple(cells)
|
||||||
|
return tuple(map(ctypes.addressof, tcells)), tcells
|
||||||
|
|
||||||
|
|
||||||
def render_box_drawing(codepoint: int, cell_width: int, cell_height: int, dpi: float) -> Tuple[int, CBufType]:
|
def render_box_drawing(codepoint: int, cell_width: int, cell_height: int, dpi: float) -> Tuple[int, CBufType]:
|
||||||
|
|||||||
@@ -143,11 +143,6 @@ static bool
|
|||||||
set_font_size(Face *self, FT_F26Dot6 char_width, FT_F26Dot6 char_height, FT_UInt xdpi, FT_UInt ydpi, unsigned int desired_height, unsigned int cell_height) {
|
set_font_size(Face *self, FT_F26Dot6 char_width, FT_F26Dot6 char_height, FT_UInt xdpi, FT_UInt ydpi, unsigned int desired_height, unsigned int cell_height) {
|
||||||
int error = FT_Set_Char_Size(self->face, 0, char_height, xdpi, ydpi);
|
int error = FT_Set_Char_Size(self->face, 0, char_height, xdpi, ydpi);
|
||||||
if (!error) {
|
if (!error) {
|
||||||
unsigned int ch = calc_cell_height(self, false);
|
|
||||||
if (desired_height && ch != desired_height) {
|
|
||||||
FT_F26Dot6 h = (FT_F26Dot6)floor((double)char_height * (double)desired_height / (double) ch);
|
|
||||||
return set_font_size(self, 0, h, xdpi, ydpi, 0, cell_height);
|
|
||||||
}
|
|
||||||
self->char_width = char_width; self->char_height = char_height; self->xdpi = xdpi; self->ydpi = ydpi;
|
self->char_width = char_width; self->char_height = char_height; self->xdpi = xdpi; self->ydpi = ydpi;
|
||||||
if (self->harfbuzz_font != NULL) hb_ft_font_changed(self->harfbuzz_font);
|
if (self->harfbuzz_font != NULL) hb_ft_font_changed(self->harfbuzz_font);
|
||||||
} else {
|
} else {
|
||||||
@@ -215,6 +210,14 @@ init_ft_face(Face *self, PyObject *path, int hinting, int hintstyle, FONTS_DATA_
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void*
|
||||||
|
set_load_error(const char *path, int error) {
|
||||||
|
char buf[2048];
|
||||||
|
snprintf(buf, sizeof(buf), "Failed to load face from path: %s with error:", path);
|
||||||
|
set_freetype_error(buf, error);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
PyObject*
|
PyObject*
|
||||||
face_from_descriptor(PyObject *descriptor, FONTS_DATA_HANDLE fg) {
|
face_from_descriptor(PyObject *descriptor, FONTS_DATA_HANDLE fg) {
|
||||||
#define D(key, conv, missing_ok) { \
|
#define D(key, conv, missing_ok) { \
|
||||||
@@ -235,7 +238,7 @@ face_from_descriptor(PyObject *descriptor, FONTS_DATA_HANDLE fg) {
|
|||||||
Face *self = (Face *)Face_Type.tp_alloc(&Face_Type, 0);
|
Face *self = (Face *)Face_Type.tp_alloc(&Face_Type, 0);
|
||||||
if (self != NULL) {
|
if (self != NULL) {
|
||||||
int error = FT_New_Face(library, path, index, &(self->face));
|
int error = FT_New_Face(library, path, index, &(self->face));
|
||||||
if(error) { set_freetype_error("Failed to load face, with error:", error); Py_CLEAR(self); return NULL; }
|
if(error) { Py_CLEAR(self); return set_load_error(path, error); }
|
||||||
if (!init_ft_face(self, PyDict_GetItemString(descriptor, "path"), hinting, hint_style, fg)) { Py_CLEAR(self); return NULL; }
|
if (!init_ft_face(self, PyDict_GetItemString(descriptor, "path"), hinting, hint_style, fg)) { Py_CLEAR(self); return NULL; }
|
||||||
}
|
}
|
||||||
return (PyObject*)self;
|
return (PyObject*)self;
|
||||||
@@ -246,7 +249,7 @@ native_face_from_path(const char *path, int index) {
|
|||||||
int error;
|
int error;
|
||||||
FT_Face ans;
|
FT_Face ans;
|
||||||
error = FT_New_Face(library, path, index, &ans);
|
error = FT_New_Face(library, path, index, &ans);
|
||||||
if (error) { set_freetype_error("Failed to load face, with error:", error); return NULL; }
|
if (error) return set_load_error(path, error);
|
||||||
return ans;
|
return ans;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +259,7 @@ face_from_path(const char *path, int index, FONTS_DATA_HANDLE fg) {
|
|||||||
if (ans == NULL) return NULL;
|
if (ans == NULL) return NULL;
|
||||||
int error;
|
int error;
|
||||||
error = FT_New_Face(library, path, index, &ans->face);
|
error = FT_New_Face(library, path, index, &ans->face);
|
||||||
if (error) { set_freetype_error("Failed to load face, with error:", error); ans->face = NULL; return NULL; }
|
if (error) { ans->face = NULL; return set_load_error(path, error); }
|
||||||
if (!init_ft_face(ans, Py_None, true, 3, fg)) { Py_CLEAR(ans); return NULL; }
|
if (!init_ft_face(ans, Py_None, true, 3, fg)) { Py_CLEAR(ans); return NULL; }
|
||||||
return (PyObject*)ans;
|
return (PyObject*)ans;
|
||||||
}
|
}
|
||||||
@@ -522,19 +525,20 @@ detect_right_edge(ProcessedBitmap *ans) {
|
|||||||
static bool
|
static bool
|
||||||
render_color_bitmap(Face *self, int glyph_id, ProcessedBitmap *ans, unsigned int cell_width, unsigned int cell_height, unsigned int num_cells, unsigned int baseline UNUSED) {
|
render_color_bitmap(Face *self, int glyph_id, ProcessedBitmap *ans, unsigned int cell_width, unsigned int cell_height, unsigned int num_cells, unsigned int baseline UNUSED) {
|
||||||
unsigned short best = 0, diff = USHRT_MAX;
|
unsigned short best = 0, diff = USHRT_MAX;
|
||||||
const short limit = self->face->num_fixed_sizes;
|
if (self->face->num_fixed_sizes > 0) {
|
||||||
for (short i = 0; i < limit; i++) {
|
const short limit = self->face->num_fixed_sizes;
|
||||||
unsigned short w = self->face->available_sizes[i].width;
|
for (short i = 0; i < limit; i++) {
|
||||||
unsigned short d = w > (unsigned short)cell_width ? w - (unsigned short)cell_width : (unsigned short)cell_width - w;
|
unsigned short w = self->face->available_sizes[i].width;
|
||||||
if (d < diff) {
|
unsigned short d = w > (unsigned short)cell_width ? w - (unsigned short)cell_width : (unsigned short)cell_width - w;
|
||||||
diff = d;
|
if (d < diff) {
|
||||||
best = i;
|
diff = d;
|
||||||
|
best = i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
FT_Error error = FT_Select_Size(self->face, best);
|
||||||
FT_Error error = FT_Select_Size(self->face, best);
|
if (error) { set_freetype_error("Failed to set char size for non-scalable font, with error:", error); return false; }
|
||||||
if (error) { set_freetype_error("Failed to set char size for non-scalable font, with error:", error); return false; }
|
} else FT_Set_Char_Size(self->face, 0, self->char_height, self->xdpi, self->ydpi);
|
||||||
if (!load_glyph(self, glyph_id, FT_LOAD_COLOR)) return false;
|
if (!load_glyph(self, glyph_id, FT_LOAD_COLOR | FT_LOAD_RENDER)) return false;
|
||||||
FT_Set_Char_Size(self->face, 0, self->char_height, self->xdpi, self->ydpi);
|
|
||||||
FT_Bitmap *bitmap = &self->face->glyph->bitmap;
|
FT_Bitmap *bitmap = &self->face->glyph->bitmap;
|
||||||
if (bitmap->pixel_mode != FT_PIXEL_MODE_BGRA) return false;
|
if (bitmap->pixel_mode != FT_PIXEL_MODE_BGRA) return false;
|
||||||
ans->buf = bitmap->buffer;
|
ans->buf = bitmap->buffer;
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ get_load_flags(int hinting, int hintstyle, int base) {
|
|||||||
static bool
|
static bool
|
||||||
load_font(FontConfigFace *info, Face *ans) {
|
load_font(FontConfigFace *info, Face *ans) {
|
||||||
ans->freetype = native_face_from_path(info->path, info->index);
|
ans->freetype = native_face_from_path(info->path, info->index);
|
||||||
if (!ans->freetype) return false;
|
if (!ans->freetype || PyErr_Occurred()) return false;
|
||||||
ans->hb = hb_ft_font_create(ans->freetype, NULL);
|
ans->hb = hb_ft_font_create(ans->freetype, NULL);
|
||||||
if (!ans->hb) { PyErr_NoMemory(); return false; }
|
if (!ans->hb) { PyErr_NoMemory(); return false; }
|
||||||
ans->hinting = info->hinting; ans->hintstyle = info->hintstyle;
|
ans->hinting = info->hinting; ans->hintstyle = info->hintstyle;
|
||||||
@@ -134,7 +134,7 @@ choose_bitmap_size(FT_Face face, FT_UInt desired_height) {
|
|||||||
static void
|
static void
|
||||||
set_pixel_size(RenderCtx *ctx, Face *face, FT_UInt sz, bool get_metrics UNUSED) {
|
set_pixel_size(RenderCtx *ctx, Face *face, FT_UInt sz, bool get_metrics UNUSED) {
|
||||||
if (sz != face->pixel_size) {
|
if (sz != face->pixel_size) {
|
||||||
if (FT_HAS_COLOR(face->freetype)) sz = choose_bitmap_size(face->freetype, font_units_to_pixels_y(main_face.freetype, main_face.freetype->height));
|
if (face->freetype->num_fixed_sizes > 0 && FT_HAS_COLOR(face->freetype)) choose_bitmap_size(face->freetype, font_units_to_pixels_y(main_face.freetype, main_face.freetype->height));
|
||||||
else FT_Set_Pixel_Sizes(face->freetype, sz, sz);
|
else FT_Set_Pixel_Sizes(face->freetype, sz, sz);
|
||||||
hb_ft_font_changed(face->hb);
|
hb_ft_font_changed(face->hb);
|
||||||
hb_ft_font_set_load_flags(face->hb, get_load_flags(face->hinting, face->hintstyle, FT_LOAD_DEFAULT));
|
hb_ft_font_set_load_flags(face->hb, get_load_flags(face->hinting, face->hintstyle, FT_LOAD_DEFAULT));
|
||||||
@@ -268,6 +268,7 @@ find_fallback_font_for(RenderCtx *ctx, char_type codep, char_type next_codep) {
|
|||||||
ensure_space_for(&main_face, fallbacks, Face, main_face.count + 1, capacity, 8, true);
|
ensure_space_for(&main_face, fallbacks, Face, main_face.count + 1, capacity, 8, true);
|
||||||
Face *ans = main_face.fallbacks + main_face.count;
|
Face *ans = main_face.fallbacks + main_face.count;
|
||||||
bool ok = load_font(&q, ans);
|
bool ok = load_font(&q, ans);
|
||||||
|
if (PyErr_Occurred()) PyErr_Print();
|
||||||
free(q.path);
|
free(q.path);
|
||||||
if (!ok) return NULL;
|
if (!ok) return NULL;
|
||||||
main_face.count++;
|
main_face.count++;
|
||||||
@@ -304,7 +305,7 @@ render_run(RenderCtx *ctx, RenderState *rs) {
|
|||||||
hb_glyph_info_t *info = hb_buffer_get_glyph_infos(hb_buffer, NULL);
|
hb_glyph_info_t *info = hb_buffer_get_glyph_infos(hb_buffer, NULL);
|
||||||
hb_glyph_position_t *positions = hb_buffer_get_glyph_positions(hb_buffer, NULL);
|
hb_glyph_position_t *positions = hb_buffer_get_glyph_positions(hb_buffer, NULL);
|
||||||
int baseline = font_units_to_pixels_y(face, face->ascender);
|
int baseline = font_units_to_pixels_y(face, face->ascender);
|
||||||
int load_flags = get_load_flags(rs->current_face->hinting, rs->current_face->hintstyle, has_color ? FT_LOAD_COLOR : FT_LOAD_RENDER);
|
int load_flags = get_load_flags(rs->current_face->hinting, rs->current_face->hintstyle, FT_LOAD_RENDER | (has_color ? FT_LOAD_COLOR : 0));
|
||||||
float pos = rs->x;
|
float pos = rs->x;
|
||||||
unsigned int limit = len;
|
unsigned int limit = len;
|
||||||
for (unsigned int i = 0; i < len; i++) {
|
for (unsigned int i = 0; i < len; i++) {
|
||||||
@@ -556,10 +557,10 @@ create_freetype_render_context(const char *family, bool bold, bool italic) {
|
|||||||
RenderCtx *ctx = calloc(1, sizeof(RenderCtx));
|
RenderCtx *ctx = calloc(1, sizeof(RenderCtx));
|
||||||
main_face_family.name = family ? strdup(family) : NULL;
|
main_face_family.name = family ? strdup(family) : NULL;
|
||||||
main_face_family.bold = bold; main_face_family.italic = italic;
|
main_face_family.bold = bold; main_face_family.italic = italic;
|
||||||
if (!information_for_font_family(main_face_family.name, main_face_family.bold, main_face_family.italic, &main_face_information)) return false;
|
if (!information_for_font_family(main_face_family.name, main_face_family.bold, main_face_family.italic, &main_face_information)) return NULL;
|
||||||
if (!load_font(&main_face_information, &main_face)) return false;
|
if (!load_font(&main_face_information, &main_face)) return NULL;
|
||||||
hb_buffer = hb_buffer_create();
|
hb_buffer = hb_buffer_create();
|
||||||
if (!hb_buffer) { PyErr_NoMemory(); return false; }
|
if (!hb_buffer) { PyErr_NoMemory(); return NULL; }
|
||||||
ctx->created = true;
|
ctx->created = true;
|
||||||
return (FreeTypeRenderCtx)ctx;
|
return (FreeTypeRenderCtx)ctx;
|
||||||
}
|
}
|
||||||
|
|||||||
6
kitty/glfw-wrapper.c
generated
6
kitty/glfw-wrapper.c
generated
@@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
|
// generated by glfw.py DO NOT edit
|
||||||
|
|
||||||
#define GFW_EXTERN
|
#define GFW_EXTERN
|
||||||
#include "data-types.h"
|
#include "data-types.h"
|
||||||
#include "glfw-wrapper.h"
|
#include "glfw-wrapper.h"
|
||||||
@@ -405,8 +407,8 @@ load_glfw(const char* path) {
|
|||||||
*(void **) (&glfwSetCocoaTextInputFilter_impl) = dlsym(handle, "glfwSetCocoaTextInputFilter");
|
*(void **) (&glfwSetCocoaTextInputFilter_impl) = dlsym(handle, "glfwSetCocoaTextInputFilter");
|
||||||
if (glfwSetCocoaTextInputFilter_impl == NULL) dlerror(); // clear error indicator
|
if (glfwSetCocoaTextInputFilter_impl == NULL) dlerror(); // clear error indicator
|
||||||
|
|
||||||
*(void **) (&glfwSetCocoaFileOpenCallback_impl) = dlsym(handle, "glfwSetCocoaFileOpenCallback");
|
*(void **) (&glfwSetCocoaURLOpenCallback_impl) = dlsym(handle, "glfwSetCocoaURLOpenCallback");
|
||||||
if (glfwSetCocoaFileOpenCallback_impl == NULL) dlerror(); // clear error indicator
|
if (glfwSetCocoaURLOpenCallback_impl == NULL) dlerror(); // clear error indicator
|
||||||
|
|
||||||
*(void **) (&glfwSetCocoaToggleFullscreenIntercept_impl) = dlsym(handle, "glfwSetCocoaToggleFullscreenIntercept");
|
*(void **) (&glfwSetCocoaToggleFullscreenIntercept_impl) = dlsym(handle, "glfwSetCocoaToggleFullscreenIntercept");
|
||||||
if (glfwSetCocoaToggleFullscreenIntercept_impl == NULL) dlerror(); // clear error indicator
|
if (glfwSetCocoaToggleFullscreenIntercept_impl == NULL) dlerror(); // clear error indicator
|
||||||
|
|||||||
8
kitty/glfw-wrapper.h
generated
8
kitty/glfw-wrapper.h
generated
@@ -1597,7 +1597,7 @@ typedef struct GLFWgamepadstate
|
|||||||
|
|
||||||
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int,unsigned long);
|
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int,unsigned long);
|
||||||
typedef bool (* GLFWapplicationshouldhandlereopenfun)(int);
|
typedef bool (* GLFWapplicationshouldhandlereopenfun)(int);
|
||||||
typedef bool (* GLFWhandlefileopen)(const char*);
|
typedef bool (* GLFWhandleurlopen)(const char*);
|
||||||
typedef void (* GLFWapplicationwillfinishlaunchingfun)(void);
|
typedef void (* GLFWapplicationwillfinishlaunchingfun)(void);
|
||||||
typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
|
typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
|
||||||
typedef void (* GLFWcocoarenderframefun)(GLFWwindow*);
|
typedef void (* GLFWcocoarenderframefun)(GLFWwindow*);
|
||||||
@@ -2124,9 +2124,9 @@ typedef GLFWcocoatextinputfilterfun (*glfwSetCocoaTextInputFilter_func)(GLFWwind
|
|||||||
GFW_EXTERN glfwSetCocoaTextInputFilter_func glfwSetCocoaTextInputFilter_impl;
|
GFW_EXTERN glfwSetCocoaTextInputFilter_func glfwSetCocoaTextInputFilter_impl;
|
||||||
#define glfwSetCocoaTextInputFilter glfwSetCocoaTextInputFilter_impl
|
#define glfwSetCocoaTextInputFilter glfwSetCocoaTextInputFilter_impl
|
||||||
|
|
||||||
typedef GLFWhandlefileopen (*glfwSetCocoaFileOpenCallback_func)(GLFWhandlefileopen);
|
typedef GLFWhandleurlopen (*glfwSetCocoaURLOpenCallback_func)(GLFWhandleurlopen);
|
||||||
GFW_EXTERN glfwSetCocoaFileOpenCallback_func glfwSetCocoaFileOpenCallback_impl;
|
GFW_EXTERN glfwSetCocoaURLOpenCallback_func glfwSetCocoaURLOpenCallback_impl;
|
||||||
#define glfwSetCocoaFileOpenCallback glfwSetCocoaFileOpenCallback_impl
|
#define glfwSetCocoaURLOpenCallback glfwSetCocoaURLOpenCallback_impl
|
||||||
|
|
||||||
typedef GLFWcocoatogglefullscreenfun (*glfwSetCocoaToggleFullscreenIntercept_func)(GLFWwindow*, GLFWcocoatogglefullscreenfun);
|
typedef GLFWcocoatogglefullscreenfun (*glfwSetCocoaToggleFullscreenIntercept_func)(GLFWwindow*, GLFWcocoatogglefullscreenfun);
|
||||||
GFW_EXTERN glfwSetCocoaToggleFullscreenIntercept_func glfwSetCocoaToggleFullscreenIntercept_impl;
|
GFW_EXTERN glfwSetCocoaToggleFullscreenIntercept_func glfwSetCocoaToggleFullscreenIntercept_impl;
|
||||||
|
|||||||
77
kitty/glfw.c
77
kitty/glfw.c
@@ -25,6 +25,7 @@ extern void cocoa_set_titlebar_appearance(void *w, unsigned int theme);
|
|||||||
extern void cocoa_set_titlebar_color(void *w, color_type color);
|
extern void cocoa_set_titlebar_color(void *w, color_type color);
|
||||||
extern bool cocoa_alt_option_key_pressed(unsigned long);
|
extern bool cocoa_alt_option_key_pressed(unsigned long);
|
||||||
extern void cocoa_toggle_secure_keyboard_entry(void);
|
extern void cocoa_toggle_secure_keyboard_entry(void);
|
||||||
|
extern void cocoa_update_menu_bar_title(PyObject*);
|
||||||
extern size_t cocoa_get_workspace_ids(void *w, size_t *workspace_ids, size_t array_sz);
|
extern size_t cocoa_get_workspace_ids(void *w, size_t *workspace_ids, size_t array_sz);
|
||||||
extern monotonic_t cocoa_cursor_blink_interval(void);
|
extern monotonic_t cocoa_cursor_blink_interval(void);
|
||||||
|
|
||||||
@@ -46,6 +47,43 @@ get_platform_dependent_config_values(void *glfw_window) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
strip_csi_(const char *title, char *buf, size_t bufsz) {
|
||||||
|
enum { NORMAL, IN_ESC, IN_CSI} state = NORMAL;
|
||||||
|
char *dest = buf, *last = &buf[bufsz-1];
|
||||||
|
*dest = 0; *last = 0;
|
||||||
|
|
||||||
|
for (; *title && dest < last; title++) {
|
||||||
|
const char ch = *title;
|
||||||
|
switch (state) {
|
||||||
|
case NORMAL: {
|
||||||
|
if (ch == 0x1b) { state = IN_ESC; }
|
||||||
|
else *(dest++) = ch;
|
||||||
|
} break;
|
||||||
|
case IN_ESC: {
|
||||||
|
if (ch == '[') { state = IN_CSI; }
|
||||||
|
else { state = NORMAL; }
|
||||||
|
} break;
|
||||||
|
case IN_CSI: {
|
||||||
|
if (!(('0' <= ch && ch <= '9') || ch == ';' || ch == ':')) state = NORMAL;
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*dest = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
update_menu_bar_title(PyObject *title UNUSED) {
|
||||||
|
#ifdef __APPLE__
|
||||||
|
static char buf[2048];
|
||||||
|
strip_csi_(PyUnicode_AsUTF8(title), buf, arraysz(buf));
|
||||||
|
DECREF_AFTER_FUNCTION PyObject *stitle = PyUnicode_FromString(buf);
|
||||||
|
cocoa_update_menu_bar_title(stitle);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
request_tick_callback(void) {
|
request_tick_callback(void) {
|
||||||
glfwPostEmptyEvent();
|
glfwPostEmptyEvent();
|
||||||
@@ -429,8 +467,8 @@ static void get_window_dpi(GLFWwindow *w, double *x, double *y);
|
|||||||
|
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
static bool
|
static bool
|
||||||
apple_file_open_callback(const char* filepath) {
|
apple_url_open_callback(const char* url) {
|
||||||
set_cocoa_pending_action(OPEN_FILE, filepath);
|
set_cocoa_pending_action(LAUNCH_URLS, url);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,7 +476,9 @@ extern bool cocoa_render_line_of_text(const char *text, const color_type fg, con
|
|||||||
|
|
||||||
bool
|
bool
|
||||||
draw_window_title(OSWindow *window UNUSED, const char *text, color_type fg, color_type bg, uint8_t *output_buf, size_t width, size_t height) {
|
draw_window_title(OSWindow *window UNUSED, const char *text, color_type fg, color_type bg, uint8_t *output_buf, size_t width, size_t height) {
|
||||||
return cocoa_render_line_of_text(text, fg, bg, output_buf, width, height);
|
static char buf[2048];
|
||||||
|
strip_csi_(text, buf, arraysz(buf));
|
||||||
|
return cocoa_render_line_of_text(buf, fg, bg, output_buf, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
extern uint8_t* render_single_ascii_char_as_mask(const char ch, size_t *result_width, size_t *result_height);
|
extern uint8_t* render_single_ascii_char_as_mask(const char ch, size_t *result_width, size_t *result_height);
|
||||||
@@ -484,10 +524,12 @@ draw_text_callback(GLFWwindow *window, const char *text, uint32_t fg, uint32_t b
|
|||||||
bool
|
bool
|
||||||
draw_window_title(OSWindow *window, const char *text, color_type fg, color_type bg, uint8_t *output_buf, size_t width, size_t height) {
|
draw_window_title(OSWindow *window, const char *text, color_type fg, color_type bg, uint8_t *output_buf, size_t width, size_t height) {
|
||||||
if (!ensure_csd_title_render_ctx()) return false;
|
if (!ensure_csd_title_render_ctx()) return false;
|
||||||
|
static char buf[2048];
|
||||||
|
strip_csi_(text, buf, arraysz(buf));
|
||||||
unsigned px_sz = (unsigned)(window->fonts_data->font_sz_in_pts * window->fonts_data->logical_dpi_y / 72.);
|
unsigned px_sz = (unsigned)(window->fonts_data->font_sz_in_pts * window->fonts_data->logical_dpi_y / 72.);
|
||||||
px_sz = MIN(px_sz, 3 * height / 4);
|
px_sz = MIN(px_sz, 3 * height / 4);
|
||||||
#define RGB2BGR(x) (x & 0xFF000000) | ((x & 0xFF0000) >> 16) | (x & 0x00FF00) | ((x & 0x0000FF) << 16)
|
#define RGB2BGR(x) (x & 0xFF000000) | ((x & 0xFF0000) >> 16) | (x & 0x00FF00) | ((x & 0x0000FF) << 16)
|
||||||
bool ok = render_single_line(csd_title_render_ctx, text, px_sz, RGB2BGR(fg), RGB2BGR(bg), output_buf, width, height, 0, 0, 0);
|
bool ok = render_single_line(csd_title_render_ctx, buf, px_sz, RGB2BGR(fg), RGB2BGR(bg), output_buf, width, height, 0, 0, 0);
|
||||||
#undef RGB2BGR
|
#undef RGB2BGR
|
||||||
if (!ok && PyErr_Occurred()) PyErr_Print();
|
if (!ok && PyErr_Occurred()) PyErr_Print();
|
||||||
return ok;
|
return ok;
|
||||||
@@ -1003,7 +1045,7 @@ glfw_init(PyObject UNUSED *self, PyObject *args) {
|
|||||||
PyObject *ans = glfwInit(monotonic_start_time) ? Py_True: Py_False;
|
PyObject *ans = glfwInit(monotonic_start_time) ? Py_True: Py_False;
|
||||||
if (ans == Py_True) {
|
if (ans == Py_True) {
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
glfwSetCocoaFileOpenCallback(apple_file_open_callback);
|
glfwSetCocoaURLOpenCallback(apple_url_open_callback);
|
||||||
#else
|
#else
|
||||||
glfwSetDrawTextFunction(draw_text_callback);
|
glfwSetDrawTextFunction(draw_text_callback);
|
||||||
#endif
|
#endif
|
||||||
@@ -1286,31 +1328,6 @@ request_window_attention(id_type kitty_window_id, bool audio_bell) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
|
||||||
strip_csi_(const char *title, char *buf, size_t bufsz) {
|
|
||||||
enum { NORMAL, IN_ESC, IN_CSI} state = NORMAL;
|
|
||||||
char *dest = buf, *last = &buf[bufsz-1];
|
|
||||||
*dest = 0; *last = 0;
|
|
||||||
|
|
||||||
for (; *title && dest < last; title++) {
|
|
||||||
const char ch = *title;
|
|
||||||
switch (state) {
|
|
||||||
case NORMAL: {
|
|
||||||
if (ch == 0x1b) { state = IN_ESC; }
|
|
||||||
else *(dest++) = ch;
|
|
||||||
} break;
|
|
||||||
case IN_ESC: {
|
|
||||||
if (ch == '[') { state = IN_CSI; }
|
|
||||||
else { state = NORMAL; }
|
|
||||||
} break;
|
|
||||||
case IN_CSI: {
|
|
||||||
if (!(('0' <= ch && ch <= '9') || ch == ';' || ch == ':')) state = NORMAL;
|
|
||||||
} break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*dest = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
set_os_window_title(OSWindow *w, const char *title) {
|
set_os_window_title(OSWindow *w, const char *title) {
|
||||||
static char buf[2048];
|
static char buf[2048];
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ known_extensions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
text_mimes = ('application/javascript', 'application/x-sh', 'application/json')
|
text_mimes = ('application/javascript', 'application/x-sh', 'application/x-shellscript', 'application/json')
|
||||||
|
|
||||||
|
|
||||||
def is_rc_file(path: str) -> bool:
|
def is_rc_file(path: str) -> bool:
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ pagerhist_push(HistoryBuf *self, ANSIBuf *as_ansi_buf) {
|
|||||||
const GPUCell *prev_cell = NULL;
|
const GPUCell *prev_cell = NULL;
|
||||||
Line l = {.xnum=self->xnum};
|
Line l = {.xnum=self->xnum};
|
||||||
init_line(self, self->start_of_data, &l);
|
init_line(self, self->start_of_data, &l);
|
||||||
line_as_ansi(&l, as_ansi_buf, &prev_cell);
|
line_as_ansi(&l, as_ansi_buf, &prev_cell, 0, l.xnum, 0);
|
||||||
if (ringbuf_bytes_used(ph->ringbuf) && !l.attrs.continued) pagerhist_write_bytes(ph, (const uint8_t*)"\n", 1);
|
if (ringbuf_bytes_used(ph->ringbuf) && !l.attrs.continued) pagerhist_write_bytes(ph, (const uint8_t*)"\n", 1);
|
||||||
pagerhist_write_bytes(ph, (const uint8_t*)"\x1b[m", 3);
|
pagerhist_write_bytes(ph, (const uint8_t*)"\x1b[m", 3);
|
||||||
if (pagerhist_write_ucs4(ph, as_ansi_buf->buf, as_ansi_buf->len)) pagerhist_write_bytes(ph, (const uint8_t*)"\r", 1);
|
if (pagerhist_write_ucs4(ph, as_ansi_buf->buf, as_ansi_buf->len)) pagerhist_write_bytes(ph, (const uint8_t*)"\r", 1);
|
||||||
@@ -324,7 +324,7 @@ as_ansi(HistoryBuf *self, PyObject *callback) {
|
|||||||
if (i < self->count - 1) {
|
if (i < self->count - 1) {
|
||||||
l.attrs.continued = attrptr(self, index_of(self, i + 1))->continued;
|
l.attrs.continued = attrptr(self, index_of(self, i + 1))->continued;
|
||||||
} else l.attrs.continued = false;
|
} else l.attrs.continued = false;
|
||||||
line_as_ansi(&l, &output, &prev_cell);
|
line_as_ansi(&l, &output, &prev_cell, 0, l.xnum, 0);
|
||||||
if (!l.attrs.continued) {
|
if (!l.attrs.continued) {
|
||||||
ensure_space_for(&output, buf, Py_UCS4, output.len + 1, capacity, 2048, false);
|
ensure_space_for(&output, buf, Py_UCS4, output.len + 1, capacity, 2048, false);
|
||||||
output.buf[output.len++] = 10; // 10 = \n
|
output.buf[output.len++] = 10; // 10 = \n
|
||||||
@@ -418,8 +418,21 @@ pagerhist_write(HistoryBuf *self, PyObject *what) {
|
|||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const uint8_t*
|
||||||
|
reverse_find(const uint8_t *haystack, size_t haystack_sz, const uint8_t *needle) {
|
||||||
|
const size_t needle_sz = strlen((const char*)needle);
|
||||||
|
if (!needle_sz || needle_sz > haystack_sz) return NULL;
|
||||||
|
const uint8_t *p = haystack + haystack_sz - (needle_sz - 1);
|
||||||
|
while (--p >= haystack) {
|
||||||
|
if (*p == needle[0] && memcmp(p, needle, MIN(needle_sz, haystack_sz - (p - haystack))) == 0) return p;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
static PyObject*
|
static PyObject*
|
||||||
pagerhist_as_bytes(HistoryBuf *self, PyObject *args UNUSED) {
|
pagerhist_as_bytes(HistoryBuf *self, PyObject *args) {
|
||||||
|
int upto_output_start = 0;
|
||||||
|
if (!PyArg_ParseTuple(args, "|p", &upto_output_start)) return NULL;
|
||||||
#define ph self->pagerhist
|
#define ph self->pagerhist
|
||||||
if (!ph || !ringbuf_bytes_used(ph->ringbuf)) return PyBytes_FromStringAndSize("", 0);
|
if (!ph || !ringbuf_bytes_used(ph->ringbuf)) return PyBytes_FromStringAndSize("", 0);
|
||||||
pagerhist_ensure_start_is_valid_utf8(ph);
|
pagerhist_ensure_start_is_valid_utf8(ph);
|
||||||
@@ -433,6 +446,13 @@ pagerhist_as_bytes(HistoryBuf *self, PyObject *args UNUSED) {
|
|||||||
uint8_t *buf = (uint8_t*)PyBytes_AS_STRING(ans);
|
uint8_t *buf = (uint8_t*)PyBytes_AS_STRING(ans);
|
||||||
ringbuf_memcpy_from(buf, ph->ringbuf, sz);
|
ringbuf_memcpy_from(buf, ph->ringbuf, sz);
|
||||||
if (!l.attrs.continued) buf[sz-1] = '\n';
|
if (!l.attrs.continued) buf[sz-1] = '\n';
|
||||||
|
if (upto_output_start) {
|
||||||
|
const uint8_t *p = reverse_find(buf, sz, (const uint8_t*)"\x1b]133;C\x1b\\");
|
||||||
|
if (p) {
|
||||||
|
PyObject *t = PyBytes_FromStringAndSize((const char*)p, sz - (p - buf));
|
||||||
|
Py_DECREF(ans); ans = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
return ans;
|
return ans;
|
||||||
#undef ph
|
#undef ph
|
||||||
}
|
}
|
||||||
@@ -501,8 +521,8 @@ static PyMethodDef methods[] = {
|
|||||||
METHOD(as_ansi, METH_O)
|
METHOD(as_ansi, METH_O)
|
||||||
METHODB(pagerhist_write, METH_O),
|
METHODB(pagerhist_write, METH_O),
|
||||||
METHODB(pagerhist_rewrap, METH_O),
|
METHODB(pagerhist_rewrap, METH_O),
|
||||||
METHODB(pagerhist_as_text, METH_NOARGS),
|
METHODB(pagerhist_as_text, METH_VARARGS),
|
||||||
METHODB(pagerhist_as_bytes, METH_NOARGS),
|
METHODB(pagerhist_as_bytes, METH_VARARGS),
|
||||||
METHODB(as_text, METH_VARARGS),
|
METHODB(as_text, METH_VARARGS),
|
||||||
METHOD(dirty_lines, METH_NOARGS)
|
METHOD(dirty_lines, METH_NOARGS)
|
||||||
METHOD(push, METH_VARARGS)
|
METHOD(push, METH_VARARGS)
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ from .child import Child
|
|||||||
from .cli import parse_args
|
from .cli import parse_args
|
||||||
from .cli_stub import LaunchCLIOptions
|
from .cli_stub import LaunchCLIOptions
|
||||||
from .constants import kitty_exe, shell_path
|
from .constants import kitty_exe, shell_path
|
||||||
from .fast_data_types import patch_color_profiles, set_clipboard_string
|
from .fast_data_types import (
|
||||||
|
get_os_window_title, patch_color_profiles, set_clipboard_string
|
||||||
|
)
|
||||||
from .options.utils import env as parse_env
|
from .options.utils import env as parse_env
|
||||||
from .tabs import Tab
|
from .tabs import Tab, TabManager
|
||||||
from .types import run_once
|
from .types import run_once
|
||||||
from .utils import log_error, resolve_custom_file, set_primary_selection, which
|
from .utils import log_error, resolve_custom_file, set_primary_selection, which
|
||||||
from .window import Watchers, Window
|
from .window import CwdRequest, CwdRequestType, Watchers, Window
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
@@ -32,12 +34,14 @@ def options_spec() -> str:
|
|||||||
return '''
|
return '''
|
||||||
--window-title --title
|
--window-title --title
|
||||||
The title to set for the new window. By default, title is controlled by the
|
The title to set for the new window. By default, title is controlled by the
|
||||||
child process.
|
child process. The special value :code:`current` will copy the title from the currently
|
||||||
|
active window.
|
||||||
|
|
||||||
|
|
||||||
--tab-title
|
--tab-title
|
||||||
The title for the new tab if launching in a new tab. By default, the title
|
The title for the new tab if launching in a new tab. By default, the title
|
||||||
of the active window in the tab is used as the tab title.
|
of the active window in the tab is used as the tab title. The special value
|
||||||
|
:code:`current` will copy the title form the title of the currently active tab.
|
||||||
|
|
||||||
|
|
||||||
--type
|
--type
|
||||||
@@ -62,6 +66,8 @@ to the newly opened window.
|
|||||||
--cwd
|
--cwd
|
||||||
The working directory for the newly launched child. Use the special value
|
The working directory for the newly launched child. Use the special value
|
||||||
:code:`current` to use the working directory of the currently active window.
|
:code:`current` to use the working directory of the currently active window.
|
||||||
|
The special value :code:`last_reported` uses the last working directory
|
||||||
|
reported by the shell (needs :ref:`shell_integration` to work).
|
||||||
|
|
||||||
|
|
||||||
--env
|
--env
|
||||||
@@ -102,16 +108,17 @@ kitty remote control feature with :code:`kitty @launch --copy-env`.
|
|||||||
--location
|
--location
|
||||||
type=choices
|
type=choices
|
||||||
default=default
|
default=default
|
||||||
choices=first,after,before,neighbor,last,vsplit,hsplit,default
|
choices=first,after,before,neighbor,last,vsplit,hsplit,split,default
|
||||||
Where to place the newly created window when it is added to a tab which
|
Where to place the newly created window when it is added to a tab which
|
||||||
already has existing windows in it. :code:`after` and :code:`before` place the new
|
already has existing windows in it. :code:`after` and :code:`before` place the new
|
||||||
window before or after the active window. :code:`neighbor` is a synonym for :code:`after`.
|
window before or after the active window. :code:`neighbor` is a synonym for :code:`after`.
|
||||||
Also applies to creating a new tab, where the value of :code:`after`
|
Also applies to creating a new tab, where the value of :code:`after`
|
||||||
will cause the new tab to be placed next to the current tab instead of at the end.
|
will cause the new tab to be placed next to the current tab instead of at the end.
|
||||||
The values of :code:`vsplit` and :code:`hsplit` are only used by the :code:`splits`
|
The values of :code:`vsplit`, :code:`hsplit` and :code:`split` are only used by the
|
||||||
layout and control if the new window is placed in a vertical or horizontal split
|
:code:`splits` layout and control if the new window is placed in a vertical,
|
||||||
with the currently active window. The default is to place the window in a
|
horizontal or automatic split with the currently active window. The default is
|
||||||
layout dependent manner, typically, after the currently active window.
|
to place the window in a layout dependent manner, typically, after the
|
||||||
|
currently active window.
|
||||||
|
|
||||||
|
|
||||||
--allow-remote-control
|
--allow-remote-control
|
||||||
@@ -172,7 +179,8 @@ Set the WM_NAME property on X11 for the newly created OS Window when using
|
|||||||
|
|
||||||
--os-window-title
|
--os-window-title
|
||||||
Set the title for the newly created OS window. This title will override any
|
Set the title for the newly created OS window. This title will override any
|
||||||
titles set by programs running in kitty.
|
titles set by programs running in kitty. The special value :code:`current`
|
||||||
|
will use the title of the current OS Window, if any.
|
||||||
|
|
||||||
|
|
||||||
--logo
|
--logo
|
||||||
@@ -227,25 +235,26 @@ def get_env(opts: LaunchCLIOptions, active_child: Optional[Child]) -> Dict[str,
|
|||||||
|
|
||||||
|
|
||||||
def tab_for_window(boss: Boss, opts: LaunchCLIOptions, target_tab: Optional[Tab] = None) -> Optional[Tab]:
|
def tab_for_window(boss: Boss, opts: LaunchCLIOptions, target_tab: Optional[Tab] = None) -> Optional[Tab]:
|
||||||
|
|
||||||
|
def create_tab(tm: Optional[TabManager] = None) -> Tab:
|
||||||
|
if tm is None:
|
||||||
|
oswid = boss.add_os_window(wclass=opts.os_window_class, wname=opts.os_window_name, override_title=opts.os_window_title or None)
|
||||||
|
tm = boss.os_window_map[oswid]
|
||||||
|
tab = tm.new_tab(empty_tab=True, location=opts.location)
|
||||||
|
if opts.tab_title:
|
||||||
|
tab.set_title(opts.tab_title)
|
||||||
|
return tab
|
||||||
|
|
||||||
if opts.type == 'tab':
|
if opts.type == 'tab':
|
||||||
if target_tab is not None:
|
if target_tab is not None:
|
||||||
tm = target_tab.tab_manager_ref() or boss.active_tab_manager
|
tm = target_tab.tab_manager_ref() or boss.active_tab_manager
|
||||||
else:
|
else:
|
||||||
tm = boss.active_tab_manager
|
tm = boss.active_tab_manager
|
||||||
if tm:
|
tab = create_tab(tm)
|
||||||
tab: Optional[Tab] = tm.new_tab(empty_tab=True, location=opts.location)
|
|
||||||
if opts.tab_title and tab is not None:
|
|
||||||
tab.set_title(opts.tab_title)
|
|
||||||
else:
|
|
||||||
tab = None
|
|
||||||
elif opts.type == 'os-window':
|
elif opts.type == 'os-window':
|
||||||
oswid = boss.add_os_window(wclass=opts.os_window_class, wname=opts.os_window_name, override_title=opts.os_window_title or None)
|
tab = create_tab()
|
||||||
tm = boss.os_window_map[oswid]
|
|
||||||
tab = tm.new_tab(empty_tab=True)
|
|
||||||
if opts.tab_title and tab is not None:
|
|
||||||
tab.set_title(opts.tab_title)
|
|
||||||
else:
|
else:
|
||||||
tab = target_tab or boss.active_tab
|
tab = target_tab or boss.active_tab or create_tab()
|
||||||
|
|
||||||
return tab
|
return tab
|
||||||
|
|
||||||
@@ -288,7 +297,7 @@ def load_watch_modules(watchers: Iterable[str]) -> Optional[Watchers]:
|
|||||||
class LaunchKwds(TypedDict):
|
class LaunchKwds(TypedDict):
|
||||||
|
|
||||||
allow_remote_control: bool
|
allow_remote_control: bool
|
||||||
cwd_from: Optional[int]
|
cwd_from: Optional[CwdRequest]
|
||||||
cwd: Optional[str]
|
cwd: Optional[str]
|
||||||
location: Optional[str]
|
location: Optional[str]
|
||||||
override_title: Optional[str]
|
override_title: Optional[str]
|
||||||
@@ -340,6 +349,14 @@ def launch(
|
|||||||
active_child = active.child
|
active_child = active.child
|
||||||
else:
|
else:
|
||||||
active_child = None
|
active_child = None
|
||||||
|
if opts.window_title == 'current':
|
||||||
|
opts.window_title = active.title if active else None
|
||||||
|
if opts.tab_title == 'current':
|
||||||
|
atab = boss.active_tab
|
||||||
|
opts.tab_title = atab.title if atab else None
|
||||||
|
if opts.os_window_title == 'current':
|
||||||
|
tm = boss.active_tab_manager
|
||||||
|
opts.os_window_title = get_os_window_title(tm.os_window_id) if tm else None
|
||||||
env = get_env(opts, active_child)
|
env = get_env(opts, active_child)
|
||||||
kw: LaunchKwds = {
|
kw: LaunchKwds = {
|
||||||
'allow_remote_control': opts.allow_remote_control,
|
'allow_remote_control': opts.allow_remote_control,
|
||||||
@@ -355,8 +372,11 @@ def launch(
|
|||||||
}
|
}
|
||||||
if opts.cwd:
|
if opts.cwd:
|
||||||
if opts.cwd == 'current':
|
if opts.cwd == 'current':
|
||||||
if active_child:
|
if active:
|
||||||
kw['cwd_from'] = active_child.pid_for_cwd
|
kw['cwd_from'] = CwdRequest(active)
|
||||||
|
elif opts.cwd == 'last_reported':
|
||||||
|
if active:
|
||||||
|
kw['cwd_from'] = CwdRequest(active, CwdRequestType.last_reported)
|
||||||
else:
|
else:
|
||||||
kw['cwd'] = opts.cwd
|
kw['cwd'] = opts.cwd
|
||||||
if opts.location != 'default':
|
if opts.location != 'default':
|
||||||
@@ -397,7 +417,7 @@ def launch(
|
|||||||
elif x == '@line-count':
|
elif x == '@line-count':
|
||||||
if 'lines' in pipe_data:
|
if 'lines' in pipe_data:
|
||||||
x = str(pipe_data['lines'])
|
x = str(pipe_data['lines'])
|
||||||
elif x in ('@cursor-x', '@cursor-y', '@scrolled-by'):
|
elif x in ('@cursor-x', '@cursor-y', '@scrolled-by', '@first-line-on-screen', '@last-line-on-screen'):
|
||||||
if active is not None:
|
if active is not None:
|
||||||
screen = active.screen
|
screen = active.screen
|
||||||
if x == '@scrolled-by':
|
if x == '@scrolled-by':
|
||||||
@@ -406,6 +426,10 @@ def launch(
|
|||||||
x = str(screen.cursor.x + 1)
|
x = str(screen.cursor.x + 1)
|
||||||
elif x == '@cursor-y':
|
elif x == '@cursor-y':
|
||||||
x = str(screen.cursor.y + 1)
|
x = str(screen.cursor.y + 1)
|
||||||
|
elif x == '@first-line-on-screen':
|
||||||
|
x = str(screen.visual_line(0) or '')
|
||||||
|
elif x == '@last-line-on-screen':
|
||||||
|
x = str(screen.visual_line(screen.lines - 1) or '')
|
||||||
final_cmd.append(x)
|
final_cmd.append(x)
|
||||||
exe = which(final_cmd[0])
|
exe = which(final_cmd[0])
|
||||||
if exe:
|
if exe:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user