mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-09-07 11:58:19 +00:00
Compare commits
814 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
facda0c3fb | ||
![]() |
75dec598cc | ||
![]() |
48a1a10330 | ||
![]() |
6dd9bf0038 | ||
![]() |
adf4066b69 | ||
![]() |
40973417d0 | ||
![]() |
a62b26cd2f | ||
![]() |
04d36361b1 | ||
![]() |
dbc4edc583 | ||
![]() |
692168f8dd | ||
![]() |
fefda69ac3 | ||
![]() |
c33b82c634 | ||
![]() |
ce2a3773d2 | ||
![]() |
09ccda4d28 | ||
![]() |
27b254db8a | ||
![]() |
e5364392ee | ||
![]() |
71d0481da8 | ||
![]() |
76fd4fa8df | ||
![]() |
d31e6c8b2a | ||
![]() |
a80cf3db9c | ||
![]() |
603639ad44 | ||
![]() |
016a26cf98 | ||
![]() |
5c8f984ea1 | ||
![]() |
ac568900a5 | ||
![]() |
4b8010a6f4 | ||
![]() |
645b4b0031 | ||
![]() |
71e62f96fa | ||
![]() |
a58b1998a9 | ||
![]() |
e2e6770ed1 | ||
![]() |
a88e30179a | ||
![]() |
3b108945f3 | ||
![]() |
136d6e9341 | ||
![]() |
a5a73f8352 | ||
![]() |
4f857fc4e9 | ||
![]() |
f73cae0738 | ||
![]() |
47ff4c96e0 | ||
![]() |
75d6ee539a | ||
![]() |
9ab2e563bb | ||
![]() |
5ad2ec8f71 | ||
![]() |
69dcea5148 | ||
![]() |
8475768ad1 | ||
![]() |
0c5ef5578c | ||
![]() |
593d70a42f | ||
![]() |
d1969f74ac | ||
![]() |
2f8b0dc899 | ||
![]() |
c4c2d06571 | ||
![]() |
076bcccde4 | ||
![]() |
fd8cacaa67 | ||
![]() |
0d6a1d3fdb | ||
![]() |
c0eb6985ee | ||
![]() |
4b82e0aa2b | ||
![]() |
78790f6ef7 | ||
![]() |
95327bff18 | ||
![]() |
9b30eb8eb8 | ||
![]() |
a4b0e6d937 | ||
![]() |
4c27743931 | ||
![]() |
deb9033739 | ||
![]() |
78a2a815f3 | ||
![]() |
098a46f077 | ||
![]() |
168dd31367 | ||
![]() |
148a009a95 | ||
![]() |
5327646d58 | ||
![]() |
0c24da1412 | ||
![]() |
956bb8f02b | ||
![]() |
b4a90a7a22 | ||
![]() |
078ee42be3 | ||
![]() |
1be89cb146 | ||
![]() |
80eb406b82 | ||
![]() |
e39745113a | ||
![]() |
e854b38872 | ||
![]() |
4a3b4ea2b2 | ||
![]() |
5477eb87c1 | ||
![]() |
9c8c53bffb | ||
![]() |
8f49a227b7 | ||
![]() |
cd57612059 | ||
![]() |
d1e45ef768 | ||
![]() |
a2018d7b20 | ||
![]() |
eb21a58aa4 | ||
![]() |
4408101b8d | ||
![]() |
ddf7173ae9 | ||
![]() |
a8d2185611 | ||
![]() |
6265adfcd4 | ||
![]() |
52936b9b68 | ||
![]() |
bf6cce23da | ||
![]() |
25ccdfe495 | ||
![]() |
5cb2fa6f75 | ||
![]() |
fccb172ae9 | ||
![]() |
2d3db866e6 | ||
![]() |
3b8ab10776 | ||
![]() |
3327d32d66 | ||
![]() |
8a0613bd27 | ||
![]() |
a977e688cc | ||
![]() |
8c1db16c79 | ||
![]() |
b82c70fd3c | ||
![]() |
977c9999dd | ||
![]() |
f8ece6392d | ||
![]() |
4cc1fa2111 | ||
![]() |
a253871942 | ||
![]() |
8ada93d0cb | ||
![]() |
e5a3be3c46 | ||
![]() |
c3ef4d2908 | ||
![]() |
0eb6f28375 | ||
![]() |
07d5ae749d | ||
![]() |
4b9281ee6e | ||
![]() |
b9b49602cd | ||
![]() |
f5ff9c0371 | ||
![]() |
67b828cf21 | ||
![]() |
2ef04826fd | ||
![]() |
9ca3cbd94b | ||
![]() |
afa23532b6 | ||
![]() |
bb58710fa8 | ||
![]() |
4956d36ee6 | ||
![]() |
3f367857fc | ||
![]() |
2ee6e005d0 | ||
![]() |
ecad3e75ff | ||
![]() |
0c2c847af3 | ||
![]() |
68124f60c7 | ||
![]() |
ccd6fd26ec | ||
![]() |
6853a5423f | ||
![]() |
9c1edb5449 | ||
![]() |
8ee4deddb4 | ||
![]() |
c2da843dfd | ||
![]() |
018a888578 | ||
![]() |
b7d76fe26f | ||
![]() |
da5ac6aeeb | ||
![]() |
72d085525b | ||
![]() |
2a1b51ec94 | ||
![]() |
85b1cfa44f | ||
![]() |
860f1f635c | ||
![]() |
a185ce317b | ||
![]() |
b4a3ca999a | ||
![]() |
d1eb8ccc52 | ||
![]() |
b9939611d3 | ||
![]() |
a5853c4de8 | ||
![]() |
6c2c436917 | ||
![]() |
62d3786c66 | ||
![]() |
df2d0b33cc | ||
![]() |
07994d10e9 | ||
![]() |
6af1850ab4 | ||
![]() |
3159a7bec7 | ||
![]() |
b1becb12c0 | ||
![]() |
7716f98856 | ||
![]() |
423133bc3c | ||
![]() |
1eeb914a4f | ||
![]() |
1ac56a7ac2 | ||
![]() |
321119e001 | ||
![]() |
e1e2f94681 | ||
![]() |
268fc1a040 | ||
![]() |
450c019b4e | ||
![]() |
26f6b3ea82 | ||
![]() |
6be0902c09 | ||
![]() |
0b16c1eeba | ||
![]() |
dddc2a50a8 | ||
![]() |
4bfb1f616c | ||
![]() |
4ff7f6df06 | ||
![]() |
0b456d14a4 | ||
![]() |
ff9414d9ea | ||
![]() |
4f93864db7 | ||
![]() |
34abe2ceba | ||
![]() |
331b7c754c | ||
![]() |
f5670d81d4 | ||
![]() |
9a47cda892 | ||
![]() |
08a0423b78 | ||
![]() |
daeed453dc | ||
![]() |
3cdb9a7dfe | ||
![]() |
4e0d9b1b27 | ||
![]() |
95debc59d1 | ||
![]() |
d1fd22ae80 | ||
![]() |
39bb949973 | ||
![]() |
5081e65570 | ||
![]() |
c1938d12f0 | ||
![]() |
a8b9c5bea5 | ||
![]() |
f24d70b7ec | ||
![]() |
fca336c32d | ||
![]() |
5cf7575967 | ||
![]() |
844f20d01f | ||
![]() |
e3ced14393 | ||
![]() |
b7eb9bfef1 | ||
![]() |
592efb4b97 | ||
![]() |
ca5471fb03 | ||
![]() |
7aed08be40 | ||
![]() |
08314d414f | ||
![]() |
132c4f1f68 | ||
![]() |
e288096c26 | ||
![]() |
7ac017b154 | ||
![]() |
5cd990bec5 | ||
![]() |
a2445359c4 | ||
![]() |
ea0704148d | ||
![]() |
caddf59db5 | ||
![]() |
e3b6bb71a0 | ||
![]() |
faea09bbde | ||
![]() |
a3bb2df94f | ||
![]() |
d4190c9c02 | ||
![]() |
c5dfabb15b | ||
![]() |
a2d2cfea59 | ||
![]() |
a06fc4ff11 | ||
![]() |
50e33a6665 | ||
![]() |
bdbd0263a1 | ||
![]() |
f7b50ce727 | ||
![]() |
bfe56d04d5 | ||
![]() |
6c5c5b2ec0 | ||
![]() |
0811b1d5ac | ||
![]() |
fc99c99b74 | ||
![]() |
af5e423ea5 | ||
![]() |
2409d46600 | ||
![]() |
95fc1d64c8 | ||
![]() |
c9636598fc | ||
![]() |
8102fddceb | ||
![]() |
918ccdba5c | ||
![]() |
941915b862 | ||
![]() |
5a4aac7e09 | ||
![]() |
0a26321e9d | ||
![]() |
16233b16e7 | ||
![]() |
da558f2678 | ||
![]() |
00137c4189 | ||
![]() |
8c1ad59de7 | ||
![]() |
b7b5b9bbf5 | ||
![]() |
126c0505e2 | ||
![]() |
200aee9acf | ||
![]() |
61a78efa83 | ||
![]() |
3a5aecc216 | ||
![]() |
4dd9fe5cfd | ||
![]() |
d3de3448cc | ||
![]() |
96e427cd6a | ||
![]() |
8e52c6d12b | ||
![]() |
b783e12b93 | ||
![]() |
f5add68100 | ||
![]() |
4af44c5460 | ||
![]() |
6237377a59 | ||
![]() |
cd638588c4 | ||
![]() |
06a57842af | ||
![]() |
13e96c7ec8 | ||
![]() |
c4ece2a141 | ||
![]() |
96b3db0b8c | ||
![]() |
e475560af0 | ||
![]() |
61a6e670eb | ||
![]() |
2fb0d99f00 | ||
![]() |
8b8c7ecf1d | ||
![]() |
d26c114b5d | ||
![]() |
010f4d167d | ||
![]() |
799f5b8239 | ||
![]() |
6e411d60f2 | ||
![]() |
2f81c360bd | ||
![]() |
be0370cb0e | ||
![]() |
ed81b62ec2 | ||
![]() |
19cfd99439 | ||
![]() |
c03828e032 | ||
![]() |
405a897230 | ||
![]() |
03fee2ac33 | ||
![]() |
6ef757a8f8 | ||
![]() |
12ce9f2e3b | ||
![]() |
b25c593309 | ||
![]() |
ae81edfcbf | ||
![]() |
2d7706ec4f | ||
![]() |
1636ac88fc | ||
![]() |
a7c108a11c | ||
![]() |
aafe7deae7 | ||
![]() |
1057fd23be | ||
![]() |
622cc3f9c7 | ||
![]() |
19ffb0b51f | ||
![]() |
08a8bddd38 | ||
![]() |
8c457fc992 | ||
![]() |
37256ec6a2 | ||
![]() |
bec690532d | ||
![]() |
dac13701e3 | ||
![]() |
5bfb3925ba | ||
![]() |
ea7c54d79d | ||
![]() |
0651586339 | ||
![]() |
c33629aae5 | ||
![]() |
eb40cce45e | ||
![]() |
e14bc5b64e | ||
![]() |
90d1023783 | ||
![]() |
d969a6b6b7 | ||
![]() |
5213edfa6c | ||
![]() |
ef12d90b74 | ||
![]() |
6f720a0b11 | ||
![]() |
3e24e96af5 | ||
![]() |
542c655348 | ||
![]() |
140ac93884 | ||
![]() |
e4033ca4df | ||
![]() |
0d679951bc | ||
![]() |
6ebc02b68d | ||
![]() |
6e54589db4 | ||
![]() |
c85c277415 | ||
![]() |
306c7ea2be | ||
![]() |
37db4578c8 | ||
![]() |
0ddc1a21a6 | ||
![]() |
7bb3c31cee | ||
![]() |
1493c55348 | ||
![]() |
e2523c25cb | ||
![]() |
2206c509be | ||
![]() |
34a0b206f8 | ||
![]() |
051fed0c24 | ||
![]() |
ef833b3861 | ||
![]() |
40442ac02f | ||
![]() |
e86b9a112e | ||
![]() |
e7c71df0b7 | ||
![]() |
c972051611 | ||
![]() |
29c2f095a6 | ||
![]() |
3731b099bb | ||
![]() |
6e42cb152e | ||
![]() |
2f6bef7ff2 | ||
![]() |
300884d50f | ||
![]() |
c83733c533 | ||
![]() |
0bb0bf9b85 | ||
![]() |
d78eb7d28f | ||
![]() |
e00e991080 | ||
![]() |
4eaf346090 | ||
![]() |
bade7be021 | ||
![]() |
8bf5c4ed7f | ||
![]() |
1b91a667fb | ||
![]() |
093bdf640a | ||
![]() |
a115e848c6 | ||
![]() |
b2716375ac | ||
![]() |
0065aae6b6 | ||
![]() |
9b21de2fe7 | ||
![]() |
41201068ef | ||
![]() |
bf3597a519 | ||
![]() |
c8d5b2da45 | ||
![]() |
a52f469e16 | ||
![]() |
298aeb7536 | ||
![]() |
540fcc0b69 | ||
![]() |
a3837a1e4e | ||
![]() |
29dd5ae605 | ||
![]() |
0974705dd9 | ||
![]() |
1cf1b886cd | ||
![]() |
262c76eace | ||
![]() |
9732a92d7a | ||
![]() |
c71da8338b | ||
![]() |
7ae94e145d | ||
![]() |
85743aebd5 | ||
![]() |
15f82858b7 | ||
![]() |
037de64ea2 | ||
![]() |
359c390218 | ||
![]() |
0929500360 | ||
![]() |
d79a02db44 | ||
![]() |
4fb253a300 | ||
![]() |
d3973b8fad | ||
![]() |
6d90a181ce | ||
![]() |
3461204741 | ||
![]() |
4838bcbb8f | ||
![]() |
ae0c4d927a | ||
![]() |
b04cb2585d | ||
![]() |
237c941395 | ||
![]() |
f0c2d3d75a | ||
![]() |
dc4774c147 | ||
![]() |
c127daa552 | ||
![]() |
d0b06bd55f | ||
![]() |
ae0248b5bc | ||
![]() |
3698b37588 | ||
![]() |
63a47d0ba5 | ||
![]() |
781159af7d | ||
![]() |
6181487bad | ||
![]() |
53468541f7 | ||
![]() |
94bf448eda | ||
![]() |
2485482aec | ||
![]() |
f6d85baadb | ||
![]() |
c9c5ad43a5 | ||
![]() |
ed221f32fe | ||
![]() |
143c01edcb | ||
![]() |
ead241f38c | ||
![]() |
e3c94210f2 | ||
![]() |
f14c0f5a63 | ||
![]() |
62dd468500 | ||
![]() |
057b196024 | ||
![]() |
2fbe680aed | ||
![]() |
ce77b91bf6 | ||
![]() |
bb83a14d7a | ||
![]() |
68624e6c45 | ||
![]() |
0ae8d9ed42 | ||
![]() |
f2c357a209 | ||
![]() |
cd90821b93 | ||
![]() |
9184395cba | ||
![]() |
31439f311d | ||
![]() |
8d7e57f64b | ||
![]() |
f4a9b65f78 | ||
![]() |
9cf9e0639f | ||
![]() |
4d4b785a58 | ||
![]() |
7a27af8bfc | ||
![]() |
4ffd281de3 | ||
![]() |
5d523116bf | ||
![]() |
8f5cbed46f | ||
![]() |
948cbfbf0e | ||
![]() |
0063dc3925 | ||
![]() |
e05c3b6fd7 | ||
![]() |
51c42795fc | ||
![]() |
a670836d7a | ||
![]() |
da80531c22 | ||
![]() |
0df4012edc | ||
![]() |
d936e7106a | ||
![]() |
6db39e827e | ||
![]() |
5fa9e88482 | ||
![]() |
f3cb95ac1f | ||
![]() |
0306c592a7 | ||
![]() |
62fae29395 | ||
![]() |
f5f30605a8 | ||
![]() |
9f9248fd28 | ||
![]() |
23b0f7dec0 | ||
![]() |
305e5b3533 | ||
![]() |
32c4a9d65e | ||
![]() |
7e1260c9e9 | ||
![]() |
40bdea7335 | ||
![]() |
1bcfff3b79 | ||
![]() |
3e89c4c2f4 | ||
![]() |
4031815a8d | ||
![]() |
8b8c53fc4c | ||
![]() |
4d103ca16d | ||
![]() |
2dc518d8b0 | ||
![]() |
8f5f432ab6 | ||
![]() |
6b30736776 | ||
![]() |
d3334ecb06 | ||
![]() |
e8811ac6fb | ||
![]() |
0599f73fac | ||
![]() |
69e4428d80 | ||
![]() |
0e63dc18ff | ||
![]() |
f14371e909 | ||
![]() |
78cdc7d0de | ||
![]() |
1baf8928a0 | ||
![]() |
6fd901fd3d | ||
![]() |
0493b79caf | ||
![]() |
1dc9157727 | ||
![]() |
72e0fb14fe | ||
![]() |
fa30a04f2a | ||
![]() |
2f6e7d6ecd | ||
![]() |
a014eee968 | ||
![]() |
2610f5b4e2 | ||
![]() |
dc90ef776e | ||
![]() |
063868b311 | ||
![]() |
25a112469c | ||
![]() |
ab9b14215c | ||
![]() |
74386be017 | ||
![]() |
1bf8b262ea | ||
![]() |
6459e5c8ca | ||
![]() |
45d005ce65 | ||
![]() |
e03c428728 | ||
![]() |
9d286de834 | ||
![]() |
7eb35d7275 | ||
![]() |
b0404867b7 | ||
![]() |
e1bc6477b1 | ||
![]() |
6b4e6d2fa5 | ||
![]() |
8a3aae2caf | ||
![]() |
9fa404c390 | ||
![]() |
65a0fa4f35 | ||
![]() |
29f040716c | ||
![]() |
e2f9eb6a6f | ||
![]() |
f2d255d423 | ||
![]() |
bec46fc2fc | ||
![]() |
bc5cbf3e87 | ||
![]() |
ac524b6c34 | ||
![]() |
4cb2fd4f79 | ||
![]() |
a10b45fb1f | ||
![]() |
3c93f00d04 | ||
![]() |
c89df01e13 | ||
![]() |
5c39d09053 | ||
![]() |
cde8b7e810 | ||
![]() |
c9dfcd2781 | ||
![]() |
7e1b7bb8b3 | ||
![]() |
bcd4b3a680 | ||
![]() |
d42e67bdad | ||
![]() |
e6399c947a | ||
![]() |
a0de1be65f | ||
![]() |
b65c26966a | ||
![]() |
95b73f197f | ||
![]() |
89e0e7e69c | ||
![]() |
82695edaff | ||
![]() |
8d7ed3e0fc | ||
![]() |
405fe377d2 | ||
![]() |
fe9bbec92e | ||
![]() |
263146ebe2 | ||
![]() |
8827b6e738 | ||
![]() |
0ef24f3c75 | ||
![]() |
8e47d0267b | ||
![]() |
9d9fa60ece | ||
![]() |
602e4eb606 | ||
![]() |
764a2365af | ||
![]() |
8c74b80704 | ||
![]() |
509cf306f5 | ||
![]() |
fb8c83e07c | ||
![]() |
d28024bb60 | ||
![]() |
e6bb1a56eb | ||
![]() |
5293e8a819 | ||
![]() |
bed37ac844 | ||
![]() |
e7354e7308 | ||
![]() |
18001c3251 | ||
![]() |
3a01beb050 | ||
![]() |
fc545cd048 | ||
![]() |
f5f887efd9 | ||
![]() |
1a530cb96a | ||
![]() |
6a4842f110 | ||
![]() |
f60068eabd | ||
![]() |
7eb6b29d4c | ||
![]() |
0d2a6c7346 | ||
![]() |
898d988799 | ||
![]() |
7a5ef3da2b | ||
![]() |
cb8d30f938 | ||
![]() |
0778c67429 | ||
![]() |
29b96be84f | ||
![]() |
e634eb102e | ||
![]() |
f49a029c49 | ||
![]() |
f9250e28b5 | ||
![]() |
16bf3b8820 | ||
![]() |
200d0d642b | ||
![]() |
970e45559b | ||
![]() |
13d935a401 | ||
![]() |
a7e3e5915c | ||
![]() |
568f1f9d72 | ||
![]() |
7195bda7a2 | ||
![]() |
9a58de6d5a | ||
![]() |
5ced72498e | ||
![]() |
d58b618c74 | ||
![]() |
713dd24ab9 | ||
![]() |
a94cf4b3a2 | ||
![]() |
600e417154 | ||
![]() |
7bd842a530 | ||
![]() |
5ae2cc01ac | ||
![]() |
138a8f1602 | ||
![]() |
d27761a499 | ||
![]() |
7c9c982df7 | ||
![]() |
f184258f0e | ||
![]() |
88674a1957 | ||
![]() |
b68e9a10e0 | ||
![]() |
afdaaf1825 | ||
![]() |
57af5f3106 | ||
![]() |
aa34b91856 | ||
![]() |
0c10db9f14 | ||
![]() |
6fca26972b | ||
![]() |
9f44ec7c21 | ||
![]() |
67794d3f6f | ||
![]() |
1941a440d8 | ||
![]() |
80c3833031 | ||
![]() |
9503c9fe50 | ||
![]() |
2030599e1d | ||
![]() |
75571fb804 | ||
![]() |
4047c89c24 | ||
![]() |
e55444e6fe | ||
![]() |
913a1404be | ||
![]() |
355ac91c0a | ||
![]() |
cd809106c4 | ||
![]() |
80fe32be32 | ||
![]() |
15ceb18fcb | ||
![]() |
78b914b3d8 | ||
![]() |
22c2fe9610 | ||
![]() |
652079b26c | ||
![]() |
f38d1585e8 | ||
![]() |
98aa046a4d | ||
![]() |
46097617b4 | ||
![]() |
c98d207eb9 | ||
![]() |
b52e76334e | ||
![]() |
94599102e9 | ||
![]() |
89982912c1 | ||
![]() |
bec9483892 | ||
![]() |
028eeb442c | ||
![]() |
f1ab7bf0f6 | ||
![]() |
9ea0aa4934 | ||
![]() |
96fd18f994 | ||
![]() |
57ace2d0b8 | ||
![]() |
cdf51b1304 | ||
![]() |
120fffa42c | ||
![]() |
180db3c77b | ||
![]() |
b1a197ef57 | ||
![]() |
d203075a2e | ||
![]() |
9ce4e36aa2 | ||
![]() |
c16dbc01f0 | ||
![]() |
60611b8a4a | ||
![]() |
e9e82d94ac | ||
![]() |
41719aa48c | ||
![]() |
1d71196de3 | ||
![]() |
3f7c3afaf9 | ||
![]() |
a857d56fb6 | ||
![]() |
4ccd564849 | ||
![]() |
df0620afe9 | ||
![]() |
5957e1101c | ||
![]() |
4543cdeac8 | ||
![]() |
5ba8fee38a | ||
![]() |
47cf5cbb40 | ||
![]() |
973467b1ca | ||
![]() |
cf34ffa28e | ||
![]() |
eaa872216b | ||
![]() |
d59a57e133 | ||
![]() |
27c3382a6a | ||
![]() |
a30b2eda39 | ||
![]() |
e20ec96fee | ||
![]() |
6a8b31571b | ||
![]() |
061a730dd3 | ||
![]() |
85ed9b626e | ||
![]() |
ecfca17ad6 | ||
![]() |
12a333dfb4 | ||
![]() |
783a06689e | ||
![]() |
789e2024a5 | ||
![]() |
d7c5017cd2 | ||
![]() |
413964774c | ||
![]() |
aa81c16ba1 | ||
![]() |
fa4d4a38c1 | ||
![]() |
2d174f9bff | ||
![]() |
3971c460d1 | ||
![]() |
f97f7e8a70 | ||
![]() |
478fe3917c | ||
![]() |
98d77788f4 | ||
![]() |
220d40e99a | ||
![]() |
d512f56005 | ||
![]() |
dd41a9447d | ||
![]() |
d54817607c | ||
![]() |
ffe1b7a872 | ||
![]() |
0da8801dc9 | ||
![]() |
9204bb888f | ||
![]() |
bdeb93fe87 | ||
![]() |
e9edd21bed | ||
![]() |
ef542c6e63 | ||
![]() |
41df2d9805 | ||
![]() |
d20446e4de | ||
![]() |
a6eec4cbe2 | ||
![]() |
7a4215abd7 | ||
![]() |
31f101c970 | ||
![]() |
2f6860fbc5 | ||
![]() |
f8c3dc1bbf | ||
![]() |
ade07c4c3c | ||
![]() |
68318e2830 | ||
![]() |
a15473d9bd | ||
![]() |
397bf41ec3 | ||
![]() |
c87e3e98a3 | ||
![]() |
a1f7a95763 | ||
![]() |
c62f64866c | ||
![]() |
716848cb58 | ||
![]() |
a7c93cdfb1 | ||
![]() |
e17bb69645 | ||
![]() |
318641f5a1 | ||
![]() |
26b1888494 | ||
![]() |
1a27ce0797 | ||
![]() |
adcaff7137 | ||
![]() |
c2c578789b | ||
![]() |
1c2532c184 | ||
![]() |
ff50b5539e | ||
![]() |
f2ac9b85e3 | ||
![]() |
66681f94e0 | ||
![]() |
011c17da41 | ||
![]() |
da186fb9df | ||
![]() |
0dc3ea35c0 | ||
![]() |
4e7982fc2b | ||
![]() |
4f2110bce0 | ||
![]() |
89e5ca2fb4 | ||
![]() |
33b1131a14 | ||
![]() |
d01b2397f1 | ||
![]() |
aed61b62ae | ||
![]() |
e9bc033b88 | ||
![]() |
c011c4622d | ||
![]() |
057dd3e209 | ||
![]() |
87bd0bb744 | ||
![]() |
cfeed2b7a2 | ||
![]() |
d2d6f8b9f4 | ||
![]() |
31c9a2fe59 | ||
![]() |
4d983a2083 | ||
![]() |
0fd65035c5 | ||
![]() |
3059d9036b | ||
![]() |
37c5f5a123 | ||
![]() |
35eefd4240 | ||
![]() |
8607b1f844 | ||
![]() |
a9c1a5c73c | ||
![]() |
06389b280a | ||
![]() |
cb96ce5dcc | ||
![]() |
936d0c0d58 | ||
![]() |
7256ebe13d | ||
![]() |
479e735e7f | ||
![]() |
b3290f6887 | ||
![]() |
3e11476d32 | ||
![]() |
d5703a57e7 | ||
![]() |
84a03aa202 | ||
![]() |
b3925b83ae | ||
![]() |
5698358c2f | ||
![]() |
ff6e60be56 | ||
![]() |
db0da9a273 | ||
![]() |
31ee48a355 | ||
![]() |
b7dba0c5f5 | ||
![]() |
3b8a0ed2b8 | ||
![]() |
9c15d8de35 | ||
![]() |
2bb3353672 | ||
![]() |
074edd3065 | ||
![]() |
27ddc2a9b2 | ||
![]() |
7881bb2bf0 | ||
![]() |
c20fe23946 | ||
![]() |
2e048c870c | ||
![]() |
329c87a2b4 | ||
![]() |
c8c3fb2900 | ||
![]() |
6508fec945 | ||
![]() |
b39850fd8b | ||
![]() |
64ea3a1a29 | ||
![]() |
65a1c0df80 | ||
![]() |
25a4a89ee3 | ||
![]() |
5be77ded3a | ||
![]() |
02538bed3c | ||
![]() |
a787e4b8fc | ||
![]() |
e24f33ae6b | ||
![]() |
64c393716a | ||
![]() |
2ab0376d80 | ||
![]() |
d359231e5c | ||
![]() |
16f81353cc | ||
![]() |
9bef43fd99 | ||
![]() |
56681741cb | ||
![]() |
c84fefc4ea | ||
![]() |
574407aacd | ||
![]() |
579de8e491 | ||
![]() |
6ca64bc410 | ||
![]() |
2b245c965c | ||
![]() |
a92ed15baa | ||
![]() |
b6e45d49a3 | ||
![]() |
5c2fb580d0 | ||
![]() |
5e14b8e501 | ||
![]() |
1a6d9590a2 | ||
![]() |
1ca25d3b5e | ||
![]() |
c8950d376a | ||
![]() |
28bbd526f2 | ||
![]() |
2993d12de7 | ||
![]() |
02ca5bedac | ||
![]() |
b50e087581 | ||
![]() |
85fc49b22c | ||
![]() |
9fc1e4e91a | ||
![]() |
a2c446cb73 | ||
![]() |
1783ec922d | ||
![]() |
b8a75c24a6 | ||
![]() |
ea8fe9a4b0 | ||
![]() |
bfde326bcb | ||
![]() |
fa83140585 | ||
![]() |
a671ca43f8 | ||
![]() |
5293fc9c2f | ||
![]() |
531a225b1e | ||
![]() |
a55bea3491 | ||
![]() |
351a7c03a5 | ||
![]() |
f95aa32965 | ||
![]() |
63dad2fb10 | ||
![]() |
c4ff873726 | ||
![]() |
ca7c954712 | ||
![]() |
b22417630b | ||
![]() |
54679ff30e | ||
![]() |
38643ec4fe | ||
![]() |
040ce0e7df | ||
![]() |
b00a4275f0 | ||
![]() |
3bccb4f05e | ||
![]() |
2a4e5e1c8e | ||
![]() |
a8261ec9f6 | ||
![]() |
1e937cf2f1 | ||
![]() |
5340aa6c96 | ||
![]() |
95ee6c1633 | ||
![]() |
e8a2950324 | ||
![]() |
2362a67f68 | ||
![]() |
e00233bebf | ||
![]() |
00d4610fa5 | ||
![]() |
8ecb11a602 | ||
![]() |
8cb13bcfad | ||
![]() |
4ed8306b02 | ||
![]() |
9632a2b956 | ||
![]() |
02b34f44f6 | ||
![]() |
034f79cfa2 | ||
![]() |
e8054c41f5 | ||
![]() |
0160f8a0d9 | ||
![]() |
6cbd69da78 | ||
![]() |
1fa2e699d1 | ||
![]() |
16e4529f69 | ||
![]() |
2555f09d88 | ||
![]() |
bee2188014 | ||
![]() |
bf46ab8d2d | ||
![]() |
8111f5b995 | ||
![]() |
590c2929e7 | ||
![]() |
27a5a50aa0 | ||
![]() |
c8dca6c9a8 | ||
![]() |
e2aee7f75b | ||
![]() |
f0b77e8972 | ||
![]() |
571d9b015d | ||
![]() |
66dbc48e6b | ||
![]() |
1622263519 | ||
![]() |
93b6bfdefe | ||
![]() |
96753aa7e0 | ||
![]() |
09470ede55 | ||
![]() |
6139cb00cf | ||
![]() |
7e4a69e7df | ||
![]() |
66ed72f486 | ||
![]() |
6f2a7d3c36 | ||
![]() |
4b3c1b031e | ||
![]() |
83987968ac | ||
![]() |
2998775152 | ||
![]() |
322f166ca5 | ||
![]() |
8335a31e45 | ||
![]() |
e07f813a50 | ||
![]() |
b9128aded5 | ||
![]() |
8cbf8d5003 | ||
![]() |
50f7632d81 | ||
![]() |
2114e0a613 | ||
![]() |
c2792a1811 | ||
![]() |
a4daabb28a | ||
![]() |
2c3847c9af | ||
![]() |
9db02fd152 | ||
![]() |
f7a461a85f | ||
![]() |
c320635c29 | ||
![]() |
a295c5e884 | ||
![]() |
600eea08cd | ||
![]() |
a8e5eef11c | ||
![]() |
73367a55f6 | ||
![]() |
b5f70b834b | ||
![]() |
869987277c | ||
![]() |
aef90ffff1 | ||
![]() |
729b7ba7ed | ||
![]() |
5411c001c8 | ||
![]() |
ebfa606c67 | ||
![]() |
7aced21a8e | ||
![]() |
eef9664ef8 | ||
![]() |
cb5fbc1041 | ||
![]() |
b2cb80dfbb | ||
![]() |
184db2654c | ||
![]() |
35b9ceee21 | ||
![]() |
6217dbebcf | ||
![]() |
c1f61b4344 | ||
![]() |
5867fe425f | ||
![]() |
a469191311 | ||
![]() |
9117842a45 | ||
![]() |
9252378c82 | ||
![]() |
1e5b02302b |
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Features, Bug Reports, Questions
|
||||
url: https://github.com/ghostty-org/ghostty/discussions/new/choose
|
||||
about: Our preferred starting point if you have any questions or suggestions about configuration, features or behavior.
|
9
.github/ISSUE_TEMPLATE/preapproved.md
vendored
Normal file
9
.github/ISSUE_TEMPLATE/preapproved.md
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
name: Pre-Discussed and Approved Topics
|
||||
about: |-
|
||||
Only for topics already discussed and approved in the GitHub Discussions section.
|
||||
---
|
||||
|
||||
**DO NOT OPEN A NEW ISSUE. PLEASE USE THE DISCUSSIONS SECTION.**
|
||||
|
||||
**I DIDN'T READ THE ABOVE LINE. PLEASE CLOSE THIS ISSUE.**
|
32
.github/workflows/milestone.yml
vendored
Normal file
32
.github/workflows/milestone.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# Description:
|
||||
# - Add milestone to a merged PR automatically
|
||||
# - Add milestone to a closed issue that has a merged PR fix (if any)
|
||||
|
||||
name: Milestone Action
|
||||
on:
|
||||
issues:
|
||||
types: [closed]
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
update-milestone:
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
name: Milestone Update
|
||||
steps:
|
||||
- name: Set Milestone for PR
|
||||
uses: hustcer/milestone-action@v2
|
||||
if: github.event.pull_request.merged == true
|
||||
with:
|
||||
action: bind-pr # `bind-pr` is the default action
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Bind milestone to closed issue that has a merged PR fix
|
||||
- name: Set Milestone for Issue
|
||||
uses: hustcer/milestone-action@v2
|
||||
if: github.event.issue.state == 'closed'
|
||||
with:
|
||||
action: bind-issue
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
25
.github/workflows/nix.yml
vendored
25
.github/workflows/nix.yml
vendored
@@ -1,6 +1,31 @@
|
||||
on: [push, pull_request]
|
||||
name: Nix
|
||||
jobs:
|
||||
required:
|
||||
name: "Required Checks: Nix"
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs:
|
||||
- check-zig-cache-hash
|
||||
steps:
|
||||
- id: status
|
||||
name: Determine status
|
||||
run: |
|
||||
results=$(tr -d '\n' <<< '${{ toJSON(needs.*.result) }}')
|
||||
if ! grep -q -v -E '(failure|cancelled)' <<< "$results"; then
|
||||
result="failed"
|
||||
else
|
||||
result="success"
|
||||
fi
|
||||
{
|
||||
echo "result=${result}"
|
||||
echo "results=${results}"
|
||||
} | tee -a "$GITHUB_OUTPUT"
|
||||
- if: always() && steps.status.outputs.result != 'success'
|
||||
name: Check for failed status
|
||||
run: |
|
||||
echo "One or more required build workflows failed: ${{ steps.status.outputs.results }}"
|
||||
exit 1
|
||||
|
||||
check-zig-cache-hash:
|
||||
if: github.repository == 'ghostty-org/ghostty'
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
|
74
.github/workflows/publish-tag.yml
vendored
Normal file
74
.github/workflows/publish-tag.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to deploy (format: vX.Y.Z)"
|
||||
required: true
|
||||
|
||||
name: Publish Tagged Release
|
||||
|
||||
# We must only run one release workflow at a time to prevent corrupting
|
||||
# our release artifacts.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
outputs:
|
||||
version: ${{ steps.extract_version.outputs.version }}
|
||||
steps:
|
||||
- name: Validate Version Input
|
||||
run: |
|
||||
if [[ ! "${{ github.event.inputs.version }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Error: Version must follow the format vX.Y.Z (e.g., v1.0.0)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Version is valid: ${{ github.event.inputs.version }}"
|
||||
|
||||
- name: Exract the Version
|
||||
id: extract_version
|
||||
run: |
|
||||
VERSION=${{ github.event.inputs.version }}
|
||||
VERSION=${VERSION#v}
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
upload:
|
||||
needs: [setup]
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
env:
|
||||
GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
|
||||
steps:
|
||||
- name: Validate Release Files
|
||||
run: |
|
||||
BASE="https://release.files.ghostty.org/${GHOSTTY_VERSION}"
|
||||
curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-${GHOSTTY_VERSION}.tar.gz" | grep -q "^200$" || exit 1
|
||||
curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-${GHOSTTY_VERSION}.tar.gz.minisig" | grep -q "^200$" || exit 1
|
||||
curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-source.tar.gz" | grep -q "^200$" || exit 1
|
||||
curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-source.tar.gz.minisig" | grep -q "^200$" || exit 1
|
||||
curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-macos-universal.zip" | grep -q "^200$" || exit 1
|
||||
curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-macos-universal-dsym.zip" | grep -q "^200$" || exit 1
|
||||
curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/Ghostty.dmg" | grep -q "^200$" || exit 1
|
||||
curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/appcast-staged.xml" | grep -q "^200$" || exit 1
|
||||
|
||||
- name: Download Staged Appcast
|
||||
run: |
|
||||
curl -L https://release.files.ghostty.org/${GHOSTTY_VERSION}/appcast-staged.xml
|
||||
mv appcast-staged.xml appcast.xml
|
||||
|
||||
- name: Upload Appcast
|
||||
run: |
|
||||
rm -rf blob
|
||||
mkdir blob
|
||||
mv appcast.xml blob/appcast.xml
|
||||
- name: Upload Appcast to R2
|
||||
uses: ryand56/r2-upload-action@latest
|
||||
with:
|
||||
r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }}
|
||||
r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }}
|
||||
r2-secret-access-key: ${{ secrets.CF_R2_RELEASE_SECRET_KEY }}
|
||||
r2-bucket: ghostty-release
|
||||
source-dir: blob
|
||||
destination-dir: ./
|
2
.github/workflows/release-pr.yml
vendored
2
.github/workflows/release-pr.yml
vendored
@@ -110,6 +110,7 @@ jobs:
|
||||
|
||||
# Updater
|
||||
/usr/libexec/PlistBuddy -c "Set :SUPublicEDKey $SPARKLE_KEY_PUB" "macos/build/Release/Ghostty.app/Contents/Info.plist"
|
||||
/usr/libexec/PlistBuddy -c "Delete :SUEnableAutomaticChecks" "macos/build/Release/Ghostty.app/Contents/Info.plist"
|
||||
|
||||
- name: Codesign app bundle
|
||||
env:
|
||||
@@ -261,6 +262,7 @@ jobs:
|
||||
|
||||
# Updater
|
||||
/usr/libexec/PlistBuddy -c "Set :SUPublicEDKey $SPARKLE_KEY_PUB" "macos/build/Release/Ghostty.app/Contents/Info.plist"
|
||||
/usr/libexec/PlistBuddy -c "Delete :SUEnableAutomaticChecks" "macos/build/Release/Ghostty.app/Contents/Info.plist"
|
||||
|
||||
- name: Codesign app bundle
|
||||
env:
|
||||
|
31
.github/workflows/release-tag.yml
vendored
31
.github/workflows/release-tag.yml
vendored
@@ -7,6 +7,7 @@ on:
|
||||
upload:
|
||||
description: "Upload final artifacts to R2"
|
||||
default: false
|
||||
|
||||
push:
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
@@ -74,6 +75,8 @@ jobs:
|
||||
source-tarball:
|
||||
runs-on: namespace-profile-ghostty-md
|
||||
needs: [setup]
|
||||
env:
|
||||
GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -87,19 +90,24 @@ jobs:
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Create Tarball
|
||||
run: git archive --format=tgz --prefix=ghostty-source/ -o ghostty-source.tar.gz HEAD
|
||||
run: |
|
||||
git archive --format=tgz --prefix="ghostty-${GHOSTTY_VERSION}/" -o "ghostty-${GHOSTTY_VERSION}.tar.gz" HEAD
|
||||
git archive --format=tgz --prefix=ghostty-source/ -o ghostty-source.tar.gz HEAD
|
||||
|
||||
- name: Sign Tarball
|
||||
run: |
|
||||
echo -n "${{ secrets.MINISIGN_KEY }}" > minisign.key
|
||||
echo -n "${{ secrets.MINISIGN_PASSWORD }}" > minisign.password
|
||||
nix develop -c minisign -S -m ghostty-source.tar.gz -s minisign.key < minisign.password
|
||||
nix develop -c minisign -S -m "ghostty-${GHOSTTY_VERSION}.tar.gz" -s minisign.key < minisign.password
|
||||
nix develop -c minisign -S -m "ghostty-source.tar.gz" -s minisign.key < minisign.password
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: source-tarball
|
||||
path: |-
|
||||
ghostty-${{ env.GHOSTTY_VERSION }}.tar.gz
|
||||
ghostty-${{ env.GHOSTTY_VERSION }}.tar.gz.minisig
|
||||
ghostty-source.tar.gz
|
||||
ghostty-source.tar.gz.minisig
|
||||
|
||||
@@ -165,6 +173,7 @@ jobs:
|
||||
|
||||
# Updater
|
||||
/usr/libexec/PlistBuddy -c "Set :SUPublicEDKey $SPARKLE_KEY_PUB" "macos/build/Release/Ghostty.app/Contents/Info.plist"
|
||||
/usr/libexec/PlistBuddy -c "Delete :SUEnableAutomaticChecks" "macos/build/Release/Ghostty.app/Contents/Info.plist"
|
||||
|
||||
- name: Codesign app bundle
|
||||
env:
|
||||
@@ -352,11 +361,14 @@ jobs:
|
||||
run: |
|
||||
mkdir blob
|
||||
mkdir -p blob/${GHOSTTY_VERSION}
|
||||
mv "ghostty-${GHOSTTY_VERSION}.tar.gz" blob/${GHOSTTY_VERSION}/ghostty-${GHOSTTY_VERSION}.tar.gz
|
||||
mv "ghostty-${GHOSTTY_VERSION}.tar.gz.minisig" blob/${GHOSTTY_VERSION}/ghostty-${GHOSTTY_VERSION}.tar.gz.minisig
|
||||
mv ghostty-source.tar.gz blob/${GHOSTTY_VERSION}/ghostty-source.tar.gz
|
||||
mv ghostty-source.tar.gz.minisig blob/${GHOSTTY_VERSION}/ghostty-source.tar.gz.minisig
|
||||
mv ghostty-macos-universal.zip blob/${GHOSTTY_VERSION}/ghostty-macos-universal.zip
|
||||
mv ghostty-macos-universal-dsym.zip blob/${GHOSTTY_VERSION}/ghostty-macos-universal-dsym.zip
|
||||
mv Ghostty.dmg blob/${GHOSTTY_VERSION}/Ghostty.dmg
|
||||
mv appcast.xml blob/${GHOSTTY_VERSION}/appcast-staged.xml
|
||||
- name: Upload to R2
|
||||
uses: ryand56/r2-upload-action@latest
|
||||
with:
|
||||
@@ -366,18 +378,3 @@ jobs:
|
||||
r2-bucket: ghostty-release
|
||||
source-dir: blob
|
||||
destination-dir: ./
|
||||
|
||||
- name: Prep Appcast
|
||||
run: |
|
||||
rm -rf blob
|
||||
mkdir blob
|
||||
mv appcast.xml blob/appcast.xml
|
||||
- name: Upload Appcast to R2
|
||||
uses: ryand56/r2-upload-action@latest
|
||||
with:
|
||||
r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }}
|
||||
r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }}
|
||||
r2-secret-access-key: ${{ secrets.CF_R2_RELEASE_SECRET_KEY }}
|
||||
r2-bucket: ghostty-release
|
||||
source-dir: blob
|
||||
destination-dir: ./
|
||||
|
3
.github/workflows/release-tip.yml
vendored
3
.github/workflows/release-tip.yml
vendored
@@ -205,6 +205,7 @@ jobs:
|
||||
|
||||
# Updater
|
||||
/usr/libexec/PlistBuddy -c "Set :SUPublicEDKey $SPARKLE_KEY_PUB" "macos/build/Release/Ghostty.app/Contents/Info.plist"
|
||||
/usr/libexec/PlistBuddy -c "Delete :SUEnableAutomaticChecks" "macos/build/Release/Ghostty.app/Contents/Info.plist"
|
||||
|
||||
- name: Codesign app bundle
|
||||
env:
|
||||
@@ -419,6 +420,7 @@ jobs:
|
||||
|
||||
# Updater
|
||||
/usr/libexec/PlistBuddy -c "Set :SUPublicEDKey $SPARKLE_KEY_PUB" "macos/build/Release/Ghostty.app/Contents/Info.plist"
|
||||
/usr/libexec/PlistBuddy -c "Delete :SUEnableAutomaticChecks" "macos/build/Release/Ghostty.app/Contents/Info.plist"
|
||||
|
||||
- name: Codesign app bundle
|
||||
env:
|
||||
@@ -593,6 +595,7 @@ jobs:
|
||||
|
||||
# Updater
|
||||
/usr/libexec/PlistBuddy -c "Set :SUPublicEDKey $SPARKLE_KEY_PUB" "macos/build/Release/Ghostty.app/Contents/Info.plist"
|
||||
/usr/libexec/PlistBuddy -c "Delete :SUEnableAutomaticChecks" "macos/build/Release/Ghostty.app/Contents/Info.plist"
|
||||
|
||||
- name: Codesign app bundle
|
||||
env:
|
||||
|
158
.github/workflows/test.yml
vendored
158
.github/workflows/test.yml
vendored
@@ -6,6 +6,45 @@ on:
|
||||
name: Test
|
||||
|
||||
jobs:
|
||||
required:
|
||||
name: "Required Checks: Test"
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs:
|
||||
- build
|
||||
- build-bench
|
||||
- build-linux-libghostty
|
||||
- build-nix
|
||||
- build-macos
|
||||
- build-macos-matrix
|
||||
- build-windows
|
||||
- test
|
||||
- test-gtk
|
||||
- test-sentry-linux
|
||||
- test-macos
|
||||
- prettier
|
||||
- alejandra
|
||||
- typos
|
||||
- test-pkg-linux
|
||||
steps:
|
||||
- id: status
|
||||
name: Determine status
|
||||
run: |
|
||||
results=$(tr -d '\n' <<< '${{ toJSON(needs.*.result) }}')
|
||||
if ! grep -q -v -E '(failure|cancelled)' <<< "$results"; then
|
||||
result="failed"
|
||||
else
|
||||
result="success"
|
||||
fi
|
||||
{
|
||||
echo "result=${result}"
|
||||
echo "results=${results}"
|
||||
} | tee -a "$GITHUB_OUTPUT"
|
||||
- if: always() && steps.status.outputs.result != 'success'
|
||||
name: Check for failed status
|
||||
run: |
|
||||
echo "One or more required build workflows failed: ${{ steps.status.outputs.results }}"
|
||||
exit 1
|
||||
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -247,10 +286,10 @@ jobs:
|
||||
run: |
|
||||
# Get the zig version from build.zig so that it only needs to be updated
|
||||
$fileContent = Get-Content -Path "build.zig" -Raw
|
||||
$pattern = 'const required_zig = "(.*?)";'
|
||||
$pattern = 'buildpkg\.requireZig\("(.*?)"\);'
|
||||
$zigVersion = [regex]::Match($fileContent, $pattern).Groups[1].Value
|
||||
Write-Output $version
|
||||
$version = "zig-windows-x86_64-$zigVersion"
|
||||
Write-Output $version
|
||||
$uri = "https://ziglang.org/download/$zigVersion/$version.zip"
|
||||
Invoke-WebRequest -Uri "$uri" -OutFile ".\zig-windows.zip"
|
||||
Expand-Archive -Path ".\zig-windows.zip" -DestinationPath ".\" -Force
|
||||
@@ -329,9 +368,6 @@ jobs:
|
||||
- name: Test GTK Build
|
||||
run: nix develop -c zig build -Dapp-runtime=gtk -Dgtk-adwaita=true -Demit-docs
|
||||
|
||||
- name: Test GTK Build (No Libadwaita)
|
||||
run: nix develop -c zig build -Dapp-runtime=gtk -Dgtk-adwaita=false -Demit-docs
|
||||
|
||||
- name: Test GLFW Build
|
||||
run: nix develop -c zig build -Dapp-runtime=glfw
|
||||
|
||||
@@ -339,6 +375,83 @@ jobs:
|
||||
- name: Test System Build
|
||||
run: nix develop -c zig build --system ${ZIG_GLOBAL_CACHE_DIR}/p
|
||||
|
||||
test-gtk:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
adwaita: ["true", "false"]
|
||||
x11: ["true", "false"]
|
||||
wayland: ["true", "false"]
|
||||
name: GTK adwaita=${{ matrix.adwaita }} x11=${{ matrix.x11 }} wayland=${{ matrix.wayland }}
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
env:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@v1.2.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Test GTK Build
|
||||
run: |
|
||||
nix develop -c \
|
||||
zig build \
|
||||
-Dapp-runtime=gtk \
|
||||
-Dgtk-adwaita=${{ matrix.adwaita }} \
|
||||
-Dgtk-x11=${{ matrix.x11 }} \
|
||||
-Dgtk-wayland=${{ matrix.wayland }}
|
||||
|
||||
test-sentry-linux:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
sentry: ["true", "false"]
|
||||
name: Build -Dsentry=${{ matrix.sentry }}
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
env:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@v1.2.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Test Sentry Build
|
||||
run: |
|
||||
nix develop -c zig build -Dsentry=${{ matrix.sentry }}
|
||||
|
||||
test-macos:
|
||||
runs-on: namespace-profile-ghostty-macos
|
||||
needs: test
|
||||
@@ -441,3 +554,38 @@ jobs:
|
||||
useDaemon: false # sometimes fails on short jobs
|
||||
- name: typos check
|
||||
run: nix develop -c typos
|
||||
|
||||
test-pkg-linux:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pkg: ["wuffs"]
|
||||
name: Test pkg/${{ matrix.pkg }}
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
env:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@v1.2.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Test ${{ matrix.pkg }} Build
|
||||
run: |
|
||||
nix develop -c sh -c "cd pkg/${{ matrix.pkg }} ; zig build test"
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,3 +17,4 @@ test/cases/**/*.actual.png
|
||||
glad.zip
|
||||
/Box_test.ppm
|
||||
/Box_test_diff.ppm
|
||||
/ghostty.qcow2
|
||||
|
@@ -77,3 +77,100 @@ pull request will be accepted with a high degree of certainty.
|
||||
> **Pull requests are NOT a place to discuss feature design.** Please do
|
||||
> not open a WIP pull request to discuss a feature. Instead, use a discussion
|
||||
> and link to your branch.
|
||||
|
||||
## Nix Virtual Machines
|
||||
|
||||
Several Nix virtual machine definitions are provided by the project for testing
|
||||
and developing Ghostty against multiple different Linux desktop environments.
|
||||
|
||||
Running these requires a working Nix installation, either Nix on your
|
||||
favorite Linux distribution, NixOS, or macOS with nix-darwin installed. Further
|
||||
requirements for macOS are detailed below.
|
||||
|
||||
VMs should only be run on your local desktop and then powered off when not in
|
||||
use, which will discard any changes to the VM.
|
||||
|
||||
The VM definitions provide minimal software "out of the box" but additional
|
||||
software can be installed by using standard Nix mechanisms like `nix run nixpkgs#<package>`.
|
||||
|
||||
### Linux
|
||||
|
||||
1. Check out the Ghostty source and change to the directory.
|
||||
2. Run `nix run .#<vmtype>`. `<vmtype>` can be any of the VMs defined in the
|
||||
`nix/vm` directory (without the `.nix` suffix) excluding any file prefixed
|
||||
with `common` or `create`.
|
||||
3. The VM will build and then launch. Depending on the speed of your system, this
|
||||
can take a while, but eventually you should get a new VM window.
|
||||
4. The Ghostty source directory should be mounted to `/tmp/shared` in the VM. Depending
|
||||
on what UID and GID of the user that you launched the VM as, `/tmp/shared` _may_ be
|
||||
writable by the VM user, so be careful!
|
||||
|
||||
### macOS
|
||||
|
||||
1. To run the VMs on macOS you will need to enable the Linux builder in your `nix-darwin`
|
||||
config. This _should_ be as simple as adding `nix.linux-builder.enable=true` to your
|
||||
configuration and then rebuilding. See [this](https://nixcademy.com/posts/macos-linux-builder/)
|
||||
blog post for more information about the Linux builder and how to tune the performance.
|
||||
2. Once the Linux builder has been enabled, you should be able to follow the Linux instructions
|
||||
above to launch a VM.
|
||||
|
||||
### Custom VMs
|
||||
|
||||
To easily create a custom VM without modifying the Ghostty source, create a new
|
||||
directory, then create a file called `flake.nix` with the following text in the
|
||||
new directory.
|
||||
|
||||
```
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "nixpkgs/nixpkgs-unstable";
|
||||
ghostty.url = "github:ghostty-org/ghostty";
|
||||
};
|
||||
outputs = {
|
||||
nixpkgs,
|
||||
ghostty,
|
||||
...
|
||||
}: {
|
||||
nixosConfigurations.custom-vm = ghostty.create-gnome-vm {
|
||||
nixpkgs = nixpkgs;
|
||||
system = "x86_64-linux";
|
||||
overlay = ghostty.overlays.releasefast;
|
||||
# module = ./configuration.nix # also works
|
||||
module = {pkgs, ...}: {
|
||||
environment.systemPackages = [
|
||||
pkgs.btop
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The custom VM can then be run with a command like this:
|
||||
|
||||
```
|
||||
nix run .#nixosConfigurations.custom-vm.config.system.build.vm
|
||||
```
|
||||
|
||||
A file named `ghostty.qcow2` will be created that is used to persist any changes
|
||||
made in the VM. To "reset" the VM to default delete the file and it will be
|
||||
recreated the next time you run the VM.
|
||||
|
||||
### Contributing new VM definitions
|
||||
|
||||
#### VM Acceptance Criteria
|
||||
|
||||
We welcome the contribution of new VM definitions, as long as they meet the following criteria:
|
||||
|
||||
1. The should be different enough from existing VM definitions that they represent a distinct
|
||||
user (and developer) experience.
|
||||
2. There's a significant Ghostty user population that uses a similar environment.
|
||||
3. The VMs can be built using only packages from the current stable NixOS release.
|
||||
|
||||
#### VM Definition Criteria
|
||||
|
||||
1. VMs should be as minimal as possible so that they build and launch quickly.
|
||||
Additional software can be added at runtime with a command like `nix run nixpkgs#<package name>`.
|
||||
2. VMs should not expose any services to the network, or run any remote access
|
||||
software like SSH daemons, VNC or RDP.
|
||||
3. VMs should auto-login using the "ghostty" user.
|
||||
|
19
PACKAGING.md
19
PACKAGING.md
@@ -19,10 +19,17 @@ at `release.files.ghostty.org` in the following URL format where
|
||||
`VERSION` is the version number with no prefix such as `1.0.0`:
|
||||
|
||||
```
|
||||
https://release.files.ghostty.org/VERSION/ghostty-source.tar.gz
|
||||
https://release.files.ghostty.org/VERSION/ghostty-source.tar.gz.minisig
|
||||
https://release.files.ghostty.org/VERSION/ghostty-VERSION.tar.gz
|
||||
https://release.files.ghostty.org/VERSION/ghostty-VERSION.tar.gz.minisig
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> **Version 1.0.0 the filename is `ghostty-source.tar.gz`.** Future
|
||||
> versions will use the `ghostty-VERSION.tar.gz` format since it is more
|
||||
> typical for source tarballs. But for version 1.0.0, the filename is
|
||||
> `ghostty-source.tar.gz`.
|
||||
|
||||
Signature files are signed with
|
||||
[minisign](https://jedisct1.github.io/minisign/)
|
||||
using the following public key:
|
||||
@@ -110,3 +117,11 @@ relevant to package maintainers:
|
||||
often necessary for system packages to specify a specific minimum Linux
|
||||
version, glibc, etc. Run `zig targets` to a get a full list of available
|
||||
targets.
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> **The GLFW runtime is not meant for distribution.** The GLFW runtime
|
||||
> (`-Dapp-runtime=glfw`) is meant for development and testing only. It is
|
||||
> missing many features, has known memory leak scenarios, known crashes,
|
||||
> and more. Please do not package the GLFW-based Ghostty runtime for
|
||||
> distribution.
|
||||
|
@@ -1,18 +1,26 @@
|
||||
.{
|
||||
.name = "ghostty",
|
||||
.version = "1.0.0",
|
||||
.version = "1.1.0",
|
||||
.paths = .{""},
|
||||
.dependencies = .{
|
||||
// Zig libs
|
||||
.libxev = .{
|
||||
.url = "https://github.com/mitchellh/libxev/archive/db6a52bafadf00360e675fefa7926e8e6c0e9931.tar.gz",
|
||||
.hash = "12206029de146b685739f69b10a6f08baee86b3d0a5f9a659fa2b2b66c9602078bbf",
|
||||
.url = "https://github.com/mitchellh/libxev/archive/31eed4e337fed7b0149319e5cdbb62b848c24fbd.tar.gz",
|
||||
.hash = "1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c",
|
||||
},
|
||||
.mach_glfw = .{
|
||||
.url = "https://github.com/mitchellh/mach-glfw/archive/37c2995f31abcf7e8378fba68ddcf4a3faa02de0.tar.gz",
|
||||
.hash = "12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62",
|
||||
.lazy = true,
|
||||
},
|
||||
.vaxis = .{
|
||||
.url = "git+https://github.com/rockorager/libvaxis/?ref=main#6d729a2dc3b934818dffe06d2ba3ce02841ed74b",
|
||||
.hash = "12200df4ebeaed45de26cb2c9f3b6f3746d8013b604e035dae658f86f586c8c91d2f",
|
||||
},
|
||||
.z2d = .{
|
||||
.url = "git+https://github.com/vancluever/z2d?ref=v0.4.0#4638bb02a9dc41cc2fb811f092811f6a951c752a",
|
||||
.hash = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a",
|
||||
},
|
||||
.zig_objc = .{
|
||||
.url = "https://github.com/mitchellh/zig-objc/archive/9b8ba849b0f58fe207ecd6ab7c147af55b17556e.tar.gz",
|
||||
.hash = "1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634",
|
||||
@@ -25,6 +33,14 @@
|
||||
.url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz",
|
||||
.hash = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25",
|
||||
},
|
||||
.zig_wayland = .{
|
||||
.url = "https://codeberg.org/ifreund/zig-wayland/archive/fbfe3b4ac0b472a27b1f1a67405436c58cbee12d.tar.gz",
|
||||
.hash = "12209ca054cb1919fa276e328967f10b253f7537c4136eb48f3332b0f7cf661cad38",
|
||||
},
|
||||
.zf = .{
|
||||
.url = "git+https://github.com/natecraddock/zf/?ref=main#ed99ca18b02dda052e20ba467e90b623c04690dd",
|
||||
.hash = "1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8",
|
||||
},
|
||||
|
||||
// C libs
|
||||
.cimgui = .{ .path = "./pkg/cimgui" },
|
||||
@@ -46,23 +62,25 @@
|
||||
.glslang = .{ .path = "./pkg/glslang" },
|
||||
.spirv_cross = .{ .path = "./pkg/spirv-cross" },
|
||||
|
||||
// Wayland
|
||||
.wayland = .{
|
||||
.url = "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz",
|
||||
.hash = "12202cdac858abc52413a6c6711d5026d2d3c8e13f95ca2c327eade0736298bb021f",
|
||||
},
|
||||
.wayland_protocols = .{
|
||||
.url = "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz",
|
||||
.hash = "12201a57c6ce0001aa034fa80fba3e1cd2253c560a45748f4f4dd21ff23b491cddef",
|
||||
},
|
||||
.plasma_wayland_protocols = .{
|
||||
.url = "git+https://github.com/KDE/plasma-wayland-protocols?ref=main#db525e8f9da548cffa2ac77618dd0fbe7f511b86",
|
||||
.hash = "12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566",
|
||||
},
|
||||
|
||||
// Other
|
||||
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
|
||||
.iterm2_themes = .{
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/d6c42066b3045292e0b1154ad84ff22d6863ebf7.tar.gz",
|
||||
.hash = "12204358b2848ffd993d3425055bff0a5ba9b1b60bead763a6dea0517965d7290a6c",
|
||||
},
|
||||
.vaxis = .{
|
||||
.url = "git+https://github.com/rockorager/libvaxis/?ref=main#6d729a2dc3b934818dffe06d2ba3ce02841ed74b",
|
||||
.hash = "12200df4ebeaed45de26cb2c9f3b6f3746d8013b604e035dae658f86f586c8c91d2f",
|
||||
},
|
||||
.zf = .{
|
||||
.url = "git+https://github.com/natecraddock/zf/?ref=main#ed99ca18b02dda052e20ba467e90b623c04690dd",
|
||||
.hash = "1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8",
|
||||
},
|
||||
.z2d = .{
|
||||
.url = "git+https://github.com/vancluever/z2d?ref=v0.4.0#4638bb02a9dc41cc2fb811f092811f6a951c752a",
|
||||
.hash = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a",
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/0e23daf59234fc892cba949562d7bf69204594bb.tar.gz",
|
||||
.hash = "12204fc99743d8232e691ac22e058519bfc6ea92de4a11c6dba59b117531c847cd6a",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@@ -1,12 +0,0 @@
|
||||
//! Reverse Index (RI) - ESC M
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
try stdout.print("A\nB\nC", .{});
|
||||
try stdout.print("\x1BM", .{});
|
||||
try stdout.print("D\n\n", .{});
|
||||
|
||||
// const stdin = std.io.getStdIn().reader();
|
||||
// _ = try stdin.readByte();
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
//! Reverse Index (RI) - ESC M
|
||||
//! Case: test that if the cursor is at the top, it scrolls down.
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
try stdout.print("A\nB\n\n", .{});
|
||||
|
||||
try stdout.print("\x1B[H", .{}); // Top-left
|
||||
try stdout.print("\x1BM", .{}); // Reverse-Index
|
||||
try stdout.print("D", .{});
|
||||
|
||||
try stdout.print("\x0D", .{}); // CR
|
||||
try stdout.print("\x0A", .{}); // LF
|
||||
try stdout.print("\x1B[H", .{}); // Top-left
|
||||
try stdout.print("\x1BM", .{}); // Reverse-Index
|
||||
try stdout.print("E", .{});
|
||||
|
||||
try stdout.print("\n", .{});
|
||||
|
||||
// const stdin = std.io.getStdIn().reader();
|
||||
// _ = try stdin.readByte();
|
||||
}
|
@@ -1,99 +0,0 @@
|
||||
//! Outputs various box glyphs for testing.
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
|
||||
// Box Drawing
|
||||
{
|
||||
try stdout.print("\x1b[4mBox Drawing\x1b[0m\n", .{});
|
||||
var i: usize = 0x2500;
|
||||
const step: usize = 32;
|
||||
while (i <= 0x257F) : (i += step) {
|
||||
var j: usize = 0;
|
||||
while (j < step) : (j += 1) {
|
||||
try stdout.print("{u} ", .{@as(u21, @intCast(i + j))});
|
||||
}
|
||||
|
||||
try stdout.print("\n\n", .{});
|
||||
}
|
||||
}
|
||||
|
||||
// Block Elements
|
||||
{
|
||||
try stdout.print("\x1b[4mBlock Elements\x1b[0m\n", .{});
|
||||
var i: usize = 0x2580;
|
||||
const step: usize = 32;
|
||||
while (i <= 0x259f) : (i += step) {
|
||||
var j: usize = 0;
|
||||
while (j < step) : (j += 1) {
|
||||
try stdout.print("{u} ", .{@as(u21, @intCast(i + j))});
|
||||
}
|
||||
|
||||
try stdout.print("\n\n", .{});
|
||||
}
|
||||
}
|
||||
|
||||
// Braille Elements
|
||||
{
|
||||
try stdout.print("\x1b[4mBraille\x1b[0m\n", .{});
|
||||
var i: usize = 0x2800;
|
||||
const step: usize = 32;
|
||||
while (i <= 0x28FF) : (i += step) {
|
||||
var j: usize = 0;
|
||||
while (j < step) : (j += 1) {
|
||||
try stdout.print("{u} ", .{@as(u21, @intCast(i + j))});
|
||||
}
|
||||
|
||||
try stdout.print("\n\n", .{});
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
try stdout.print("\x1b[4mSextants\x1b[0m\n", .{});
|
||||
var i: usize = 0x1FB00;
|
||||
const step: usize = 32;
|
||||
const end = 0x1FB3B;
|
||||
while (i <= end) : (i += step) {
|
||||
var j: usize = 0;
|
||||
while (j < step) : (j += 1) {
|
||||
const v = i + j;
|
||||
if (v <= end) try stdout.print("{u} ", .{@as(u21, @intCast(v))});
|
||||
}
|
||||
|
||||
try stdout.print("\n\n", .{});
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
try stdout.print("\x1b[4mWedge Triangles\x1b[0m\n", .{});
|
||||
var i: usize = 0x1FB3C;
|
||||
const step: usize = 32;
|
||||
const end = 0x1FB6B;
|
||||
while (i <= end) : (i += step) {
|
||||
var j: usize = 0;
|
||||
while (j < step) : (j += 1) {
|
||||
const v = i + j;
|
||||
if (v <= end) try stdout.print("{u} ", .{@as(u21, @intCast(v))});
|
||||
}
|
||||
|
||||
try stdout.print("\n\n", .{});
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
try stdout.print("\x1b[4mOther\x1b[0m\n", .{});
|
||||
var i: usize = 0x1FB70;
|
||||
const step: usize = 32;
|
||||
const end = 0x1FB8B;
|
||||
while (i <= end) : (i += step) {
|
||||
var j: usize = 0;
|
||||
while (j < step) : (j += 1) {
|
||||
const v = i + j;
|
||||
if (v <= end) try stdout.print("{u} ", .{@as(u21, @intCast(v))});
|
||||
}
|
||||
|
||||
try stdout.print("\n\n", .{});
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
//! Set Top and Bottom Margins (DECSTBM) - ESC [ r
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
try stdout.print("A\nB\nC\nD", .{});
|
||||
try stdout.print("\x1B[1;3r", .{}); // cursor up
|
||||
try stdout.print("\x1B[1;1H", .{}); // top-left
|
||||
try stdout.print("\x1B[M", .{}); // delete line
|
||||
try stdout.print("E\n", .{});
|
||||
try stdout.print("\x1B[7;1H", .{}); // cursor up
|
||||
|
||||
// const stdin = std.io.getStdIn().reader();
|
||||
// _ = try stdin.readByte();
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
//! Delete Line (DL) - Esc [ M
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
try stdout.print("A\nB\nC\nD", .{});
|
||||
try stdout.print("\x1B[2A", .{}); // cursor up
|
||||
try stdout.print("\x1B[M", .{});
|
||||
try stdout.print("E\n", .{});
|
||||
try stdout.print("\x1B[B", .{});
|
||||
|
||||
// const stdin = std.io.getStdIn().reader();
|
||||
// _ = try stdin.readByte();
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
//! Insert Line (IL) - Esc [ L
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
try stdout.print("\x1B[2J", .{}); // clear screen
|
||||
try stdout.print("\x1B[1;1H", .{}); // set cursor position
|
||||
try stdout.print("A\nB\nC\nD\nE", .{});
|
||||
try stdout.print("\x1B[1;2r", .{}); // set scroll region
|
||||
try stdout.print("\x1B[1;1H", .{}); // set cursor position
|
||||
try stdout.print("\x1B[1L", .{}); // insert lines
|
||||
try stdout.print("X", .{});
|
||||
try stdout.print("\x1B[7;1H", .{}); // set cursor position
|
||||
|
||||
// const stdin = std.io.getStdIn().reader();
|
||||
// _ = try stdin.readByte();
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
//! DECALN - ESC # 8
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
try stdout.print("\x1B#8", .{});
|
||||
|
||||
// const stdin = std.io.getStdIn().reader();
|
||||
// _ = try stdin.readByte();
|
||||
}
|
13
default.nix
Normal file
13
default.nix
Normal file
@@ -0,0 +1,13 @@
|
||||
(import (
|
||||
let
|
||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||
nodeName = lock.nodes.root.inputs.flake-compat;
|
||||
in
|
||||
fetchTarball {
|
||||
url =
|
||||
lock.nodes.${nodeName}.locked.url
|
||||
or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz";
|
||||
sha256 = lock.nodes.${nodeName}.locked.narHash;
|
||||
}
|
||||
) {src = ./.;})
|
||||
.defaultNix
|
6
dist/linux/app.desktop
vendored
6
dist/linux/app.desktop
vendored
@@ -7,9 +7,15 @@ Icon=com.mitchellh.ghostty
|
||||
Categories=System;TerminalEmulator;
|
||||
Keywords=terminal;tty;pty;
|
||||
StartupNotify=true
|
||||
StartupWMClass=com.mitchellh.ghostty
|
||||
Terminal=false
|
||||
Actions=new-window;
|
||||
X-GNOME-UsesNotifications=true
|
||||
X-TerminalArgExec=-e
|
||||
X-TerminalArgTitle=--title=
|
||||
X-TerminalArgAppId=--class=
|
||||
X-TerminalArgDir=--working-directory=
|
||||
X-TerminalArgHold=--wait-after-command
|
||||
|
||||
[Desktop Action new-window]
|
||||
Name=New Window
|
||||
|
0
dist/linux/ghostty_dolphin.desktop
vendored
Normal file → Executable file
0
dist/linux/ghostty_dolphin.desktop
vendored
Normal file → Executable file
97
dist/linux/ghostty_nautilus.py
vendored
Normal file
97
dist/linux/ghostty_nautilus.py
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
# Adapted from wezterm: https://github.com/wez/wezterm/blob/main/assets/wezterm-nautilus.py
|
||||
# original copyright notice:
|
||||
#
|
||||
# Copyright (C) 2022 Sebastian Wiesner <sebastian@swsnr.de>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from os.path import isdir
|
||||
from gi import require_version
|
||||
from gi.repository import Nautilus, GObject, Gio, GLib
|
||||
|
||||
|
||||
class OpenInGhosttyAction(GObject.GObject, Nautilus.MenuProvider):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
session = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||
self._systemd = None
|
||||
# Check if the this system runs under systemd, per sd_booted(3)
|
||||
if isdir('/run/systemd/system/'):
|
||||
self._systemd = Gio.DBusProxy.new_sync(session,
|
||||
Gio.DBusProxyFlags.NONE,
|
||||
None,
|
||||
"org.freedesktop.systemd1",
|
||||
"/org/freedesktop/systemd1",
|
||||
"org.freedesktop.systemd1.Manager", None)
|
||||
|
||||
def _open_terminal(self, path):
|
||||
cmd = ['ghostty', f'--working-directory={path}', '--gtk-single-instance=false']
|
||||
child = Gio.Subprocess.new(cmd, Gio.SubprocessFlags.NONE)
|
||||
if self._systemd:
|
||||
# Move new terminal into a dedicated systemd scope to make systemd
|
||||
# track the terminal separately; in particular this makes systemd
|
||||
# keep a separate CPU and memory account for the terminal which in turn
|
||||
# ensures that oomd doesn't take nautilus down if a process in
|
||||
# ghostty consumes a lot of memory.
|
||||
pid = int(child.get_identifier())
|
||||
props = [("PIDs", GLib.Variant('au', [pid])),
|
||||
('CollectMode', GLib.Variant('s', 'inactive-or-failed'))]
|
||||
name = 'app-nautilus-com.mitchellh.ghostty-{}.scope'.format(pid)
|
||||
args = GLib.Variant('(ssa(sv)a(sa(sv)))', (name, 'fail', props, []))
|
||||
self._systemd.call_sync('StartTransientUnit', args,
|
||||
Gio.DBusCallFlags.NO_AUTO_START, 500, None)
|
||||
|
||||
def _menu_item_activated(self, _menu, paths):
|
||||
for path in paths:
|
||||
self._open_terminal(path)
|
||||
|
||||
def _make_item(self, name, paths):
|
||||
item = Nautilus.MenuItem(name=name, label='Open in Ghostty',
|
||||
icon='com.mitchellh.ghostty')
|
||||
item.connect('activate', self._menu_item_activated, paths)
|
||||
return item
|
||||
|
||||
def _paths_to_open(self, files):
|
||||
paths = []
|
||||
for file in files:
|
||||
location = file.get_location() if file.is_directory() else file.get_parent_location()
|
||||
path = location.get_path()
|
||||
if path and path not in paths:
|
||||
paths.append(path)
|
||||
if 10 < len(paths):
|
||||
# Let's not open anything if the user selected a lot of directories,
|
||||
# to avoid accidentally spamming their desktop with dozends of
|
||||
# new windows or tabs. Ten is a totally arbitrary limit :)
|
||||
return []
|
||||
else:
|
||||
return paths
|
||||
|
||||
def get_file_items(self, *args):
|
||||
# Nautilus 3.0 API passes args (window, files), 4.0 API just passes files
|
||||
files = args[0] if len(args) == 1 else args[1]
|
||||
paths = self._paths_to_open(files)
|
||||
if paths:
|
||||
return [self._make_item(name='GhosttyNautilus::open_in_ghostty', paths=paths)]
|
||||
else:
|
||||
return []
|
||||
|
||||
def get_background_items(self, *args):
|
||||
# Nautilus 3.0 API passes args (window, file), 4.0 API just passes file
|
||||
file = args[0] if len(args) == 1 else args[1]
|
||||
paths = self._paths_to_open([file])
|
||||
if paths:
|
||||
return [self._make_item(name='GhosttyNautilus::open_folder_in_ghostty', paths=paths)]
|
||||
else:
|
||||
return []
|
7
dist/macos/update_appcast_tag.py
vendored
7
dist/macos/update_appcast_tag.py
vendored
@@ -21,6 +21,7 @@ from datetime import datetime, timezone
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
version = os.environ["GHOSTTY_VERSION"]
|
||||
version_dash = version.replace('.', '-')
|
||||
build = os.environ["GHOSTTY_BUILD"]
|
||||
commit = os.environ["GHOSTTY_COMMIT"]
|
||||
commit_long = os.environ["GHOSTTY_COMMIT_LONG"]
|
||||
@@ -82,6 +83,8 @@ elem = ET.SubElement(item, "sparkle:shortVersionString")
|
||||
elem.text = f"{version}"
|
||||
elem = ET.SubElement(item, "sparkle:minimumSystemVersion")
|
||||
elem.text = "13.0.0"
|
||||
elem = ET.SubElement(item, "sparkle:fullReleaseNotesLink")
|
||||
elem.text = f"https://ghostty.org/docs/install/release-notes/{version_dash}"
|
||||
elem = ET.SubElement(item, "description")
|
||||
elem.text = f"""
|
||||
<h1>Ghostty v{version}</h1>
|
||||
@@ -91,8 +94,8 @@ on {now.strftime('%Y-%m-%d')}.
|
||||
</p>
|
||||
<p>
|
||||
We don't currently generate release notes for auto-updates.
|
||||
You can view the complete changelog and release notes on
|
||||
the <a href="https://ghostty.org">Ghostty website</a>.
|
||||
You can view the complete changelog and release notes
|
||||
at <a href="https://ghostty.org/docs/install/release-notes/{version_dash}">ghostty.org/docs/install/release-notes/{version_dash}</a>.
|
||||
</p>
|
||||
"""
|
||||
elem = ET.SubElement(item, "enclosure")
|
||||
|
17
flake.lock
generated
17
flake.lock
generated
@@ -1,5 +1,21 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
@@ -52,6 +68,7 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"nixpkgs-stable": "nixpkgs-stable",
|
||||
"nixpkgs-unstable": "nixpkgs-unstable",
|
||||
"zig": "zig"
|
||||
|
102
flake.nix
102
flake.nix
@@ -9,6 +9,12 @@
|
||||
# system glibc that the user is building for.
|
||||
nixpkgs-stable.url = "github:nixos/nixpkgs/release-24.11";
|
||||
|
||||
# Used for shell.nix
|
||||
flake-compat = {
|
||||
url = "github:edolstra/flake-compat";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
zig = {
|
||||
url = "github:mitchellh/zig-overlay";
|
||||
inputs = {
|
||||
@@ -25,34 +31,82 @@
|
||||
zig,
|
||||
...
|
||||
}:
|
||||
builtins.foldl' nixpkgs-stable.lib.recursiveUpdate {} (builtins.map (system: let
|
||||
pkgs-stable = nixpkgs-stable.legacyPackages.${system};
|
||||
pkgs-unstable = nixpkgs-unstable.legacyPackages.${system};
|
||||
in {
|
||||
devShell.${system} = pkgs-stable.callPackage ./nix/devShell.nix {
|
||||
zig = zig.packages.${system}."0.13.0";
|
||||
wraptest = pkgs-stable.callPackage ./nix/wraptest.nix {};
|
||||
};
|
||||
builtins.foldl' nixpkgs-stable.lib.recursiveUpdate {} (
|
||||
builtins.map (
|
||||
system: let
|
||||
pkgs-stable = nixpkgs-stable.legacyPackages.${system};
|
||||
pkgs-unstable = nixpkgs-unstable.legacyPackages.${system};
|
||||
in {
|
||||
devShell.${system} = pkgs-stable.callPackage ./nix/devShell.nix {
|
||||
zig = zig.packages.${system}."0.13.0";
|
||||
wraptest = pkgs-stable.callPackage ./nix/wraptest.nix {};
|
||||
};
|
||||
|
||||
packages.${system} = let
|
||||
mkArgs = optimize: {
|
||||
inherit optimize;
|
||||
packages.${system} = let
|
||||
mkArgs = optimize: {
|
||||
inherit optimize;
|
||||
|
||||
revision = self.shortRev or self.dirtyShortRev or "dirty";
|
||||
revision = self.shortRev or self.dirtyShortRev or "dirty";
|
||||
};
|
||||
in rec {
|
||||
ghostty-debug = pkgs-stable.callPackage ./nix/package.nix (mkArgs "Debug");
|
||||
ghostty-releasesafe = pkgs-stable.callPackage ./nix/package.nix (mkArgs "ReleaseSafe");
|
||||
ghostty-releasefast = pkgs-stable.callPackage ./nix/package.nix (mkArgs "ReleaseFast");
|
||||
|
||||
ghostty = ghostty-releasefast;
|
||||
default = ghostty;
|
||||
};
|
||||
|
||||
formatter.${system} = pkgs-stable.alejandra;
|
||||
|
||||
apps.${system} = let
|
||||
runVM = (
|
||||
module: let
|
||||
vm = import ./nix/vm/create.nix {
|
||||
inherit system module;
|
||||
nixpkgs = nixpkgs-stable;
|
||||
overlay = self.overlays.debug;
|
||||
};
|
||||
program = pkgs-stable.writeShellScript "run-ghostty-vm" ''
|
||||
SHARED_DIR=$(pwd)
|
||||
export SHARED_DIR
|
||||
|
||||
${pkgs-stable.lib.getExe vm.config.system.build.vm} "$@"
|
||||
'';
|
||||
in {
|
||||
type = "app";
|
||||
program = "${program}";
|
||||
}
|
||||
);
|
||||
in {
|
||||
wayland-cinnamon = runVM ./nix/vm/wayland-cinnamon.nix;
|
||||
wayland-gnome = runVM ./nix/vm/wayland-gnome.nix;
|
||||
wayland-plasma6 = runVM ./nix/vm/wayland-plasma6.nix;
|
||||
x11-cinnamon = runVM ./nix/vm/x11-cinnamon.nix;
|
||||
x11-gnome = runVM ./nix/vm/x11-gnome.nix;
|
||||
x11-plasma6 = runVM ./nix/vm/x11-plasma6.nix;
|
||||
x11-xfce = runVM ./nix/vm/x11-xfce.nix;
|
||||
};
|
||||
}
|
||||
# Our supported systems are the same supported systems as the Zig binaries.
|
||||
) (builtins.attrNames zig.packages)
|
||||
)
|
||||
// {
|
||||
overlays = {
|
||||
default = self.overlays.releasefast;
|
||||
releasefast = final: prev: {
|
||||
ghostty = self.packages.${prev.system}.ghostty-releasefast;
|
||||
};
|
||||
debug = final: prev: {
|
||||
ghostty = self.packages.${prev.system}.ghostty-debug;
|
||||
};
|
||||
in rec {
|
||||
ghostty-debug = pkgs-stable.callPackage ./nix/package.nix (mkArgs "Debug");
|
||||
ghostty-releasesafe = pkgs-stable.callPackage ./nix/package.nix (mkArgs "ReleaseSafe");
|
||||
ghostty-releasefast = pkgs-stable.callPackage ./nix/package.nix (mkArgs "ReleaseFast");
|
||||
|
||||
ghostty = ghostty-releasefast;
|
||||
default = ghostty;
|
||||
};
|
||||
|
||||
formatter.${system} = pkgs-stable.alejandra;
|
||||
|
||||
# Our supported systems are the same supported systems as the Zig binaries.
|
||||
}) (builtins.attrNames zig.packages));
|
||||
create-vm = import ./nix/vm/create.nix;
|
||||
create-cinnamon-vm = import ./nix/vm/create-cinnamon.nix;
|
||||
create-gnome-vm = import ./nix/vm/create-gnome.nix;
|
||||
create-plasma6-vm = import ./nix/vm/create-plasma6.nix;
|
||||
create-xfce-vm = import ./nix/vm/create-xfce.nix;
|
||||
};
|
||||
|
||||
nixConfig = {
|
||||
extra-substituters = ["https://ghostty.cachix.org"];
|
||||
|
@@ -159,7 +159,7 @@ typedef enum {
|
||||
GHOSTTY_KEY_EQUAL,
|
||||
GHOSTTY_KEY_LEFT_BRACKET, // [
|
||||
GHOSTTY_KEY_RIGHT_BRACKET, // ]
|
||||
GHOSTTY_KEY_BACKSLASH, // /
|
||||
GHOSTTY_KEY_BACKSLASH, // \
|
||||
|
||||
// control
|
||||
GHOSTTY_KEY_UP,
|
||||
@@ -375,9 +375,9 @@ typedef enum {
|
||||
typedef enum {
|
||||
GHOSTTY_GOTO_SPLIT_PREVIOUS,
|
||||
GHOSTTY_GOTO_SPLIT_NEXT,
|
||||
GHOSTTY_GOTO_SPLIT_TOP,
|
||||
GHOSTTY_GOTO_SPLIT_UP,
|
||||
GHOSTTY_GOTO_SPLIT_LEFT,
|
||||
GHOSTTY_GOTO_SPLIT_BOTTOM,
|
||||
GHOSTTY_GOTO_SPLIT_DOWN,
|
||||
GHOSTTY_GOTO_SPLIT_RIGHT,
|
||||
} ghostty_action_goto_split_e;
|
||||
|
||||
@@ -559,10 +559,13 @@ typedef struct {
|
||||
|
||||
// apprt.Action.Key
|
||||
typedef enum {
|
||||
GHOSTTY_ACTION_QUIT,
|
||||
GHOSTTY_ACTION_NEW_WINDOW,
|
||||
GHOSTTY_ACTION_NEW_TAB,
|
||||
GHOSTTY_ACTION_CLOSE_TAB,
|
||||
GHOSTTY_ACTION_NEW_SPLIT,
|
||||
GHOSTTY_ACTION_CLOSE_ALL_WINDOWS,
|
||||
GHOSTTY_ACTION_TOGGLE_MAXIMIZE,
|
||||
GHOSTTY_ACTION_TOGGLE_FULLSCREEN,
|
||||
GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW,
|
||||
GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS,
|
||||
@@ -681,10 +684,11 @@ void ghostty_config_open();
|
||||
ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*,
|
||||
ghostty_config_t);
|
||||
void ghostty_app_free(ghostty_app_t);
|
||||
bool ghostty_app_tick(ghostty_app_t);
|
||||
void ghostty_app_tick(ghostty_app_t);
|
||||
void* ghostty_app_userdata(ghostty_app_t);
|
||||
void ghostty_app_set_focus(ghostty_app_t, bool);
|
||||
bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s);
|
||||
bool ghostty_app_key_is_binding(ghostty_app_t, ghostty_input_key_s);
|
||||
void ghostty_app_keyboard_changed(ghostty_app_t);
|
||||
void ghostty_app_open_config(ghostty_app_t);
|
||||
void ghostty_app_update_config(ghostty_app_t, ghostty_config_t);
|
||||
@@ -712,7 +716,8 @@ void ghostty_surface_set_color_scheme(ghostty_surface_t,
|
||||
ghostty_color_scheme_e);
|
||||
ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t,
|
||||
ghostty_input_mods_e);
|
||||
void ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s);
|
||||
bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s);
|
||||
bool ghostty_surface_key_is_binding(ghostty_surface_t, ghostty_input_key_s);
|
||||
void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t);
|
||||
bool ghostty_surface_mouse_captured(ghostty_surface_t);
|
||||
bool ghostty_surface_mouse_button(ghostty_surface_t,
|
||||
|
@@ -51,6 +51,8 @@
|
||||
<key>GHOSTTY_MAC_APP</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
<key>MDItemKeywords</key>
|
||||
<string>Terminal</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>NSServices</key>
|
||||
@@ -94,6 +96,8 @@
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>SUEnableAutomaticChecks</key>
|
||||
<false/>
|
||||
<key>SUPublicEDKey</key>
|
||||
<string>wsNcGf5hirwtdXMVnYoxRIX/SqZQLMOsYlD3q3imeok=</string>
|
||||
</dict>
|
||||
|
@@ -11,6 +11,7 @@
|
||||
55154BE02B33911F001622DC /* ghostty in Resources */ = {isa = PBXBuildFile; fileRef = 55154BDF2B33911F001622DC /* ghostty */; };
|
||||
552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; };
|
||||
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; };
|
||||
9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; };
|
||||
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; };
|
||||
A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; };
|
||||
A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; };
|
||||
@@ -68,8 +69,12 @@
|
||||
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */; };
|
||||
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; };
|
||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; };
|
||||
A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3C92D4445E20033CF96 /* Dock.swift */; };
|
||||
A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */; };
|
||||
A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* Xcode.swift */; };
|
||||
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
||||
A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; };
|
||||
A5CA378E2D31D6C300931030 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378D2D31D6C100931030 /* Weak.swift */; };
|
||||
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; };
|
||||
A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0572C9F30860017A1AE /* Cursor.swift */; };
|
||||
A5CBD0592C9F37B10017A1AE /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; };
|
||||
@@ -86,6 +91,8 @@
|
||||
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDB29B8009000646FDA /* SplitView.swift */; };
|
||||
A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */; };
|
||||
A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; };
|
||||
A5CF66D42D289CEE00139794 /* NSEvent+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CF66D32D289CEA00139794 /* NSEvent+Extension.swift */; };
|
||||
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CF66D62D29DDB100139794 /* Ghostty.Event.swift */; };
|
||||
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */; };
|
||||
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3C2B37804400D21823 /* CodableBridge.swift */; };
|
||||
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */; };
|
||||
@@ -98,6 +105,7 @@
|
||||
C159E89D2B69A2EF00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; };
|
||||
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F26EA62B738B9900404083 /* NSView+Extension.swift */; };
|
||||
C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = C1F26EE82B76CBFC00404083 /* VibrantLayer.m */; };
|
||||
CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFBB5FE92D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift */; };
|
||||
FC5218FA2D10FFCE004C93E0 /* zsh in Resources */ = {isa = PBXBuildFile; fileRef = FC5218F92D10FFC7004C93E0 /* zsh */; };
|
||||
FC9ABA9C2D0F53F80020D4C8 /* bash-completion in Resources */ = {isa = PBXBuildFile; fileRef = FC9ABA9B2D0F538D0020D4C8 /* bash-completion */; };
|
||||
/* End PBXBuildFile section */
|
||||
@@ -108,6 +116,7 @@
|
||||
55154BDF2B33911F001622DC /* ghostty */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ghostty; path = "../zig-out/share/ghostty"; sourceTree = "<group>"; };
|
||||
552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = "<group>"; };
|
||||
857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
|
||||
9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = "<group>"; };
|
||||
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = "<group>"; };
|
||||
A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = "<group>"; };
|
||||
A51BFC1D2B2FB5CE00E92F16 /* About.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = About.xib; sourceTree = "<group>"; };
|
||||
@@ -156,10 +165,14 @@
|
||||
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = "<group>"; };
|
||||
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalView.swift; sourceTree = "<group>"; };
|
||||
A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = "<group>"; };
|
||||
A5A2A3C92D4445E20033CF96 /* Dock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dock.swift; sourceTree = "<group>"; };
|
||||
A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSApplication+Extension.swift"; sourceTree = "<group>"; };
|
||||
A5A6F7292CC41B8700B232A5 /* Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xcode.swift; sourceTree = "<group>"; };
|
||||
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; };
|
||||
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayout.swift; sourceTree = "<group>"; };
|
||||
A5CA378D2D31D6C100931030 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
|
||||
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = "<group>"; };
|
||||
A5CBD0572C9F30860017A1AE /* Cursor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cursor.swift; sourceTree = "<group>"; };
|
||||
A5CBD05B2CA0C5C70017A1AE /* QuickTerminal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QuickTerminal.xib; sourceTree = "<group>"; };
|
||||
@@ -175,6 +188,8 @@
|
||||
A5CEAFDB29B8009000646FDA /* SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.swift; sourceTree = "<group>"; };
|
||||
A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.Divider.swift; sourceTree = "<group>"; };
|
||||
A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
|
||||
A5CF66D32D289CEA00139794 /* NSEvent+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSEvent+Extension.swift"; sourceTree = "<group>"; };
|
||||
A5CF66D62D29DDB100139794 /* Ghostty.Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Event.swift; sourceTree = "<group>"; };
|
||||
A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalRestorable.swift; sourceTree = "<group>"; };
|
||||
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableBridge.swift; sourceTree = "<group>"; };
|
||||
A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Ghostty-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -190,6 +205,7 @@
|
||||
C1F26EE72B76CBFC00404083 /* VibrantLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VibrantLayer.h; sourceTree = "<group>"; };
|
||||
C1F26EE82B76CBFC00404083 /* VibrantLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VibrantLayer.m; sourceTree = "<group>"; };
|
||||
C1F26EEA2B76CC2400404083 /* ghostty-bridging-header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ghostty-bridging-header.h"; sourceTree = "<group>"; };
|
||||
CFBB5FE92D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalSpaceBehavior.swift; sourceTree = "<group>"; };
|
||||
FC5218F92D10FFC7004C93E0 /* zsh */ = {isa = PBXFileReference; lastKnownFileType = folder; name = zsh; path = "../zig-out/share/zsh"; sourceTree = "<group>"; };
|
||||
FC9ABA9B2D0F538D0020D4C8 /* bash-completion */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "bash-completion"; path = "../zig-out/share/bash-completion"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -259,18 +275,22 @@
|
||||
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */,
|
||||
A5CBD0572C9F30860017A1AE /* Cursor.swift */,
|
||||
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */,
|
||||
A5A2A3C92D4445E20033CF96 /* Dock.swift */,
|
||||
A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */,
|
||||
A59630962AEE163600D64628 /* HostingWindow.swift */,
|
||||
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */,
|
||||
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */,
|
||||
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */,
|
||||
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */,
|
||||
A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */,
|
||||
A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */,
|
||||
A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */,
|
||||
A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */,
|
||||
C1F26EA62B738B9900404083 /* NSView+Extension.swift */,
|
||||
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */,
|
||||
A5985CD62C320C4500C57AD3 /* String+Extension.swift */,
|
||||
A5CC36142C9CDA03004D6760 /* View+Extension.swift */,
|
||||
A5CA378D2D31D6C100931030 /* Weak.swift */,
|
||||
C1F26EE72B76CBFC00404083 /* VibrantLayer.h */,
|
||||
C1F26EE82B76CBFC00404083 /* VibrantLayer.m */,
|
||||
A5CEAFDA29B8005900646FDA /* SplitView */,
|
||||
@@ -349,12 +369,14 @@
|
||||
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */,
|
||||
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */,
|
||||
A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */,
|
||||
A5CF66D62D29DDB100139794 /* Ghostty.Event.swift */,
|
||||
A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */,
|
||||
A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */,
|
||||
A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */,
|
||||
A59630A12AF0415000D64628 /* Ghostty.TerminalSplit.swift */,
|
||||
A55685DF29A03A9F004303CE /* AppError.swift */,
|
||||
A52FFF5A2CAA54A8000C6A5B /* FullscreenMode+Extension.swift */,
|
||||
A5CF66D32D289CEA00139794 /* NSEvent+Extension.swift */,
|
||||
);
|
||||
path = Ghostty;
|
||||
sourceTree = "<group>";
|
||||
@@ -397,11 +419,12 @@
|
||||
children = (
|
||||
FC9ABA9B2D0F538D0020D4C8 /* bash-completion */,
|
||||
29C15B1C2CDC3B2000520DD4 /* bat */,
|
||||
55154BDF2B33911F001622DC /* ghostty */,
|
||||
552964E52B34A9B400030505 /* vim */,
|
||||
A586167B2B7703CC009BDB1D /* fish */,
|
||||
55154BDF2B33911F001622DC /* ghostty */,
|
||||
A5985CE52C33060F00C57AD3 /* man */,
|
||||
9351BE8E2D22937F003B3499 /* nvim */,
|
||||
A5A1F8842A489D6800D1E8BC /* terminfo */,
|
||||
552964E52B34A9B400030505 /* vim */,
|
||||
FC5218F92D10FFC7004C93E0 /* zsh */,
|
||||
);
|
||||
name = Resources;
|
||||
@@ -436,6 +459,7 @@
|
||||
children = (
|
||||
A5CBD05B2CA0C5C70017A1AE /* QuickTerminal.xib */,
|
||||
A5CBD05D2CA0C5E70017A1AE /* QuickTerminalController.swift */,
|
||||
CFBB5FE92D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift */,
|
||||
A5CBD0632CA122E70017A1AE /* QuickTerminalPosition.swift */,
|
||||
A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */,
|
||||
A5CBD05F2CA0C9080017A1AE /* QuickTerminalWindow.swift */,
|
||||
@@ -579,6 +603,7 @@
|
||||
A5985CE62C33060F00C57AD3 /* man in Resources */,
|
||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */,
|
||||
552964E62B34A9B400030505 /* vim in Resources */,
|
||||
9351BE8E3D22937F003B3499 /* nvim in Resources */,
|
||||
A5CBD05C2CA0C5C70017A1AE /* QuickTerminal.xib in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -603,16 +628,20 @@
|
||||
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */,
|
||||
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */,
|
||||
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
|
||||
CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */,
|
||||
A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */,
|
||||
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */,
|
||||
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
|
||||
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */,
|
||||
A5CF66D42D289CEE00139794 /* NSEvent+Extension.swift in Sources */,
|
||||
A5CBD0642CA122E70017A1AE /* QuickTerminalPosition.swift in Sources */,
|
||||
A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */,
|
||||
A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */,
|
||||
A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */,
|
||||
A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */,
|
||||
A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */,
|
||||
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */,
|
||||
A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */,
|
||||
A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */,
|
||||
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */,
|
||||
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */,
|
||||
@@ -628,12 +657,14 @@
|
||||
A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */,
|
||||
A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */,
|
||||
A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */,
|
||||
A5CA378E2D31D6C300931030 /* Weak.swift in Sources */,
|
||||
A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */,
|
||||
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */,
|
||||
A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,
|
||||
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */,
|
||||
A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */,
|
||||
A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */,
|
||||
A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */,
|
||||
A59630A22AF0415000D64628 /* Ghostty.TerminalSplit.swift in Sources */,
|
||||
A5FEB3002ABB69450068369E /* main.swift in Sources */,
|
||||
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */,
|
||||
@@ -643,6 +674,7 @@
|
||||
A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */,
|
||||
A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */,
|
||||
A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */,
|
||||
A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */,
|
||||
A54B0CEF2D0D2E2800CBEFF8 /* ColorizedGhosttyIconImage.swift in Sources */,
|
||||
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */,
|
||||
A55685E029A03A9F004303CE /* AppError.swift in Sources */,
|
||||
@@ -761,21 +793,22 @@
|
||||
INFOPLIST_FILE = "Ghostty-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Ghostty;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
|
||||
INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program in Ghostty wants to use AppleScript.";
|
||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "A program in Ghostty wants to use your calendar.";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "A program in Ghostty wants to use the camera.";
|
||||
INFOPLIST_KEY_NSContactsUsageDescription = "A program in Ghostty wants to use your contacts.";
|
||||
INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program running within Ghostty would like to use AppleScript.";
|
||||
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "A program running within Ghostty would like to use Bluetooth.";
|
||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "A program running within Ghostty would like to access your Calendar.";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "A program running within Ghostty would like to use the camera.";
|
||||
INFOPLIST_KEY_NSContactsUsageDescription = "A program running within Ghostty would like to access your Contacts.";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "A program in Ghostty wants to access the local network.";
|
||||
INFOPLIST_KEY_NSLocationTemporaryUsageDescriptionDictionary = "A program in Ghostty wants to use your location temporarily.";
|
||||
INFOPLIST_KEY_NSLocationUsageDescription = "A program in Ghostty wants to use your location information.";
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "A program running within Ghostty would like to access the local network.";
|
||||
INFOPLIST_KEY_NSLocationTemporaryUsageDescriptionDictionary = "A program running within Ghostty would like to use your location temporarily.";
|
||||
INFOPLIST_KEY_NSLocationUsageDescription = "A program running within Ghostty would like to access your location information.";
|
||||
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "A program in Ghostty wants to use your microphone.";
|
||||
INFOPLIST_KEY_NSMotionUsageDescription = "A program in Ghostty wants to access motion data.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "A program in Ghostty wants to use your photo library.";
|
||||
INFOPLIST_KEY_NSRemindersUsageDescription = "A program in Ghostty wants to access your reminders.";
|
||||
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program in Ghostty wants to use speech recognition.";
|
||||
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program in Ghostty requires elevated privileges.";
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "A program running within Ghostty would like to use your microphone.";
|
||||
INFOPLIST_KEY_NSMotionUsageDescription = "A program running within Ghostty would like to access motion data.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "A program running within Ghostty would like to access your Photo Library.";
|
||||
INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders.";
|
||||
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition.";
|
||||
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
@@ -930,21 +963,22 @@
|
||||
INFOPLIST_FILE = "Ghostty-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Ghostty;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
|
||||
INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program in Ghostty wants to use AppleScript.";
|
||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "A program in Ghostty wants to use your calendar.";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "A program in Ghostty wants to use the camera.";
|
||||
INFOPLIST_KEY_NSContactsUsageDescription = "A program in Ghostty wants to use your contacts.";
|
||||
INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program running within Ghostty would like to use AppleScript.";
|
||||
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "A program running within Ghostty would like to use Bluetooth.";
|
||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "A program running within Ghostty would like to access your Calendar.";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "A program running within Ghostty would like to use the camera.";
|
||||
INFOPLIST_KEY_NSContactsUsageDescription = "A program running within Ghostty would like to access your Contacts.";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "A program in Ghostty wants to access the local network.";
|
||||
INFOPLIST_KEY_NSLocationTemporaryUsageDescriptionDictionary = "A program in Ghostty wants to use your location temporarily.";
|
||||
INFOPLIST_KEY_NSLocationUsageDescription = "A program in Ghostty wants to use your location information.";
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "A program running within Ghostty would like to access the local network.";
|
||||
INFOPLIST_KEY_NSLocationTemporaryUsageDescriptionDictionary = "A program running within Ghostty would like to use your location temporarily.";
|
||||
INFOPLIST_KEY_NSLocationUsageDescription = "A program running within Ghostty would like to access your location information.";
|
||||
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "A program in Ghostty wants to use your microphone.";
|
||||
INFOPLIST_KEY_NSMotionUsageDescription = "A program in Ghostty wants to access motion data.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "A program in Ghostty wants to use your photo library.";
|
||||
INFOPLIST_KEY_NSRemindersUsageDescription = "A program in Ghostty wants to access your reminders.";
|
||||
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program in Ghostty wants to use speech recognition.";
|
||||
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program in Ghostty requires elevated privileges.";
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "A program running within Ghostty would like to use your microphone.";
|
||||
INFOPLIST_KEY_NSMotionUsageDescription = "A program running within Ghostty would like to access motion data.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "A program running within Ghostty would like to access your Photo Library.";
|
||||
INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders.";
|
||||
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition.";
|
||||
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
@@ -983,21 +1017,22 @@
|
||||
INFOPLIST_FILE = "Ghostty-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Ghostty;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
|
||||
INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program in Ghostty wants to use AppleScript.";
|
||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "A program in Ghostty wants to use your calendar.";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "A program in Ghostty wants to use the camera.";
|
||||
INFOPLIST_KEY_NSContactsUsageDescription = "A program in Ghostty wants to use your contacts.";
|
||||
INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program running within Ghostty would like to use AppleScript.";
|
||||
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "A program running within Ghostty would like to use Bluetooth.";
|
||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "A program running within Ghostty would like to access your Calendar.";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "A program running within Ghostty would like to use the camera.";
|
||||
INFOPLIST_KEY_NSContactsUsageDescription = "A program running within Ghostty would like to access your Contacts.";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "A program in Ghostty wants to access the local network.";
|
||||
INFOPLIST_KEY_NSLocationTemporaryUsageDescriptionDictionary = "A program in Ghostty wants to use your location temporarily.";
|
||||
INFOPLIST_KEY_NSLocationUsageDescription = "A program in Ghostty wants to use your location information.";
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "A program running within Ghostty would like to access the local network.";
|
||||
INFOPLIST_KEY_NSLocationTemporaryUsageDescriptionDictionary = "A program running within Ghostty would like to use your location temporarily.";
|
||||
INFOPLIST_KEY_NSLocationUsageDescription = "A program running within Ghostty would like to access your location information.";
|
||||
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "A program in Ghostty wants to use your microphone.";
|
||||
INFOPLIST_KEY_NSMotionUsageDescription = "A program in Ghostty wants to access motion data.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "A program in Ghostty wants to use your photo library.";
|
||||
INFOPLIST_KEY_NSRemindersUsageDescription = "A program in Ghostty wants to access your reminders.";
|
||||
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program in Ghostty wants to use speech recognition.";
|
||||
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program in Ghostty requires elevated privileges.";
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "A program running within Ghostty would like to use your microphone.";
|
||||
INFOPLIST_KEY_NSMotionUsageDescription = "A program running within Ghostty would like to access motion data.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "A program running within Ghostty would like to access your Photo Library.";
|
||||
INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders.";
|
||||
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition.";
|
||||
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
|
@@ -30,11 +30,13 @@ class AppDelegate: NSObject,
|
||||
@IBOutlet private var menuSplitRight: NSMenuItem?
|
||||
@IBOutlet private var menuSplitDown: NSMenuItem?
|
||||
@IBOutlet private var menuClose: NSMenuItem?
|
||||
@IBOutlet private var menuCloseTab: NSMenuItem?
|
||||
@IBOutlet private var menuCloseWindow: NSMenuItem?
|
||||
@IBOutlet private var menuCloseAllWindows: NSMenuItem?
|
||||
|
||||
@IBOutlet private var menuCopy: NSMenuItem?
|
||||
@IBOutlet private var menuPaste: NSMenuItem?
|
||||
@IBOutlet private var menuPasteSelection: NSMenuItem?
|
||||
@IBOutlet private var menuSelectAll: NSMenuItem?
|
||||
|
||||
@IBOutlet private var menuToggleVisibility: NSMenuItem?
|
||||
@@ -90,10 +92,8 @@ class AppDelegate: NSObject,
|
||||
return ProcessInfo.processInfo.systemUptime - applicationLaunchTime
|
||||
}
|
||||
|
||||
/// Tracks whether the application is currently visible. This can be gamed, i.e. if a user manually
|
||||
/// brings each window one by one to the front. But at worst its off by one set of toggles and this
|
||||
/// makes our logic very easy.
|
||||
private var isVisible: Bool = true
|
||||
/// Tracks the windows that we hid for toggleVisibility.
|
||||
private var hiddenWindows: [Weak<NSWindow>] = []
|
||||
|
||||
/// The observer for the app appearance.
|
||||
private var appearanceObserver: NSKeyValueObservation? = nil
|
||||
@@ -217,15 +217,20 @@ class AppDelegate: NSObject,
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ notification: Notification) {
|
||||
guard !applicationHasBecomeActive else { return }
|
||||
applicationHasBecomeActive = true
|
||||
// If we're back then clear the hidden windows
|
||||
self.hiddenWindows = []
|
||||
|
||||
// Let's launch our first window. We only do this if we have no other windows. It
|
||||
// is possible to have other windows in a few scenarios:
|
||||
// - if we're opening a URL since `application(_:openFile:)` is called before this.
|
||||
// - if we're restoring from persisted state
|
||||
if terminalManager.windows.count == 0 && derivedConfig.initialWindow {
|
||||
terminalManager.newWindow()
|
||||
// First launch stuff
|
||||
if (!applicationHasBecomeActive) {
|
||||
applicationHasBecomeActive = true
|
||||
|
||||
// Let's launch our first window. We only do this if we have no other windows. It
|
||||
// is possible to have other windows in a few scenarios:
|
||||
// - if we're opening a URL since `application(_:openFile:)` is called before this.
|
||||
// - if we're restoring from persisted state
|
||||
if terminalManager.windows.count == 0 && derivedConfig.initialWindow {
|
||||
terminalManager.newWindow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,6 +351,7 @@ class AppDelegate: NSObject,
|
||||
syncMenuShortcut(config, action: "new_window", menuItem: self.menuNewWindow)
|
||||
syncMenuShortcut(config, action: "new_tab", menuItem: self.menuNewTab)
|
||||
syncMenuShortcut(config, action: "close_surface", menuItem: self.menuClose)
|
||||
syncMenuShortcut(config, action: "close_tab", menuItem: self.menuCloseTab)
|
||||
syncMenuShortcut(config, action: "close_window", menuItem: self.menuCloseWindow)
|
||||
syncMenuShortcut(config, action: "close_all_windows", menuItem: self.menuCloseAllWindows)
|
||||
syncMenuShortcut(config, action: "new_split:right", menuItem: self.menuSplitRight)
|
||||
@@ -353,13 +359,14 @@ class AppDelegate: NSObject,
|
||||
|
||||
syncMenuShortcut(config, action: "copy_to_clipboard", menuItem: self.menuCopy)
|
||||
syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste)
|
||||
syncMenuShortcut(config, action: "paste_from_selection", menuItem: self.menuPasteSelection)
|
||||
syncMenuShortcut(config, action: "select_all", menuItem: self.menuSelectAll)
|
||||
|
||||
syncMenuShortcut(config, action: "toggle_split_zoom", menuItem: self.menuZoomSplit)
|
||||
syncMenuShortcut(config, action: "goto_split:previous", menuItem: self.menuPreviousSplit)
|
||||
syncMenuShortcut(config, action: "goto_split:next", menuItem: self.menuNextSplit)
|
||||
syncMenuShortcut(config, action: "goto_split:top", menuItem: self.menuSelectSplitAbove)
|
||||
syncMenuShortcut(config, action: "goto_split:bottom", menuItem: self.menuSelectSplitBelow)
|
||||
syncMenuShortcut(config, action: "goto_split:up", menuItem: self.menuSelectSplitAbove)
|
||||
syncMenuShortcut(config, action: "goto_split:down", menuItem: self.menuSelectSplitBelow)
|
||||
syncMenuShortcut(config, action: "goto_split:left", menuItem: self.menuSelectSplitLeft)
|
||||
syncMenuShortcut(config, action: "goto_split:right", menuItem: self.menuSelectSplitRight)
|
||||
syncMenuShortcut(config, action: "resize_split:up,10", menuItem: self.menuMoveSplitDividerUp)
|
||||
@@ -424,32 +431,42 @@ class AppDelegate: NSObject,
|
||||
// If we have a main window then we don't process any of the keys
|
||||
// because we let it capture and propagate.
|
||||
guard NSApp.mainWindow == nil else { return event }
|
||||
|
||||
|
||||
// If this event as-is would result in a key binding then we send it.
|
||||
if let app = ghostty.app,
|
||||
ghostty_app_key_is_binding(
|
||||
app,
|
||||
event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS)) {
|
||||
// If the key was handled by Ghostty we stop the event chain. If
|
||||
// the key wasn't handled then we let it fall through and continue
|
||||
// processing. This is important because some bindings may have no
|
||||
// affect at this scope.
|
||||
if (ghostty_app_key(
|
||||
app,
|
||||
event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS))) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// If this event would be handled by our menu then we do nothing.
|
||||
if let mainMenu = NSApp.mainMenu,
|
||||
mainMenu.performKeyEquivalent(with: event) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// If we reach this point then we try to process the key event
|
||||
// through the Ghostty key mechanism.
|
||||
|
||||
|
||||
// Ghostty must be loaded
|
||||
guard let ghostty = self.ghostty.app else { return event }
|
||||
|
||||
|
||||
// Build our event input and call ghostty
|
||||
var key_ev = ghostty_input_key_s()
|
||||
key_ev.action = GHOSTTY_ACTION_PRESS
|
||||
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
key_ev.keycode = UInt32(event.keyCode)
|
||||
key_ev.text = nil
|
||||
key_ev.composing = false
|
||||
if (ghostty_app_key(ghostty, key_ev)) {
|
||||
if (ghostty_app_key(ghostty, event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS))) {
|
||||
// The key was used so we want to stop it from going to our Mac app
|
||||
Ghostty.logger.debug("local key event handled event=\(event)")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
@@ -484,11 +501,19 @@ class AppDelegate: NSObject,
|
||||
default: UserDefaults.standard.removeObject(forKey: "NSQuitAlwaysKeepsWindows")
|
||||
}
|
||||
|
||||
// Sync our auto-update settings
|
||||
updaterController.updater.automaticallyChecksForUpdates =
|
||||
config.autoUpdate == .check || config.autoUpdate == .download
|
||||
updaterController.updater.automaticallyDownloadsUpdates =
|
||||
config.autoUpdate == .download
|
||||
// Sync our auto-update settings. If SUEnableAutomaticChecks (in our Info.plist) is
|
||||
// explicitly false (NO), auto-updates are disabled. Otherwise, we use the behavior
|
||||
// defined by our "auto-update" configuration (if set) or fall back to Sparkle
|
||||
// user-based defaults.
|
||||
if Bundle.main.infoDictionary?["SUEnableAutomaticChecks"] as? Bool == false {
|
||||
updaterController.updater.automaticallyChecksForUpdates = false
|
||||
updaterController.updater.automaticallyDownloadsUpdates = false
|
||||
} else if let autoUpdate = config.autoUpdate {
|
||||
updaterController.updater.automaticallyChecksForUpdates =
|
||||
autoUpdate == .check || autoUpdate == .download
|
||||
updaterController.updater.automaticallyDownloadsUpdates =
|
||||
autoUpdate == .download
|
||||
}
|
||||
|
||||
// Config could change keybindings, so update everything that depends on that
|
||||
syncMenuShortcuts(config)
|
||||
@@ -662,7 +687,7 @@ class AppDelegate: NSObject,
|
||||
}
|
||||
|
||||
@IBAction func showHelp(_ sender: Any) {
|
||||
guard let url = URL(string: "https://github.com/ghostty-org/ghostty") else { return }
|
||||
guard let url = URL(string: "https://ghostty.org/docs") else { return }
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
|
||||
@@ -684,21 +709,23 @@ class AppDelegate: NSObject,
|
||||
|
||||
/// Toggles visibility of all Ghosty Terminal windows. When hidden, activates Ghostty as the frontmost application
|
||||
@IBAction func toggleVisibility(_ sender: Any) {
|
||||
// We only care about terminal windows.
|
||||
for window in NSApp.windows.filter({ $0.windowController is BaseTerminalController }) {
|
||||
if isVisible {
|
||||
window.orderOut(nil)
|
||||
} else {
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
// If we have focus, then we hide all windows.
|
||||
if NSApp.isActive {
|
||||
// We need to keep track of the windows that were visible because we only
|
||||
// want to bring back these windows if we remove the toggle.
|
||||
self.hiddenWindows = NSApp.windows.filter { $0.isVisible }.map { Weak($0) }
|
||||
NSApp.hide(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// After bringing them all to front we make sure our app is active too.
|
||||
if !isVisible {
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
// If we're not active, we want to become active
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
|
||||
isVisible.toggle()
|
||||
// Bring all windows to the front. Note: we don't use NSApp.unhide because
|
||||
// that will unhide ALL hidden windows. We want to only bring forward the
|
||||
// ones that we hid.
|
||||
self.hiddenWindows.forEach { $0.value?.orderFrontRegardless() }
|
||||
self.hiddenWindows = []
|
||||
}
|
||||
|
||||
private struct DerivedConfig {
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23094" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23094"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23504"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||
@@ -17,6 +17,7 @@
|
||||
<outlet property="menuCheckForUpdates" destination="GEA-5y-yzH" id="0nV-Tf-nJQ"/>
|
||||
<outlet property="menuClose" destination="DVo-aG-piG" id="R3t-0C-aSU"/>
|
||||
<outlet property="menuCloseAllWindows" destination="yKr-Vi-Yqw" id="Zet-Ir-zbm"/>
|
||||
<outlet property="menuCloseTab" destination="Obb-Mk-j8J" id="Gda-L0-gdz"/>
|
||||
<outlet property="menuCloseWindow" destination="W5w-UZ-crk" id="6ff-BT-ENV"/>
|
||||
<outlet property="menuCopy" destination="Jqf-pv-Zcu" id="bKd-1C-oy9"/>
|
||||
<outlet property="menuDecreaseFontSize" destination="kzb-SZ-dOA" id="Y1B-Vh-6Z2"/>
|
||||
@@ -31,6 +32,7 @@
|
||||
<outlet property="menuNextSplit" destination="bD7-ei-wKU" id="LeT-xw-eh4"/>
|
||||
<outlet property="menuOpenConfig" destination="BOF-NM-1cW" id="Nze-Go-glw"/>
|
||||
<outlet property="menuPaste" destination="i27-pK-umN" id="ICc-X2-gV3"/>
|
||||
<outlet property="menuPasteSelection" destination="akq-ov-Jjh" id="GS8-aQ-hVw"/>
|
||||
<outlet property="menuPreviousSplit" destination="Lic-px-1wg" id="Rto-CG-yRe"/>
|
||||
<outlet property="menuQuickTerminal" destination="1pv-LF-NBJ" id="glN-5B-IGi"/>
|
||||
<outlet property="menuQuit" destination="4sb-4s-VLi" id="qYN-S1-6UW"/>
|
||||
@@ -154,6 +156,12 @@
|
||||
<action selector="close:" target="-1" id="tTZ-2b-Mbm"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Close Tab" id="Obb-Mk-j8J">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="closeTab:" target="-1" id="UBb-Bd-nkj"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Close Window" id="W5w-UZ-crk">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
@@ -185,6 +193,12 @@
|
||||
<action selector="paste:" target="-1" id="ZKe-2B-mel"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste Selection" id="akq-ov-Jjh">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="pasteSelection:" target="-1" id="vo3-Rf-Udb"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select All" id="q2h-lq-e4r">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
|
@@ -3,6 +3,12 @@ import Cocoa
|
||||
import SwiftUI
|
||||
import GhosttyKit
|
||||
|
||||
// This is a Apple's private function that we need to call to get the active space.
|
||||
@_silgen_name("CGSGetActiveSpace")
|
||||
func CGSGetActiveSpace(_ cid: Int) -> size_t
|
||||
@_silgen_name("CGSMainConnectionID")
|
||||
func CGSMainConnectionID() -> Int
|
||||
|
||||
/// Controller for the "quick" terminal.
|
||||
class QuickTerminalController: BaseTerminalController {
|
||||
override var windowNibName: NSNib.Name? { "QuickTerminal" }
|
||||
@@ -18,6 +24,12 @@ class QuickTerminalController: BaseTerminalController {
|
||||
/// application to the front.
|
||||
private var previousApp: NSRunningApplication? = nil
|
||||
|
||||
// The active space when the quick terminal was last shown.
|
||||
private var previousActiveSpace: size_t = 0
|
||||
|
||||
/// Non-nil if we have hidden dock state.
|
||||
private var hiddenDock: HiddenDock? = nil
|
||||
|
||||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||
private var derivedConfig: DerivedConfig
|
||||
|
||||
@@ -32,6 +44,11 @@ class QuickTerminalController: BaseTerminalController {
|
||||
|
||||
// Setup our notifications for behaviors
|
||||
let center = NotificationCenter.default
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(applicationWillTerminate(_:)),
|
||||
name: NSApplication.willTerminateNotification,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onToggleFullscreen),
|
||||
@@ -52,6 +69,9 @@ class QuickTerminalController: BaseTerminalController {
|
||||
// Remove all of our notificationcenter subscriptions
|
||||
let center = NotificationCenter.default
|
||||
center.removeObserver(self)
|
||||
|
||||
// Make sure we restore our hidden dock
|
||||
hiddenDock = nil
|
||||
}
|
||||
|
||||
// MARK: NSWindowController
|
||||
@@ -69,7 +89,7 @@ class QuickTerminalController: BaseTerminalController {
|
||||
window.isRestorable = false
|
||||
|
||||
// Setup our configured appearance that we support.
|
||||
syncAppearance(ghostty.config)
|
||||
syncAppearance()
|
||||
|
||||
// Setup our initial size based on our configured position
|
||||
position.setLoaded(window)
|
||||
@@ -87,6 +107,17 @@ class QuickTerminalController: BaseTerminalController {
|
||||
|
||||
// MARK: NSWindowDelegate
|
||||
|
||||
override func windowDidBecomeKey(_ notification: Notification) {
|
||||
super.windowDidBecomeKey(notification)
|
||||
|
||||
// If we're not visible we don't care to run the logic below. It only
|
||||
// applies if we can be seen.
|
||||
guard visible else { return }
|
||||
|
||||
// Re-hide the dock if we were hiding it before.
|
||||
hiddenDock?.hide()
|
||||
}
|
||||
|
||||
override func windowDidResignKey(_ notification: Notification) {
|
||||
super.windowDidResignKey(notification)
|
||||
|
||||
@@ -107,8 +138,32 @@ class QuickTerminalController: BaseTerminalController {
|
||||
self.previousApp = nil
|
||||
}
|
||||
|
||||
if (derivedConfig.quickTerminalAutoHide) {
|
||||
animateOut()
|
||||
// Regardless of autohide, we always want to bring the dock back
|
||||
// when we lose focus.
|
||||
hiddenDock?.restore()
|
||||
|
||||
if derivedConfig.quickTerminalAutoHide {
|
||||
switch derivedConfig.quickTerminalSpaceBehavior {
|
||||
case .remain:
|
||||
// If we lose focus on the active space, then we can animate out
|
||||
animateOut()
|
||||
|
||||
case .move:
|
||||
let currentActiveSpace = CGSGetActiveSpace(CGSMainConnectionID())
|
||||
if previousActiveSpace == currentActiveSpace {
|
||||
// We haven't moved spaces. We lost focus to another app on the
|
||||
// current space. Animate out.
|
||||
animateOut()
|
||||
} else {
|
||||
// We've moved to a different space. Bring the quick terminal back
|
||||
// into view.
|
||||
DispatchQueue.main.async {
|
||||
self.window?.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
self.previousActiveSpace = currentActiveSpace
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +218,9 @@ class QuickTerminalController: BaseTerminalController {
|
||||
}
|
||||
}
|
||||
|
||||
// Set previous active space
|
||||
self.previousActiveSpace = CGSGetActiveSpace(CGSMainConnectionID())
|
||||
|
||||
// Animate the window in
|
||||
animateWindowIn(window: window, from: position)
|
||||
|
||||
@@ -198,8 +256,29 @@ class QuickTerminalController: BaseTerminalController {
|
||||
// Move our window off screen to the top
|
||||
position.setInitial(in: window, on: screen)
|
||||
|
||||
// We need to set our window level to a high value. In testing, only
|
||||
// popUpMenu and above do what we want. This gets it above the menu bar
|
||||
// and lets us render off screen.
|
||||
window.level = .popUpMenu
|
||||
|
||||
// Move it to the visible position since animation requires this
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
DispatchQueue.main.async {
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
// If our dock position would conflict with our target location then
|
||||
// we autohide the dock.
|
||||
if position.conflictsWithDock(on: screen) {
|
||||
if (hiddenDock == nil) {
|
||||
hiddenDock = .init()
|
||||
}
|
||||
|
||||
hiddenDock?.hide()
|
||||
} else {
|
||||
// Ensure we don't have any hidden dock if we don't conflict.
|
||||
// The deinit will restore.
|
||||
hiddenDock = nil
|
||||
}
|
||||
|
||||
// Run the animation that moves our window into the proper place and makes
|
||||
// it visible.
|
||||
@@ -211,8 +290,20 @@ class QuickTerminalController: BaseTerminalController {
|
||||
// There is a very minor delay here so waiting at least an event loop tick
|
||||
// keeps us safe from the view not being on the window.
|
||||
DispatchQueue.main.async {
|
||||
// If we canceled our animation in we do nothing
|
||||
guard self.visible else { return }
|
||||
// If we canceled our animation clean up some state.
|
||||
guard self.visible else {
|
||||
self.hiddenDock = nil
|
||||
return
|
||||
}
|
||||
|
||||
// After animating in, we reset the window level to a value that
|
||||
// is above other windows but not as high as popUpMenu. This allows
|
||||
// things like IME dropdowns to appear properly.
|
||||
window.level = .floating
|
||||
|
||||
// Now that the window is visible, sync our appearance. This function
|
||||
// requires the window is visible.
|
||||
self.syncAppearance()
|
||||
|
||||
// Once our animation is done, we must grab focus since we can't grab
|
||||
// focus of a non-visible window.
|
||||
@@ -272,6 +363,17 @@ class QuickTerminalController: BaseTerminalController {
|
||||
}
|
||||
|
||||
private func animateWindowOut(window: NSWindow, to position: QuickTerminalPosition) {
|
||||
// If we hid the dock then we unhide it.
|
||||
hiddenDock = nil
|
||||
|
||||
// If the window isn't on our active space then we don't animate, we just
|
||||
// hide it.
|
||||
if !window.isOnActiveSpace {
|
||||
self.previousApp = nil
|
||||
window.orderOut(self)
|
||||
return
|
||||
}
|
||||
|
||||
// We always animate out to whatever screen the window is actually on.
|
||||
guard let screen = window.screen ?? NSScreen.main else { return }
|
||||
|
||||
@@ -293,6 +395,11 @@ class QuickTerminalController: BaseTerminalController {
|
||||
}
|
||||
}
|
||||
|
||||
// We need to set our window level to a high value. In testing, only
|
||||
// popUpMenu and above do what we want. This gets it above the menu bar
|
||||
// and lets us render off screen.
|
||||
window.level = .popUpMenu
|
||||
|
||||
NSAnimationContext.runAnimationGroup({ context in
|
||||
context.duration = derivedConfig.quickTerminalAnimationDuration
|
||||
context.timingFunction = .init(name: .easeIn)
|
||||
@@ -304,34 +411,18 @@ class QuickTerminalController: BaseTerminalController {
|
||||
})
|
||||
}
|
||||
|
||||
private func syncAppearance(_ config: Ghostty.Config) {
|
||||
private func syncAppearance() {
|
||||
guard let window else { return }
|
||||
|
||||
// If our window is not visible, then delay this. This is possible specifically
|
||||
// during state restoration but probably in other scenarios as well. To delay,
|
||||
// we just loop directly on the dispatch queue. We have to delay because some
|
||||
// APIs such as window blur have no effect unless the window is visible.
|
||||
guard window.isVisible else {
|
||||
// Weak window so that if the window changes or is destroyed we aren't holding a ref
|
||||
DispatchQueue.main.async { [weak self] in self?.syncAppearance(config) }
|
||||
return
|
||||
}
|
||||
// Change the collection behavior of the window depending on the configuration.
|
||||
window.collectionBehavior = derivedConfig.quickTerminalSpaceBehavior.collectionBehavior
|
||||
|
||||
// Terminals typically operate in sRGB color space and macOS defaults
|
||||
// to "native" which is typically P3. There is a lot more resources
|
||||
// covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
|
||||
// Ghostty defaults to sRGB but this can be overridden.
|
||||
switch (config.windowColorspace) {
|
||||
case "display-p3":
|
||||
window.colorSpace = .displayP3
|
||||
case "srgb":
|
||||
fallthrough
|
||||
default:
|
||||
window.colorSpace = .sRGB
|
||||
}
|
||||
// If our window is not visible, then no need to sync the appearance yet.
|
||||
// Some APIs such as window blur have no effect unless the window is visible.
|
||||
guard window.isVisible else { return }
|
||||
|
||||
// If we have window transparency then set it transparent. Otherwise set it opaque.
|
||||
if (config.backgroundOpacity < 1) {
|
||||
if (self.derivedConfig.backgroundOpacity < 1) {
|
||||
window.isOpaque = false
|
||||
|
||||
// This is weird, but we don't use ".clear" because this creates a look that
|
||||
@@ -370,6 +461,13 @@ class QuickTerminalController: BaseTerminalController {
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
@objc private func applicationWillTerminate(_ notification: Notification) {
|
||||
// If the application is going to terminate we want to make sure we
|
||||
// restore any global dock state. I think deinit should be called which
|
||||
// would call this anyways but I can't be sure so I will do this too.
|
||||
hiddenDock = nil
|
||||
}
|
||||
|
||||
@objc private func onToggleFullscreen(notification: SwiftUI.Notification) {
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard target == self.focusedSurface else { return }
|
||||
@@ -391,24 +489,59 @@ class QuickTerminalController: BaseTerminalController {
|
||||
// Update our derived config
|
||||
self.derivedConfig = DerivedConfig(config)
|
||||
|
||||
syncAppearance(config)
|
||||
syncAppearance()
|
||||
}
|
||||
|
||||
private struct DerivedConfig {
|
||||
let quickTerminalScreen: QuickTerminalScreen
|
||||
let quickTerminalAnimationDuration: Double
|
||||
let quickTerminalAutoHide: Bool
|
||||
let quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior
|
||||
let backgroundOpacity: Double
|
||||
|
||||
init() {
|
||||
self.quickTerminalScreen = .main
|
||||
self.quickTerminalAnimationDuration = 0.2
|
||||
self.quickTerminalAutoHide = true
|
||||
self.quickTerminalSpaceBehavior = .move
|
||||
self.backgroundOpacity = 1.0
|
||||
}
|
||||
|
||||
init(_ config: Ghostty.Config) {
|
||||
self.quickTerminalScreen = config.quickTerminalScreen
|
||||
self.quickTerminalAnimationDuration = config.quickTerminalAnimationDuration
|
||||
self.quickTerminalAutoHide = config.quickTerminalAutoHide
|
||||
self.quickTerminalSpaceBehavior = config.quickTerminalSpaceBehavior
|
||||
self.backgroundOpacity = config.backgroundOpacity
|
||||
}
|
||||
}
|
||||
|
||||
/// Hides the dock globally (not just NSApp). This is only used if the quick terminal is
|
||||
/// in a conflicting position with the dock.
|
||||
private class HiddenDock {
|
||||
let previousAutoHide: Bool
|
||||
private var hidden: Bool = false
|
||||
|
||||
init() {
|
||||
previousAutoHide = Dock.autoHideEnabled
|
||||
}
|
||||
|
||||
deinit {
|
||||
restore()
|
||||
}
|
||||
|
||||
func hide() {
|
||||
guard !hidden else { return }
|
||||
NSApp.acquirePresentationOption(.autoHideDock)
|
||||
Dock.autoHideEnabled = true
|
||||
hidden = true
|
||||
}
|
||||
|
||||
func restore() {
|
||||
guard hidden else { return }
|
||||
NSApp.releasePresentationOption(.autoHideDock)
|
||||
Dock.autoHideEnabled = previousAutoHide
|
||||
hidden = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -69,7 +69,7 @@ enum QuickTerminalPosition : String {
|
||||
finalSize.width = screen.frame.width
|
||||
|
||||
case .left, .right:
|
||||
finalSize.height = screen.frame.height
|
||||
finalSize.height = screen.visibleFrame.height
|
||||
|
||||
case .center:
|
||||
finalSize.width = screen.frame.width / 2
|
||||
@@ -89,13 +89,13 @@ enum QuickTerminalPosition : String {
|
||||
return .init(x: screen.frame.minX, y: -window.frame.height)
|
||||
|
||||
case .left:
|
||||
return .init(x: -window.frame.width, y: 0)
|
||||
return .init(x: screen.frame.minX-window.frame.width, y: 0)
|
||||
|
||||
case .right:
|
||||
return .init(x: screen.frame.maxX, y: 0)
|
||||
|
||||
case .center:
|
||||
return .init(x: (screen.visibleFrame.maxX - window.frame.width) / 2, y: screen.visibleFrame.maxY - window.frame.width)
|
||||
return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.height - window.frame.width)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,25 @@ enum QuickTerminalPosition : String {
|
||||
return .init(x: screen.visibleFrame.maxX - window.frame.width, y: window.frame.origin.y)
|
||||
|
||||
case .center:
|
||||
return .init(x: (screen.visibleFrame.maxX - window.frame.width) / 2, y: (screen.visibleFrame.maxY - window.frame.height) / 2)
|
||||
return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)
|
||||
}
|
||||
}
|
||||
|
||||
func conflictsWithDock(on screen: NSScreen) -> Bool {
|
||||
// Screen must have a dock for it to conflict
|
||||
guard screen.hasDock else { return false }
|
||||
|
||||
// Get the dock orientation for this screen
|
||||
guard let orientation = Dock.orientation else { return false }
|
||||
|
||||
// Depending on the orientation of the dock, we conflict if our quick terminal
|
||||
// would potentially "hit" the dock. In the future we should probably consider
|
||||
// the frame of the quick terminal.
|
||||
return switch (orientation) {
|
||||
case .top: self == .top || self == .left || self == .right
|
||||
case .bottom: self == .bottom || self == .left || self == .right
|
||||
case .left: self == .top || self == .bottom
|
||||
case .right: self == .top || self == .bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,36 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
enum QuickTerminalSpaceBehavior {
|
||||
case remain
|
||||
case move
|
||||
|
||||
init?(fromGhosttyConfig string: String) {
|
||||
switch (string) {
|
||||
case "move":
|
||||
self = .move
|
||||
|
||||
case "remain":
|
||||
self = .remain
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var collectionBehavior: NSWindow.CollectionBehavior {
|
||||
let commonBehavior: [NSWindow.CollectionBehavior] = [
|
||||
.ignoresCycle,
|
||||
.fullScreenAuxiliary
|
||||
]
|
||||
|
||||
switch (self) {
|
||||
case .move:
|
||||
// We want this to move the window to the active space.
|
||||
return NSWindow.CollectionBehavior([.canJoinAllSpaces] + commonBehavior)
|
||||
case .remain:
|
||||
// We want this to remain the window in the current space.
|
||||
return NSWindow.CollectionBehavior([.moveToActiveSpace] + commonBehavior)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
import Cocoa
|
||||
|
||||
class QuickTerminalWindow: NSWindow {
|
||||
class QuickTerminalWindow: NSPanel {
|
||||
// Both of these must be true for windows without decorations to be able to
|
||||
// still become key/main and receive events.
|
||||
override var canBecomeKey: Bool { return true }
|
||||
@@ -26,22 +26,7 @@ class QuickTerminalWindow: NSWindow {
|
||||
// window remains resizable.
|
||||
self.styleMask.remove(.titled)
|
||||
|
||||
// We need to set our window level to a high value. In testing, only
|
||||
// popUpMenu and above do what we want. This gets it above the menu bar
|
||||
// and lets us render off screen.
|
||||
self.level = .popUpMenu
|
||||
|
||||
// This plus the level above was what was needed for the animation to work,
|
||||
// because it gets the window off screen properly. Plus we add some fields
|
||||
// we just want the behavior of.
|
||||
self.collectionBehavior = [
|
||||
// We want this to be part of every space because it is a singleton.
|
||||
.canJoinAllSpaces,
|
||||
|
||||
// We don't want to be part of command-tilde
|
||||
.ignoresCycle,
|
||||
|
||||
// We never support fullscreen
|
||||
.fullScreenNone]
|
||||
// We don't want to activate the owning app when quick terminal is triggered.
|
||||
self.styleMask.insert(.nonactivatingPanel)
|
||||
}
|
||||
}
|
||||
|
@@ -45,6 +45,11 @@ class BaseTerminalController: NSWindowController,
|
||||
didSet { surfaceTreeDidChange(from: oldValue, to: surfaceTree) }
|
||||
}
|
||||
|
||||
/// Whether the terminal surface should focus when the mouse is over it.
|
||||
var focusFollowsMouse: Bool {
|
||||
self.derivedConfig.focusFollowsMouse
|
||||
}
|
||||
|
||||
/// Non-nil when an alert is active so we don't overlap multiple.
|
||||
private var alert: NSAlert? = nil
|
||||
|
||||
@@ -106,8 +111,8 @@ class BaseTerminalController: NSWindowController,
|
||||
// Listen for local events that we need to know of outside of
|
||||
// single surface handlers.
|
||||
self.eventMonitor = NSEvent.addLocalMonitorForEvents(
|
||||
matching: [.flagsChanged],
|
||||
handler: localEventHandler)
|
||||
matching: [.flagsChanged]
|
||||
) { [weak self] event in self?.localEventHandler(event) }
|
||||
}
|
||||
|
||||
deinit {
|
||||
@@ -155,7 +160,7 @@ class BaseTerminalController: NSWindowController,
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
|
||||
@objc private func didChangeScreenParametersNotification(_ notification: Notification) {
|
||||
// If we have a window that is visible and it is outside the bounds of the
|
||||
// screen then we clamp it back to within the screen.
|
||||
@@ -262,7 +267,6 @@ class BaseTerminalController: NSWindowController,
|
||||
|
||||
// Set the main window title
|
||||
window.title = to
|
||||
|
||||
}
|
||||
|
||||
func pwdDidChange(to: URL?) {
|
||||
@@ -309,11 +313,11 @@ class BaseTerminalController: NSWindowController,
|
||||
// We consider our mode changed if the types change (obvious) but
|
||||
// also if its nil (not obvious) because nil means that the style has
|
||||
// likely changed but we don't support it.
|
||||
if newStyle == nil || type(of: newStyle) != type(of: oldStyle) {
|
||||
if newStyle == nil || type(of: newStyle!) != type(of: oldStyle) {
|
||||
// Our mode changed. Exit fullscreen (since we're toggling anyways)
|
||||
// and then unset the style so that we replace it next time.
|
||||
// and then set the new style for future use
|
||||
oldStyle.exit()
|
||||
self.fullscreenStyle = nil
|
||||
self.fullscreenStyle = newStyle
|
||||
|
||||
// We're done
|
||||
return
|
||||
@@ -385,9 +389,9 @@ class BaseTerminalController: NSWindowController,
|
||||
}
|
||||
|
||||
switch (request) {
|
||||
case .osc_52_write:
|
||||
case let .osc_52_write(pasteboard):
|
||||
guard case .confirm = action else { break }
|
||||
let pb = NSPasteboard.general
|
||||
let pb = pasteboard ?? NSPasteboard.general
|
||||
pb.declareTypes([.string], owner: nil)
|
||||
pb.setString(cc.contents, forType: .string)
|
||||
case .osc_52_read, .paste:
|
||||
@@ -448,6 +452,7 @@ class BaseTerminalController: NSWindowController,
|
||||
self.alert = nil
|
||||
switch (response) {
|
||||
case .alertFirstButtonReturn:
|
||||
alert.window.orderOut(nil)
|
||||
window.close()
|
||||
|
||||
default:
|
||||
@@ -536,11 +541,11 @@ class BaseTerminalController: NSWindowController,
|
||||
}
|
||||
|
||||
@IBAction func splitMoveFocusAbove(_ sender: Any) {
|
||||
splitMoveFocus(direction: .top)
|
||||
splitMoveFocus(direction: .up)
|
||||
}
|
||||
|
||||
@IBAction func splitMoveFocusBelow(_ sender: Any) {
|
||||
splitMoveFocus(direction: .bottom)
|
||||
splitMoveFocus(direction: .down)
|
||||
}
|
||||
|
||||
@IBAction func splitMoveFocusLeft(_ sender: Any) {
|
||||
@@ -604,15 +609,18 @@ class BaseTerminalController: NSWindowController,
|
||||
private struct DerivedConfig {
|
||||
let macosTitlebarProxyIcon: Ghostty.MacOSTitlebarProxyIcon
|
||||
let windowStepResize: Bool
|
||||
let focusFollowsMouse: Bool
|
||||
|
||||
init() {
|
||||
self.macosTitlebarProxyIcon = .visible
|
||||
self.windowStepResize = false
|
||||
self.focusFollowsMouse = false
|
||||
}
|
||||
|
||||
init(_ config: Ghostty.Config) {
|
||||
self.macosTitlebarProxyIcon = config.macosTitlebarProxyIcon
|
||||
self.windowStepResize = config.windowStepResize
|
||||
self.focusFollowsMouse = config.focusFollowsMouse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -22,7 +22,7 @@ class TerminalController: BaseTerminalController {
|
||||
private var restorable: Bool = true
|
||||
|
||||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||
private var derivedConfig: DerivedConfig
|
||||
private(set) var derivedConfig: DerivedConfig
|
||||
|
||||
/// The notification cancellable for focused surface property changes.
|
||||
private var surfaceAppearanceCancellables: Set<AnyCancellable> = []
|
||||
@@ -60,6 +60,11 @@ class TerminalController: BaseTerminalController {
|
||||
selector: #selector(onGotoTab),
|
||||
name: Ghostty.Notification.ghosttyGotoTab,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onCloseTab),
|
||||
name: .ghosttyCloseTab,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyConfigDidChange(_:)),
|
||||
@@ -101,6 +106,12 @@ class TerminalController: BaseTerminalController {
|
||||
// When our fullscreen state changes, we resync our appearance because some
|
||||
// properties change when fullscreen or not.
|
||||
guard let focusedSurface else { return }
|
||||
if (!(fullscreenStyle?.isFullscreen ?? false) &&
|
||||
ghostty.config.macosTitlebarStyle == "hidden")
|
||||
{
|
||||
applyHiddenTitlebarStyle()
|
||||
}
|
||||
|
||||
syncAppearance(focusedSurface.derivedConfig)
|
||||
}
|
||||
|
||||
@@ -117,9 +128,6 @@ class TerminalController: BaseTerminalController {
|
||||
// Update our derived config
|
||||
self.derivedConfig = DerivedConfig(config)
|
||||
|
||||
guard let window = window as? TerminalWindow else { return }
|
||||
window.focusFollowsMouse = config.focusFollowsMouse
|
||||
|
||||
// If we have no surfaces in our window (is that possible?) then we update
|
||||
// our window appearance based on the root config. If we have surfaces, we
|
||||
// don't call this because the TODO
|
||||
@@ -247,7 +255,9 @@ class TerminalController: BaseTerminalController {
|
||||
let backgroundColor: OSColor
|
||||
if let surfaceTree {
|
||||
if let focusedSurface, surfaceTree.doesBorderTop(view: focusedSurface) {
|
||||
backgroundColor = OSColor(focusedSurface.backgroundColor ?? surfaceConfig.backgroundColor)
|
||||
// Similar to above, an alpha component of "0" causes compositor issues, so
|
||||
// we use 0.001. See: https://github.com/ghostty-org/ghostty/pull/4308
|
||||
backgroundColor = OSColor(focusedSurface.backgroundColor ?? surfaceConfig.backgroundColor).withAlphaComponent(0.001)
|
||||
} else {
|
||||
// We don't have a focused surface or our surface doesn't border the
|
||||
// top. We choose to match the color of the top-left most surface.
|
||||
@@ -270,6 +280,28 @@ class TerminalController: BaseTerminalController {
|
||||
}
|
||||
}
|
||||
|
||||
private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) {
|
||||
guard let window else { return }
|
||||
|
||||
// If we don't have both an X and Y we center.
|
||||
guard let x, let y else {
|
||||
window.center()
|
||||
return
|
||||
}
|
||||
|
||||
// Prefer the screen our window is being placed on otherwise our primary screen.
|
||||
guard let screen = window.screen ?? NSScreen.screens.first else {
|
||||
window.center()
|
||||
return
|
||||
}
|
||||
|
||||
// Orient based on the top left of the primary monitor
|
||||
let frame = screen.visibleFrame
|
||||
window.setFrameOrigin(.init(
|
||||
x: frame.minX + CGFloat(x),
|
||||
y: frame.maxY - (CGFloat(y) + window.frame.height)))
|
||||
}
|
||||
|
||||
//MARK: - NSWindowController
|
||||
|
||||
override func windowWillLoad() {
|
||||
@@ -277,6 +309,43 @@ class TerminalController: BaseTerminalController {
|
||||
shouldCascadeWindows = false
|
||||
}
|
||||
|
||||
fileprivate func applyHiddenTitlebarStyle() {
|
||||
guard let window else { return }
|
||||
|
||||
window.styleMask = [
|
||||
// We need `titled` in the mask to get the normal window frame
|
||||
.titled,
|
||||
|
||||
// Full size content view so we can extend
|
||||
// content in to the hidden titlebar's area
|
||||
.fullSizeContentView,
|
||||
|
||||
.resizable,
|
||||
.closable,
|
||||
.miniaturizable,
|
||||
]
|
||||
|
||||
// Hide the title
|
||||
window.titleVisibility = .hidden
|
||||
window.titlebarAppearsTransparent = true
|
||||
|
||||
// Hide the traffic lights (window control buttons)
|
||||
window.standardWindowButton(.closeButton)?.isHidden = true
|
||||
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
|
||||
window.standardWindowButton(.zoomButton)?.isHidden = true
|
||||
|
||||
// Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar.
|
||||
window.tabbingMode = .disallowed
|
||||
|
||||
// Nuke it from orbit -- hide the titlebar container entirely, just in case. There are
|
||||
// some operations that appear to bring back the titlebar visibility so this ensures
|
||||
// it is gone forever.
|
||||
if let themeFrame = window.contentView?.superview,
|
||||
let titleBarContainer = themeFrame.firstDescendant(withClassName: "NSTitlebarContainerView") {
|
||||
titleBarContainer.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
override func windowDidLoad() {
|
||||
super.windowDidLoad()
|
||||
guard let window = window as? TerminalWindow else { return }
|
||||
@@ -297,40 +366,41 @@ class TerminalController: BaseTerminalController {
|
||||
// If window decorations are disabled, remove our title
|
||||
if (!config.windowDecorations) { window.styleMask.remove(.titled) }
|
||||
|
||||
// Terminals typically operate in sRGB color space and macOS defaults
|
||||
// to "native" which is typically P3. There is a lot more resources
|
||||
// covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
|
||||
// Ghostty defaults to sRGB but this can be overridden.
|
||||
switch (config.windowColorspace) {
|
||||
case "display-p3":
|
||||
window.colorSpace = .displayP3
|
||||
case "srgb":
|
||||
fallthrough
|
||||
default:
|
||||
window.colorSpace = .sRGB
|
||||
}
|
||||
|
||||
// If we have only a single surface (no splits) and that surface requested
|
||||
// an initial size then we set it here now.
|
||||
if case let .leaf(leaf) = surfaceTree {
|
||||
if let initialSize = leaf.surface.initialSize,
|
||||
let screen = window.screen ?? NSScreen.main {
|
||||
// Setup our frame. We need to first subtract the views frame so that we can
|
||||
// just get the chrome frame so that we only affect the surface view size.
|
||||
// Get the current frame of the window
|
||||
var frame = window.frame
|
||||
frame.size.width -= leaf.surface.frame.size.width
|
||||
frame.size.height -= leaf.surface.frame.size.height
|
||||
frame.size.width += min(initialSize.width, screen.frame.width)
|
||||
frame.size.height += min(initialSize.height, screen.frame.height)
|
||||
|
||||
// We have no tabs and we are not a split, so set the initial size of the window.
|
||||
// Calculate the chrome size (window size minus view size)
|
||||
let chromeWidth = frame.size.width - leaf.surface.frame.size.width
|
||||
let chromeHeight = frame.size.height - leaf.surface.frame.size.height
|
||||
|
||||
// Calculate the new width and height, clamping to the screen's size
|
||||
let newWidth = min(initialSize.width + chromeWidth, screen.visibleFrame.width)
|
||||
let newHeight = min(initialSize.height + chromeHeight, screen.visibleFrame.height)
|
||||
|
||||
// Update the frame size while keeping the window's position intact
|
||||
frame.size.width = newWidth
|
||||
frame.size.height = newHeight
|
||||
|
||||
// Ensure the window doesn't go outside the screen boundaries
|
||||
frame.origin.x = max(screen.frame.origin.x, min(frame.origin.x, screen.frame.maxX - newWidth))
|
||||
frame.origin.y = max(screen.frame.origin.y, min(frame.origin.y, screen.frame.maxY - newHeight))
|
||||
|
||||
// Set the updated frame to the window
|
||||
window.setFrame(frame, display: true)
|
||||
}
|
||||
}
|
||||
|
||||
// Center the window to start, we'll move the window frame automatically
|
||||
// when cascading.
|
||||
window.center()
|
||||
// Set our window positioning to coordinates if config value exists, otherwise
|
||||
// fallback to original centering behavior
|
||||
setInitialWindowPosition(
|
||||
x: config.windowPositionX,
|
||||
y: config.windowPositionY,
|
||||
windowDecorations: config.windowDecorations)
|
||||
|
||||
// Make sure our theme is set on the window so styling is correct.
|
||||
if let windowTheme = config.windowTheme {
|
||||
@@ -368,38 +438,7 @@ class TerminalController: BaseTerminalController {
|
||||
|
||||
// If our titlebar style is "hidden" we adjust the style appropriately
|
||||
if (config.macosTitlebarStyle == "hidden") {
|
||||
window.styleMask = [
|
||||
// We need `titled` in the mask to get the normal window frame
|
||||
.titled,
|
||||
|
||||
// Full size content view so we can extend
|
||||
// content in to the hidden titlebar's area
|
||||
.fullSizeContentView,
|
||||
|
||||
.resizable,
|
||||
.closable,
|
||||
.miniaturizable,
|
||||
]
|
||||
|
||||
// Hide the title
|
||||
window.titleVisibility = .hidden
|
||||
window.titlebarAppearsTransparent = true
|
||||
|
||||
// Hide the traffic lights (window control buttons)
|
||||
window.standardWindowButton(.closeButton)?.isHidden = true
|
||||
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
|
||||
window.standardWindowButton(.zoomButton)?.isHidden = true
|
||||
|
||||
// Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar.
|
||||
window.tabbingMode = .disallowed
|
||||
|
||||
// Nuke it from orbit -- hide the titlebar container entirely, just in case. There are
|
||||
// some operations that appear to bring back the titlebar visibility so this ensures
|
||||
// it is gone forever.
|
||||
if let themeFrame = window.contentView?.superview,
|
||||
let titleBarContainer = themeFrame.firstDescendant(withClassName: "NSTitlebarContainerView") {
|
||||
titleBarContainer.isHidden = true
|
||||
}
|
||||
applyHiddenTitlebarStyle()
|
||||
}
|
||||
|
||||
// In various situations, macOS automatically tabs new windows. Ghostty handles
|
||||
@@ -422,8 +461,6 @@ class TerminalController: BaseTerminalController {
|
||||
}
|
||||
}
|
||||
|
||||
window.focusFollowsMouse = config.focusFollowsMouse
|
||||
|
||||
// Apply any additional appearance-related properties to the new window. We
|
||||
// apply this based on the root config but change it later based on surface
|
||||
// config (see focused surface change callback).
|
||||
@@ -474,7 +511,50 @@ class TerminalController: BaseTerminalController {
|
||||
ghostty.newTab(surface: surface)
|
||||
}
|
||||
|
||||
@IBAction override func closeWindow(_ sender: Any) {
|
||||
private func confirmClose(
|
||||
window: NSWindow,
|
||||
messageText: String,
|
||||
informativeText: String,
|
||||
completion: @escaping () -> Void
|
||||
) {
|
||||
// If we need confirmation by any, show one confirmation for all windows
|
||||
// in the tab group.
|
||||
let alert = NSAlert()
|
||||
alert.messageText = messageText
|
||||
alert.informativeText = informativeText
|
||||
alert.addButton(withTitle: "Close")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.alertStyle = .warning
|
||||
alert.beginSheetModal(for: window) { response in
|
||||
if response == .alertFirstButtonReturn {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func closeTab(_ sender: Any?) {
|
||||
guard let window = window else { return }
|
||||
guard window.tabGroup != nil else {
|
||||
// No tabs, no tab group, just perform a normal close.
|
||||
window.performClose(sender)
|
||||
return
|
||||
}
|
||||
|
||||
if surfaceTree?.needsConfirmQuit() ?? false {
|
||||
confirmClose(
|
||||
window: window,
|
||||
messageText: "Close Tab?",
|
||||
informativeText: "The terminal still has a running process. If you close the tab the process will be killed."
|
||||
) {
|
||||
window.close()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
window.close()
|
||||
}
|
||||
|
||||
@IBAction override func closeWindow(_ sender: Any?) {
|
||||
guard let window = window else { return }
|
||||
guard let tabGroup = window.tabGroup else {
|
||||
// No tabs, no tab group, just perform a normal close.
|
||||
@@ -489,47 +569,34 @@ class TerminalController: BaseTerminalController {
|
||||
}
|
||||
|
||||
// Check if any windows require close confirmation.
|
||||
var needsConfirm: Bool = false
|
||||
for tabWindow in tabGroup.windows {
|
||||
guard let c = tabWindow.windowController as? TerminalController else { continue }
|
||||
if (c.surfaceTree?.needsConfirmQuit() ?? false) {
|
||||
needsConfirm = true
|
||||
break
|
||||
let needsConfirm = tabGroup.windows.contains { tabWindow in
|
||||
guard let controller = tabWindow.windowController as? TerminalController else {
|
||||
return false
|
||||
}
|
||||
return controller.surfaceTree?.needsConfirmQuit() ?? false
|
||||
}
|
||||
|
||||
// If none need confirmation then we can just close all the windows.
|
||||
if (!needsConfirm) {
|
||||
for tabWindow in tabGroup.windows {
|
||||
tabWindow.close()
|
||||
}
|
||||
|
||||
if !needsConfirm {
|
||||
tabGroup.windows.forEach { $0.close() }
|
||||
return
|
||||
}
|
||||
|
||||
// If we need confirmation by any, show one confirmation for all windows
|
||||
// in the tab group.
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Close Window?"
|
||||
alert.informativeText = "All terminal sessions in this window will be terminated."
|
||||
alert.addButton(withTitle: "Close Window")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.alertStyle = .warning
|
||||
alert.beginSheetModal(for: window, completionHandler: { response in
|
||||
if (response == .alertFirstButtonReturn) {
|
||||
for tabWindow in tabGroup.windows {
|
||||
tabWindow.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
confirmClose(
|
||||
window: window,
|
||||
messageText: "Close Window?",
|
||||
informativeText: "All terminal sessions in this window will be terminated."
|
||||
) {
|
||||
tabGroup.windows.forEach { $0.close() }
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func toggleGhosttyFullScreen(_ sender: Any) {
|
||||
@IBAction func toggleGhosttyFullScreen(_ sender: Any?) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.toggleFullscreen(surface: surface)
|
||||
}
|
||||
|
||||
@IBAction func toggleTerminalInspector(_ sender: Any) {
|
||||
@IBAction func toggleTerminalInspector(_ sender: Any?) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.toggleTerminalInspector(surface: surface)
|
||||
}
|
||||
@@ -686,6 +753,12 @@ class TerminalController: BaseTerminalController {
|
||||
targetWindow.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
@objc private func onCloseTab(notification: SwiftUI.Notification) {
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard surfaceTree?.contains(view: target) ?? false else { return }
|
||||
closeTab(self)
|
||||
}
|
||||
|
||||
@objc private func onToggleFullscreen(notification: SwiftUI.Notification) {
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard target == self.focusedSurface else { return }
|
||||
@@ -703,7 +776,7 @@ class TerminalController: BaseTerminalController {
|
||||
toggleFullscreen(mode: fullscreenMode)
|
||||
}
|
||||
|
||||
private struct DerivedConfig {
|
||||
struct DerivedConfig {
|
||||
let backgroundColor: Color
|
||||
let macosTitlebarStyle: String
|
||||
|
||||
|
@@ -10,7 +10,7 @@ protocol TerminalViewDelegate: AnyObject {
|
||||
|
||||
/// The title of the terminal should change.
|
||||
func titleDidChange(to: String)
|
||||
|
||||
|
||||
/// The URL of the pwd should change.
|
||||
func pwdDidChange(to: URL?)
|
||||
|
||||
@@ -56,23 +56,18 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
|
||||
|
||||
// The title for our window
|
||||
private var title: String {
|
||||
var title = "👻"
|
||||
|
||||
if let surfaceTitle = surfaceTitle {
|
||||
if (surfaceTitle.count > 0) {
|
||||
title = surfaceTitle
|
||||
}
|
||||
if let surfaceTitle, !surfaceTitle.isEmpty {
|
||||
return surfaceTitle
|
||||
}
|
||||
|
||||
return title
|
||||
return "👻"
|
||||
}
|
||||
|
||||
// The pwd of the focused surface as a URL
|
||||
private var pwdURL: URL? {
|
||||
guard let surfacePwd else { return nil }
|
||||
guard let surfacePwd, surfacePwd != "" else { return nil }
|
||||
return URL(fileURLWithPath: surfacePwd)
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
switch ghostty.readiness {
|
||||
case .loading:
|
||||
|
@@ -115,6 +115,21 @@ class TerminalWindow: NSWindow {
|
||||
}
|
||||
}
|
||||
|
||||
// We override this so that with the hidden titlebar style the titlebar
|
||||
// area is not draggable.
|
||||
override var contentLayoutRect: CGRect {
|
||||
var rect = super.contentLayoutRect
|
||||
|
||||
// If we are using a hidden titlebar style, the content layout is the
|
||||
// full frame making it so that it is not draggable.
|
||||
if let controller = windowController as? TerminalController,
|
||||
controller.derivedConfig.macosTitlebarStyle == "hidden" {
|
||||
rect.origin.y = 0
|
||||
rect.size.height = self.frame.height
|
||||
}
|
||||
return rect
|
||||
}
|
||||
|
||||
// The window theme configuration from Ghostty. This is used to control some
|
||||
// behaviors that don't look quite right in certain situations.
|
||||
var windowTheme: TerminalWindowTheme?
|
||||
@@ -414,8 +429,6 @@ class TerminalWindow: NSWindow {
|
||||
}
|
||||
}
|
||||
|
||||
var focusFollowsMouse: Bool = false
|
||||
|
||||
// Find the NSTextField responsible for displaying the titlebar's title.
|
||||
private var titlebarTextField: NSTextField? {
|
||||
guard let titlebarView = titlebarContainer?.subviews
|
||||
@@ -669,12 +682,16 @@ fileprivate class WindowDragView: NSView {
|
||||
|
||||
// A view that matches the color of selected and unselected tabs in the adjacent tab bar.
|
||||
fileprivate class WindowButtonsBackdropView: NSView {
|
||||
private let terminalWindow: TerminalWindow
|
||||
// This must be weak because the window has this view. Otherwise
|
||||
// a retain cycle occurs.
|
||||
private weak var terminalWindow: TerminalWindow?
|
||||
private let isLightTheme: Bool
|
||||
private let overlayLayer = VibrantLayer()
|
||||
|
||||
var isHighlighted: Bool = true {
|
||||
didSet {
|
||||
guard let terminalWindow else { return }
|
||||
|
||||
if isLightTheme {
|
||||
overlayLayer.isHidden = isHighlighted
|
||||
layer?.backgroundColor = .clear
|
||||
|
@@ -62,7 +62,7 @@ extension Ghostty {
|
||||
// uses to interface with the application runtime environment.
|
||||
var runtime_cfg = ghostty_runtime_config_s(
|
||||
userdata: Unmanaged.passUnretained(self).toOpaque(),
|
||||
supports_selection_clipboard: false,
|
||||
supports_selection_clipboard: true,
|
||||
wakeup_cb: { userdata in App.wakeup(userdata) },
|
||||
action_cb: { app, target, action in App.action(app!, target: target, action: action) },
|
||||
read_clipboard_cb: { userdata, loc, state in App.readClipboard(userdata, location: loc, state: state) },
|
||||
@@ -117,23 +117,7 @@ extension Ghostty {
|
||||
|
||||
func appTick() {
|
||||
guard let app = self.app else { return }
|
||||
|
||||
// Tick our app, which lets us know if we want to quit
|
||||
let exit = ghostty_app_tick(app)
|
||||
if (!exit) { return }
|
||||
|
||||
// On iOS, applications do not terminate programmatically like they do
|
||||
// on macOS. On iOS, applications are only terminated when a user physically
|
||||
// closes the application (i.e. going to the home screen). If we request
|
||||
// exit on iOS we ignore it.
|
||||
#if os(iOS)
|
||||
logger.info("quit request received, ignoring on iOS")
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
// We want to quit, start that process
|
||||
NSApplication.shared.terminate(nil)
|
||||
#endif
|
||||
ghostty_app_tick(app)
|
||||
}
|
||||
|
||||
func openConfig() {
|
||||
@@ -336,13 +320,13 @@ extension Ghostty {
|
||||
let surfaceView = self.surfaceUserdata(from: userdata)
|
||||
guard let surface = surfaceView.surface else { return }
|
||||
|
||||
// We only support the standard clipboard
|
||||
if (location != GHOSTTY_CLIPBOARD_STANDARD) {
|
||||
// Get our pasteboard
|
||||
guard let pasteboard = NSPasteboard.ghostty(location) else {
|
||||
return completeClipboardRequest(surface, data: "", state: state)
|
||||
}
|
||||
|
||||
// Get our string
|
||||
let str = NSPasteboard.general.getOpinionatedStringContents() ?? ""
|
||||
let str = pasteboard.getOpinionatedStringContents() ?? ""
|
||||
completeClipboardRequest(surface, data: str, state: state)
|
||||
}
|
||||
|
||||
@@ -380,14 +364,12 @@ extension Ghostty {
|
||||
static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer<CChar>?, location: ghostty_clipboard_e, confirm: Bool) {
|
||||
let surface = self.surfaceUserdata(from: userdata)
|
||||
|
||||
// We only support the standard clipboard
|
||||
if (location != GHOSTTY_CLIPBOARD_STANDARD) { return }
|
||||
|
||||
guard let pasteboard = NSPasteboard.ghostty(location) else { return }
|
||||
guard let valueStr = String(cString: string!, encoding: .utf8) else { return }
|
||||
if !confirm {
|
||||
let pb = NSPasteboard.general
|
||||
pb.declareTypes([.string], owner: nil)
|
||||
pb.setString(valueStr, forType: .string)
|
||||
pasteboard.declareTypes([.string], owner: nil)
|
||||
pasteboard.setString(valueStr, forType: .string)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -396,7 +378,7 @@ extension Ghostty {
|
||||
object: surface,
|
||||
userInfo: [
|
||||
Notification.ConfirmClipboardStrKey: valueStr,
|
||||
Notification.ConfirmClipboardRequestKey: Ghostty.ClipboardRequest.osc_52_write,
|
||||
Notification.ConfirmClipboardRequestKey: Ghostty.ClipboardRequest.osc_52_write(pasteboard),
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -454,6 +436,9 @@ extension Ghostty {
|
||||
|
||||
// Action dispatch
|
||||
switch (action.tag) {
|
||||
case GHOSTTY_ACTION_QUIT:
|
||||
quit(app)
|
||||
|
||||
case GHOSTTY_ACTION_NEW_WINDOW:
|
||||
newWindow(app, target: target)
|
||||
|
||||
@@ -463,6 +448,9 @@ extension Ghostty {
|
||||
case GHOSTTY_ACTION_NEW_SPLIT:
|
||||
newSplit(app, target: target, direction: action.action.new_split)
|
||||
|
||||
case GHOSTTY_ACTION_CLOSE_TAB:
|
||||
closeTab(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_TOGGLE_FULLSCREEN:
|
||||
toggleFullscreen(app, target: target, mode: action.action.toggle_fullscreen)
|
||||
|
||||
@@ -559,6 +547,21 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
private static func quit(_ app: ghostty_app_t) {
|
||||
// On iOS, applications do not terminate programmatically like they do
|
||||
// on macOS. On iOS, applications are only terminated when a user physically
|
||||
// closes the application (i.e. going to the home screen). If we request
|
||||
// exit on iOS we ignore it.
|
||||
#if os(iOS)
|
||||
logger.info("quit request received, ignoring on iOS")
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
// We want to quit, start that process
|
||||
NSApplication.shared.terminate(nil)
|
||||
#endif
|
||||
}
|
||||
|
||||
private static func newWindow(_ app: ghostty_app_t, target: ghostty_target_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
@@ -651,6 +654,27 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("close tab does nothing with an app target")
|
||||
return
|
||||
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
guard let surface = target.target.surface else { return }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||
|
||||
NotificationCenter.default.post(
|
||||
name: .ghosttyCloseTab,
|
||||
object: surfaceView
|
||||
)
|
||||
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func toggleFullscreen(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
|
@@ -132,15 +132,6 @@ extension Ghostty {
|
||||
return v
|
||||
}
|
||||
|
||||
var windowColorspace: String {
|
||||
guard let config = self.config else { return "" }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
let key = "window-colorspace"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
|
||||
guard let ptr = v else { return "" }
|
||||
return String(cString: ptr)
|
||||
}
|
||||
|
||||
var windowSaveState: String {
|
||||
guard let config = self.config else { return "" }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
@@ -149,6 +140,20 @@ extension Ghostty {
|
||||
guard let ptr = v else { return "" }
|
||||
return String(cString: ptr)
|
||||
}
|
||||
|
||||
var windowPositionX: Int16? {
|
||||
guard let config = self.config else { return nil }
|
||||
var v: Int16 = 0
|
||||
let key = "window-position-x"
|
||||
return ghostty_config_get(config, &v, key, UInt(key.count)) ? v : nil
|
||||
}
|
||||
|
||||
var windowPositionY: Int16? {
|
||||
guard let config = self.config else { return nil }
|
||||
var v: Int16 = 0
|
||||
let key = "window-position-y"
|
||||
return ghostty_config_get(config, &v, key, UInt(key.count)) ? v : nil
|
||||
}
|
||||
|
||||
var windowNewTabPosition: String {
|
||||
guard let config = self.config else { return "" }
|
||||
@@ -160,11 +165,14 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
var windowDecorations: Bool {
|
||||
guard let config = self.config else { return true }
|
||||
var v = false;
|
||||
let defaultValue = true
|
||||
guard let config = self.config else { return defaultValue }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
let key = "window-decoration"
|
||||
_ = ghostty_config_get(config, &v, key, UInt(key.count))
|
||||
return v;
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
|
||||
guard let ptr = v else { return defaultValue }
|
||||
let str = String(cString: ptr)
|
||||
return WindowDecoration(rawValue: str)?.enabled() ?? defaultValue
|
||||
}
|
||||
|
||||
var windowTheme: String? {
|
||||
@@ -331,7 +339,7 @@ extension Ghostty {
|
||||
var backgroundBlurRadius: Int {
|
||||
guard let config = self.config else { return 1 }
|
||||
var v: Int = 0
|
||||
let key = "background-blur-radius"
|
||||
let key = "background-blur"
|
||||
_ = ghostty_config_get(config, &v, key, UInt(key.count))
|
||||
return v;
|
||||
}
|
||||
@@ -361,13 +369,24 @@ extension Ghostty {
|
||||
)
|
||||
}
|
||||
|
||||
// This isn't actually a configurable value currently but it could be done day.
|
||||
// We put it here because it is a color that changes depending on the configuration.
|
||||
var splitDividerColor: Color {
|
||||
let backgroundColor = OSColor(backgroundColor)
|
||||
let isLightBackground = backgroundColor.isLightColor
|
||||
let newColor = isLightBackground ? backgroundColor.darken(by: 0.08) : backgroundColor.darken(by: 0.4)
|
||||
return Color(newColor)
|
||||
|
||||
guard let config = self.config else { return Color(newColor) }
|
||||
|
||||
var color: ghostty_config_color_s = .init();
|
||||
let key = "split-divider-color"
|
||||
if (!ghostty_config_get(config, &color, key, UInt(key.count))) {
|
||||
return Color(newColor)
|
||||
}
|
||||
|
||||
return .init(
|
||||
red: Double(color.r) / 255,
|
||||
green: Double(color.g) / 255,
|
||||
blue: Double(color.b) / 255
|
||||
)
|
||||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
@@ -406,6 +425,16 @@ extension Ghostty {
|
||||
_ = ghostty_config_get(config, &v, key, UInt(key.count))
|
||||
return v
|
||||
}
|
||||
|
||||
var quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior {
|
||||
guard let config = self.config else { return .move }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
let key = "quick-terminal-space-behavior"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .move }
|
||||
guard let ptr = v else { return .move }
|
||||
let str = String(cString: ptr)
|
||||
return QuickTerminalSpaceBehavior(fromGhosttyConfig: str) ?? .move
|
||||
}
|
||||
#endif
|
||||
|
||||
var resizeOverlay: ResizeOverlay {
|
||||
@@ -437,15 +466,14 @@ extension Ghostty {
|
||||
return v;
|
||||
}
|
||||
|
||||
var autoUpdate: AutoUpdate {
|
||||
let defaultValue = AutoUpdate.check
|
||||
guard let config = self.config else { return defaultValue }
|
||||
var autoUpdate: AutoUpdate? {
|
||||
guard let config = self.config else { return nil }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
let key = "auto-update"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
|
||||
guard let ptr = v else { return defaultValue }
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return nil }
|
||||
guard let ptr = v else { return nil }
|
||||
let str = String(cString: ptr)
|
||||
return AutoUpdate(rawValue: str) ?? defaultValue
|
||||
return AutoUpdate(rawValue: str)
|
||||
}
|
||||
|
||||
var autoUpdateChannel: AutoUpdateChannel {
|
||||
@@ -529,4 +557,18 @@ extension Ghostty.Config {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum WindowDecoration: String {
|
||||
case none
|
||||
case client
|
||||
case server
|
||||
case auto
|
||||
|
||||
func enabled() -> Bool {
|
||||
switch self {
|
||||
case .client, .server, .auto: return true
|
||||
case .none: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
15
macos/Sources/Ghostty/Ghostty.Event.swift
Normal file
15
macos/Sources/Ghostty/Ghostty.Event.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
import Cocoa
|
||||
import GhosttyKit
|
||||
|
||||
extension Ghostty {
|
||||
/// A comparable event.
|
||||
struct ComparableKeyEvent: Equatable {
|
||||
let keyCode: UInt16
|
||||
let flags: NSEvent.ModifierFlags
|
||||
|
||||
init(event: NSEvent) {
|
||||
self.keyCode = event.keyCode
|
||||
self.flags = event.modifierFlags
|
||||
}
|
||||
}
|
||||
}
|
@@ -51,7 +51,7 @@ extension Ghostty {
|
||||
/// Returns the view that would prefer receiving focus in this tree. This is always the
|
||||
/// top-left-most view. This is used when creating a split or closing a split to find the
|
||||
/// next view to send focus to.
|
||||
func preferredFocus(_ direction: SplitFocusDirection = .top) -> SurfaceView {
|
||||
func preferredFocus(_ direction: SplitFocusDirection = .up) -> SurfaceView {
|
||||
let container: Container
|
||||
switch (self) {
|
||||
case .leaf(let leaf):
|
||||
@@ -64,10 +64,10 @@ extension Ghostty {
|
||||
|
||||
let node: SplitNode
|
||||
switch (direction) {
|
||||
case .previous, .top, .left:
|
||||
case .previous, .up, .left:
|
||||
node = container.bottomRight
|
||||
|
||||
case .next, .bottom, .right:
|
||||
case .next, .down, .right:
|
||||
node = container.topLeft
|
||||
}
|
||||
|
||||
@@ -431,12 +431,12 @@ extension Ghostty {
|
||||
struct Neighbors {
|
||||
var left: SplitNode?
|
||||
var right: SplitNode?
|
||||
var top: SplitNode?
|
||||
var bottom: SplitNode?
|
||||
var up: SplitNode?
|
||||
var down: SplitNode?
|
||||
|
||||
/// These are the previous/next nodes. It will certainly be one of the above as well
|
||||
/// but we keep track of these separately because depending on the split direction
|
||||
/// of the containing node, previous may be left OR top (same for next).
|
||||
/// of the containing node, previous may be left OR up (same for next).
|
||||
var previous: SplitNode?
|
||||
var next: SplitNode?
|
||||
|
||||
@@ -448,8 +448,8 @@ extension Ghostty {
|
||||
let map: [SplitFocusDirection : KeyPath<Self, SplitNode?>] = [
|
||||
.previous: \.previous,
|
||||
.next: \.next,
|
||||
.top: \.top,
|
||||
.bottom: \.bottom,
|
||||
.up: \.up,
|
||||
.down: \.down,
|
||||
.left: \.left,
|
||||
.right: \.right,
|
||||
]
|
||||
|
@@ -205,6 +205,7 @@ extension Ghostty {
|
||||
alert.beginSheetModal(for: window, completionHandler: { response in
|
||||
switch (response) {
|
||||
case .alertFirstButtonReturn:
|
||||
alert.window.orderOut(nil)
|
||||
node = nil
|
||||
|
||||
default:
|
||||
@@ -308,7 +309,7 @@ extension Ghostty {
|
||||
resizeIncrements: .init(width: 1, height: 1),
|
||||
resizePublisher: container.resizeEvent,
|
||||
left: {
|
||||
let neighborKey: WritableKeyPath<SplitNode.Neighbors, SplitNode?> = container.direction == .horizontal ? \.right : \.bottom
|
||||
let neighborKey: WritableKeyPath<SplitNode.Neighbors, SplitNode?> = container.direction == .horizontal ? \.right : \.down
|
||||
|
||||
TerminalSplitNested(
|
||||
node: closeableTopLeft(),
|
||||
@@ -318,7 +319,7 @@ extension Ghostty {
|
||||
])
|
||||
)
|
||||
}, right: {
|
||||
let neighborKey: WritableKeyPath<SplitNode.Neighbors, SplitNode?> = container.direction == .horizontal ? \.left : \.top
|
||||
let neighborKey: WritableKeyPath<SplitNode.Neighbors, SplitNode?> = container.direction == .horizontal ? \.left : \.up
|
||||
|
||||
TerminalSplitNested(
|
||||
node: closeableBottomRight(),
|
||||
|
15
macos/Sources/Ghostty/NSEvent+Extension.swift
Normal file
15
macos/Sources/Ghostty/NSEvent+Extension.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
import Cocoa
|
||||
import GhosttyKit
|
||||
|
||||
extension NSEvent {
|
||||
/// Create a Ghostty key event for a given keyboard action.
|
||||
func ghosttyKeyEvent(_ action: ghostty_input_action_e) -> ghostty_input_key_s {
|
||||
var key_ev = ghostty_input_key_s()
|
||||
key_ev.action = action
|
||||
key_ev.mods = Ghostty.ghosttyMods(modifierFlags)
|
||||
key_ev.keycode = UInt32(keyCode)
|
||||
key_ev.text = nil
|
||||
key_ev.composing = false
|
||||
return key_ev
|
||||
}
|
||||
}
|
@@ -66,7 +66,7 @@ extension Ghostty {
|
||||
|
||||
/// An enum that is used for the directions that a split focus event can change.
|
||||
enum SplitFocusDirection {
|
||||
case previous, next, top, bottom, left, right
|
||||
case previous, next, up, down, left, right
|
||||
|
||||
/// Initialize from a Ghostty API enum.
|
||||
static func from(direction: ghostty_action_goto_split_e) -> Self? {
|
||||
@@ -77,11 +77,11 @@ extension Ghostty {
|
||||
case GHOSTTY_GOTO_SPLIT_NEXT:
|
||||
return .next
|
||||
|
||||
case GHOSTTY_GOTO_SPLIT_TOP:
|
||||
return .top
|
||||
case GHOSTTY_GOTO_SPLIT_UP:
|
||||
return .up
|
||||
|
||||
case GHOSTTY_GOTO_SPLIT_BOTTOM:
|
||||
return .bottom
|
||||
case GHOSTTY_GOTO_SPLIT_DOWN:
|
||||
return .down
|
||||
|
||||
case GHOSTTY_GOTO_SPLIT_LEFT:
|
||||
return .left
|
||||
@@ -102,11 +102,11 @@ extension Ghostty {
|
||||
case .next:
|
||||
return GHOSTTY_GOTO_SPLIT_NEXT
|
||||
|
||||
case .top:
|
||||
return GHOSTTY_GOTO_SPLIT_TOP
|
||||
case .up:
|
||||
return GHOSTTY_GOTO_SPLIT_UP
|
||||
|
||||
case .bottom:
|
||||
return GHOSTTY_GOTO_SPLIT_BOTTOM
|
||||
case .down:
|
||||
return GHOSTTY_GOTO_SPLIT_DOWN
|
||||
|
||||
case .left:
|
||||
return GHOSTTY_GOTO_SPLIT_LEFT
|
||||
@@ -159,7 +159,7 @@ extension Ghostty {
|
||||
case osc_52_read
|
||||
|
||||
/// An application is attempting to write to the clipboard using OSC 52
|
||||
case osc_52_write
|
||||
case osc_52_write(OSPasteboard?)
|
||||
|
||||
/// The text to show in the clipboard confirmation prompt for a given request type
|
||||
func text() -> String {
|
||||
@@ -188,7 +188,7 @@ extension Ghostty {
|
||||
case GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ:
|
||||
return .osc_52_read
|
||||
case GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE:
|
||||
return .osc_52_write
|
||||
return .osc_52_write(nil)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
@@ -236,6 +236,9 @@ extension Notification.Name {
|
||||
/// Goto tab. Has tab index in the userinfo.
|
||||
static let ghosttyMoveTab = Notification.Name("com.mitchellh.ghostty.moveTab")
|
||||
static let GhosttyMoveTabKey = ghosttyMoveTab.rawValue
|
||||
|
||||
/// Close tab
|
||||
static let ghosttyCloseTab = Notification.Name("com.mitchellh.ghostty.closeTab")
|
||||
}
|
||||
|
||||
// NOTE: I am moving all of these to Notification.Name extensions over time. This
|
||||
|
@@ -92,22 +92,6 @@ extension Ghostty {
|
||||
windowFocus = false
|
||||
}
|
||||
}
|
||||
.onDrop(of: [.fileURL], isTargeted: nil) { providers in
|
||||
providers.forEach { provider in
|
||||
_ = provider.loadObject(ofClass: URL.self) { url, _ in
|
||||
guard let url = url else { return }
|
||||
let path = Shell.escape(url.path)
|
||||
DispatchQueue.main.async {
|
||||
surfaceView.insertText(
|
||||
path,
|
||||
replacementRange: NSMakeRange(0, 0)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
#endif
|
||||
|
||||
// If our geo size changed then we show the resize overlay as configured.
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
import CoreText
|
||||
import UserNotifications
|
||||
@@ -12,7 +13,14 @@ extension Ghostty {
|
||||
// The current title of the surface as defined by the pty. This can be
|
||||
// changed with escape codes. This is public because the callbacks go
|
||||
// to the app level and it is set from there.
|
||||
@Published private(set) var title: String = "👻"
|
||||
@Published private(set) var title: String = "" {
|
||||
didSet {
|
||||
if !title.isEmpty {
|
||||
titleFallbackTimer?.invalidate()
|
||||
titleFallbackTimer = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The current pwd of the surface as defined by the pty. This can be
|
||||
// changed with escape codes.
|
||||
@@ -113,6 +121,12 @@ extension Ghostty {
|
||||
// A small delay that is introduced before a title change to avoid flickers
|
||||
private var titleChangeTimer: Timer?
|
||||
|
||||
// A timer to fallback to ghost emoji if no title is set within the grace period
|
||||
private var titleFallbackTimer: Timer?
|
||||
|
||||
/// Event monitor (see individual events for why)
|
||||
private var eventMonitor: Any? = nil
|
||||
|
||||
// We need to support being a first responder so that we can get input events
|
||||
override var acceptsFirstResponder: Bool { return true }
|
||||
|
||||
@@ -136,6 +150,13 @@ extension Ghostty {
|
||||
// can do SOMETHING.
|
||||
super.init(frame: NSMakeRect(0, 0, 800, 600))
|
||||
|
||||
// Set a timer to show the ghost emoji after 500ms if no title is set
|
||||
titleFallbackTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
|
||||
if let self = self, self.title.isEmpty {
|
||||
self.title = "👻"
|
||||
}
|
||||
}
|
||||
|
||||
// Before we initialize the surface we want to register our notifications
|
||||
// so there is no window where we can't receive them.
|
||||
let center = NotificationCenter.default
|
||||
@@ -170,6 +191,15 @@ extension Ghostty {
|
||||
name: NSWindow.didChangeScreenNotification,
|
||||
object: nil)
|
||||
|
||||
// Listen for local events that we need to know of outside of
|
||||
// single surface handlers.
|
||||
self.eventMonitor = NSEvent.addLocalMonitorForEvents(
|
||||
matching: [
|
||||
// We need keyUp because command+key events don't trigger keyUp.
|
||||
.keyUp
|
||||
]
|
||||
) { [weak self] event in self?.localEventHandler(event) }
|
||||
|
||||
// Setup our surface. This will also initialize all the terminal IO.
|
||||
let surface_cfg = baseConfig ?? SurfaceConfiguration()
|
||||
var surface_cfg_c = surface_cfg.ghosttyConfig(view: self)
|
||||
@@ -201,6 +231,9 @@ extension Ghostty {
|
||||
|
||||
ghostty_surface_set_color_scheme(surface, scheme)
|
||||
}
|
||||
|
||||
// The UTTypes that can be dragged onto this view.
|
||||
registerForDraggedTypes(Array(Self.dropTypes))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@@ -212,6 +245,11 @@ extension Ghostty {
|
||||
let center = NotificationCenter.default
|
||||
center.removeObserver(self)
|
||||
|
||||
// Remove our event monitor
|
||||
if let eventMonitor {
|
||||
NSEvent.removeMonitor(eventMonitor)
|
||||
}
|
||||
|
||||
// Whenever the surface is removed, we need to note that our restorable
|
||||
// state is invalid to prevent the surface from being restored.
|
||||
invalidateRestorableState()
|
||||
@@ -356,22 +394,52 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Local Events
|
||||
|
||||
private func localEventHandler(_ event: NSEvent) -> NSEvent? {
|
||||
return switch event.type {
|
||||
case .keyUp:
|
||||
localEventKeyUp(event)
|
||||
|
||||
default:
|
||||
event
|
||||
}
|
||||
}
|
||||
|
||||
private func localEventKeyUp(_ event: NSEvent) -> NSEvent? {
|
||||
// We only care about events with "command" because all others will
|
||||
// trigger the normal responder chain.
|
||||
if (!event.modifierFlags.contains(.command)) { return event }
|
||||
|
||||
// Command keyUp events are never sent to the normal responder chain
|
||||
// so we send them here.
|
||||
guard focused else { return event }
|
||||
self.keyUp(with: event)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
@objc private func onUpdateRendererHealth(notification: SwiftUI.Notification) {
|
||||
guard let healthAny = notification.userInfo?["health"] else { return }
|
||||
guard let health = healthAny as? ghostty_action_renderer_health_e else { return }
|
||||
healthy = health == GHOSTTY_RENDERER_HEALTH_OK
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.healthy = health == GHOSTTY_RENDERER_HEALTH_OK
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func ghosttyDidContinueKeySequence(notification: SwiftUI.Notification) {
|
||||
guard let keyAny = notification.userInfo?[Ghostty.Notification.KeySequenceKey] else { return }
|
||||
guard let key = keyAny as? Ghostty.KeyEquivalent else { return }
|
||||
keySequence.append(key)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.keySequence.append(key)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func ghosttyDidEndKeySequence(notification: SwiftUI.Notification) {
|
||||
keySequence = []
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.keySequence = []
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func ghosttyConfigDidChange(_ notification: SwiftUI.Notification) {
|
||||
@@ -381,7 +449,9 @@ extension Ghostty {
|
||||
] as? Ghostty.Config else { return }
|
||||
|
||||
// Update our derived config
|
||||
self.derivedConfig = DerivedConfig(config)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.derivedConfig = DerivedConfig(config)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func ghosttyColorDidChange(_ notification: SwiftUI.Notification) {
|
||||
@@ -391,7 +461,9 @@ extension Ghostty {
|
||||
|
||||
switch (change.kind) {
|
||||
case .background:
|
||||
self.backgroundColor = change.color
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.backgroundColor = change.color
|
||||
}
|
||||
|
||||
default:
|
||||
// We don't do anything for the other colors yet.
|
||||
@@ -413,7 +485,9 @@ extension Ghostty {
|
||||
// We also just trigger a backing property change. Just in case the screen has
|
||||
// a different scaling factor, this ensures that we update our content scale.
|
||||
// Issue: https://github.com/ghostty-org/ghostty/issues/2731
|
||||
viewDidChangeBackingProperties()
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.viewDidChangeBackingProperties()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NSView
|
||||
@@ -605,11 +679,12 @@ extension Ghostty {
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y, mods)
|
||||
|
||||
// If focus follows mouse is enabled then move focus to this surface.
|
||||
if let window = self.window as? TerminalWindow,
|
||||
window.isKeyWindow &&
|
||||
window.focusFollowsMouse &&
|
||||
!self.focused
|
||||
// Handle focus-follows-mouse
|
||||
if let window,
|
||||
let controller = window.windowController as? BaseTerminalController,
|
||||
(window.isKeyWindow &&
|
||||
!self.focused &&
|
||||
controller.focusFollowsMouse)
|
||||
{
|
||||
Ghostty.moveFocus(to: self)
|
||||
}
|
||||
@@ -751,16 +826,51 @@ extension Ghostty {
|
||||
// know if these events cleared it.
|
||||
let markedTextBefore = markedText.length > 0
|
||||
|
||||
// We need to know the keyboard layout before below because some keyboard
|
||||
// input events will change our keyboard layout and we don't want those
|
||||
// going to the terminal.
|
||||
let keyboardIdBefore: String? = if (!markedTextBefore) {
|
||||
KeyboardLayout.id
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
|
||||
self.interpretKeyEvents([translationEvent])
|
||||
|
||||
// If our keyboard changed from this we just assume an input method
|
||||
// grabbed it and do nothing.
|
||||
if (!markedTextBefore && keyboardIdBefore != KeyboardLayout.id) {
|
||||
return
|
||||
}
|
||||
|
||||
// If we have text, then we've composed a character, send that down. We do this
|
||||
// first because if we completed a preedit, the text will be available here
|
||||
// AND we'll have a preedit.
|
||||
var handled: Bool = false
|
||||
if let list = keyTextAccumulator, list.count > 0 {
|
||||
handled = true
|
||||
for text in list {
|
||||
keyAction(action, event: event, text: text)
|
||||
|
||||
// This is a hack. libghostty on macOS treats ctrl input as not having
|
||||
// text because some keyboard layouts generate bogus characters for
|
||||
// ctrl+key. libghostty can't tell this is from an IM keyboard giving
|
||||
// us direct values. So, we just remove control.
|
||||
var modifierFlags = event.modifierFlags
|
||||
modifierFlags.remove(.control)
|
||||
if let keyTextEvent = NSEvent.keyEvent(
|
||||
with: .keyDown,
|
||||
location: event.locationInWindow,
|
||||
modifierFlags: modifierFlags,
|
||||
timestamp: event.timestamp,
|
||||
windowNumber: event.windowNumber,
|
||||
context: nil,
|
||||
characters: event.characters ?? "",
|
||||
charactersIgnoringModifiers: event.charactersIgnoringModifiers ?? "",
|
||||
isARepeat: event.isARepeat,
|
||||
keyCode: event.keyCode
|
||||
) {
|
||||
for text in list {
|
||||
_ = keyAction(action, event: keyTextEvent, text: text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -770,38 +880,49 @@ extension Ghostty {
|
||||
// the preedit.
|
||||
if (markedText.length > 0 || markedTextBefore) {
|
||||
handled = true
|
||||
keyAction(action, event: event, preedit: markedText.string)
|
||||
_ = keyAction(action, event: event, preedit: markedText.string)
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
// No text or anything, we want to handle this manually.
|
||||
keyAction(action, event: event)
|
||||
_ = keyAction(action, event: event)
|
||||
}
|
||||
}
|
||||
|
||||
override func keyUp(with event: NSEvent) {
|
||||
keyAction(GHOSTTY_ACTION_RELEASE, event: event)
|
||||
_ = keyAction(GHOSTTY_ACTION_RELEASE, event: event)
|
||||
}
|
||||
|
||||
/// Special case handling for some control keys
|
||||
override func performKeyEquivalent(with event: NSEvent) -> Bool {
|
||||
// Only process key down events
|
||||
if (event.type != .keyDown) {
|
||||
switch (event.type) {
|
||||
case .keyDown:
|
||||
// Continue, we care about key down events
|
||||
break
|
||||
|
||||
default:
|
||||
// Any other key event we don't care about. I don't think its even
|
||||
// possible to receive any other event type.
|
||||
return false
|
||||
}
|
||||
|
||||
// Only process events if we're focused. Some key events like C-/ macOS
|
||||
// appears to send to the first view in the hierarchy rather than the
|
||||
// the first responder (I don't know why). This prevents us from handling it.
|
||||
// Besides C-/, its important we don't process key equivalents if unfocused
|
||||
// because there are other event listeners for that (i.e. AppDelegate's
|
||||
// local event handler).
|
||||
if (!focused) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Only process keys when Control is active. All known issues we're
|
||||
// resolving happen only in this scenario. This probably isn't fully robust
|
||||
// but we can broaden the scope as we find more cases.
|
||||
if (!event.modifierFlags.contains(.control)) {
|
||||
return false
|
||||
// If this event as-is would result in a key binding then we send it.
|
||||
if let surface,
|
||||
ghostty_surface_key_is_binding(
|
||||
surface,
|
||||
event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS)) {
|
||||
self.keyDown(with: event)
|
||||
return true
|
||||
}
|
||||
|
||||
let equivalent: String
|
||||
@@ -819,14 +940,25 @@ extension Ghostty {
|
||||
case "\r":
|
||||
// Pass C-<return> through verbatim
|
||||
// (prevent the default context menu equivalent)
|
||||
if (!event.modifierFlags.contains(.control)) {
|
||||
return false
|
||||
}
|
||||
|
||||
equivalent = "\r"
|
||||
|
||||
case ".":
|
||||
if (!event.modifierFlags.contains(.command)) {
|
||||
return false
|
||||
}
|
||||
|
||||
equivalent = "."
|
||||
|
||||
default:
|
||||
// Ignore other events
|
||||
return false
|
||||
}
|
||||
|
||||
let newEvent = NSEvent.keyEvent(
|
||||
let finalEvent = NSEvent.keyEvent(
|
||||
with: .keyDown,
|
||||
location: event.locationInWindow,
|
||||
modifierFlags: event.modifierFlags,
|
||||
@@ -839,7 +971,7 @@ extension Ghostty {
|
||||
keyCode: event.keyCode
|
||||
)
|
||||
|
||||
self.keyDown(with: newEvent!)
|
||||
self.keyDown(with: finalEvent!)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -854,6 +986,9 @@ extension Ghostty {
|
||||
default: return
|
||||
}
|
||||
|
||||
// If we're in the middle of a preedit, don't do anything with mods.
|
||||
if hasMarkedText() { return }
|
||||
|
||||
// The keyAction function will do this AGAIN below which sucks to repeat
|
||||
// but this is super cheap and flagsChanged isn't that common.
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
@@ -884,45 +1019,38 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
keyAction(action, event: event)
|
||||
_ = keyAction(action, event: event)
|
||||
}
|
||||
|
||||
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) {
|
||||
guard let surface = self.surface else { return }
|
||||
|
||||
var key_ev = ghostty_input_key_s()
|
||||
key_ev.action = action
|
||||
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
key_ev.keycode = UInt32(event.keyCode)
|
||||
key_ev.text = nil
|
||||
key_ev.composing = false
|
||||
ghostty_surface_key(surface, key_ev)
|
||||
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) -> Bool {
|
||||
guard let surface = self.surface else { return false }
|
||||
return ghostty_surface_key(surface, event.ghosttyKeyEvent(action))
|
||||
}
|
||||
|
||||
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent, preedit: String) {
|
||||
guard let surface = self.surface else { return }
|
||||
private func keyAction(
|
||||
_ action: ghostty_input_action_e,
|
||||
event: NSEvent, preedit: String
|
||||
) -> Bool {
|
||||
guard let surface = self.surface else { return false }
|
||||
|
||||
preedit.withCString { ptr in
|
||||
var key_ev = ghostty_input_key_s()
|
||||
key_ev.action = action
|
||||
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
key_ev.keycode = UInt32(event.keyCode)
|
||||
return preedit.withCString { ptr in
|
||||
var key_ev = event.ghosttyKeyEvent(action)
|
||||
key_ev.text = ptr
|
||||
key_ev.composing = true
|
||||
ghostty_surface_key(surface, key_ev)
|
||||
return ghostty_surface_key(surface, key_ev)
|
||||
}
|
||||
}
|
||||
|
||||
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent, text: String) {
|
||||
guard let surface = self.surface else { return }
|
||||
private func keyAction(
|
||||
_ action: ghostty_input_action_e,
|
||||
event: NSEvent, text: String
|
||||
) -> Bool {
|
||||
guard let surface = self.surface else { return false }
|
||||
|
||||
text.withCString { ptr in
|
||||
var key_ev = ghostty_input_key_s()
|
||||
key_ev.action = action
|
||||
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
key_ev.keycode = UInt32(event.keyCode)
|
||||
return text.withCString { ptr in
|
||||
var key_ev = event.ghosttyKeyEvent(action)
|
||||
key_ev.text = ptr
|
||||
ghostty_surface_key(surface, key_ev)
|
||||
return ghostty_surface_key(surface, key_ev)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1040,6 +1168,14 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func pasteSelection(_ sender: Any?) {
|
||||
guard let surface = self.surface else { return }
|
||||
let action = "paste_from_selection"
|
||||
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
||||
AppDelegate.logger.warning("action failed action=\(action)")
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction override func selectAll(_ sender: Any?) {
|
||||
guard let surface = self.surface else { return }
|
||||
let action = "select_all"
|
||||
@@ -1361,3 +1497,78 @@ extension Ghostty.SurfaceView: NSServicesMenuRequestor {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: NSMenuItemValidation
|
||||
|
||||
extension Ghostty.SurfaceView: NSMenuItemValidation {
|
||||
func validateMenuItem(_ item: NSMenuItem) -> Bool {
|
||||
switch item.action {
|
||||
case #selector(pasteSelection):
|
||||
let pb = NSPasteboard.ghosttySelection
|
||||
guard let str = pb.getOpinionatedStringContents() else { return false }
|
||||
return !str.isEmpty
|
||||
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: NSDraggingDestination
|
||||
|
||||
extension Ghostty.SurfaceView {
|
||||
static let dropTypes: Set<NSPasteboard.PasteboardType> = [
|
||||
.string,
|
||||
.fileURL,
|
||||
.URL
|
||||
]
|
||||
|
||||
override func draggingEntered(_ sender: any NSDraggingInfo) -> NSDragOperation {
|
||||
guard let types = sender.draggingPasteboard.types else { return [] }
|
||||
|
||||
// If the dragging object contains none of our types then we return none.
|
||||
// This shouldn't happen because AppKit should guarantee that we only
|
||||
// receive types we registered for but its good to check.
|
||||
if Set(types).isDisjoint(with: Self.dropTypes) {
|
||||
return []
|
||||
}
|
||||
|
||||
// We use copy to get the proper icon
|
||||
return .copy
|
||||
}
|
||||
|
||||
override func performDragOperation(_ sender: any NSDraggingInfo) -> Bool {
|
||||
let pb = sender.draggingPasteboard
|
||||
|
||||
let content: String?
|
||||
if let url = pb.string(forType: .URL) {
|
||||
// URLs first, they get escaped as-is.
|
||||
content = Ghostty.Shell.escape(url)
|
||||
} else if let urls = pb.readObjects(forClasses: [NSURL.self]) as? [URL],
|
||||
urls.count > 0 {
|
||||
// File URLs next. They get escaped individually and then joined by a
|
||||
// space if there are multiple.
|
||||
content = urls
|
||||
.map { Ghostty.Shell.escape($0.path) }
|
||||
.joined(separator: " ")
|
||||
} else if let str = pb.string(forType: .string) {
|
||||
// Strings are not escaped because they may be copy/pasting a
|
||||
// command they want to execute.
|
||||
content = str
|
||||
} else {
|
||||
content = nil
|
||||
}
|
||||
|
||||
if let content {
|
||||
DispatchQueue.main.async {
|
||||
self.insertText(
|
||||
content,
|
||||
replacementRange: NSMakeRange(0, 0)
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ import AppKit
|
||||
typealias OSView = NSView
|
||||
typealias OSColor = NSColor
|
||||
typealias OSSize = NSSize
|
||||
typealias OSPasteboard = NSPasteboard
|
||||
|
||||
protocol OSViewRepresentable: NSViewRepresentable where NSViewType == OSViewType {
|
||||
associatedtype OSViewType: NSView
|
||||
@@ -34,6 +35,7 @@ import UIKit
|
||||
typealias OSView = UIView
|
||||
typealias OSColor = UIColor
|
||||
typealias OSSize = CGSize
|
||||
typealias OSPasteboard = UIPasteboard
|
||||
|
||||
protocol OSViewRepresentable: UIViewRepresentable {
|
||||
associatedtype OSViewType: UIView
|
||||
|
38
macos/Sources/Helpers/Dock.swift
Normal file
38
macos/Sources/Helpers/Dock.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
import Cocoa
|
||||
|
||||
// Private API to get Dock location
|
||||
@_silgen_name("CoreDockGetOrientationAndPinning")
|
||||
func CoreDockGetOrientationAndPinning(
|
||||
_ outOrientation: UnsafeMutablePointer<Int32>,
|
||||
_ outPinning: UnsafeMutablePointer<Int32>)
|
||||
|
||||
// Private API to get the current Dock auto-hide state
|
||||
@_silgen_name("CoreDockGetAutoHideEnabled")
|
||||
func CoreDockGetAutoHideEnabled() -> Bool
|
||||
|
||||
// Toggles the Dock's auto-hide state
|
||||
@_silgen_name("CoreDockSetAutoHideEnabled")
|
||||
func CoreDockSetAutoHideEnabled(_ flag: Bool)
|
||||
|
||||
enum DockOrientation: Int {
|
||||
case top = 1
|
||||
case bottom = 2
|
||||
case left = 3
|
||||
case right = 4
|
||||
}
|
||||
|
||||
class Dock {
|
||||
/// Returns the orientation of the dock or nil if it can't be determined.
|
||||
static var orientation: DockOrientation? {
|
||||
var orientation: Int32 = 0
|
||||
var pinning: Int32 = 0
|
||||
CoreDockGetOrientationAndPinning(&orientation, &pinning)
|
||||
return .init(rawValue: Int(orientation)) ?? nil
|
||||
}
|
||||
|
||||
/// Set the dock autohide.
|
||||
static var autoHideEnabled: Bool {
|
||||
get { return CoreDockGetAutoHideEnabled() }
|
||||
set { CoreDockSetAutoHideEnabled(newValue) }
|
||||
}
|
||||
}
|
@@ -307,21 +307,21 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
|
||||
// MARK: Dock
|
||||
|
||||
private func hideDock() {
|
||||
NSApp.presentationOptions.insert(.autoHideDock)
|
||||
NSApp.acquirePresentationOption(.autoHideDock)
|
||||
}
|
||||
|
||||
private func unhideDock() {
|
||||
NSApp.presentationOptions.remove(.autoHideDock)
|
||||
NSApp.releasePresentationOption(.autoHideDock)
|
||||
}
|
||||
|
||||
// MARK: Menu
|
||||
|
||||
func hideMenu() {
|
||||
NSApp.presentationOptions.insert(.autoHideMenuBar)
|
||||
NSApp.acquirePresentationOption(.autoHideMenuBar)
|
||||
}
|
||||
|
||||
func unhideMenu() {
|
||||
NSApp.presentationOptions.remove(.autoHideMenuBar)
|
||||
NSApp.releasePresentationOption(.autoHideMenuBar)
|
||||
}
|
||||
|
||||
/// The state that must be saved for non-native fullscreen to exit fullscreen.
|
||||
|
14
macos/Sources/Helpers/KeyboardLayout.swift
Normal file
14
macos/Sources/Helpers/KeyboardLayout.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
import Carbon
|
||||
|
||||
class KeyboardLayout {
|
||||
/// Return a string ID of the current keyboard input source.
|
||||
static var id: String? {
|
||||
if let source = TISCopyCurrentKeyboardInputSource()?.takeRetainedValue(),
|
||||
let sourceIdPointer = TISGetInputSourceProperty(source, kTISPropertyInputSourceID) {
|
||||
let sourceId = unsafeBitCast(sourceIdPointer, to: CFString.self)
|
||||
return sourceId as String
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
31
macos/Sources/Helpers/NSApplication+Extension.swift
Normal file
31
macos/Sources/Helpers/NSApplication+Extension.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
import Cocoa
|
||||
|
||||
extension NSApplication {
|
||||
private static var presentationOptionCounts: [NSApplication.PresentationOptions.Element: UInt] = [:]
|
||||
|
||||
/// Add a presentation option to the application and main a reference count so that and equal
|
||||
/// number of pops is required to disable it. This is useful so that multiple classes can affect global
|
||||
/// app state without overriding others.
|
||||
func acquirePresentationOption(_ option: NSApplication.PresentationOptions.Element) {
|
||||
Self.presentationOptionCounts[option, default: 0] += 1
|
||||
presentationOptions.insert(option)
|
||||
}
|
||||
|
||||
/// See acquirePresentationOption
|
||||
func releasePresentationOption(_ option: NSApplication.PresentationOptions.Element) {
|
||||
guard let value = Self.presentationOptionCounts[option] else { return }
|
||||
guard value > 0 else { return }
|
||||
if (value == 1) {
|
||||
presentationOptions.remove(option)
|
||||
Self.presentationOptionCounts.removeValue(forKey: option)
|
||||
} else {
|
||||
Self.presentationOptionCounts[option] = value - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NSApplication.PresentationOptions.Element: @retroactive Hashable {
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(rawValue)
|
||||
}
|
||||
}
|
@@ -1,17 +1,39 @@
|
||||
import AppKit
|
||||
import GhosttyKit
|
||||
|
||||
extension NSPasteboard {
|
||||
/// The pasteboard to used for Ghostty selection.
|
||||
static var ghosttySelection: NSPasteboard = {
|
||||
NSPasteboard(name: .init("com.mitchellh.ghostty.selection"))
|
||||
}()
|
||||
|
||||
/// Gets the contents of the pasteboard as a string following a specific set of semantics.
|
||||
/// Does these things in order:
|
||||
/// - Tries to get the absolute filesystem path of the file in the pasteboard if there is one.
|
||||
/// - Tries to get the absolute filesystem path of the file in the pasteboard if there is one and ensures the file path is properly escaped.
|
||||
/// - Tries to get any string from the pasteboard.
|
||||
/// If all of the above fail, returns None.
|
||||
func getOpinionatedStringContents() -> String? {
|
||||
if let file = self.string(forType: .fileURL) {
|
||||
if let path = NSURL(string: file)?.path {
|
||||
return path
|
||||
}
|
||||
if let urls = readObjects(forClasses: [NSURL.self]) as? [URL],
|
||||
urls.count > 0 {
|
||||
return urls
|
||||
.map { $0.isFileURL ? Ghostty.Shell.escape($0.path) : $0.absoluteString }
|
||||
.joined(separator: " ")
|
||||
}
|
||||
|
||||
return self.string(forType: .string)
|
||||
}
|
||||
|
||||
/// The pasteboard for the Ghostty enum type.
|
||||
static func ghostty(_ clipboard: ghostty_clipboard_e) -> NSPasteboard? {
|
||||
switch (clipboard) {
|
||||
case GHOSTTY_CLIPBOARD_STANDARD:
|
||||
return Self.general
|
||||
|
||||
case GHOSTTY_CLIPBOARD_SELECTION:
|
||||
return Self.ghosttySelection
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
9
macos/Sources/Helpers/Weak.swift
Normal file
9
macos/Sources/Helpers/Weak.swift
Normal file
@@ -0,0 +1,9 @@
|
||||
/// A wrapper that holds a weak reference to an object. This lets us create native containers
|
||||
/// of weak references.
|
||||
class Weak<T: AnyObject> {
|
||||
weak var value: T?
|
||||
|
||||
init(_ value: T) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
@@ -51,6 +51,9 @@
|
||||
pandoc,
|
||||
hyperfine,
|
||||
typos,
|
||||
wayland,
|
||||
wayland-scanner,
|
||||
wayland-protocols,
|
||||
}: let
|
||||
# See package.nix. Keep in sync.
|
||||
rpathLibs =
|
||||
@@ -80,6 +83,7 @@
|
||||
libadwaita
|
||||
gtk4
|
||||
glib
|
||||
wayland
|
||||
];
|
||||
in
|
||||
mkShell {
|
||||
@@ -153,6 +157,9 @@ in
|
||||
libadwaita
|
||||
gtk4
|
||||
glib
|
||||
wayland
|
||||
wayland-scanner
|
||||
wayland-protocols
|
||||
];
|
||||
|
||||
# This should be set onto the rpath of the ghostty binary if you want
|
||||
|
@@ -10,10 +10,6 @@
|
||||
oniguruma,
|
||||
zlib,
|
||||
libGL,
|
||||
libX11,
|
||||
libXcursor,
|
||||
libXi,
|
||||
libXrandr,
|
||||
glib,
|
||||
gtk4,
|
||||
libadwaita,
|
||||
@@ -26,6 +22,15 @@
|
||||
pandoc,
|
||||
revision ? "dirty",
|
||||
optimize ? "Debug",
|
||||
enableX11 ? true,
|
||||
libX11,
|
||||
libXcursor,
|
||||
libXi,
|
||||
libXrandr,
|
||||
enableWayland ? true,
|
||||
wayland,
|
||||
wayland-protocols,
|
||||
wayland-scanner,
|
||||
}: let
|
||||
# The Zig hook has no way to select the release type without actual
|
||||
# overriding of the default flags.
|
||||
@@ -35,7 +40,7 @@
|
||||
# ultimately acted on and has made its way to a nixpkgs implementation, this
|
||||
# can probably be removed in favor of that.
|
||||
zig_hook = zig_0_13.hook.overrideAttrs {
|
||||
zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize}";
|
||||
zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize} --color off";
|
||||
};
|
||||
|
||||
# We limit source like this to try and reduce the amount of rebuilds as possible
|
||||
@@ -48,7 +53,6 @@
|
||||
fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) (
|
||||
lib.fileset.unions [
|
||||
../dist/linux
|
||||
../conformance
|
||||
../images
|
||||
../include
|
||||
../pkg
|
||||
@@ -110,17 +114,22 @@
|
||||
in
|
||||
stdenv.mkDerivation (finalAttrs: {
|
||||
pname = "ghostty";
|
||||
version = "1.0.0";
|
||||
version = "1.1.0";
|
||||
inherit src;
|
||||
|
||||
nativeBuildInputs = [
|
||||
git
|
||||
ncurses
|
||||
pandoc
|
||||
pkg-config
|
||||
zig_hook
|
||||
wrapGAppsHook4
|
||||
];
|
||||
nativeBuildInputs =
|
||||
[
|
||||
git
|
||||
ncurses
|
||||
pandoc
|
||||
pkg-config
|
||||
zig_hook
|
||||
wrapGAppsHook4
|
||||
]
|
||||
++ lib.optionals enableWayland [
|
||||
wayland-scanner
|
||||
wayland-protocols
|
||||
];
|
||||
|
||||
buildInputs =
|
||||
[
|
||||
@@ -136,28 +145,37 @@ in
|
||||
oniguruma
|
||||
zlib
|
||||
|
||||
libX11
|
||||
libXcursor
|
||||
libXi
|
||||
libXrandr
|
||||
|
||||
libadwaita
|
||||
gtk4
|
||||
glib
|
||||
gsettings-desktop-schemas
|
||||
]
|
||||
++ lib.optionals enableX11 [
|
||||
libX11
|
||||
libXcursor
|
||||
libXi
|
||||
libXrandr
|
||||
]
|
||||
++ lib.optionals enableWayland [
|
||||
wayland
|
||||
];
|
||||
|
||||
dontConfigure = true;
|
||||
|
||||
zigBuildFlags = "-Dversion-string=${finalAttrs.version}-${revision}-nix";
|
||||
zigBuildFlags = [
|
||||
"--system"
|
||||
"${zigCache}/p"
|
||||
"-Dversion-string=${finalAttrs.version}-${revision}-nix"
|
||||
"-Dgtk-x11=${lib.boolToString enableX11}"
|
||||
"-Dgtk-wayland=${lib.boolToString enableWayland}"
|
||||
];
|
||||
|
||||
preBuild = ''
|
||||
rm -rf $ZIG_GLOBAL_CACHE_DIR
|
||||
cp -r --reflink=auto ${zigCache} $ZIG_GLOBAL_CACHE_DIR
|
||||
chmod u+rwX -R $ZIG_GLOBAL_CACHE_DIR
|
||||
'';
|
||||
|
||||
outputs = ["out" "terminfo" "shell_integration" "vim"];
|
||||
outputs = [
|
||||
"out"
|
||||
"terminfo"
|
||||
"shell_integration"
|
||||
"vim"
|
||||
];
|
||||
|
||||
postInstall = ''
|
||||
terminfo_src=${
|
||||
@@ -183,14 +201,13 @@ in
|
||||
echo "$vim" >> "$out/nix-support/propagated-user-env-packages"
|
||||
'';
|
||||
|
||||
postFixup = ''
|
||||
patchelf --add-rpath "${lib.makeLibraryPath [libX11]}" "$out/bin/.ghostty-wrapped"
|
||||
'';
|
||||
|
||||
meta = {
|
||||
homepage = "https://github.com/ghostty-org/ghostty";
|
||||
homepage = "https://ghostty.org";
|
||||
license = lib.licenses.mit;
|
||||
platforms = ["x86_64-linux" "aarch64-linux"];
|
||||
platforms = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
];
|
||||
mainProgram = "ghostty";
|
||||
};
|
||||
})
|
||||
|
18
nix/vm/common-cinnamon.nix
Normal file
18
nix/vm/common-cinnamon.nix
Normal file
@@ -0,0 +1,18 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common.nix
|
||||
];
|
||||
|
||||
services.xserver = {
|
||||
displayManager = {
|
||||
lightdm = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
desktopManager = {
|
||||
cinnamon = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
136
nix/vm/common-gnome.nix
Normal file
136
nix/vm/common-gnome.nix
Normal file
@@ -0,0 +1,136 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
imports = [
|
||||
./common.nix
|
||||
];
|
||||
|
||||
services.xserver = {
|
||||
displayManager = {
|
||||
gdm = {
|
||||
enable = true;
|
||||
autoSuspend = false;
|
||||
};
|
||||
};
|
||||
desktopManager = {
|
||||
gnome = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
environment.systemPackages = [
|
||||
pkgs.gnomeExtensions.no-overview
|
||||
];
|
||||
|
||||
environment.gnome.excludePackages = with pkgs; [
|
||||
atomix
|
||||
baobab
|
||||
cheese
|
||||
epiphany
|
||||
evince
|
||||
file-roller
|
||||
geary
|
||||
gnome-backgrounds
|
||||
gnome-calculator
|
||||
gnome-calendar
|
||||
gnome-clocks
|
||||
gnome-connections
|
||||
gnome-contacts
|
||||
gnome-disk-utility
|
||||
gnome-extension-manager
|
||||
gnome-logs
|
||||
gnome-maps
|
||||
gnome-music
|
||||
gnome-photos
|
||||
gnome-software
|
||||
gnome-system-monitor
|
||||
gnome-text-editor
|
||||
gnome-themes-extra
|
||||
gnome-tour
|
||||
gnome-user-docs
|
||||
gnome-weather
|
||||
hitori
|
||||
iagno
|
||||
loupe
|
||||
nautilus
|
||||
orca
|
||||
seahorse
|
||||
simple-scan
|
||||
snapshot
|
||||
sushi
|
||||
tali
|
||||
totem
|
||||
yelp
|
||||
];
|
||||
|
||||
programs.dconf = {
|
||||
enable = true;
|
||||
profiles.user.databases = [
|
||||
{
|
||||
settings = with lib.gvariant; {
|
||||
"org/gnome/desktop/background" = {
|
||||
picture-uri = "file://${pkgs.ghostty}/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png";
|
||||
picture-uri-dark = "file://${pkgs.ghostty}/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png";
|
||||
picture-options = "centered";
|
||||
primary-color = "#000000000000";
|
||||
secondary-color = "#000000000000";
|
||||
};
|
||||
"org/gnome/desktop/interface" = {
|
||||
color-scheme = "prefer-dark";
|
||||
};
|
||||
"org/gnome/desktop/notifications" = {
|
||||
show-in-lock-screen = false;
|
||||
};
|
||||
"org/gnome/desktop/screensaver" = {
|
||||
lock-enabled = false;
|
||||
picture-uri = "file://${pkgs.ghostty}/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png";
|
||||
picture-options = "centered";
|
||||
primary-color = "#000000000000";
|
||||
secondary-color = "#000000000000";
|
||||
};
|
||||
"org/gnome/desktop/session" = {
|
||||
idle-delay = mkUint32 0;
|
||||
};
|
||||
"org/gnome/shell" = {
|
||||
disable-user-extensions = false;
|
||||
enabled-extensions = builtins.map (x: x.extensionUuid) (
|
||||
lib.filter (p: p ? extensionUuid) config.environment.systemPackages
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
programs.geary.enable = false;
|
||||
|
||||
services.gnome = {
|
||||
gnome-browser-connector.enable = false;
|
||||
gnome-initial-setup.enable = false;
|
||||
gnome-online-accounts.enable = false;
|
||||
gnome-remote-desktop.enable = false;
|
||||
rygel.enable = false;
|
||||
};
|
||||
|
||||
system.activationScripts = {
|
||||
face = {
|
||||
text = ''
|
||||
mkdir -p /var/lib/AccountsService/{icons,users}
|
||||
|
||||
cp ${pkgs.ghostty}/share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png /var/lib/AccountsService/icons/ghostty
|
||||
|
||||
echo -e "[User]\nIcon=/var/lib/AccountsService/icons/ghostty\n" > /var/lib/AccountsService/users/ghostty
|
||||
|
||||
chown root:root /var/lib/AccountsService/users/ghostty
|
||||
chmod 0600 /var/lib/AccountsService/users/ghostty
|
||||
|
||||
chown root:root /var/lib/AccountsService/icons/ghostty
|
||||
chmod 0444 /var/lib/AccountsService/icons/ghostty
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
21
nix/vm/common-plasma6.nix
Normal file
21
nix/vm/common-plasma6.nix
Normal file
@@ -0,0 +1,21 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common.nix
|
||||
];
|
||||
|
||||
services = {
|
||||
displayManager = {
|
||||
sddm = {
|
||||
enable = true;
|
||||
wayland = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
desktopManager = {
|
||||
plasma6 = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
18
nix/vm/common-xfce.nix
Normal file
18
nix/vm/common-xfce.nix
Normal file
@@ -0,0 +1,18 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common.nix
|
||||
];
|
||||
|
||||
services.xserver = {
|
||||
displayManager = {
|
||||
lightdm = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
desktopManager = {
|
||||
xfce = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
83
nix/vm/common.nix
Normal file
83
nix/vm/common.nix
Normal file
@@ -0,0 +1,83 @@
|
||||
{pkgs, ...}: {
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
|
||||
documentation.nixos.enable = false;
|
||||
|
||||
networking.hostName = "ghostty";
|
||||
networking.domain = "mitchellh.com";
|
||||
|
||||
virtualisation.vmVariant = {
|
||||
virtualisation.memorySize = 2048;
|
||||
};
|
||||
|
||||
nix = {
|
||||
settings = {
|
||||
trusted-users = [
|
||||
"root"
|
||||
"ghostty"
|
||||
];
|
||||
};
|
||||
extraOptions = ''
|
||||
experimental-features = nix-command flakes
|
||||
'';
|
||||
};
|
||||
|
||||
users.mutableUsers = false;
|
||||
|
||||
users.groups.ghostty = {};
|
||||
|
||||
users.users.ghostty = {
|
||||
description = "Ghostty";
|
||||
group = "ghostty";
|
||||
extraGroups = ["wheel"];
|
||||
isNormalUser = true;
|
||||
initialPassword = "ghostty";
|
||||
};
|
||||
|
||||
environment.etc = {
|
||||
"xdg/autostart/com.mitchellh.ghostty.desktop" = {
|
||||
source = "${pkgs.ghostty}/share/applications/com.mitchellh.ghostty.desktop";
|
||||
};
|
||||
};
|
||||
|
||||
environment.systemPackages = [
|
||||
pkgs.kitty
|
||||
pkgs.fish
|
||||
pkgs.ghostty
|
||||
pkgs.helix
|
||||
pkgs.neovim
|
||||
pkgs.xterm
|
||||
pkgs.zsh
|
||||
];
|
||||
|
||||
security.polkit = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
services.dbus = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
services.displayManager = {
|
||||
autoLogin = {
|
||||
user = "ghostty";
|
||||
};
|
||||
};
|
||||
|
||||
services.libinput = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
services.qemuGuest = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
services.spice-vdagentd = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
services.xserver = {
|
||||
enable = true;
|
||||
};
|
||||
}
|
12
nix/vm/create-cinnamon.nix
Normal file
12
nix/vm/create-cinnamon.nix
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
system,
|
||||
nixpkgs,
|
||||
overlay,
|
||||
module,
|
||||
uid ? 1000,
|
||||
gid ? 1000,
|
||||
}:
|
||||
import ./create.nix {
|
||||
inherit system nixpkgs overlay module uid gid;
|
||||
common = ./common-cinnamon.nix;
|
||||
}
|
12
nix/vm/create-gnome.nix
Normal file
12
nix/vm/create-gnome.nix
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
system,
|
||||
nixpkgs,
|
||||
overlay,
|
||||
module,
|
||||
uid ? 1000,
|
||||
gid ? 1000,
|
||||
}:
|
||||
import ./create.nix {
|
||||
inherit system nixpkgs overlay module uid gid;
|
||||
common = ./common-gnome.nix;
|
||||
}
|
12
nix/vm/create-plasma6.nix
Normal file
12
nix/vm/create-plasma6.nix
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
system,
|
||||
nixpkgs,
|
||||
overlay,
|
||||
module,
|
||||
uid ? 1000,
|
||||
gid ? 1000,
|
||||
}:
|
||||
import ./create.nix {
|
||||
inherit system nixpkgs overlay module uid gid;
|
||||
common = ./common-plasma6.nix;
|
||||
}
|
12
nix/vm/create-xfce.nix
Normal file
12
nix/vm/create-xfce.nix
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
system,
|
||||
nixpkgs,
|
||||
overlay,
|
||||
module,
|
||||
uid ? 1000,
|
||||
gid ? 1000,
|
||||
}:
|
||||
import ./create.nix {
|
||||
inherit system nixpkgs overlay module uid gid;
|
||||
common = ./common-xfce.nix;
|
||||
}
|
42
nix/vm/create.nix
Normal file
42
nix/vm/create.nix
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
system,
|
||||
nixpkgs,
|
||||
overlay,
|
||||
module,
|
||||
common ? ./common.nix,
|
||||
uid ? 1000,
|
||||
gid ? 1000,
|
||||
}: let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [
|
||||
overlay
|
||||
];
|
||||
};
|
||||
in
|
||||
nixpkgs.lib.nixosSystem {
|
||||
system = builtins.replaceStrings ["darwin"] ["linux"] system;
|
||||
modules = [
|
||||
{
|
||||
virtualisation.vmVariant = {
|
||||
virtualisation.host.pkgs = pkgs;
|
||||
};
|
||||
|
||||
nixpkgs.overlays = [
|
||||
overlay
|
||||
];
|
||||
|
||||
users.groups.ghostty = {
|
||||
gid = gid;
|
||||
};
|
||||
|
||||
users.users.ghostty = {
|
||||
uid = uid;
|
||||
};
|
||||
|
||||
system.stateVersion = nixpkgs.lib.trivial.release;
|
||||
}
|
||||
common
|
||||
module
|
||||
];
|
||||
}
|
7
nix/vm/wayland-cinnamon.nix
Normal file
7
nix/vm/wayland-cinnamon.nix
Normal file
@@ -0,0 +1,7 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-cinnamon.nix
|
||||
];
|
||||
|
||||
services.displayManager.defaultSession = "cinnamon-wayland";
|
||||
}
|
9
nix/vm/wayland-gnome.nix
Normal file
9
nix/vm/wayland-gnome.nix
Normal file
@@ -0,0 +1,9 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-gnome.nix
|
||||
];
|
||||
|
||||
services.displayManager = {
|
||||
defaultSession = "gnome";
|
||||
};
|
||||
}
|
6
nix/vm/wayland-plasma6.nix
Normal file
6
nix/vm/wayland-plasma6.nix
Normal file
@@ -0,0 +1,6 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-plasma6.nix
|
||||
];
|
||||
services.displayManager.defaultSession = "plasma";
|
||||
}
|
7
nix/vm/x11-cinnamon.nix
Normal file
7
nix/vm/x11-cinnamon.nix
Normal file
@@ -0,0 +1,7 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-cinnamon.nix
|
||||
];
|
||||
|
||||
services.displayManager.defaultSession = "cinnamon";
|
||||
}
|
9
nix/vm/x11-gnome.nix
Normal file
9
nix/vm/x11-gnome.nix
Normal file
@@ -0,0 +1,9 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-gnome.nix
|
||||
];
|
||||
|
||||
services.displayManager = {
|
||||
defaultSession = "gnome-xorg";
|
||||
};
|
||||
}
|
6
nix/vm/x11-plasma6.nix
Normal file
6
nix/vm/x11-plasma6.nix
Normal file
@@ -0,0 +1,6 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-plasma6.nix
|
||||
];
|
||||
services.displayManager.defaultSession = "plasmax11";
|
||||
}
|
7
nix/vm/x11-xfce.nix
Normal file
7
nix/vm/x11-xfce.nix
Normal file
@@ -0,0 +1,7 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-xfce.nix
|
||||
];
|
||||
|
||||
services.displayManager.defaultSession = "xfce";
|
||||
}
|
@@ -1,3 +1,3 @@
|
||||
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
|
||||
# more details.
|
||||
"sha256-lS5v5VdFCLnIyCq9mp7fd2pXhQmkkDFHHVdg4pf37PA="
|
||||
"sha256-Bjy31evaKgpRX1mGwAFkai44eiiorTV1gW3VdP9Ins8="
|
||||
|
@@ -1,10 +1,10 @@
|
||||
.{
|
||||
.name = "cimgui",
|
||||
.version = "1.89.9",
|
||||
.version = "1.90.6", // -docking branch
|
||||
.paths = .{""},
|
||||
.dependencies = .{
|
||||
// This should be kept in sync with the submodule in the cimgui source
|
||||
// code to be safe that they're compatible.
|
||||
// code in ./vendor/ to be safe that they're compatible.
|
||||
.imgui = .{
|
||||
.url = "https://github.com/ocornut/imgui/archive/e391fe2e66eb1c96b1624ae8444dc64c23146ef4.tar.gz",
|
||||
.hash = "1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402",
|
||||
|
@@ -13,7 +13,56 @@ pub fn build(b: *std.Build) !void {
|
||||
) orelse (target.result.os.tag != .windows);
|
||||
const freetype_enabled = b.option(bool, "enable-freetype", "Build freetype") orelse true;
|
||||
|
||||
const module = b.addModule("fontconfig", .{ .root_source_file = b.path("main.zig") });
|
||||
const module = b.addModule("fontconfig", .{
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// For dynamic linking, we prefer dynamic linking and to search by
|
||||
// mode first. Mode first will search all paths for a dynamic library
|
||||
// before falling back to static.
|
||||
const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{
|
||||
.preferred_link_mode = .dynamic,
|
||||
.search_strategy = .mode_first,
|
||||
};
|
||||
|
||||
const test_exe = b.addTest(.{
|
||||
.name = "test",
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
const tests_run = b.addRunArtifact(test_exe);
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&tests_run.step);
|
||||
|
||||
if (b.systemIntegrationOption("fontconfig", .{})) {
|
||||
module.linkSystemLibrary("fontconfig", dynamic_link_opts);
|
||||
test_exe.linkSystemLibrary2("fontconfig", dynamic_link_opts);
|
||||
} else {
|
||||
const lib = try buildLib(b, module, .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
|
||||
.libxml2_enabled = libxml2_enabled,
|
||||
.libxml2_iconv_enabled = libxml2_iconv_enabled,
|
||||
.freetype_enabled = freetype_enabled,
|
||||
|
||||
.dynamic_link_opts = dynamic_link_opts,
|
||||
});
|
||||
|
||||
test_exe.linkLibrary(lib);
|
||||
}
|
||||
}
|
||||
|
||||
fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Build.Step.Compile {
|
||||
const target = options.target;
|
||||
const optimize = options.optimize;
|
||||
|
||||
const libxml2_enabled = options.libxml2_enabled;
|
||||
const libxml2_iconv_enabled = options.libxml2_iconv_enabled;
|
||||
const freetype_enabled = options.freetype_enabled;
|
||||
|
||||
const upstream = b.dependency("fontconfig", .{});
|
||||
const lib = b.addStaticLibrary(.{
|
||||
@@ -131,19 +180,13 @@ pub fn build(b: *std.Build) !void {
|
||||
}
|
||||
}
|
||||
|
||||
// For dynamic linking, we prefer dynamic linking and to search by
|
||||
// mode first. Mode first will search all paths for a dynamic library
|
||||
// before falling back to static.
|
||||
const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{
|
||||
.preferred_link_mode = .dynamic,
|
||||
.search_strategy = .mode_first,
|
||||
};
|
||||
const dynamic_link_opts = options.dynamic_link_opts;
|
||||
|
||||
// Freetype2
|
||||
_ = b.systemIntegrationOption("freetype", .{}); // So it shows up in help
|
||||
if (freetype_enabled) {
|
||||
if (b.systemIntegrationOption("freetype", .{})) {
|
||||
lib.linkSystemLibrary2("freetype", dynamic_link_opts);
|
||||
lib.linkSystemLibrary2("freetype2", dynamic_link_opts);
|
||||
} else {
|
||||
const freetype_dep = b.dependency(
|
||||
"freetype",
|
||||
@@ -194,16 +237,7 @@ pub fn build(b: *std.Build) !void {
|
||||
|
||||
b.installArtifact(lib);
|
||||
|
||||
const test_exe = b.addTest(.{
|
||||
.name = "test",
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
test_exe.linkLibrary(lib);
|
||||
const tests_run = b.addRunArtifact(test_exe);
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&tests_run.step);
|
||||
return lib;
|
||||
}
|
||||
|
||||
const headers = &.{
|
||||
|
@@ -5,7 +5,61 @@ pub fn build(b: *std.Build) !void {
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
const libpng_enabled = b.option(bool, "enable-libpng", "Build libpng") orelse false;
|
||||
|
||||
const module = b.addModule("freetype", .{ .root_source_file = b.path("main.zig") });
|
||||
const module = b.addModule("freetype", .{
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// For dynamic linking, we prefer dynamic linking and to search by
|
||||
// mode first. Mode first will search all paths for a dynamic library
|
||||
// before falling back to static.
|
||||
const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{
|
||||
.preferred_link_mode = .dynamic,
|
||||
.search_strategy = .mode_first,
|
||||
};
|
||||
|
||||
var test_exe: ?*std.Build.Step.Compile = null;
|
||||
if (target.query.isNative()) {
|
||||
test_exe = b.addTest(.{
|
||||
.name = "test",
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
const tests_run = b.addRunArtifact(test_exe.?);
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&tests_run.step);
|
||||
}
|
||||
|
||||
module.addIncludePath(b.path(""));
|
||||
|
||||
if (b.systemIntegrationOption("freetype", .{})) {
|
||||
module.linkSystemLibrary("freetype2", dynamic_link_opts);
|
||||
if (test_exe) |exe| {
|
||||
exe.linkSystemLibrary2("freetype2", dynamic_link_opts);
|
||||
}
|
||||
} else {
|
||||
const lib = try buildLib(b, module, .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
|
||||
.libpng_enabled = libpng_enabled,
|
||||
|
||||
.dynamic_link_opts = dynamic_link_opts,
|
||||
});
|
||||
|
||||
if (test_exe) |exe| {
|
||||
exe.linkLibrary(lib);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Build.Step.Compile {
|
||||
const target = options.target;
|
||||
const optimize = options.optimize;
|
||||
|
||||
const libpng_enabled = options.libpng_enabled;
|
||||
|
||||
const upstream = b.dependency("freetype", .{});
|
||||
const lib = b.addStaticLibrary(.{
|
||||
@@ -21,16 +75,6 @@ pub fn build(b: *std.Build) !void {
|
||||
}
|
||||
|
||||
module.addIncludePath(upstream.path("include"));
|
||||
module.addIncludePath(b.path(""));
|
||||
|
||||
// For dynamic linking, we prefer dynamic linking and to search by
|
||||
// mode first. Mode first will search all paths for a dynamic library
|
||||
// before falling back to static.
|
||||
const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{
|
||||
.preferred_link_mode = .dynamic,
|
||||
.search_strategy = .mode_first,
|
||||
};
|
||||
|
||||
var flags = std.ArrayList([]const u8).init(b.allocator);
|
||||
defer flags.deinit();
|
||||
try flags.appendSlice(&.{
|
||||
@@ -44,6 +88,8 @@ pub fn build(b: *std.Build) !void {
|
||||
"-fno-sanitize=undefined",
|
||||
});
|
||||
|
||||
const dynamic_link_opts = options.dynamic_link_opts;
|
||||
|
||||
// Zlib
|
||||
if (b.systemIntegrationOption("zlib", .{})) {
|
||||
lib.linkSystemLibrary2("zlib", dynamic_link_opts);
|
||||
@@ -113,18 +159,7 @@ pub fn build(b: *std.Build) !void {
|
||||
|
||||
b.installArtifact(lib);
|
||||
|
||||
if (target.query.isNative()) {
|
||||
const test_exe = b.addTest(.{
|
||||
.name = "test",
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
test_exe.linkLibrary(lib);
|
||||
const tests_run = b.addRunArtifact(test_exe);
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&tests_run.step);
|
||||
}
|
||||
return lib;
|
||||
}
|
||||
|
||||
const srcs: []const []const u8 = &.{
|
||||
|
@@ -14,7 +14,6 @@ pub fn build(b: *std.Build) !void {
|
||||
.@"enable-libpng" = true,
|
||||
});
|
||||
const macos = b.dependency("macos", .{ .target = target, .optimize = optimize });
|
||||
const upstream = b.dependency("harfbuzz", .{});
|
||||
|
||||
const module = b.addModule("harfbuzz", .{
|
||||
.root_source_file = b.path("main.zig"),
|
||||
@@ -26,6 +25,66 @@ pub fn build(b: *std.Build) !void {
|
||||
},
|
||||
});
|
||||
|
||||
// For dynamic linking, we prefer dynamic linking and to search by
|
||||
// mode first. Mode first will search all paths for a dynamic library
|
||||
// before falling back to static.
|
||||
const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{
|
||||
.preferred_link_mode = .dynamic,
|
||||
.search_strategy = .mode_first,
|
||||
};
|
||||
|
||||
const test_exe = b.addTest(.{
|
||||
.name = "test",
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
{
|
||||
var it = module.import_table.iterator();
|
||||
while (it.next()) |entry| test_exe.root_module.addImport(entry.key_ptr.*, entry.value_ptr.*);
|
||||
if (b.systemIntegrationOption("freetype", .{})) {
|
||||
test_exe.linkSystemLibrary2("freetype2", dynamic_link_opts);
|
||||
} else {
|
||||
test_exe.linkLibrary(freetype.artifact("freetype"));
|
||||
}
|
||||
const tests_run = b.addRunArtifact(test_exe);
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&tests_run.step);
|
||||
}
|
||||
|
||||
if (b.systemIntegrationOption("harfbuzz", .{})) {
|
||||
module.linkSystemLibrary("harfbuzz", dynamic_link_opts);
|
||||
test_exe.linkSystemLibrary2("harfbuzz", dynamic_link_opts);
|
||||
} else {
|
||||
const lib = try buildLib(b, module, .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
|
||||
.coretext_enabled = coretext_enabled,
|
||||
.freetype_enabled = freetype_enabled,
|
||||
|
||||
.dynamic_link_opts = dynamic_link_opts,
|
||||
});
|
||||
|
||||
test_exe.linkLibrary(lib);
|
||||
}
|
||||
}
|
||||
|
||||
fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Build.Step.Compile {
|
||||
const target = options.target;
|
||||
const optimize = options.optimize;
|
||||
|
||||
const coretext_enabled = options.coretext_enabled;
|
||||
const freetype_enabled = options.freetype_enabled;
|
||||
|
||||
const freetype = b.dependency("freetype", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.@"enable-libpng" = true,
|
||||
});
|
||||
|
||||
const upstream = b.dependency("harfbuzz", .{});
|
||||
const lib = b.addStaticLibrary(.{
|
||||
.name = "harfbuzz",
|
||||
.target = target,
|
||||
@@ -41,13 +100,7 @@ pub fn build(b: *std.Build) !void {
|
||||
try apple_sdk.addPaths(b, module);
|
||||
}
|
||||
|
||||
// For dynamic linking, we prefer dynamic linking and to search by
|
||||
// mode first. Mode first will search all paths for a dynamic library
|
||||
// before falling back to static.
|
||||
const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{
|
||||
.preferred_link_mode = .dynamic,
|
||||
.search_strategy = .mode_first,
|
||||
};
|
||||
const dynamic_link_opts = options.dynamic_link_opts;
|
||||
|
||||
var flags = std.ArrayList([]const u8).init(b.allocator);
|
||||
defer flags.deinit();
|
||||
@@ -102,20 +155,5 @@ pub fn build(b: *std.Build) !void {
|
||||
|
||||
b.installArtifact(lib);
|
||||
|
||||
{
|
||||
const test_exe = b.addTest(.{
|
||||
.name = "test",
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
test_exe.linkLibrary(lib);
|
||||
|
||||
var it = module.import_table.iterator();
|
||||
while (it.next()) |entry| test_exe.root_module.addImport(entry.key_ptr.*, entry.value_ptr.*);
|
||||
test_exe.linkLibrary(freetype.artifact("freetype"));
|
||||
const tests_run = b.addRunArtifact(test_exe);
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&tests_run.step);
|
||||
}
|
||||
return lib;
|
||||
}
|
||||
|
@@ -18,9 +18,72 @@ pub const ColorSpace = opaque {
|
||||
) orelse Allocator.Error.OutOfMemory;
|
||||
}
|
||||
|
||||
pub fn createNamed(name: Name) Allocator.Error!*ColorSpace {
|
||||
return @as(
|
||||
?*ColorSpace,
|
||||
@ptrFromInt(@intFromPtr(c.CGColorSpaceCreateWithName(name.cfstring()))),
|
||||
) orelse Allocator.Error.OutOfMemory;
|
||||
}
|
||||
|
||||
pub fn release(self: *ColorSpace) void {
|
||||
c.CGColorSpaceRelease(@ptrCast(self));
|
||||
}
|
||||
|
||||
pub const Name = enum {
|
||||
/// This color space uses the DCI P3 primaries, a D65 white point, and
|
||||
/// the sRGB transfer function.
|
||||
displayP3,
|
||||
/// The Display P3 color space with a linear transfer function and
|
||||
/// extended-range values.
|
||||
extendedLinearDisplayP3,
|
||||
/// The sRGB colorimetry and non-linear transfer function are specified
|
||||
/// in IEC 61966-2-1.
|
||||
sRGB,
|
||||
/// This color space has the same colorimetry as `sRGB`, but uses a
|
||||
/// linear transfer function.
|
||||
linearSRGB,
|
||||
/// This color space has the same colorimetry as `sRGB`, but you can
|
||||
/// encode component values below `0.0` and above `1.0`. Negative values
|
||||
/// are encoded as the signed reflection of the original encoding
|
||||
/// function, as shown in the formula below:
|
||||
/// ```
|
||||
/// extendedTransferFunction(x) = sign(x) * sRGBTransferFunction(abs(x))
|
||||
/// ```
|
||||
extendedSRGB,
|
||||
/// This color space has the same colorimetry as `sRGB`; in addition,
|
||||
/// you may encode component values below `0.0` and above `1.0`.
|
||||
extendedLinearSRGB,
|
||||
/// ...
|
||||
genericGrayGamma2_2,
|
||||
/// ...
|
||||
linearGray,
|
||||
/// This color space has the same colorimetry as `genericGrayGamma2_2`,
|
||||
/// but you can encode component values below `0.0` and above `1.0`.
|
||||
/// Negative values are encoded as the signed reflection of the
|
||||
/// original encoding function, as shown in the formula below:
|
||||
/// ```
|
||||
/// extendedGrayTransferFunction(x) = sign(x) * gamma22Function(abs(x))
|
||||
/// ```
|
||||
extendedGray,
|
||||
/// This color space has the same colorimetry as `linearGray`; in
|
||||
/// addition, you may encode component values below `0.0` and above `1.0`.
|
||||
extendedLinearGray,
|
||||
|
||||
fn cfstring(self: Name) c.CFStringRef {
|
||||
return switch (self) {
|
||||
.displayP3 => c.kCGColorSpaceDisplayP3,
|
||||
.extendedLinearDisplayP3 => c.kCGColorSpaceExtendedLinearDisplayP3,
|
||||
.sRGB => c.kCGColorSpaceSRGB,
|
||||
.extendedSRGB => c.kCGColorSpaceExtendedSRGB,
|
||||
.linearSRGB => c.kCGColorSpaceLinearSRGB,
|
||||
.extendedLinearSRGB => c.kCGColorSpaceExtendedLinearSRGB,
|
||||
.genericGrayGamma2_2 => c.kCGColorSpaceGenericGrayGamma2_2,
|
||||
.extendedGray => c.kCGColorSpaceExtendedGray,
|
||||
.linearGray => c.kCGColorSpaceLinearGray,
|
||||
.extendedLinearGray => c.kCGColorSpaceExtendedLinearGray,
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
test {
|
||||
|
@@ -5,36 +5,59 @@ pub fn build(b: *std.Build) !void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const module = b.addModule("oniguruma", .{ .root_source_file = b.path("main.zig") });
|
||||
const module = b.addModule("oniguruma", .{
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const upstream = b.dependency("oniguruma", .{});
|
||||
const lib = try buildOniguruma(b, upstream, target, optimize);
|
||||
module.addIncludePath(upstream.path("src"));
|
||||
b.installArtifact(lib);
|
||||
// For dynamic linking, we prefer dynamic linking and to search by
|
||||
// mode first. Mode first will search all paths for a dynamic library
|
||||
// before falling back to static.
|
||||
const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{
|
||||
.preferred_link_mode = .dynamic,
|
||||
.search_strategy = .mode_first,
|
||||
};
|
||||
|
||||
var test_exe: ?*std.Build.Step.Compile = null;
|
||||
if (target.query.isNative()) {
|
||||
const test_exe = b.addTest(.{
|
||||
test_exe = b.addTest(.{
|
||||
.name = "test",
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
test_exe.linkLibrary(lib);
|
||||
const tests_run = b.addRunArtifact(test_exe);
|
||||
const tests_run = b.addRunArtifact(test_exe.?);
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&tests_run.step);
|
||||
|
||||
// Uncomment this if we're debugging tests
|
||||
b.installArtifact(test_exe);
|
||||
b.installArtifact(test_exe.?);
|
||||
}
|
||||
|
||||
if (b.systemIntegrationOption("oniguruma", .{})) {
|
||||
module.linkSystemLibrary("oniguruma", dynamic_link_opts);
|
||||
|
||||
if (test_exe) |exe| {
|
||||
exe.linkSystemLibrary2("oniguruma", dynamic_link_opts);
|
||||
}
|
||||
} else {
|
||||
const lib = try buildLib(b, module, .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
if (test_exe) |exe| {
|
||||
exe.linkLibrary(lib);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn buildOniguruma(
|
||||
b: *std.Build,
|
||||
upstream: *std.Build.Dependency,
|
||||
target: std.Build.ResolvedTarget,
|
||||
optimize: std.builtin.OptimizeMode,
|
||||
) !*std.Build.Step.Compile {
|
||||
fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Build.Step.Compile {
|
||||
const target = options.target;
|
||||
const optimize = options.optimize;
|
||||
|
||||
const upstream = b.dependency("oniguruma", .{});
|
||||
const lib = b.addStaticLibrary(.{
|
||||
.name = "oniguruma",
|
||||
.target = target,
|
||||
@@ -43,6 +66,7 @@ fn buildOniguruma(
|
||||
const t = target.result;
|
||||
lib.linkLibC();
|
||||
lib.addIncludePath(upstream.path("src"));
|
||||
module.addIncludePath(upstream.path("src"));
|
||||
|
||||
if (target.result.isDarwin()) {
|
||||
const apple_sdk = @import("apple_sdk");
|
||||
@@ -134,5 +158,7 @@ fn buildOniguruma(
|
||||
.{ .include_extensions = &.{".h"} },
|
||||
);
|
||||
|
||||
b.installArtifact(lib);
|
||||
|
||||
return lib;
|
||||
}
|
||||
|
@@ -162,4 +162,26 @@ pub const Binding = struct {
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn copySubImage2D(
|
||||
b: Binding,
|
||||
level: c.GLint,
|
||||
xoffset: c.GLint,
|
||||
yoffset: c.GLint,
|
||||
x: c.GLint,
|
||||
y: c.GLint,
|
||||
width: c.GLsizei,
|
||||
height: c.GLsizei,
|
||||
) !void {
|
||||
glad.context.CopyTexSubImage2D.?(
|
||||
@intFromEnum(b.target),
|
||||
level,
|
||||
xoffset,
|
||||
yoffset,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@@ -1,6 +1,6 @@
|
||||
.{
|
||||
.name = "simdutf",
|
||||
.version = "4.0.9",
|
||||
.version = "5.2.8",
|
||||
.paths = .{""},
|
||||
.dependencies = .{
|
||||
.apple_sdk = .{ .path = "../apple-sdk" },
|
||||
|
@@ -30,4 +30,36 @@ pub fn build(b: *std.Build) !void {
|
||||
.file = wuffs.path("release/c/wuffs-v0.4.c"),
|
||||
.flags = flags.items,
|
||||
});
|
||||
|
||||
const unit_tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
unit_tests.linkLibC();
|
||||
unit_tests.addIncludePath(wuffs.path("release/c"));
|
||||
unit_tests.addCSourceFile(.{
|
||||
.file = wuffs.path("release/c/wuffs-v0.4.c"),
|
||||
.flags = flags.items,
|
||||
});
|
||||
|
||||
const pixels = b.dependency("pixels", .{});
|
||||
|
||||
inline for (.{ "000000", "FFFFFF" }) |color| {
|
||||
inline for (.{ "gif", "jpg", "png", "ppm" }) |extension| {
|
||||
const filename = std.fmt.comptimePrint("1x1#{s}.{s}", .{ color, extension });
|
||||
unit_tests.root_module.addAnonymousImport(
|
||||
filename,
|
||||
.{
|
||||
.root_source_file = pixels.path(filename),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const run_unit_tests = b.addRunArtifact(unit_tests);
|
||||
|
||||
const test_step = b.step("test", "Run unit tests");
|
||||
test_step.dependOn(&run_unit_tests.step);
|
||||
}
|
||||
|
@@ -3,8 +3,13 @@
|
||||
.version = "0.0.0",
|
||||
.dependencies = .{
|
||||
.wuffs = .{
|
||||
.url = "https://github.com/google/wuffs/archive/refs/tags/v0.4.0-alpha.8.tar.gz",
|
||||
.hash = "12200984439edc817fbcbbaff564020e5104a0d04a2d0f53080700827052de700462",
|
||||
.url = "https://github.com/google/wuffs/archive/refs/tags/v0.4.0-alpha.9.tar.gz",
|
||||
.hash = "122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd",
|
||||
},
|
||||
|
||||
.pixels = .{
|
||||
.url = "git+https://github.com/make-github-pseudonymous-again/pixels?ref=main#d843c2714d32e15b48b8d7eeb480295af537f877",
|
||||
.hash = "12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806",
|
||||
},
|
||||
|
||||
.apple_sdk = .{ .path = "../apple-sdk" },
|
||||
|
@@ -1,3 +1,13 @@
|
||||
const std = @import("std");
|
||||
|
||||
const c = @import("c.zig").c;
|
||||
|
||||
pub const Error = std.mem.Allocator.Error || error{WuffsError};
|
||||
|
||||
pub fn check(log: anytype, status: *const c.struct_wuffs_base__status__struct) error{WuffsError}!void {
|
||||
if (!c.wuffs_base__status__is_ok(status)) {
|
||||
const e = c.wuffs_base__status__message(status);
|
||||
log.warn("decode err={s}", .{e});
|
||||
return error.WuffsError;
|
||||
}
|
||||
}
|
||||
|
133
pkg/wuffs/src/jpeg.zig
Normal file
133
pkg/wuffs/src/jpeg.zig
Normal file
@@ -0,0 +1,133 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const c = @import("c.zig").c;
|
||||
const Error = @import("error.zig").Error;
|
||||
const check = @import("error.zig").check;
|
||||
const ImageData = @import("main.zig").ImageData;
|
||||
|
||||
const log = std.log.scoped(.wuffs_jpeg);
|
||||
|
||||
/// Decode a JPEG image.
|
||||
pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData {
|
||||
// Work around some weirdness in WUFFS/Zig, there are some structs that
|
||||
// are defined as "extern" by the Zig compiler which means that Zig won't
|
||||
// allocate them on the stack at compile time. WUFFS has functions for
|
||||
// dynamically allocating these structs but they use the C malloc/free. This
|
||||
// gets around that by using the Zig allocator to allocate enough memory for
|
||||
// the struct and then casts it to the appropriate pointer.
|
||||
|
||||
const decoder_buf = try alloc.alloc(u8, c.sizeof__wuffs_jpeg__decoder());
|
||||
defer alloc.free(decoder_buf);
|
||||
|
||||
const decoder: ?*c.wuffs_jpeg__decoder = @ptrCast(decoder_buf);
|
||||
{
|
||||
const status = c.wuffs_jpeg__decoder__initialize(
|
||||
decoder,
|
||||
c.sizeof__wuffs_jpeg__decoder(),
|
||||
c.WUFFS_VERSION,
|
||||
0,
|
||||
);
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
var source_buffer: c.wuffs_base__io_buffer = .{
|
||||
.data = .{ .ptr = @constCast(@ptrCast(data.ptr)), .len = data.len },
|
||||
.meta = .{
|
||||
.wi = data.len,
|
||||
.ri = 0,
|
||||
.pos = 0,
|
||||
.closed = true,
|
||||
},
|
||||
};
|
||||
|
||||
var image_config: c.wuffs_base__image_config = undefined;
|
||||
{
|
||||
const status = c.wuffs_jpeg__decoder__decode_image_config(
|
||||
decoder,
|
||||
&image_config,
|
||||
&source_buffer,
|
||||
);
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
const width = c.wuffs_base__pixel_config__width(&image_config.pixcfg);
|
||||
const height = c.wuffs_base__pixel_config__height(&image_config.pixcfg);
|
||||
|
||||
c.wuffs_base__pixel_config__set(
|
||||
&image_config.pixcfg,
|
||||
c.WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL,
|
||||
c.WUFFS_BASE__PIXEL_SUBSAMPLING__NONE,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
|
||||
const destination = try alloc.alloc(
|
||||
u8,
|
||||
width * height * @sizeOf(c.wuffs_base__color_u32_argb_premul),
|
||||
);
|
||||
errdefer alloc.free(destination);
|
||||
|
||||
// temporary buffer for intermediate processing of image
|
||||
const work_buffer = try alloc.alloc(
|
||||
u8,
|
||||
|
||||
// The type of this is a u64 on all systems but our allocator
|
||||
// uses a usize which is a u32 on 32-bit systems.
|
||||
std.math.cast(
|
||||
usize,
|
||||
c.wuffs_jpeg__decoder__workbuf_len(decoder).max_incl,
|
||||
) orelse return error.OutOfMemory,
|
||||
);
|
||||
defer alloc.free(work_buffer);
|
||||
|
||||
const work_slice = c.wuffs_base__make_slice_u8(
|
||||
work_buffer.ptr,
|
||||
work_buffer.len,
|
||||
);
|
||||
|
||||
var pixel_buffer: c.wuffs_base__pixel_buffer = undefined;
|
||||
{
|
||||
const status = c.wuffs_base__pixel_buffer__set_from_slice(
|
||||
&pixel_buffer,
|
||||
&image_config.pixcfg,
|
||||
c.wuffs_base__make_slice_u8(destination.ptr, destination.len),
|
||||
);
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
{
|
||||
const status = c.wuffs_jpeg__decoder__decode_frame(
|
||||
decoder,
|
||||
&pixel_buffer,
|
||||
&source_buffer,
|
||||
c.WUFFS_BASE__PIXEL_BLEND__SRC,
|
||||
work_slice,
|
||||
null,
|
||||
);
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
return .{
|
||||
.width = width,
|
||||
.height = height,
|
||||
.data = destination,
|
||||
};
|
||||
}
|
||||
|
||||
test "jpeg_decode_000000" {
|
||||
const data = try decode(std.testing.allocator, @embedFile("1x1#000000.jpg"));
|
||||
defer std.testing.allocator.free(data.data);
|
||||
|
||||
try std.testing.expectEqual(1, data.width);
|
||||
try std.testing.expectEqual(1, data.height);
|
||||
try std.testing.expectEqualSlices(u8, &.{ 0, 0, 0, 255 }, data.data);
|
||||
}
|
||||
|
||||
test "jpeg_decode_FFFFFF" {
|
||||
const data = try decode(std.testing.allocator, @embedFile("1x1#FFFFFF.jpg"));
|
||||
defer std.testing.allocator.free(data.data);
|
||||
|
||||
try std.testing.expectEqual(1, data.width);
|
||||
try std.testing.expectEqual(1, data.height);
|
||||
try std.testing.expectEqualSlices(u8, &.{ 255, 255, 255, 255 }, data.data);
|
||||
}
|
@@ -1,2 +1,15 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const png = @import("png.zig");
|
||||
pub const jpeg = @import("jpeg.zig");
|
||||
pub const swizzle = @import("swizzle.zig");
|
||||
|
||||
pub const ImageData = struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
data: []const u8,
|
||||
};
|
||||
|
||||
test {
|
||||
std.testing.refAllDeclsRecursive(@This());
|
||||
}
|
||||
|
@@ -2,15 +2,13 @@ const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const c = @import("c.zig").c;
|
||||
const Error = @import("error.zig").Error;
|
||||
const check = @import("error.zig").check;
|
||||
const ImageData = @import("main.zig").ImageData;
|
||||
|
||||
const log = std.log.scoped(.wuffs_png);
|
||||
|
||||
/// Decode a PNG image.
|
||||
pub fn decode(alloc: Allocator, data: []const u8) Error!struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
data: []const u8,
|
||||
} {
|
||||
pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData {
|
||||
// Work around some weirdness in WUFFS/Zig, there are some structs that
|
||||
// are defined as "extern" by the Zig compiler which means that Zig won't
|
||||
// allocate them on the stack at compile time. WUFFS has functions for
|
||||
@@ -29,11 +27,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!struct {
|
||||
c.WUFFS_VERSION,
|
||||
0,
|
||||
);
|
||||
if (!c.wuffs_base__status__is_ok(&status)) {
|
||||
const e = c.wuffs_base__status__message(&status);
|
||||
log.warn("decode err={s}", .{e});
|
||||
return error.WuffsError;
|
||||
}
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
var source_buffer: c.wuffs_base__io_buffer = .{
|
||||
@@ -53,11 +47,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!struct {
|
||||
&image_config,
|
||||
&source_buffer,
|
||||
);
|
||||
if (!c.wuffs_base__status__is_ok(&status)) {
|
||||
const e = c.wuffs_base__status__message(&status);
|
||||
log.warn("decode err={s}", .{e});
|
||||
return error.WuffsError;
|
||||
}
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
const width = c.wuffs_base__pixel_config__width(&image_config.pixcfg);
|
||||
@@ -65,7 +55,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!struct {
|
||||
|
||||
c.wuffs_base__pixel_config__set(
|
||||
&image_config.pixcfg,
|
||||
c.WUFFS_BASE__PIXEL_FORMAT__RGBA_PREMUL,
|
||||
c.WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL,
|
||||
c.WUFFS_BASE__PIXEL_SUBSAMPLING__NONE,
|
||||
width,
|
||||
height,
|
||||
@@ -102,25 +92,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!struct {
|
||||
&image_config.pixcfg,
|
||||
c.wuffs_base__make_slice_u8(destination.ptr, destination.len),
|
||||
);
|
||||
if (!c.wuffs_base__status__is_ok(&status)) {
|
||||
const e = c.wuffs_base__status__message(&status);
|
||||
log.warn("decode err={s}", .{e});
|
||||
return error.WuffsError;
|
||||
}
|
||||
}
|
||||
|
||||
var frame_config: c.wuffs_base__frame_config = undefined;
|
||||
{
|
||||
const status = c.wuffs_png__decoder__decode_frame_config(
|
||||
decoder,
|
||||
&frame_config,
|
||||
&source_buffer,
|
||||
);
|
||||
if (!c.wuffs_base__status__is_ok(&status)) {
|
||||
const e = c.wuffs_base__status__message(&status);
|
||||
log.warn("decode err={s}", .{e});
|
||||
return error.WuffsError;
|
||||
}
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -132,11 +104,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!struct {
|
||||
work_slice,
|
||||
null,
|
||||
);
|
||||
if (!c.wuffs_base__status__is_ok(&status)) {
|
||||
const e = c.wuffs_base__status__message(&status);
|
||||
log.warn("decode err={s}", .{e});
|
||||
return error.WuffsError;
|
||||
}
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
return .{
|
||||
@@ -145,3 +113,21 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!struct {
|
||||
.data = destination,
|
||||
};
|
||||
}
|
||||
|
||||
test "png_decode_000000" {
|
||||
const data = try decode(std.testing.allocator, @embedFile("1x1#000000.png"));
|
||||
defer std.testing.allocator.free(data.data);
|
||||
|
||||
try std.testing.expectEqual(1, data.width);
|
||||
try std.testing.expectEqual(1, data.height);
|
||||
try std.testing.expectEqualSlices(u8, &.{ 0, 0, 0, 255 }, data.data);
|
||||
}
|
||||
|
||||
test "png_decode_FFFFFF" {
|
||||
const data = try decode(std.testing.allocator, @embedFile("1x1#FFFFFF.png"));
|
||||
defer std.testing.allocator.free(data.data);
|
||||
|
||||
try std.testing.expectEqual(1, data.width);
|
||||
try std.testing.expectEqual(1, data.height);
|
||||
try std.testing.expectEqualSlices(u8, &.{ 255, 255, 255, 255 }, data.data);
|
||||
}
|
||||
|
44
src/App.zig
44
src/App.zig
@@ -54,9 +54,6 @@ focused_surface: ?*Surface = null,
|
||||
/// this is a blocking queue so if it is full you will get errors (or block).
|
||||
mailbox: Mailbox.Queue,
|
||||
|
||||
/// Set to true once we're quitting. This never goes false again.
|
||||
quit: bool,
|
||||
|
||||
/// The set of font GroupCache instances shared by surfaces with the
|
||||
/// same font configuration.
|
||||
font_grid_set: font.SharedGridSet,
|
||||
@@ -98,7 +95,6 @@ pub fn create(
|
||||
.alloc = alloc,
|
||||
.surfaces = .{},
|
||||
.mailbox = .{},
|
||||
.quit = false,
|
||||
.font_grid_set = font_grid_set,
|
||||
.config_conditional_state = .{},
|
||||
};
|
||||
@@ -125,9 +121,7 @@ pub fn destroy(self: *App) void {
|
||||
/// Tick ticks the app loop. This will drain our mailbox and process those
|
||||
/// events. This should be called by the application runtime on every loop
|
||||
/// tick.
|
||||
///
|
||||
/// This returns whether the app should quit or not.
|
||||
pub fn tick(self: *App, rt_app: *apprt.App) !bool {
|
||||
pub fn tick(self: *App, rt_app: *apprt.App) !void {
|
||||
// If any surfaces are closing, destroy them
|
||||
var i: usize = 0;
|
||||
while (i < self.surfaces.items.len) {
|
||||
@@ -142,13 +136,6 @@ pub fn tick(self: *App, rt_app: *apprt.App) !bool {
|
||||
|
||||
// Drain our mailbox
|
||||
try self.drainMailbox(rt_app);
|
||||
|
||||
// No matter what, we reset the quit flag after a tick. If the apprt
|
||||
// doesn't want to quit, then we can't force it to.
|
||||
defer self.quit = false;
|
||||
|
||||
// We quit if our quit flag is on
|
||||
return self.quit;
|
||||
}
|
||||
|
||||
/// Update the configuration associated with the app. This can only be
|
||||
@@ -272,7 +259,7 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
|
||||
// can try to quit as quickly as possible.
|
||||
.quit => {
|
||||
log.info("quit message received, short circuiting mailbox drain", .{});
|
||||
self.setQuit();
|
||||
try self.performAction(rt_app, .quit);
|
||||
return;
|
||||
},
|
||||
}
|
||||
@@ -314,12 +301,6 @@ pub fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void {
|
||||
);
|
||||
}
|
||||
|
||||
/// Start quitting
|
||||
pub fn setQuit(self: *App) void {
|
||||
if (self.quit) return;
|
||||
self.quit = true;
|
||||
}
|
||||
|
||||
/// Handle an app-level focus event. This should be called whenever
|
||||
/// the focus state of the entire app containing Ghostty changes.
|
||||
/// This is separate from surface focus events. See the `focused`
|
||||
@@ -332,6 +313,25 @@ pub fn focusEvent(self: *App, focused: bool) void {
|
||||
self.focused = focused;
|
||||
}
|
||||
|
||||
/// Returns true if the given key event would trigger a keybinding
|
||||
/// if it were to be processed. This is useful for determining if
|
||||
/// a key event should be sent to the terminal or not.
|
||||
pub fn keyEventIsBinding(
|
||||
self: *App,
|
||||
rt_app: *apprt.App,
|
||||
event: input.KeyEvent,
|
||||
) bool {
|
||||
_ = self;
|
||||
|
||||
switch (event.action) {
|
||||
.release => return false,
|
||||
.press, .repeat => {},
|
||||
}
|
||||
|
||||
// If we have a keybinding for this event then we return true.
|
||||
return rt_app.config.keybind.set.getEvent(event) != null;
|
||||
}
|
||||
|
||||
/// Handle a key event at the app-scope. If this key event is used,
|
||||
/// this will return true and the caller shouldn't continue processing
|
||||
/// the event. If the event is not used, this will return false.
|
||||
@@ -437,7 +437,7 @@ pub fn performAction(
|
||||
switch (action) {
|
||||
.unbind => unreachable,
|
||||
.ignore => {},
|
||||
.quit => self.setQuit(),
|
||||
.quit => try rt_app.performAction(.app, .quit, {}),
|
||||
.new_window => try self.newWindow(rt_app, .{ .parent = null }),
|
||||
.open_config => try rt_app.performAction(.app, .open_config, {}),
|
||||
.reload_config => try rt_app.performAction(.app, .reload_config, .{}),
|
||||
|
@@ -18,6 +18,7 @@ const Command = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const global_state = &@import("global.zig").state;
|
||||
const internal_os = @import("os/main.zig");
|
||||
const windows = internal_os.windows;
|
||||
const TempDir = internal_os.TempDir;
|
||||
@@ -175,6 +176,10 @@ fn startPosix(self: *Command, arena: Allocator) !void {
|
||||
// We don't log because that'll show up in the output.
|
||||
};
|
||||
|
||||
// Restore any rlimits that were set by Ghostty. This might fail but
|
||||
// any failures are ignored (its best effort).
|
||||
global_state.rlimits.restore();
|
||||
|
||||
// If the user requested a pre exec callback, call it now.
|
||||
if (self.pre_exec) |f| f(self);
|
||||
|
||||
@@ -587,8 +592,8 @@ test "createNullDelimitedEnvMap" {
|
||||
test "Command: pre exec" {
|
||||
if (builtin.os.tag == .windows) return error.SkipZigTest;
|
||||
var cmd: Command = .{
|
||||
.path = "/usr/bin/env",
|
||||
.args = &.{ "/usr/bin/env", "-v" },
|
||||
.path = "/bin/sh",
|
||||
.args = &.{ "/bin/sh", "-v" },
|
||||
.pre_exec = (struct {
|
||||
fn do(_: *Command) void {
|
||||
// This runs in the child, so we can exit and it won't
|
||||
@@ -598,7 +603,7 @@ test "Command: pre exec" {
|
||||
}).do,
|
||||
};
|
||||
|
||||
try cmd.start(testing.allocator);
|
||||
try cmd.testingStart();
|
||||
try testing.expect(cmd.pid != null);
|
||||
const exit = try cmd.wait(true);
|
||||
try testing.expect(exit == .Exited);
|
||||
@@ -629,12 +634,12 @@ test "Command: redirect stdout to file" {
|
||||
.args = &.{"C:\\Windows\\System32\\whoami.exe"},
|
||||
.stdout = stdout,
|
||||
} else .{
|
||||
.path = "/usr/bin/env",
|
||||
.args = &.{ "/usr/bin/env", "-v" },
|
||||
.path = "/bin/sh",
|
||||
.args = &.{ "/bin/sh", "-c", "echo hello" },
|
||||
.stdout = stdout,
|
||||
};
|
||||
|
||||
try cmd.start(testing.allocator);
|
||||
try cmd.testingStart();
|
||||
try testing.expect(cmd.pid != null);
|
||||
const exit = try cmd.wait(true);
|
||||
try testing.expect(exit == .Exited);
|
||||
@@ -663,13 +668,13 @@ test "Command: custom env vars" {
|
||||
.stdout = stdout,
|
||||
.env = &env,
|
||||
} else .{
|
||||
.path = "/usr/bin/env",
|
||||
.args = &.{ "/usr/bin/env", "sh", "-c", "echo $VALUE" },
|
||||
.path = "/bin/sh",
|
||||
.args = &.{ "/bin/sh", "-c", "echo $VALUE" },
|
||||
.stdout = stdout,
|
||||
.env = &env,
|
||||
};
|
||||
|
||||
try cmd.start(testing.allocator);
|
||||
try cmd.testingStart();
|
||||
try testing.expect(cmd.pid != null);
|
||||
const exit = try cmd.wait(true);
|
||||
try testing.expect(exit == .Exited);
|
||||
@@ -699,13 +704,13 @@ test "Command: custom working directory" {
|
||||
.stdout = stdout,
|
||||
.cwd = "C:\\Windows\\System32",
|
||||
} else .{
|
||||
.path = "/usr/bin/env",
|
||||
.args = &.{ "/usr/bin/env", "sh", "-c", "pwd" },
|
||||
.path = "/bin/sh",
|
||||
.args = &.{ "/bin/sh", "-c", "pwd" },
|
||||
.stdout = stdout,
|
||||
.cwd = "/usr/bin",
|
||||
.cwd = "/tmp",
|
||||
};
|
||||
|
||||
try cmd.start(testing.allocator);
|
||||
try cmd.testingStart();
|
||||
try testing.expect(cmd.pid != null);
|
||||
const exit = try cmd.wait(true);
|
||||
try testing.expect(exit == .Exited);
|
||||
@@ -718,7 +723,51 @@ test "Command: custom working directory" {
|
||||
|
||||
if (builtin.os.tag == .windows) {
|
||||
try testing.expectEqualStrings("C:\\Windows\\System32\r\n", contents);
|
||||
} else if (builtin.os.tag == .macos) {
|
||||
try testing.expectEqualStrings("/private/tmp\n", contents);
|
||||
} else {
|
||||
try testing.expectEqualStrings("/usr/bin\n", contents);
|
||||
try testing.expectEqualStrings("/tmp\n", contents);
|
||||
}
|
||||
}
|
||||
|
||||
// Test validate an execveZ failure correctly terminates when error.ExecFailedInChild is correctly handled
|
||||
//
|
||||
// Incorrectly handling an error.ExecFailedInChild results in a second copy of the test process running.
|
||||
// Duplicating the test process leads to weird behavior
|
||||
// zig build test will hang
|
||||
// test binary created via -Demit-test-exe will run 2 copies of the test suite
|
||||
test "Command: posix fork handles execveZ failure" {
|
||||
if (builtin.os.tag == .windows) {
|
||||
return error.SkipZigTest;
|
||||
}
|
||||
var td = try TempDir.init();
|
||||
defer td.deinit();
|
||||
var stdout = try createTestStdout(td.dir);
|
||||
defer stdout.close();
|
||||
|
||||
var cmd: Command = .{
|
||||
.path = "/not/a/binary",
|
||||
.args = &.{ "/not/a/binary", "" },
|
||||
.stdout = stdout,
|
||||
.cwd = "/bin",
|
||||
};
|
||||
|
||||
try cmd.testingStart();
|
||||
try testing.expect(cmd.pid != null);
|
||||
const exit = try cmd.wait(true);
|
||||
try testing.expect(exit == .Exited);
|
||||
try testing.expect(exit.Exited == 1);
|
||||
}
|
||||
|
||||
// If cmd.start fails with error.ExecFailedInChild it's the _child_ process that is running. If it does not
|
||||
// terminate in response to that error both the parent and child will continue as if they _are_ the test suite
|
||||
// process.
|
||||
fn testingStart(self: *Command) !void {
|
||||
self.start(testing.allocator) catch |err| {
|
||||
if (err == error.ExecFailedInChild) {
|
||||
// I am a child process, I must not get confused and continue running the rest of the test suite.
|
||||
posix.exit(1);
|
||||
}
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
304
src/Surface.zig
304
src/Surface.zig
@@ -236,7 +236,7 @@ const DerivedConfig = struct {
|
||||
clipboard_paste_protection: bool,
|
||||
clipboard_paste_bracketed_safe: bool,
|
||||
copy_on_select: configpkg.CopyOnSelect,
|
||||
confirm_close_surface: bool,
|
||||
confirm_close_surface: configpkg.ConfirmCloseSurface,
|
||||
cursor_click_to_move: bool,
|
||||
desktop_notifications: bool,
|
||||
font: font.SharedGridSet.DerivedConfig,
|
||||
@@ -253,6 +253,7 @@ const DerivedConfig = struct {
|
||||
window_padding_right: u32,
|
||||
window_padding_balance: bool,
|
||||
title: ?[:0]const u8,
|
||||
title_report: bool,
|
||||
links: []Link,
|
||||
|
||||
const Link = struct {
|
||||
@@ -313,6 +314,7 @@ const DerivedConfig = struct {
|
||||
.window_padding_right = config.@"window-padding-x".bottom_right,
|
||||
.window_padding_balance = config.@"window-padding-balance",
|
||||
.title = config.title,
|
||||
.title_report = config.@"title-report",
|
||||
.links = links,
|
||||
|
||||
// Assignments happen sequentially so we have to do this last
|
||||
@@ -567,12 +569,16 @@ pub fn init(
|
||||
|
||||
// Set a minimum size that is cols=10 h=4. This matches Mac's Terminal.app
|
||||
// but is otherwise somewhat arbitrary.
|
||||
|
||||
const min_window_width_cells: u32 = 10;
|
||||
const min_window_height_cells: u32 = 4;
|
||||
|
||||
try rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.size_limit,
|
||||
.{
|
||||
.min_width = size.cell.width * 10,
|
||||
.min_height = size.cell.height * 4,
|
||||
.min_width = size.cell.width * min_window_width_cells,
|
||||
.min_height = size.cell.height * min_window_height_cells,
|
||||
// No max:
|
||||
.max_width = 0,
|
||||
.max_height = 0,
|
||||
@@ -615,8 +621,8 @@ pub fn init(
|
||||
// start messing with the window.
|
||||
if (config.@"window-height" > 0 and config.@"window-width" > 0) init: {
|
||||
const scale = rt_surface.getContentScale() catch break :init;
|
||||
const height = @max(config.@"window-height" * cell_size.height, 480);
|
||||
const width = @max(config.@"window-width" * cell_size.width, 640);
|
||||
const height = @max(config.@"window-height", min_window_height_cells) * cell_size.height;
|
||||
const width = @max(config.@"window-width", min_window_width_cells) * cell_size.width;
|
||||
const width_f32: f32 = @floatFromInt(width);
|
||||
const height_f32: f32 = @floatFromInt(height);
|
||||
|
||||
@@ -784,18 +790,20 @@ pub fn deactivateInspector(self: *Surface) void {
|
||||
/// True if the surface requires confirmation to quit. This should be called
|
||||
/// by apprt to determine if the surface should confirm before quitting.
|
||||
pub fn needsConfirmQuit(self: *Surface) bool {
|
||||
// If the child has exited then our process is certainly not alive.
|
||||
// If the child has exited, then our process is certainly not alive.
|
||||
// We check this first to avoid the locking overhead below.
|
||||
if (self.child_exited) return false;
|
||||
|
||||
// If we are configured to not hold open surfaces explicitly, just
|
||||
// always say there is nothing alive.
|
||||
if (!self.config.confirm_close_surface) return false;
|
||||
|
||||
// We have to talk to the terminal.
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
return !self.io.terminal.cursorIsAtPrompt();
|
||||
// Check the configuration for confirming close behavior.
|
||||
return switch (self.config.confirm_close_surface) {
|
||||
.always => true,
|
||||
.false => false,
|
||||
.true => true: {
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
break :true !self.io.terminal.cursorIsAtPrompt();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Called from the app thread to handle mailbox messages to our specific
|
||||
@@ -822,7 +830,12 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
);
|
||||
},
|
||||
|
||||
.report_title => |style| {
|
||||
.report_title => |style| report_title: {
|
||||
if (!self.config.title_report) {
|
||||
log.info("report_title requested, but disabled via config", .{});
|
||||
break :report_title;
|
||||
}
|
||||
|
||||
const title: ?[:0]const u8 = self.rt_surface.getTitle();
|
||||
const data = switch (style) {
|
||||
.csi_21_t => try std.fmt.allocPrint(
|
||||
@@ -844,11 +857,8 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
},
|
||||
|
||||
.color_change => |change| {
|
||||
// On any color change, we have to report for mode 2031
|
||||
// if it is enabled.
|
||||
self.reportColorScheme(false);
|
||||
|
||||
// Notify our apprt
|
||||
// Notify our apprt, but don't send a mode 2031 DSR report
|
||||
// because VT sequences were used to change the color.
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.color_change,
|
||||
@@ -1031,6 +1041,9 @@ fn mouseRefreshLinks(
|
||||
pos_vp: terminal.point.Coordinate,
|
||||
over_link: bool,
|
||||
) !void {
|
||||
// If the position is outside our viewport, do nothing
|
||||
if (pos.x < 0 or pos.y < 0) return;
|
||||
|
||||
self.mouse.link_point = pos_vp;
|
||||
|
||||
if (try self.linkAtPos(pos)) |link| {
|
||||
@@ -1150,7 +1163,6 @@ pub fn updateConfig(
|
||||
}
|
||||
|
||||
// If we are in the middle of a key sequence, clear it.
|
||||
self.keyboard.bindings = null;
|
||||
self.endKeySequence(.drop, .free);
|
||||
|
||||
// Before sending any other config changes, we give the renderer a new font
|
||||
@@ -1307,8 +1319,8 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos {
|
||||
const content_scale = self.rt_surface.getContentScale() catch .{ .x = 1, .y = 1 };
|
||||
|
||||
const x: f64 = x: {
|
||||
// Simple x * cell width gives the top-left corner
|
||||
var x: f64 = @floatFromInt(cursor.x * self.size.cell.width);
|
||||
// Simple x * cell width gives the top-left corner, then add padding offset
|
||||
var x: f64 = @floatFromInt(cursor.x * self.size.cell.width + self.size.padding.left);
|
||||
|
||||
// We want the midpoint
|
||||
x += @as(f64, @floatFromInt(self.size.cell.width)) / 2;
|
||||
@@ -1320,8 +1332,8 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos {
|
||||
};
|
||||
|
||||
const y: f64 = y: {
|
||||
// Simple x * cell width gives the top-left corner
|
||||
var y: f64 = @floatFromInt(cursor.y * self.size.cell.height);
|
||||
// Simple y * cell height gives the top-left corner, then add padding offset
|
||||
var y: f64 = @floatFromInt(cursor.y * self.size.cell.height + self.size.padding.top);
|
||||
|
||||
// We want the bottom
|
||||
y += @floatFromInt(self.size.cell.height);
|
||||
@@ -1582,6 +1594,15 @@ pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void {
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
|
||||
// We clear our selection when ANY OF:
|
||||
// 1. We have an existing preedit
|
||||
// 2. We have preedit text
|
||||
if (self.renderer_state.preedit != null or
|
||||
preedit_ != null)
|
||||
{
|
||||
self.setSelection(null) catch {};
|
||||
}
|
||||
|
||||
// We always clear our prior preedit
|
||||
if (self.renderer_state.preedit) |p| {
|
||||
self.alloc.free(p.codepoints);
|
||||
@@ -1632,6 +1653,31 @@ pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void {
|
||||
try self.queueRender();
|
||||
}
|
||||
|
||||
/// Returns true if the given key event would trigger a keybinding
|
||||
/// if it were to be processed. This is useful for determining if
|
||||
/// a key event should be sent to the terminal or not.
|
||||
///
|
||||
/// Note that this function does not check if the binding itself
|
||||
/// is performable, only if the key event would trigger a binding.
|
||||
/// If a performable binding is found and the event is not performable,
|
||||
/// then Ghosty will act as though the binding does not exist.
|
||||
pub fn keyEventIsBinding(
|
||||
self: *Surface,
|
||||
event: input.KeyEvent,
|
||||
) bool {
|
||||
switch (event.action) {
|
||||
.release => return false,
|
||||
.press, .repeat => {},
|
||||
}
|
||||
|
||||
// Our keybinding set is either our current nested set (for
|
||||
// sequences) or the root set.
|
||||
const set = self.keyboard.bindings orelse &self.config.keybind.set;
|
||||
|
||||
// If we have a keybinding for this event then we return true.
|
||||
return set.getEvent(event) != null;
|
||||
}
|
||||
|
||||
/// Called for any key events. This handles keybindings, encoding and
|
||||
/// sending to the terminal, etc.
|
||||
pub fn keyCallback(
|
||||
@@ -1701,16 +1747,37 @@ pub fn keyCallback(
|
||||
// Update our modifiers, this will update mouse mods too
|
||||
self.modsChanged(event.mods);
|
||||
|
||||
// Refresh our link state
|
||||
const pos = self.rt_surface.getCursorPos() catch break :mouse_mods;
|
||||
self.mouseRefreshLinks(
|
||||
pos,
|
||||
self.posToViewport(pos.x, pos.y),
|
||||
self.mouse.over_link,
|
||||
) catch |err| {
|
||||
log.warn("failed to refresh links err={}", .{err});
|
||||
break :mouse_mods;
|
||||
};
|
||||
// We only refresh links if
|
||||
// 1. mouse reporting is off
|
||||
// OR
|
||||
// 2. mouse reporting is on and we are not reporting shift to the terminal
|
||||
if (self.io.terminal.flags.mouse_event == .none or
|
||||
(self.mouse.mods.shift and !self.mouseShiftCapture(false)))
|
||||
{
|
||||
// Refresh our link state
|
||||
const pos = self.rt_surface.getCursorPos() catch break :mouse_mods;
|
||||
self.mouseRefreshLinks(
|
||||
pos,
|
||||
self.posToViewport(pos.x, pos.y),
|
||||
self.mouse.over_link,
|
||||
) catch |err| {
|
||||
log.warn("failed to refresh links err={}", .{err});
|
||||
break :mouse_mods;
|
||||
};
|
||||
} else if (self.io.terminal.flags.mouse_event != .none and !self.mouse.mods.shift) {
|
||||
// If we have mouse reports on and we don't have shift pressed, we reset state
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_shape,
|
||||
self.io.terminal.mouse_shape,
|
||||
);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_over_link,
|
||||
.{ .url = "" },
|
||||
);
|
||||
try self.queueRender();
|
||||
}
|
||||
}
|
||||
|
||||
// Process the cursor state logic. This will update the cursor shape if
|
||||
@@ -1826,9 +1893,6 @@ fn maybeHandleBinding(
|
||||
if (self.keyboard.bindings != null and
|
||||
!event.key.modifier())
|
||||
{
|
||||
// Reset to the root set
|
||||
self.keyboard.bindings = null;
|
||||
|
||||
// Encode everything up to this point
|
||||
self.endKeySequence(.flush, .retain);
|
||||
}
|
||||
@@ -1914,10 +1978,21 @@ fn maybeHandleBinding(
|
||||
return .closed;
|
||||
}
|
||||
|
||||
// If we have the performable flag and the action was not performed,
|
||||
// then we act as though a binding didn't exist.
|
||||
if (leaf.flags.performable and !performed) {
|
||||
// If we're in a sequence, we treat this as if we pressed a key
|
||||
// that doesn't exist in the sequence. Reset our sequence and flush
|
||||
// any queued events.
|
||||
self.endKeySequence(.flush, .retain);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// If we consume this event, then we are done. If we don't consume
|
||||
// it, we processed the action but we still want to process our
|
||||
// encodings, too.
|
||||
if (performed and consumed) {
|
||||
if (consumed) {
|
||||
// If we had queued events, we deinit them since we consumed
|
||||
self.endKeySequence(.drop, .retain);
|
||||
|
||||
@@ -1959,6 +2034,10 @@ fn endKeySequence(
|
||||
);
|
||||
};
|
||||
|
||||
// No matter what we clear our current binding set. This restores
|
||||
// the set we look at to the root set.
|
||||
self.keyboard.bindings = null;
|
||||
|
||||
if (self.keyboard.queued.items.len > 0) {
|
||||
switch (action) {
|
||||
.flush => for (self.keyboard.queued.items) |write_req| {
|
||||
@@ -3186,7 +3265,7 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
|
||||
.trim = false,
|
||||
});
|
||||
defer self.alloc.free(str);
|
||||
try internal_os.open(self.alloc, str);
|
||||
try internal_os.open(self.alloc, .unknown, str);
|
||||
},
|
||||
|
||||
._open_osc8 => {
|
||||
@@ -3194,7 +3273,7 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
|
||||
log.warn("failed to get URI for OSC8 hyperlink", .{});
|
||||
return false;
|
||||
};
|
||||
try internal_os.open(self.alloc, uri);
|
||||
try internal_os.open(self.alloc, .unknown, uri);
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3341,6 +3420,27 @@ pub fn cursorPosCallback(
|
||||
try self.queueRender();
|
||||
}
|
||||
|
||||
// Handle link hovering
|
||||
// We refresh links when
|
||||
// 1. we were previously over a link
|
||||
// OR
|
||||
// 2. the cursor position has changed (either we have no previous state, or the state has
|
||||
// changed)
|
||||
// AND
|
||||
// 1. mouse reporting is off
|
||||
// OR
|
||||
// 2. mouse reporting is on and we are not reporting shift to the terminal
|
||||
if ((over_link or
|
||||
self.mouse.link_point == null or
|
||||
(self.mouse.link_point != null and !self.mouse.link_point.?.eql(pos_vp))) and
|
||||
(self.io.terminal.flags.mouse_event == .none or
|
||||
(self.mouse.mods.shift and !self.mouseShiftCapture(false))))
|
||||
{
|
||||
// If we were previously over a link, we always update. We do this so that if the text
|
||||
// changed underneath us, even if the mouse didn't move, we update the URL hints and state
|
||||
try self.mouseRefreshLinks(pos, pos_vp, over_link);
|
||||
}
|
||||
|
||||
// Do a mouse report
|
||||
if (self.io.terminal.flags.mouse_event != .none) report: {
|
||||
// Shift overrides mouse "grabbing" in the window, taken from Kitty.
|
||||
@@ -3361,18 +3461,6 @@ pub fn cursorPosCallback(
|
||||
|
||||
try self.mouseReport(button, .motion, self.mouse.mods, pos);
|
||||
|
||||
// If we were previously over a link, we need to undo the link state.
|
||||
// We also queue a render so the renderer can undo the rendered link
|
||||
// state.
|
||||
if (over_link) {
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_over_link,
|
||||
.{ .url = "" },
|
||||
);
|
||||
try self.queueRender();
|
||||
}
|
||||
|
||||
// If we're doing mouse motion tracking, we do not support text
|
||||
// selection.
|
||||
return;
|
||||
@@ -3428,30 +3516,6 @@ pub fn cursorPosCallback(
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle link hovering
|
||||
if (self.mouse.link_point) |last_vp| {
|
||||
// Mark the link's row as dirty.
|
||||
if (over_link) {
|
||||
self.renderer_state.terminal.screen.dirty.hyperlink_hover = true;
|
||||
}
|
||||
|
||||
// If our last link viewport point is unchanged, then don't process
|
||||
// links. This avoids constantly reprocessing regular expressions
|
||||
// for every pixel change.
|
||||
if (last_vp.eql(pos_vp)) {
|
||||
// We have to restore old values that are always cleared
|
||||
if (over_link) {
|
||||
self.mouse.over_link = over_link;
|
||||
self.renderer_state.mouse.point = pos_vp;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// We can process new links.
|
||||
try self.mouseRefreshLinks(pos, pos_vp, over_link);
|
||||
}
|
||||
|
||||
/// Double-click dragging moves the selection one "word" at a time.
|
||||
@@ -3502,22 +3566,21 @@ fn dragLeftClickTriple(
|
||||
const screen = &self.io.terminal.screen;
|
||||
const click_pin = self.mouse.left_click_pin.?.*;
|
||||
|
||||
// Get the word under our current point. If there isn't a word, do nothing.
|
||||
const word = screen.selectLine(.{ .pin = drag_pin }) orelse return;
|
||||
// Get the line selection under our current drag point. If there isn't a
|
||||
// line, do nothing.
|
||||
const line = screen.selectLine(.{ .pin = drag_pin }) orelse return;
|
||||
|
||||
// Get our selection to grow it. If we don't have a selection, start it now.
|
||||
// We may not have a selection if we started our dbl-click in an area
|
||||
// that had no data, then we dragged our mouse into an area with data.
|
||||
var sel = screen.selectLine(.{ .pin = click_pin }) orelse {
|
||||
try self.setSelection(word);
|
||||
return;
|
||||
};
|
||||
// Get the selection under our click point. We first try to trim
|
||||
// whitespace if we've selected a word. But if no word exists then
|
||||
// we select the blank line.
|
||||
const sel_ = screen.selectLine(.{ .pin = click_pin }) orelse
|
||||
screen.selectLine(.{ .pin = click_pin, .whitespace = null });
|
||||
|
||||
// Grow our selection
|
||||
var sel = sel_ orelse return;
|
||||
if (drag_pin.before(click_pin)) {
|
||||
sel.startPtr().* = word.start();
|
||||
sel.startPtr().* = line.start();
|
||||
} else {
|
||||
sel.endPtr().* = word.end();
|
||||
sel.endPtr().* = line.end();
|
||||
}
|
||||
try self.setSelection(sel);
|
||||
}
|
||||
@@ -3877,7 +3940,38 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
log.err("error setting clipboard string err={}", .{err});
|
||||
return true;
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
.copy_url_to_clipboard => {
|
||||
// If the mouse isn't over a link, nothing we can do.
|
||||
if (!self.mouse.over_link) return false;
|
||||
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
if (try self.linkAtPos(pos)) |link_info| {
|
||||
// Get the URL text from selection
|
||||
const url_text = (self.io.terminal.screen.selectionString(self.alloc, .{
|
||||
.sel = link_info[1],
|
||||
.trim = self.config.clipboard_trim_trailing_spaces,
|
||||
})) catch |err| {
|
||||
log.err("error reading url string err={}", .{err});
|
||||
return false;
|
||||
};
|
||||
defer self.alloc.free(url_text);
|
||||
|
||||
self.rt_surface.setClipboardString(url_text, .standard, false) catch |err| {
|
||||
log.err("error copying url to clipboard err={}", .{err});
|
||||
return true;
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
.paste_from_clipboard => try self.startClipboardRequest(
|
||||
@@ -4005,6 +4099,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
{},
|
||||
),
|
||||
|
||||
.close_tab => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.close_tab,
|
||||
{},
|
||||
),
|
||||
|
||||
inline .previous_tab,
|
||||
.next_tab,
|
||||
.last_tab,
|
||||
@@ -4079,6 +4179,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
{},
|
||||
),
|
||||
|
||||
.toggle_maximize => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.toggle_maximize,
|
||||
{},
|
||||
),
|
||||
|
||||
.toggle_fullscreen => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.toggle_fullscreen,
|
||||
@@ -4204,6 +4310,7 @@ fn closingAction(action: input.Binding.Action) bool {
|
||||
return switch (action) {
|
||||
.close_surface,
|
||||
.close_window,
|
||||
.close_tab,
|
||||
=> true,
|
||||
|
||||
else => false,
|
||||
@@ -4230,7 +4337,13 @@ fn writeScreenFile(
|
||||
const filename = try std.fmt.bufPrint(&filename_buf, "{s}.txt", .{@tagName(loc)});
|
||||
|
||||
// Open our scrollback file
|
||||
var file = try tmp_dir.dir.createFile(filename, .{});
|
||||
var file = try tmp_dir.dir.createFile(
|
||||
filename,
|
||||
switch (builtin.os.tag) {
|
||||
.windows => .{},
|
||||
else => .{ .mode = 0o600 },
|
||||
},
|
||||
);
|
||||
defer file.close();
|
||||
|
||||
// Screen.dumpString writes byte-by-byte, so buffer it
|
||||
@@ -4278,11 +4391,16 @@ fn writeScreenFile(
|
||||
tmp_dir.deinit();
|
||||
return;
|
||||
};
|
||||
|
||||
// Use topLeft and bottomRight to ensure correct coordinate ordering
|
||||
const tl = sel.topLeft(&self.io.terminal.screen);
|
||||
const br = sel.bottomRight(&self.io.terminal.screen);
|
||||
|
||||
try self.io.terminal.screen.dumpString(
|
||||
buf_writer.writer(),
|
||||
.{
|
||||
.tl = sel.start(),
|
||||
.br = sel.end(),
|
||||
.tl = tl,
|
||||
.br = br,
|
||||
.unwrap = true,
|
||||
},
|
||||
);
|
||||
@@ -4294,7 +4412,7 @@ fn writeScreenFile(
|
||||
const path = try tmp_dir.dir.realpath(filename, &path_buf);
|
||||
|
||||
switch (write_action) {
|
||||
.open => try internal_os.open(self.alloc, path),
|
||||
.open => try internal_os.open(self.alloc, .text, path),
|
||||
.paste => self.io.queueMessage(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
path,
|
||||
|
@@ -70,6 +70,9 @@ pub const Action = union(Key) {
|
||||
// entry. If the value type is void then only the key needs to be
|
||||
// added. Ensure the order matches exactly with the Zig code.
|
||||
|
||||
/// Quit the application.
|
||||
quit,
|
||||
|
||||
/// Open a new window. The target determines whether properties such
|
||||
/// as font size should be inherited.
|
||||
new_window,
|
||||
@@ -79,6 +82,9 @@ pub const Action = union(Key) {
|
||||
/// the tab should be opened in a new window.
|
||||
new_tab,
|
||||
|
||||
/// Closes the tab belonging to the currently focused split.
|
||||
close_tab,
|
||||
|
||||
/// Create a new split. The value determines the location of the split
|
||||
/// relative to the target.
|
||||
new_split: SplitDirection,
|
||||
@@ -86,6 +92,9 @@ pub const Action = union(Key) {
|
||||
/// Close all open windows.
|
||||
close_all_windows,
|
||||
|
||||
/// Toggle maximized window state.
|
||||
toggle_maximize,
|
||||
|
||||
/// Toggle fullscreen mode.
|
||||
toggle_fullscreen: Fullscreen,
|
||||
|
||||
@@ -219,10 +228,13 @@ pub const Action = union(Key) {
|
||||
|
||||
/// Sync with: ghostty_action_tag_e
|
||||
pub const Key = enum(c_int) {
|
||||
quit,
|
||||
new_window,
|
||||
new_tab,
|
||||
close_tab,
|
||||
new_split,
|
||||
close_all_windows,
|
||||
toggle_maximize,
|
||||
toggle_fullscreen,
|
||||
toggle_tab_overview,
|
||||
toggle_window_decorations,
|
||||
@@ -332,9 +344,9 @@ pub const GotoSplit = enum(c_int) {
|
||||
previous,
|
||||
next,
|
||||
|
||||
top,
|
||||
up,
|
||||
left,
|
||||
bottom,
|
||||
down,
|
||||
right,
|
||||
};
|
||||
|
||||
|
@@ -147,12 +147,12 @@ pub const App = struct {
|
||||
self.core_app.focusEvent(focused);
|
||||
}
|
||||
|
||||
/// See CoreApp.keyEvent.
|
||||
pub fn keyEvent(
|
||||
/// Convert a C key event into a Zig key event.
|
||||
fn coreKeyEvent(
|
||||
self: *App,
|
||||
target: KeyTarget,
|
||||
event: KeyEvent,
|
||||
) !bool {
|
||||
) !?input.KeyEvent {
|
||||
const action = event.action;
|
||||
const keycode = event.keycode;
|
||||
const mods = event.mods;
|
||||
@@ -199,6 +199,11 @@ pub const App = struct {
|
||||
// This logic only applies to macOS.
|
||||
if (comptime builtin.os.tag != .macos) break :event_text event.text;
|
||||
|
||||
// If we're in a preedit state then we allow it through. This
|
||||
// allows ctrl sequences that affect IME to work. For example,
|
||||
// Ctrl+H deletes a character with Japanese input.
|
||||
if (event.composing) break :event_text event.text;
|
||||
|
||||
// If the modifiers are ONLY "control" then we never process
|
||||
// the event text because we want to do our own translation so
|
||||
// we can handle ctrl+c, ctrl+z, etc.
|
||||
@@ -243,7 +248,7 @@ pub const App = struct {
|
||||
result.text,
|
||||
) catch |err| {
|
||||
log.err("error in preedit callback err={}", .{err});
|
||||
return false;
|
||||
return null;
|
||||
},
|
||||
}
|
||||
} else {
|
||||
@@ -251,7 +256,7 @@ pub const App = struct {
|
||||
.app => {},
|
||||
.surface => |surface| surface.core_surface.preeditCallback(null) catch |err| {
|
||||
log.err("error in preedit callback err={}", .{err});
|
||||
return false;
|
||||
return null;
|
||||
},
|
||||
}
|
||||
|
||||
@@ -335,7 +340,7 @@ pub const App = struct {
|
||||
} else .invalid;
|
||||
|
||||
// Build our final key event
|
||||
const input_event: input.KeyEvent = .{
|
||||
return .{
|
||||
.action = action,
|
||||
.key = key,
|
||||
.physical_key = physical_key,
|
||||
@@ -345,24 +350,39 @@ pub const App = struct {
|
||||
.utf8 = result.text,
|
||||
.unshifted_codepoint = unshifted_codepoint,
|
||||
};
|
||||
}
|
||||
|
||||
/// See CoreApp.keyEvent.
|
||||
pub fn keyEvent(
|
||||
self: *App,
|
||||
target: KeyTarget,
|
||||
event: KeyEvent,
|
||||
) !bool {
|
||||
// Convert our C key event into a Zig one.
|
||||
const input_event: input.KeyEvent = (try self.coreKeyEvent(
|
||||
target,
|
||||
event,
|
||||
)) orelse return false;
|
||||
|
||||
// Invoke the core Ghostty logic to handle this input.
|
||||
const effect: CoreSurface.InputEffect = switch (target) {
|
||||
.app => if (self.core_app.keyEvent(
|
||||
self,
|
||||
input_event,
|
||||
))
|
||||
.consumed
|
||||
else
|
||||
.ignored,
|
||||
)) .consumed else .ignored,
|
||||
|
||||
.surface => |surface| try surface.core_surface.keyCallback(input_event),
|
||||
.surface => |surface| try surface.core_surface.keyCallback(
|
||||
input_event,
|
||||
),
|
||||
};
|
||||
|
||||
return switch (effect) {
|
||||
.closed => true,
|
||||
.ignored => false,
|
||||
.consumed => consumed: {
|
||||
const is_down = input_event.action == .press or
|
||||
input_event.action == .repeat;
|
||||
|
||||
if (is_down) {
|
||||
// If we consume the key then we want to reset the dead
|
||||
// key state.
|
||||
@@ -618,7 +638,7 @@ pub const Surface = struct {
|
||||
.y = @floatCast(opts.scale_factor),
|
||||
},
|
||||
.size = .{ .width = 800, .height = 600 },
|
||||
.cursor_pos = .{ .x = 0, .y = 0 },
|
||||
.cursor_pos = .{ .x = -1, .y = -1 },
|
||||
.keymap_state = .{},
|
||||
};
|
||||
|
||||
@@ -1332,10 +1352,9 @@ pub const CAPI = struct {
|
||||
|
||||
/// Tick the event loop. This should be called whenever the "wakeup"
|
||||
/// callback is invoked for the runtime.
|
||||
export fn ghostty_app_tick(v: *App) bool {
|
||||
return v.core_app.tick(v) catch |err| err: {
|
||||
export fn ghostty_app_tick(v: *App) void {
|
||||
v.core_app.tick(v) catch |err| {
|
||||
log.err("error app tick err={}", .{err});
|
||||
break :err false;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1372,6 +1391,28 @@ pub const CAPI = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns true if the given key event would trigger a binding
|
||||
/// if it were sent to the surface right now. The "right now"
|
||||
/// is important because things like trigger sequences are only
|
||||
/// valid until the next key event.
|
||||
export fn ghostty_app_key_is_binding(
|
||||
app: *App,
|
||||
event: KeyEvent,
|
||||
) bool {
|
||||
const core_event = app.coreKeyEvent(
|
||||
.app,
|
||||
event.keyEvent(),
|
||||
) catch |err| {
|
||||
log.warn("error processing key event err={}", .{err});
|
||||
return false;
|
||||
} orelse {
|
||||
log.warn("error processing key event", .{});
|
||||
return false;
|
||||
};
|
||||
|
||||
return app.core_app.keyEventIsBinding(app, core_event);
|
||||
}
|
||||
|
||||
/// Notify the app that the keyboard was changed. This causes the
|
||||
/// keyboard layout to be reloaded from the OS.
|
||||
export fn ghostty_app_keyboard_changed(v: *App) void {
|
||||
@@ -1592,16 +1633,38 @@ pub const CAPI = struct {
|
||||
export fn ghostty_surface_key(
|
||||
surface: *Surface,
|
||||
event: KeyEvent,
|
||||
) void {
|
||||
_ = surface.app.keyEvent(
|
||||
) bool {
|
||||
return surface.app.keyEvent(
|
||||
.{ .surface = surface },
|
||||
event.keyEvent(),
|
||||
) catch |err| {
|
||||
log.warn("error processing key event err={}", .{err});
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns true if the given key event would trigger a binding
|
||||
/// if it were sent to the surface right now. The "right now"
|
||||
/// is important because things like trigger sequences are only
|
||||
/// valid until the next key event.
|
||||
export fn ghostty_surface_key_is_binding(
|
||||
surface: *Surface,
|
||||
event: KeyEvent,
|
||||
) bool {
|
||||
const core_event = surface.app.coreKeyEvent(
|
||||
.{ .surface = surface },
|
||||
event.keyEvent(),
|
||||
) catch |err| {
|
||||
log.warn("error processing key event err={}", .{err});
|
||||
return false;
|
||||
} orelse {
|
||||
log.warn("error processing key event", .{});
|
||||
return false;
|
||||
};
|
||||
|
||||
return surface.core_surface.keyEventIsBinding(core_event);
|
||||
}
|
||||
|
||||
/// Send raw text to the terminal. This is treated like a paste
|
||||
/// so this isn't useful for sending escape sequences. For that,
|
||||
/// individual key input should be used.
|
||||
@@ -1891,14 +1954,11 @@ pub const CAPI = struct {
|
||||
// Do nothing if we don't have background transparency enabled
|
||||
if (config.@"background-opacity" >= 1.0) return;
|
||||
|
||||
// Do nothing if our blur value is zero
|
||||
if (config.@"background-blur-radius" == 0) return;
|
||||
|
||||
const nswindow = objc.Object.fromId(window);
|
||||
_ = CGSSetWindowBackgroundBlurRadius(
|
||||
CGSDefaultConnectionForThread(),
|
||||
nswindow.msgSend(usize, objc.sel("windowNumber"), .{}),
|
||||
@intCast(config.@"background-blur-radius"),
|
||||
@intCast(config.@"background-blur".cval()),
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -35,6 +35,10 @@ pub const App = struct {
|
||||
app: *CoreApp,
|
||||
config: Config,
|
||||
|
||||
/// Flips to true to quit on the next event loop tick. This
|
||||
/// never goes false and forces the event loop to exit.
|
||||
quit: bool = false,
|
||||
|
||||
/// Mac-specific state.
|
||||
darwin: if (Darwin.enabled) Darwin else void,
|
||||
|
||||
@@ -124,8 +128,10 @@ pub const App = struct {
|
||||
glfw.waitEvents();
|
||||
|
||||
// Tick the terminal app
|
||||
const should_quit = try self.app.tick(self);
|
||||
if (should_quit or self.app.surfaces.items.len == 0) {
|
||||
try self.app.tick(self);
|
||||
|
||||
// If the tick caused us to quit, then we're done.
|
||||
if (self.quit or self.app.surfaces.items.len == 0) {
|
||||
for (self.app.surfaces.items) |surface| {
|
||||
surface.close(false);
|
||||
}
|
||||
@@ -149,6 +155,8 @@ pub const App = struct {
|
||||
value: apprt.Action.Value(action),
|
||||
) !void {
|
||||
switch (action) {
|
||||
.quit => self.quit = true,
|
||||
|
||||
.new_window => _ = try self.newSurface(switch (target) {
|
||||
.app => null,
|
||||
.surface => |v| v,
|
||||
@@ -210,6 +218,7 @@ pub const App = struct {
|
||||
.toggle_split_zoom,
|
||||
.present_terminal,
|
||||
.close_all_windows,
|
||||
.close_tab,
|
||||
.toggle_tab_overview,
|
||||
.toggle_window_decorations,
|
||||
.toggle_quick_terminal,
|
||||
@@ -228,6 +237,7 @@ pub const App = struct {
|
||||
.color_change,
|
||||
.pwd,
|
||||
.config_change,
|
||||
.toggle_maximize,
|
||||
=> log.info("unimplemented action={}", .{action}),
|
||||
}
|
||||
}
|
||||
@@ -510,6 +520,13 @@ pub const Surface = struct {
|
||||
) orelse return glfw.mustGetErrorCode();
|
||||
errdefer win.destroy();
|
||||
|
||||
// Setup our
|
||||
setInitialWindowPosition(
|
||||
win,
|
||||
app.config.@"window-position-x",
|
||||
app.config.@"window-position-y",
|
||||
);
|
||||
|
||||
// Get our physical DPI - debug only because we don't have a use for
|
||||
// this but the logging of it may be useful
|
||||
if (builtin.mode == .Debug) {
|
||||
@@ -663,6 +680,17 @@ pub const Surface = struct {
|
||||
});
|
||||
}
|
||||
|
||||
/// Set the initial window position. This is called exactly once at
|
||||
/// surface initialization time. This may be called before "self"
|
||||
/// is fully initialized.
|
||||
fn setInitialWindowPosition(win: glfw.Window, x: ?i16, y: ?i16) void {
|
||||
const start_position_x = x orelse return;
|
||||
const start_position_y = y orelse return;
|
||||
|
||||
log.debug("setting initial window position ({},{})", .{ start_position_x, start_position_y });
|
||||
win.setPos(.{ .x = start_position_x, .y = start_position_y });
|
||||
}
|
||||
|
||||
/// Set the size limits of the window.
|
||||
/// Note: this interface is not good, we should redo it if we plan
|
||||
/// to use this more. i.e. you can't set max width but no max height,
|
||||
|
@@ -15,6 +15,7 @@ const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const builtin = @import("builtin");
|
||||
const build_config = @import("../../build_config.zig");
|
||||
const build_options = @import("build_options");
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const configpkg = @import("../../config.zig");
|
||||
const input = @import("../../input.zig");
|
||||
@@ -35,7 +36,7 @@ const c = @import("c.zig").c;
|
||||
const version = @import("version.zig");
|
||||
const inspector = @import("inspector.zig");
|
||||
const key = @import("key.zig");
|
||||
const x11 = @import("x11.zig");
|
||||
const winproto = @import("winproto.zig");
|
||||
const testing = std.testing;
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
@@ -48,6 +49,9 @@ config: Config,
|
||||
app: *c.GtkApplication,
|
||||
ctx: *c.GMainContext,
|
||||
|
||||
/// State and logic for the underlying windowing protocol.
|
||||
winproto: winproto.App,
|
||||
|
||||
/// True if the app was launched with single instance mode.
|
||||
single_instance: bool,
|
||||
|
||||
@@ -69,8 +73,10 @@ clipboard_confirmation_window: ?*ClipboardConfirmationWindow = null,
|
||||
/// This is set to false when the main loop should exit.
|
||||
running: bool = true,
|
||||
|
||||
/// Xkb state (X11 only). Will be null on Wayland.
|
||||
x11_xkb: ?x11.Xkb = null,
|
||||
/// If we should retry querying D-Bus for the color scheme with the deprecated
|
||||
/// Read method, instead of the recommended ReadOne method. This is kind of
|
||||
/// nasty to have as struct state but its just a byte...
|
||||
dbus_color_scheme_retry: bool = true,
|
||||
|
||||
/// The base path of the transient cgroup used to put all surfaces
|
||||
/// into their own cgroup. This is only set if cgroups are enabled
|
||||
@@ -80,6 +86,9 @@ transient_cgroup_base: ?[]const u8 = null,
|
||||
/// CSS Provider for any styles based on ghostty configuration values
|
||||
css_provider: *c.GtkCssProvider,
|
||||
|
||||
/// Providers for loading custom stylesheets defined by user
|
||||
custom_css_providers: std.ArrayListUnmanaged(*c.GtkCssProvider) = .{},
|
||||
|
||||
/// The timer used to quit the application after the last window is closed.
|
||||
quit_timer: union(enum) {
|
||||
off: void,
|
||||
@@ -100,42 +109,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
c.gtk_get_micro_version(),
|
||||
});
|
||||
|
||||
// Disabling Vulkan can improve startup times by hundreds of
|
||||
// milliseconds on some systems. We don't use Vulkan so we can just
|
||||
// disable it.
|
||||
if (version.atLeast(4, 16, 0)) {
|
||||
// From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE.
|
||||
// For the remainder of "why" see the 4.14 comment below.
|
||||
_ = internal_os.setenv("GDK_DISABLE", "gles-api,vulkan");
|
||||
_ = internal_os.setenv("GDK_DEBUG", "opengl");
|
||||
} else if (version.atLeast(4, 14, 0)) {
|
||||
// We need to export GDK_DEBUG to run on Wayland after GTK 4.14.
|
||||
// Older versions of GTK do not support these values so it is safe
|
||||
// to always set this. Forwards versions are uncertain so we'll have to
|
||||
// reassess...
|
||||
//
|
||||
// Upstream issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6589
|
||||
//
|
||||
// Specific details about values:
|
||||
// - "opengl" - output OpenGL debug information
|
||||
// - "gl-disable-gles" - disable GLES, Ghostty can't use GLES
|
||||
// - "vulkan-disable" - disable Vulkan, Ghostty can't use Vulkan
|
||||
// and initializing a Vulkan context was causing a longer delay
|
||||
// on some systems.
|
||||
_ = internal_os.setenv("GDK_DEBUG", "opengl,gl-disable-gles,vulkan-disable");
|
||||
} else {
|
||||
// Versions prior to 4.14 are a bit of an unknown for Ghostty. It
|
||||
// is an environment that isn't tested well and we don't have a
|
||||
// good understanding of what we may need to do.
|
||||
_ = internal_os.setenv("GDK_DEBUG", "vulkan-disable");
|
||||
}
|
||||
|
||||
if (version.atLeast(4, 14, 0)) {
|
||||
// We need to export GSK_RENDERER to opengl because GTK uses ngl by
|
||||
// default after 4.14
|
||||
_ = internal_os.setenv("GSK_RENDERER", "opengl");
|
||||
}
|
||||
|
||||
// Load our configuration
|
||||
var config = try Config.load(core_app.alloc);
|
||||
errdefer config.deinit();
|
||||
@@ -157,8 +130,111 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
}
|
||||
}
|
||||
|
||||
var gdk_debug: struct {
|
||||
/// output OpenGL debug information
|
||||
opengl: bool = false,
|
||||
/// disable GLES, Ghostty can't use GLES
|
||||
@"gl-disable-gles": bool = false,
|
||||
@"gl-no-fractional": bool = false,
|
||||
/// Disabling Vulkan can improve startup times by hundreds of
|
||||
/// milliseconds on some systems. We don't use Vulkan so we can just
|
||||
/// disable it.
|
||||
@"vulkan-disable": bool = false,
|
||||
} = .{
|
||||
.opengl = config.@"gtk-opengl-debug",
|
||||
};
|
||||
|
||||
var gdk_disable: struct {
|
||||
@"gles-api": bool = false,
|
||||
/// Disabling Vulkan can improve startup times by hundreds of
|
||||
/// milliseconds on some systems. We don't use Vulkan so we can just
|
||||
/// disable it.
|
||||
vulkan: bool = false,
|
||||
} = .{};
|
||||
|
||||
environment: {
|
||||
if (version.runtimeAtLeast(4, 16, 0)) {
|
||||
// From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE.
|
||||
// For the remainder of "why" see the 4.14 comment below.
|
||||
gdk_disable.@"gles-api" = true;
|
||||
gdk_disable.vulkan = true;
|
||||
gdk_debug.@"gl-no-fractional" = true;
|
||||
break :environment;
|
||||
}
|
||||
if (version.runtimeAtLeast(4, 14, 0)) {
|
||||
// We need to export GDK_DEBUG to run on Wayland after GTK 4.14.
|
||||
// Older versions of GTK do not support these values so it is safe
|
||||
// to always set this. Forwards versions are uncertain so we'll have
|
||||
// to reassess...
|
||||
//
|
||||
// Upstream issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6589
|
||||
gdk_debug.@"gl-disable-gles" = true;
|
||||
gdk_debug.@"gl-no-fractional" = true;
|
||||
gdk_debug.@"vulkan-disable" = true;
|
||||
break :environment;
|
||||
}
|
||||
// Versions prior to 4.14 are a bit of an unknown for Ghostty. It
|
||||
// is an environment that isn't tested well and we don't have a
|
||||
// good understanding of what we may need to do.
|
||||
gdk_debug.@"vulkan-disable" = true;
|
||||
}
|
||||
|
||||
{
|
||||
var buf: [128]u8 = undefined;
|
||||
var fmt = std.io.fixedBufferStream(&buf);
|
||||
const writer = fmt.writer();
|
||||
var first: bool = true;
|
||||
inline for (@typeInfo(@TypeOf(gdk_debug)).Struct.fields) |field| {
|
||||
if (@field(gdk_debug, field.name)) {
|
||||
if (!first) try writer.writeAll(",");
|
||||
try writer.writeAll(field.name);
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
try writer.writeByte(0);
|
||||
const value = fmt.getWritten();
|
||||
log.warn("setting GDK_DEBUG={s}", .{value[0 .. value.len - 1]});
|
||||
_ = internal_os.setenv("GDK_DEBUG", value[0 .. value.len - 1 :0]);
|
||||
}
|
||||
|
||||
{
|
||||
var buf: [128]u8 = undefined;
|
||||
var fmt = std.io.fixedBufferStream(&buf);
|
||||
const writer = fmt.writer();
|
||||
var first: bool = true;
|
||||
inline for (@typeInfo(@TypeOf(gdk_disable)).Struct.fields) |field| {
|
||||
if (@field(gdk_disable, field.name)) {
|
||||
if (!first) try writer.writeAll(",");
|
||||
try writer.writeAll(field.name);
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
try writer.writeByte(0);
|
||||
const value = fmt.getWritten();
|
||||
log.warn("setting GDK_DISABLE={s}", .{value[0 .. value.len - 1]});
|
||||
_ = internal_os.setenv("GDK_DISABLE", value[0 .. value.len - 1 :0]);
|
||||
}
|
||||
|
||||
if (version.runtimeAtLeast(4, 14, 0)) {
|
||||
switch (config.@"gtk-gsk-renderer") {
|
||||
.default => {},
|
||||
else => |renderer| {
|
||||
// Force the GSK renderer to a specific value. After GTK 4.14 the
|
||||
// `ngl` renderer is used by default which causes artifacts when
|
||||
// used with Ghostty so it should be avoided.
|
||||
log.warn("setting GSK_RENDERER={s}", .{@tagName(renderer)});
|
||||
_ = internal_os.setenv("GSK_RENDERER", @tagName(renderer));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
c.gtk_init();
|
||||
const display = c.gdk_display_get_default();
|
||||
const display: *c.GdkDisplay = c.gdk_display_get_default() orelse {
|
||||
// I'm unsure of any scenario where this happens. Because we don't
|
||||
// want to litter null checks everywhere, we just exit here.
|
||||
log.warn("gdk display is null, exiting", .{});
|
||||
std.posix.exit(1);
|
||||
};
|
||||
|
||||
// If we're using libadwaita, log the version
|
||||
if (adwaita.enabled(&config)) {
|
||||
@@ -356,41 +432,15 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
return error.GtkApplicationRegisterFailed;
|
||||
}
|
||||
|
||||
// Perform all X11 initialization. This ultimately returns the X11
|
||||
// keyboard state but the block does more than that (i.e. setting up
|
||||
// WM_CLASS).
|
||||
const x11_xkb: ?x11.Xkb = x11_xkb: {
|
||||
if (!x11.is_display(display)) break :x11_xkb null;
|
||||
|
||||
// Set the X11 window class property (WM_CLASS) if are are on an X11
|
||||
// display.
|
||||
//
|
||||
// Note that we also set the program name here using g_set_prgname.
|
||||
// This is how the instance name field for WM_CLASS is derived when
|
||||
// calling gdk_x11_display_set_program_class; there does not seem to be
|
||||
// a way to set it directly. It does not look like this is being set by
|
||||
// our other app initialization routines currently, but since we're
|
||||
// currently deriving its value from x11-instance-name effectively, I
|
||||
// feel like gating it behind an X11 check is better intent.
|
||||
//
|
||||
// This makes the property show up like so when using xprop:
|
||||
//
|
||||
// WM_CLASS(STRING) = "ghostty", "com.mitchellh.ghostty"
|
||||
//
|
||||
// Append "-debug" on both when using the debug build.
|
||||
//
|
||||
const prgname = if (config.@"x11-instance-name") |pn|
|
||||
pn
|
||||
else if (builtin.mode == .Debug)
|
||||
"ghostty-debug"
|
||||
else
|
||||
"ghostty";
|
||||
c.g_set_prgname(prgname);
|
||||
c.gdk_x11_display_set_program_class(display, app_id);
|
||||
|
||||
// Set up Xkb
|
||||
break :x11_xkb try x11.Xkb.init(display);
|
||||
};
|
||||
// Setup our windowing protocol logic
|
||||
var winproto_app = try winproto.App.init(
|
||||
core_app.alloc,
|
||||
display,
|
||||
app_id,
|
||||
&config,
|
||||
);
|
||||
errdefer winproto_app.deinit(core_app.alloc);
|
||||
log.debug("windowing protocol={s}", .{@tagName(winproto_app)});
|
||||
|
||||
// This just calls the `activate` signal but its part of the normal startup
|
||||
// routine so we just call it, but only if the config allows it (this allows
|
||||
@@ -416,7 +466,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
.config = config,
|
||||
.ctx = ctx,
|
||||
.cursor_none = cursor_none,
|
||||
.x11_xkb = x11_xkb,
|
||||
.winproto = winproto_app,
|
||||
.single_instance = single_instance,
|
||||
// If we are NOT the primary instance, then we never want to run.
|
||||
// This means that another instance of the GTK app is running and
|
||||
@@ -439,6 +489,13 @@ pub fn terminate(self: *App) void {
|
||||
if (self.context_menu) |context_menu| c.g_object_unref(context_menu);
|
||||
if (self.transient_cgroup_base) |path| self.core_app.alloc.free(path);
|
||||
|
||||
for (self.custom_css_providers.items) |provider| {
|
||||
c.g_object_unref(provider);
|
||||
}
|
||||
self.custom_css_providers.deinit(self.core_app.alloc);
|
||||
|
||||
self.winproto.deinit(self.core_app.alloc);
|
||||
|
||||
self.config.deinit();
|
||||
}
|
||||
|
||||
@@ -450,13 +507,16 @@ pub fn performAction(
|
||||
value: apprt.Action.Value(action),
|
||||
) !void {
|
||||
switch (action) {
|
||||
.quit => self.quit(),
|
||||
.new_window => _ = try self.newWindow(switch (target) {
|
||||
.app => null,
|
||||
.surface => |v| v,
|
||||
}),
|
||||
.toggle_maximize => self.toggleMaximize(target),
|
||||
.toggle_fullscreen => self.toggleFullscreen(target, value),
|
||||
|
||||
.new_tab => try self.newTab(target),
|
||||
.close_tab => try self.closeTab(target),
|
||||
.goto_tab => self.gotoTab(target, value),
|
||||
.move_tab => self.moveTab(target, value),
|
||||
.new_split => try self.newSplit(target, value),
|
||||
@@ -472,6 +532,7 @@ pub fn performAction(
|
||||
.pwd => try self.setPwd(target, value),
|
||||
.present_terminal => self.presentTerminal(target),
|
||||
.initial_size => try self.setInitialSize(target, value),
|
||||
.size_limit => try self.setSizeLimit(target, value),
|
||||
.mouse_visibility => self.setMouseVisibility(target, value),
|
||||
.mouse_shape => try self.setMouseShape(target, value),
|
||||
.mouse_over_link => self.setMouseOverLink(target, value),
|
||||
@@ -484,7 +545,6 @@ pub fn performAction(
|
||||
.close_all_windows,
|
||||
.toggle_quick_terminal,
|
||||
.toggle_visibility,
|
||||
.size_limit,
|
||||
.cell_size,
|
||||
.secure_input,
|
||||
.key_sequence,
|
||||
@@ -512,6 +572,23 @@ fn newTab(_: *App, target: apprt.Target) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn closeTab(_: *App, target: apprt.Target) !void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const tab = v.rt_surface.container.tab() orelse {
|
||||
log.info(
|
||||
"close_tab invalid for container={s}",
|
||||
.{@tagName(v.rt_surface.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
tab.closeWithConfirmation();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn gotoTab(_: *App, target: apprt.Target, tab: apprt.action.GotoTab) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
@@ -638,6 +715,22 @@ fn controlInspector(
|
||||
surface.controlInspector(mode);
|
||||
}
|
||||
|
||||
fn toggleMaximize(_: *App, target: apprt.Target) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const window = v.rt_surface.container.window() orelse {
|
||||
log.info(
|
||||
"toggleMaximize invalid for container={s}",
|
||||
.{@tagName(v.rt_surface.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
window.toggleMaximize();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn toggleFullscreen(
|
||||
_: *App,
|
||||
target: apprt.Target,
|
||||
@@ -784,6 +877,24 @@ fn setInitialSize(
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn setSizeLimit(
|
||||
_: *App,
|
||||
target: apprt.Target,
|
||||
value: apprt.action.SizeLimit,
|
||||
) !void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| try v.rt_surface.setSizeLimits(.{
|
||||
.width = value.min_width,
|
||||
.height = value.min_height,
|
||||
}, if (value.max_width > 0) .{
|
||||
.width = value.max_width,
|
||||
.height = value.max_height,
|
||||
} else null),
|
||||
}
|
||||
}
|
||||
|
||||
fn showDesktopNotification(
|
||||
self: *App,
|
||||
target: apprt.Target,
|
||||
@@ -826,9 +937,12 @@ fn configChange(
|
||||
new_config: *const Config,
|
||||
) void {
|
||||
switch (target) {
|
||||
// We don't do anything for surface config change events. There
|
||||
// is nothing to sync with regards to a surface today.
|
||||
.surface => {},
|
||||
.surface => |surface| surface: {
|
||||
const window = surface.rt_surface.container.window() orelse break :surface;
|
||||
window.updateConfig(new_config) catch |err| {
|
||||
log.warn("error updating config for window err={}", .{err});
|
||||
};
|
||||
},
|
||||
|
||||
.app => {
|
||||
// We clone (to take ownership) and update our configuration.
|
||||
@@ -890,7 +1004,7 @@ fn syncConfigChanges(self: *App) !void {
|
||||
try self.updateConfigErrors();
|
||||
try self.syncActionAccelerators();
|
||||
|
||||
// Load our runtime CSS. If this fails then our window is just stuck
|
||||
// Load our runtime and custom CSS. If this fails then our window is just stuck
|
||||
// with the old CSS but we don't want to fail the entire sync operation.
|
||||
self.loadRuntimeCss() catch |err| switch (err) {
|
||||
error.OutOfMemory => log.warn(
|
||||
@@ -898,6 +1012,9 @@ fn syncConfigChanges(self: *App) !void {
|
||||
.{},
|
||||
),
|
||||
};
|
||||
self.loadCustomCss() catch |err| {
|
||||
log.warn("Failed to load custom CSS, no custom CSS applied, err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// This should be called whenever the configuration changes to update
|
||||
@@ -966,8 +1083,8 @@ fn loadRuntimeCss(
|
||||
const config: *const Config = &self.config;
|
||||
const window_theme = config.@"window-theme";
|
||||
const unfocused_fill: Config.Color = config.@"unfocused-split-fill" orelse config.background;
|
||||
const headerbar_background = config.background;
|
||||
const headerbar_foreground = config.foreground;
|
||||
const headerbar_background = config.@"window-titlebar-background" orelse config.background;
|
||||
const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground;
|
||||
|
||||
try writer.print(
|
||||
\\widget.unfocused-split {{
|
||||
@@ -981,13 +1098,42 @@ fn loadRuntimeCss(
|
||||
unfocused_fill.b,
|
||||
});
|
||||
|
||||
if (version.atLeast(4, 16, 0)) {
|
||||
if (config.@"split-divider-color") |color| {
|
||||
try writer.print(
|
||||
\\.terminal-window .notebook separator {{
|
||||
\\ color: rgb({[r]d},{[g]d},{[b]d});
|
||||
\\ background: rgb({[r]d},{[g]d},{[b]d});
|
||||
\\}}
|
||||
, .{
|
||||
.r = color.r,
|
||||
.g = color.g,
|
||||
.b = color.b,
|
||||
});
|
||||
}
|
||||
|
||||
if (config.@"window-title-font-family") |font_family| {
|
||||
try writer.print(
|
||||
\\.window headerbar {{
|
||||
\\ font-family: "{[font_family]s}";
|
||||
\\}}
|
||||
, .{ .font_family = font_family });
|
||||
}
|
||||
|
||||
if (version.runtimeAtLeast(4, 16, 0)) {
|
||||
switch (window_theme) {
|
||||
.ghostty => try writer.print(
|
||||
\\:root {{
|
||||
\\ --headerbar-fg-color: rgb({d},{d},{d});
|
||||
\\ --headerbar-bg-color: rgb({d},{d},{d});
|
||||
\\ --ghostty-fg: rgb({d},{d},{d});
|
||||
\\ --ghostty-bg: rgb({d},{d},{d});
|
||||
\\ --headerbar-fg-color: var(--ghostty-fg);
|
||||
\\ --headerbar-bg-color: var(--ghostty-bg);
|
||||
\\ --headerbar-backdrop-color: oklab(from var(--headerbar-bg-color) calc(l * 0.9) a b / alpha);
|
||||
\\ --overview-fg-color: var(--ghostty-fg);
|
||||
\\ --overview-bg-color: var(--ghostty-bg);
|
||||
\\ --popover-fg-color: var(--ghostty-fg);
|
||||
\\ --popover-bg-color: var(--ghostty-bg);
|
||||
\\ --window-fg-color: var(--ghostty-fg);
|
||||
\\ --window-bg-color: var(--ghostty-bg);
|
||||
\\}}
|
||||
\\windowhandle {{
|
||||
\\ background-color: var(--headerbar-bg-color);
|
||||
@@ -1025,11 +1171,66 @@ fn loadRuntimeCss(
|
||||
}
|
||||
|
||||
// Clears any previously loaded CSS from this provider
|
||||
c.gtk_css_provider_load_from_data(
|
||||
self.css_provider,
|
||||
buf.items.ptr,
|
||||
@intCast(buf.items.len),
|
||||
);
|
||||
loadCssProviderFromData(self.css_provider, buf.items);
|
||||
}
|
||||
|
||||
fn loadCustomCss(self: *App) !void {
|
||||
const display = c.gdk_display_get_default();
|
||||
|
||||
// unload the previously loaded style providers
|
||||
for (self.custom_css_providers.items) |provider| {
|
||||
c.gtk_style_context_remove_provider_for_display(
|
||||
display,
|
||||
@ptrCast(provider),
|
||||
);
|
||||
c.g_object_unref(provider);
|
||||
}
|
||||
self.custom_css_providers.clearRetainingCapacity();
|
||||
|
||||
for (self.config.@"gtk-custom-css".value.items) |p| {
|
||||
const path, const optional = switch (p) {
|
||||
.optional => |path| .{ path, true },
|
||||
.required => |path| .{ path, false },
|
||||
};
|
||||
const file = std.fs.openFileAbsolute(path, .{}) catch |err| {
|
||||
if (err != error.FileNotFound or !optional) {
|
||||
log.err("error opening gtk-custom-css file {s}: {}", .{ path, err });
|
||||
}
|
||||
continue;
|
||||
};
|
||||
defer file.close();
|
||||
|
||||
log.info("loading gtk-custom-css path={s}", .{path});
|
||||
const contents = try file.reader().readAllAlloc(self.core_app.alloc, 5 * 1024 * 1024 // 5MB
|
||||
);
|
||||
defer self.core_app.alloc.free(contents);
|
||||
|
||||
const provider = c.gtk_css_provider_new();
|
||||
c.gtk_style_context_add_provider_for_display(
|
||||
display,
|
||||
@ptrCast(provider),
|
||||
c.GTK_STYLE_PROVIDER_PRIORITY_USER,
|
||||
);
|
||||
|
||||
loadCssProviderFromData(provider, contents);
|
||||
|
||||
try self.custom_css_providers.append(self.core_app.alloc, provider);
|
||||
}
|
||||
}
|
||||
|
||||
fn loadCssProviderFromData(provider: *c.GtkCssProvider, data: []const u8) void {
|
||||
if (version.atLeast(4, 12, 0)) {
|
||||
const g_bytes = c.g_bytes_new(data.ptr, data.len);
|
||||
defer c.g_bytes_unref(g_bytes);
|
||||
|
||||
c.gtk_css_provider_load_from_bytes(provider, g_bytes);
|
||||
} else {
|
||||
c.gtk_css_provider_load_from_data(
|
||||
provider,
|
||||
data.ptr,
|
||||
@intCast(data.len),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Called by CoreApp to wake up the event loop.
|
||||
@@ -1075,7 +1276,8 @@ pub fn run(self: *App) !void {
|
||||
self.transient_cgroup_base = path;
|
||||
} else log.debug("cgroup isolation disabled config={}", .{self.config.@"linux-cgroup"});
|
||||
|
||||
// Setup our D-Bus connection for listening to settings changes.
|
||||
// Setup our D-Bus connection for listening to settings changes,
|
||||
// and asynchronously request the initial color scheme
|
||||
self.initDbus();
|
||||
|
||||
// Setup our menu items
|
||||
@@ -1083,9 +1285,6 @@ pub fn run(self: *App) !void {
|
||||
self.initMenu();
|
||||
self.initContextMenu();
|
||||
|
||||
// Setup our initial color scheme
|
||||
self.colorSchemeEvent(self.getColorScheme());
|
||||
|
||||
// On startup, we want to check for configuration errors right away
|
||||
// so we can show our error window. We also need to setup other initial
|
||||
// state.
|
||||
@@ -1097,14 +1296,10 @@ pub fn run(self: *App) !void {
|
||||
_ = c.g_main_context_iteration(self.ctx, 1);
|
||||
|
||||
// Tick the terminal app and see if we should quit.
|
||||
const should_quit = try self.core_app.tick(self);
|
||||
try self.core_app.tick(self);
|
||||
|
||||
// Check if we must quit based on the current state.
|
||||
const must_quit = q: {
|
||||
// If we've been told by GTK that we should quit, do so regardless
|
||||
// of any other setting.
|
||||
if (should_quit) break :q true;
|
||||
|
||||
// If we are configured to always stay running, don't quit.
|
||||
if (!self.config.@"quit-after-last-window-closed") break :q false;
|
||||
|
||||
@@ -1137,6 +1332,22 @@ fn initDbus(self: *App) void {
|
||||
self,
|
||||
null,
|
||||
);
|
||||
|
||||
// Request the initial color scheme asynchronously.
|
||||
c.g_dbus_connection_call(
|
||||
dbus,
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.Settings",
|
||||
"ReadOne",
|
||||
c.g_variant_new("(ss)", "org.freedesktop.appearance", "color-scheme"),
|
||||
c.G_VARIANT_TYPE("(v)"),
|
||||
c.G_DBUS_CALL_FLAGS_NONE,
|
||||
-1,
|
||||
null,
|
||||
dbusColorSchemeCallback,
|
||||
self,
|
||||
);
|
||||
}
|
||||
|
||||
// This timeout function is started when no surfaces are open. It can be
|
||||
@@ -1208,6 +1419,9 @@ fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
|
||||
}
|
||||
|
||||
fn quit(self: *App) void {
|
||||
// If we're already not running, do nothing.
|
||||
if (!self.running) return;
|
||||
|
||||
// If we have no toplevel windows, then we're done.
|
||||
const list = c.gtk_window_list_toplevels();
|
||||
if (list == null) {
|
||||
@@ -1371,42 +1585,58 @@ fn gtkWindowIsActive(
|
||||
core_app.focusEvent(false);
|
||||
}
|
||||
|
||||
/// Call a D-Bus method to determine the current color scheme. If there
|
||||
/// is any error at any point we'll log the error and return "light"
|
||||
pub fn getColorScheme(self: *App) apprt.ColorScheme {
|
||||
const dbus_connection = c.g_application_get_dbus_connection(@ptrCast(self.app));
|
||||
fn dbusColorSchemeCallback(
|
||||
source_object: [*c]c.GObject,
|
||||
res: ?*c.GAsyncResult,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *App = @ptrCast(@alignCast(ud.?));
|
||||
const dbus: *c.GDBusConnection = @ptrCast(source_object);
|
||||
|
||||
var err: ?*c.GError = null;
|
||||
defer if (err) |e| c.g_error_free(e);
|
||||
|
||||
const value = c.g_dbus_connection_call_sync(
|
||||
dbus_connection,
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.Settings",
|
||||
"ReadOne",
|
||||
c.g_variant_new("(ss)", "org.freedesktop.appearance", "color-scheme"),
|
||||
c.G_VARIANT_TYPE("(v)"),
|
||||
c.G_DBUS_CALL_FLAGS_NONE,
|
||||
-1,
|
||||
null,
|
||||
&err,
|
||||
) orelse {
|
||||
if (err) |e| log.err("unable to get current color scheme: {s}", .{e.message});
|
||||
return .light;
|
||||
};
|
||||
defer c.g_variant_unref(value);
|
||||
|
||||
if (c.g_variant_is_of_type(value, c.G_VARIANT_TYPE("(v)")) == 1) {
|
||||
var inner: ?*c.GVariant = null;
|
||||
c.g_variant_get(value, "(v)", &inner);
|
||||
defer c.g_variant_unref(inner);
|
||||
if (c.g_variant_is_of_type(inner, c.G_VARIANT_TYPE("u")) == 1) {
|
||||
return if (c.g_variant_get_uint32(inner) == 1) .dark else .light;
|
||||
if (c.g_dbus_connection_call_finish(dbus, res, &err)) |value| {
|
||||
if (c.g_variant_is_of_type(value, c.G_VARIANT_TYPE("(v)")) == 1) {
|
||||
var inner: ?*c.GVariant = null;
|
||||
c.g_variant_get(value, "(v)", &inner);
|
||||
defer c.g_variant_unref(inner);
|
||||
if (c.g_variant_is_of_type(inner, c.G_VARIANT_TYPE("u")) == 1) {
|
||||
self.colorSchemeEvent(if (c.g_variant_get_uint32(inner) == 1)
|
||||
.dark
|
||||
else
|
||||
.light);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (err) |e| {
|
||||
// If ReadOne is not yet implemented, fall back to deprecated "Read" method
|
||||
// Error code: GDBus.Error:org.freedesktop.DBus.Error.UnknownMethod: No such method “ReadOne”
|
||||
if (self.dbus_color_scheme_retry and e.code == 19) {
|
||||
self.dbus_color_scheme_retry = false;
|
||||
c.g_dbus_connection_call(
|
||||
dbus,
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.Settings",
|
||||
"Read",
|
||||
c.g_variant_new("(ss)", "org.freedesktop.appearance", "color-scheme"),
|
||||
c.G_VARIANT_TYPE("(v)"),
|
||||
c.G_DBUS_CALL_FLAGS_NONE,
|
||||
-1,
|
||||
null,
|
||||
dbusColorSchemeCallback,
|
||||
self,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, log the error and return .light
|
||||
log.warn("unable to get current color scheme: {s}", .{e.message});
|
||||
}
|
||||
|
||||
return .light;
|
||||
// Fall back
|
||||
self.colorSchemeEvent(.light);
|
||||
}
|
||||
|
||||
/// This will be called by D-Bus when the style changes between light & dark.
|
||||
@@ -1497,7 +1727,9 @@ fn gtkActionQuit(
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *App = @ptrCast(@alignCast(ud orelse return));
|
||||
self.core_app.setQuit();
|
||||
self.core_app.performAction(self, .quit) catch |err| {
|
||||
log.err("error quitting err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// Action sent by the window manager asking us to present a specific surface to
|
||||
@@ -1516,7 +1748,7 @@ fn gtkActionPresentSurface(
|
||||
|
||||
// Convert that u64 to pointer to a core surface. A value of zero
|
||||
// means that there was no target surface for the notification so
|
||||
// we dont' focus any surface.
|
||||
// we don't focus any surface.
|
||||
const ptr_int: u64 = c.g_variant_get_uint64(parameter);
|
||||
if (ptr_int == 0) return;
|
||||
const surface: *CoreSurface = @ptrFromInt(ptr_int);
|
||||
@@ -1569,18 +1801,17 @@ fn initActions(self: *App) void {
|
||||
}
|
||||
}
|
||||
|
||||
/// This sets the self.menu property to the application menu that can be
|
||||
/// shared by all application windows.
|
||||
fn initMenu(self: *App) void {
|
||||
const menu = c.g_menu_new();
|
||||
errdefer c.g_object_unref(menu);
|
||||
|
||||
/// Initializes and populates the provided GMenu with sections and actions.
|
||||
/// This function is used to set up the application's menu structure, either for
|
||||
/// the main menu button or as a context menu when window decorations are disabled.
|
||||
fn initMenuContent(menu: *c.GMenu) void {
|
||||
{
|
||||
const section = c.g_menu_new();
|
||||
defer c.g_object_unref(section);
|
||||
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
||||
c.g_menu_append(section, "New Window", "win.new_window");
|
||||
c.g_menu_append(section, "New Tab", "win.new_tab");
|
||||
c.g_menu_append(section, "Close Tab", "win.close_tab");
|
||||
c.g_menu_append(section, "Split Right", "win.split_right");
|
||||
c.g_menu_append(section, "Split Down", "win.split_down");
|
||||
c.g_menu_append(section, "Close Window", "win.close");
|
||||
@@ -1595,13 +1826,14 @@ fn initMenu(self: *App) void {
|
||||
c.g_menu_append(section, "Reload Configuration", "app.reload-config");
|
||||
c.g_menu_append(section, "About Ghostty", "win.about");
|
||||
}
|
||||
}
|
||||
|
||||
// {
|
||||
// const section = c.g_menu_new();
|
||||
// defer c.g_object_unref(section);
|
||||
// c.g_menu_append_submenu(menu, "File", @ptrCast(@alignCast(section)));
|
||||
// }
|
||||
|
||||
/// This sets the self.menu property to the application menu that can be
|
||||
/// shared by all application windows.
|
||||
fn initMenu(self: *App) void {
|
||||
const menu = c.g_menu_new();
|
||||
errdefer c.g_object_unref(menu);
|
||||
initMenuContent(@ptrCast(menu));
|
||||
self.menu = menu;
|
||||
}
|
||||
|
||||
@@ -1609,7 +1841,13 @@ fn initContextMenu(self: *App) void {
|
||||
const menu = c.g_menu_new();
|
||||
errdefer c.g_object_unref(menu);
|
||||
|
||||
createContextMenuCopyPasteSection(menu, false);
|
||||
{
|
||||
const section = c.g_menu_new();
|
||||
defer c.g_object_unref(section);
|
||||
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
||||
c.g_menu_append(section, "Copy", "win.copy");
|
||||
c.g_menu_append(section, "Paste", "win.paste");
|
||||
}
|
||||
|
||||
{
|
||||
const section = c.g_menu_new();
|
||||
@@ -1627,21 +1865,21 @@ fn initContextMenu(self: *App) void {
|
||||
c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector");
|
||||
}
|
||||
|
||||
const section = c.g_menu_new();
|
||||
defer c.g_object_unref(section);
|
||||
const submenu = c.g_menu_new();
|
||||
defer c.g_object_unref(submenu);
|
||||
|
||||
initMenuContent(@ptrCast(submenu));
|
||||
c.g_menu_append_submenu(section, "Menu", @ptrCast(@alignCast(submenu)));
|
||||
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
||||
|
||||
self.context_menu = menu;
|
||||
}
|
||||
|
||||
fn createContextMenuCopyPasteSection(menu: ?*c.GMenu, has_selection: bool) void {
|
||||
const section = c.g_menu_new();
|
||||
defer c.g_object_unref(section);
|
||||
c.g_menu_prepend_section(menu, null, @ptrCast(@alignCast(section)));
|
||||
// FIXME: Feels really hackish, but disabling sensitivity on this doesn't seems to work(?)
|
||||
c.g_menu_append(section, "Copy", if (has_selection) "win.copy" else "noop");
|
||||
c.g_menu_append(section, "Paste", "win.paste");
|
||||
}
|
||||
|
||||
pub fn refreshContextMenu(self: *App, has_selection: bool) void {
|
||||
c.g_menu_remove(self.context_menu, 0);
|
||||
createContextMenuCopyPasteSection(self.context_menu, has_selection);
|
||||
pub fn refreshContextMenu(_: *App, window: ?*c.GtkWindow, has_selection: bool) void {
|
||||
const action: ?*c.GSimpleAction = @ptrCast(c.g_action_map_lookup_action(@ptrCast(window), "copy"));
|
||||
c.g_simple_action_set_enabled(action, if (has_selection) 1 else 0);
|
||||
}
|
||||
|
||||
fn isValidAppId(app_id: [:0]const u8) bool {
|
||||
|
@@ -64,6 +64,8 @@ fn init(
|
||||
c.gtk_window_set_title(gtk_window, titleText(request));
|
||||
c.gtk_window_set_default_size(gtk_window, 550, 275);
|
||||
c.gtk_window_set_resizable(gtk_window, 0);
|
||||
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "window");
|
||||
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "clipboard-confirmation-window");
|
||||
_ = c.g_signal_connect_data(
|
||||
window,
|
||||
"destroy",
|
||||
@@ -87,6 +89,8 @@ fn init(
|
||||
const view = try PrimaryView.init(self, data);
|
||||
self.view = view;
|
||||
c.gtk_window_set_child(@ptrCast(window), view.root);
|
||||
_ = c.gtk_widget_grab_focus(view.buttons.cancel_button);
|
||||
|
||||
c.gtk_widget_show(window);
|
||||
|
||||
// Block the main window from input.
|
||||
@@ -102,6 +106,7 @@ fn gtkDestroy(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
||||
const PrimaryView = struct {
|
||||
root: *c.GtkWidget,
|
||||
text: *c.GtkTextView,
|
||||
buttons: ButtonsView,
|
||||
|
||||
pub fn init(root: *ClipboardConfirmation, data: []const u8) !PrimaryView {
|
||||
// All our widgets
|
||||
@@ -131,8 +136,9 @@ const PrimaryView = struct {
|
||||
c.gtk_text_view_set_bottom_margin(@ptrCast(text), 8);
|
||||
c.gtk_text_view_set_left_margin(@ptrCast(text), 8);
|
||||
c.gtk_text_view_set_right_margin(@ptrCast(text), 8);
|
||||
c.gtk_text_view_set_monospace(@ptrCast(text), 1);
|
||||
|
||||
return .{ .root = view.root, .text = @ptrCast(text) };
|
||||
return .{ .root = view.root, .text = @ptrCast(text), .buttons = buttons };
|
||||
}
|
||||
|
||||
/// Returns the GtkTextBuffer for the data that was unsafe.
|
||||
@@ -155,6 +161,8 @@ const PrimaryView = struct {
|
||||
|
||||
const ButtonsView = struct {
|
||||
root: *c.GtkWidget,
|
||||
confirm_button: *c.GtkWidget,
|
||||
cancel_button: *c.GtkWidget,
|
||||
|
||||
pub fn init(root: *ClipboardConfirmation) !ButtonsView {
|
||||
const cancel_text, const confirm_text = switch (root.pending_req) {
|
||||
@@ -168,8 +176,8 @@ const ButtonsView = struct {
|
||||
const confirm_button = c.gtk_button_new_with_label(confirm_text);
|
||||
errdefer c.g_object_unref(confirm_button);
|
||||
|
||||
// TODO: Focus on the paste button
|
||||
// c.gtk_widget_grab_focus(confirm_button);
|
||||
c.gtk_widget_add_css_class(confirm_button, "destructive-action");
|
||||
c.gtk_widget_add_css_class(cancel_button, "suggested-action");
|
||||
|
||||
// Create our view
|
||||
const view = try View.init(&.{
|
||||
@@ -195,7 +203,7 @@ const ButtonsView = struct {
|
||||
c.G_CONNECT_DEFAULT,
|
||||
);
|
||||
|
||||
return .{ .root = view.root };
|
||||
return .{ .root = view.root, .confirm_button = confirm_button, .cancel_button = cancel_button };
|
||||
}
|
||||
|
||||
fn gtkCancelClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
||||
@@ -238,7 +246,7 @@ fn promptText(req: apprt.ClipboardRequest) [:0]const u8 {
|
||||
\\Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.
|
||||
,
|
||||
.osc_52_read =>
|
||||
\\An appliclication is attempting to read from the clipboard.
|
||||
\\An application is attempting to read from the clipboard.
|
||||
\\The current clipboard contents are shown below.
|
||||
,
|
||||
.osc_52_write =>
|
||||
|
@@ -55,6 +55,8 @@ fn init(self: *ConfigErrors, app: *App) !void {
|
||||
c.gtk_window_set_default_size(gtk_window, 600, 275);
|
||||
c.gtk_window_set_resizable(gtk_window, 0);
|
||||
c.gtk_window_set_icon_name(gtk_window, build_config.bundle_id);
|
||||
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "window");
|
||||
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "config-errors-window");
|
||||
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
// Set some state
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user