Compare commits
509 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16a4845a72 | ||
|
|
16da031f68 | ||
|
|
43b2935e52 | ||
|
|
fba759a47c | ||
|
|
3bcb694b1b | ||
|
|
555020cfd5 | ||
|
|
391e2b8488 | ||
|
|
c856d5c058 | ||
|
|
e330c38d4a | ||
|
|
d595084395 | ||
|
|
b32ee5ee46 | ||
|
|
098dd10789 | ||
|
|
b5fa643c5d | ||
|
|
664dd29970 | ||
|
|
b7a4281985 | ||
|
|
9e9aa52553 | ||
|
|
77fbdfbb53 | ||
|
|
214416f1e3 | ||
|
|
800437ec6b | ||
|
|
957eafbef0 | ||
|
|
7f228bcbf5 | ||
|
|
fd4a682c5b | ||
|
|
ba9afc61a4 | ||
|
|
9a70709bba | ||
|
|
c437a36794 | ||
|
|
0e0578ff49 | ||
|
|
64a202a125 | ||
|
|
740d1ca146 | ||
|
|
7b75f055ea | ||
|
|
8d6a3cff25 | ||
|
|
10fdde6647 | ||
|
|
e1c45dc23f | ||
|
|
2efb8f6dc2 | ||
|
|
3b77f3b4bc | ||
|
|
31f763282f | ||
|
|
39434fb1f6 | ||
|
|
44ccdd36d6 | ||
|
|
4cc0138a28 | ||
|
|
6ca39d9d83 | ||
|
|
26c099639c | ||
|
|
329043ba08 | ||
|
|
e289f4959f | ||
|
|
b6760a59fa | ||
|
|
cd35f92607 | ||
|
|
ea8f223298 | ||
|
|
a4c3154dab | ||
|
|
6aa3405c78 | ||
|
|
da618584af | ||
|
|
9bc31a5651 | ||
|
|
314dd97059 | ||
|
|
0b66f20934 | ||
|
|
aba634ce88 | ||
|
|
033950dae9 | ||
|
|
a59e1adee1 | ||
|
|
5350eb29c1 | ||
|
|
2bb42e67d7 | ||
|
|
8745c4cd4a | ||
|
|
3288400005 | ||
|
|
1f499a7c6c | ||
|
|
012731c3be | ||
|
|
5deed81737 | ||
|
|
6f6e23bf61 | ||
|
|
d2d0487356 | ||
|
|
30df4e5de2 | ||
|
|
3f4588c408 | ||
|
|
4e4ca366d9 | ||
|
|
040ff0cc9d | ||
|
|
b01ad4bb3b | ||
|
|
4476251b76 | ||
|
|
0f4cb1955f | ||
|
|
c161ee7b5e | ||
|
|
a5bb2548a6 | ||
|
|
2898a3ef8c | ||
|
|
a0568334b5 | ||
|
|
4005cfdff2 | ||
|
|
1d8c5e65b9 | ||
|
|
56f45e8b0f | ||
|
|
48a9584688 | ||
|
|
bfcd324993 | ||
|
|
a75235a260 | ||
|
|
e8de2def96 | ||
|
|
5ffc4c6498 | ||
|
|
d0c50248ea | ||
|
|
ca2a121696 | ||
|
|
02d06d6726 | ||
|
|
18bf76d49e | ||
|
|
6961845167 | ||
|
|
4c5f7f50f7 | ||
|
|
572e920466 | ||
|
|
b81fb3c865 | ||
|
|
814dd8a275 | ||
|
|
1619687d1d | ||
|
|
65fc61a507 | ||
|
|
d027f524ce | ||
|
|
df5e6e1563 | ||
|
|
5541e3c2ff | ||
|
|
57dfc379ae | ||
|
|
b5467b8e26 | ||
|
|
a9f1928913 | ||
|
|
ae16fed39b | ||
|
|
848a795d26 | ||
|
|
deb8c3dacd | ||
|
|
3cb572b47a | ||
|
|
df3846fbb8 | ||
|
|
d73ceb26ae | ||
|
|
da9ffc9b93 | ||
|
|
63fa0c4e94 | ||
|
|
ce91b6d9d9 | ||
|
|
c982f24faf | ||
|
|
9f865adb83 | ||
|
|
b54dd1cb48 | ||
|
|
ef621aa099 | ||
|
|
df4738f080 | ||
|
|
8d0a06325c | ||
|
|
f3329fcd34 | ||
|
|
af48547d8b | ||
|
|
f228f8368a | ||
|
|
9bb2c1a27b | ||
|
|
aa0b07a90d | ||
|
|
d4075bfa9a | ||
|
|
2b7455e353 | ||
|
|
f42d2c63a6 | ||
|
|
6be3ae9efc | ||
|
|
2fd2e07342 | ||
|
|
1e0e32e282 | ||
|
|
b2d8f24499 | ||
|
|
12c99741a0 | ||
|
|
91dbcf4df0 | ||
|
|
6ebd82a84c | ||
|
|
c07178f43a | ||
|
|
78a5957863 | ||
|
|
cd6882d21e | ||
|
|
926a2828ab | ||
|
|
daa7df96d1 | ||
|
|
84ca83ef69 | ||
|
|
ac9c85e783 | ||
|
|
331f6d4903 | ||
|
|
2e422e5ba8 | ||
|
|
20cffc728e | ||
|
|
b06cbc9465 | ||
|
|
1348327414 | ||
|
|
cd3eeb7a95 | ||
|
|
7be6b28011 | ||
|
|
ef1bee0f48 | ||
|
|
702da542dd | ||
|
|
5ba92d06cf | ||
|
|
34a7b42063 | ||
|
|
963d1b58f8 | ||
|
|
fe598bac8a | ||
|
|
45736d7f1d | ||
|
|
0fe4f5a686 | ||
|
|
e9ce5c02d0 | ||
|
|
5916d82580 | ||
|
|
2c83b9902e | ||
|
|
fe07825ad9 | ||
|
|
56e83d7d07 | ||
|
|
c7e3c92a0a | ||
|
|
1a643441f3 | ||
|
|
2aee746da9 | ||
|
|
e64b1ba67c | ||
|
|
31be4f041e | ||
|
|
1f5ebc408d | ||
|
|
b4d9a22df8 | ||
|
|
d4168e3b80 | ||
|
|
241c7af855 | ||
|
|
080fcd8fc1 | ||
|
|
1aad4c7ec3 | ||
|
|
e0e0582b53 | ||
|
|
0a6276b007 | ||
|
|
2a2256f7d9 | ||
|
|
ed81a60065 | ||
|
|
5ad55dd165 | ||
|
|
6860f7ffd6 | ||
|
|
33a3e3fb61 | ||
|
|
d4df3f67b6 | ||
|
|
f15a2f0c1f | ||
|
|
252288aff2 | ||
|
|
65a20a2407 | ||
|
|
5f180dbd9a | ||
|
|
ee4e9aa678 | ||
|
|
9f1c9529aa | ||
|
|
5bba1a0851 | ||
|
|
e9823eb40c | ||
|
|
14fc3f6ac4 | ||
|
|
74714f942a | ||
|
|
0d116e6ef0 | ||
|
|
06108d66b1 | ||
|
|
21eece4c6d | ||
|
|
4dd696e4e0 | ||
|
|
d04d4444af | ||
|
|
a487a78200 | ||
|
|
f06a72b418 | ||
|
|
3270c9a527 | ||
|
|
a7b7fb560a | ||
|
|
384eba6cd4 | ||
|
|
dc7e3ba1ec | ||
|
|
b1e0adcfdc | ||
|
|
2a5922ce92 | ||
|
|
93694925ce | ||
|
|
adfcbb004f | ||
|
|
6595898b73 | ||
|
|
81b33f1f6f | ||
|
|
a1c4ffec20 | ||
|
|
89854cca8b | ||
|
|
4fe5211ed7 | ||
|
|
795b32efd7 | ||
|
|
153064ef3a | ||
|
|
183a31c884 | ||
|
|
adf47bed89 | ||
|
|
cc297a8984 | ||
|
|
4ef17edace | ||
|
|
e56fa9d31f | ||
|
|
791d4b30fb | ||
|
|
a9a9f1ac00 | ||
|
|
fd6bc55db6 | ||
|
|
97d7329d14 | ||
|
|
609d42e2bc | ||
|
|
3463931dad | ||
|
|
f637bf2377 | ||
|
|
2271eb5e60 | ||
|
|
a5cbb552f0 | ||
|
|
e43ca5b031 | ||
|
|
cc7e668ad6 | ||
|
|
a01dbbdf04 | ||
|
|
e0ce0a67cc | ||
|
|
0d83a8866b | ||
|
|
f7a7e39a36 | ||
|
|
67cb466ea9 | ||
|
|
376688ab9f | ||
|
|
8668087d15 | ||
|
|
9b1c23987c | ||
|
|
784daa1a4b | ||
|
|
06f9b73a7b | ||
|
|
62c0272df2 | ||
|
|
7ce2b4c641 | ||
|
|
08c2de541f | ||
|
|
94551623ec | ||
|
|
cb0d23bae8 | ||
|
|
cb5157f2d1 | ||
|
|
32dcbbb5e1 | ||
|
|
4eedf0d36c | ||
|
|
be458303c5 | ||
|
|
78542ede14 | ||
|
|
df8800b7ff | ||
|
|
9c30cd8891 | ||
|
|
dda28efd66 | ||
|
|
b78183972c | ||
|
|
fc06196a3e | ||
|
|
2a1809a2d8 | ||
|
|
ac5c312267 | ||
|
|
8f3174038b | ||
|
|
2d8113e0e0 | ||
|
|
e881850bb4 | ||
|
|
4cbae1db89 | ||
|
|
97bce7be18 | ||
|
|
5d2a276172 | ||
|
|
762dfe8315 | ||
|
|
1c0d1ec8a9 | ||
|
|
e650699217 | ||
|
|
5ff2b5be1c | ||
|
|
f1841d7c81 | ||
|
|
fd6231a778 | ||
|
|
751a52153d | ||
|
|
61b733bae9 | ||
|
|
624e96df9b | ||
|
|
a3f1a44d83 | ||
|
|
ed1547938e | ||
|
|
f96a13e9e4 | ||
|
|
7b70ed96b6 | ||
|
|
9379853d47 | ||
|
|
6ccfebd9e3 | ||
|
|
45186a17ca | ||
|
|
24bb4585af | ||
|
|
feb96f1be8 | ||
|
|
b7d3d64975 | ||
|
|
6114b49630 | ||
|
|
87d95caae0 | ||
|
|
9535bc94ff | ||
|
|
0841be7803 | ||
|
|
6cec89181b | ||
|
|
cb78e17594 | ||
|
|
285c399ae5 | ||
|
|
c9b14f6f78 | ||
|
|
6a101c51d2 | ||
|
|
8998970adc | ||
|
|
991fbacb99 | ||
|
|
72f3e8cd40 | ||
|
|
cf8113ea24 | ||
|
|
5d1cf49fb6 | ||
|
|
90f3c0f9af | ||
|
|
d02598688a | ||
|
|
1991f1ccd7 | ||
|
|
8fad8972d5 | ||
|
|
fc3e78e226 | ||
|
|
15aace29a1 | ||
|
|
12a3a2d7fe | ||
|
|
7e26a340a3 | ||
|
|
f0764c24d2 | ||
|
|
d080bf3b9c | ||
|
|
51698dff07 | ||
|
|
4154926be7 | ||
|
|
806ca2b224 | ||
|
|
92bbf08343 | ||
|
|
dc84e3de67 | ||
|
|
8011801549 | ||
|
|
dfec88ed15 | ||
|
|
5d3e2c7111 | ||
|
|
af1c48e0f7 | ||
|
|
d4bc906c5c | ||
|
|
62fe467336 | ||
|
|
ef3e9d1c2e | ||
|
|
09ddbbf600 | ||
|
|
4195c239c8 | ||
|
|
a6c3c57d40 | ||
|
|
de9263a117 | ||
|
|
7b7f1ecc54 | ||
|
|
d1b028c27a | ||
|
|
055aae2382 | ||
|
|
dbb084da7a | ||
|
|
8332cd2f79 | ||
|
|
4e29c0c16b | ||
|
|
320d45a3f5 | ||
|
|
73795b5257 | ||
|
|
16e59784c6 | ||
|
|
65a7ec3f85 | ||
|
|
360ff35061 | ||
|
|
5098792723 | ||
|
|
a4883e6d41 | ||
|
|
44c9d66dd4 | ||
|
|
c7718d078c | ||
|
|
2525994db4 | ||
|
|
81e6afa9b3 | ||
|
|
d8f5ccf9e4 | ||
|
|
774489c578 | ||
|
|
39bb0ecfcf | ||
|
|
b222ab1bf6 | ||
|
|
7a31c7ff50 | ||
|
|
132f87d152 | ||
|
|
aedda4bf9b | ||
|
|
40bba70bfe | ||
|
|
8202247b27 | ||
|
|
e2117ee8a4 | ||
|
|
e3a4150fea | ||
|
|
0ce2a23af3 | ||
|
|
6b2337c9c0 | ||
|
|
54661b7fc4 | ||
|
|
a1f895b781 | ||
|
|
199c271edc | ||
|
|
9ac744ae63 | ||
|
|
93b05bcd1e | ||
|
|
546cdbefae | ||
|
|
352c07987b | ||
|
|
f6a1eb19d7 | ||
|
|
a352804c14 | ||
|
|
28ae8c4d20 | ||
|
|
1eef584382 | ||
|
|
4e1cd0590b | ||
|
|
fa670e1f5d | ||
|
|
c7fa6f620a | ||
|
|
04690c8c7c | ||
|
|
4345ea6602 | ||
|
|
d39c212336 | ||
|
|
54d2f06abe | ||
|
|
237bfc9a6e | ||
|
|
29b3d49cd5 | ||
|
|
fe8892ec54 | ||
|
|
2d8dd7a53e | ||
|
|
b493599690 | ||
|
|
d9371dc265 | ||
|
|
ef2a12fbdf | ||
|
|
7215c6d6be | ||
|
|
bd9e1f58fe | ||
|
|
79dec269e1 | ||
|
|
d6ffbf4f3c | ||
|
|
32588939ae | ||
|
|
a02e07bfe8 | ||
|
|
03df0c3cca | ||
|
|
1aa50b73a1 | ||
|
|
c910f483bf | ||
|
|
14f4a8d28e | ||
|
|
ee931a17b0 | ||
|
|
0a2ba3fb66 | ||
|
|
512db660a1 | ||
|
|
b0666c9180 | ||
|
|
57d3d09679 | ||
|
|
3a17f54bca | ||
|
|
4c2800b294 | ||
|
|
8358377ec3 | ||
|
|
3ab12c616c | ||
|
|
ef4e784121 | ||
|
|
1ead20a5b9 | ||
|
|
31dfee8a95 | ||
|
|
d6492264c7 | ||
|
|
7281b9dbb1 | ||
|
|
4b63ee036e | ||
|
|
fb8eb69c20 | ||
|
|
4b1fa2609d | ||
|
|
a8b756f040 | ||
|
|
a94a0f3026 | ||
|
|
71c902eaf8 | ||
|
|
6e7af8ce2e | ||
|
|
6c80cd040c | ||
|
|
4ed413eaa7 | ||
|
|
86357d2c38 | ||
|
|
c0d4d01b88 | ||
|
|
b3fcb53625 | ||
|
|
4f29dea8ef | ||
|
|
4f8ef05058 | ||
|
|
7fc1735a21 | ||
|
|
1e49038747 | ||
|
|
295fbe42ad | ||
|
|
60d33f2847 | ||
|
|
cd369f633a | ||
|
|
c3df6d2fe3 | ||
|
|
72dfa74b3e | ||
|
|
1411269187 | ||
|
|
185d959f25 | ||
|
|
1652d41992 | ||
|
|
297dea881d | ||
|
|
01df828353 | ||
|
|
617115d447 | ||
|
|
31f9978599 | ||
|
|
dc068c16e8 | ||
|
|
89626a462f | ||
|
|
a043588b4a | ||
|
|
2e08ee16c2 | ||
|
|
2cc506476b | ||
|
|
0a462ad706 | ||
|
|
81c1bfe1c5 | ||
|
|
8ce80e39d9 | ||
|
|
fa823a37ae | ||
|
|
17f67fbe28 | ||
|
|
560b78bc35 | ||
|
|
f023f047ff | ||
|
|
5673359be2 | ||
|
|
666696d142 | ||
|
|
f95bc48f9b | ||
|
|
8d18fc09fa | ||
|
|
a3d42a90f2 | ||
|
|
1da5f2ecc6 | ||
|
|
d226e82f68 | ||
|
|
60cc2cb26c | ||
|
|
1ac90cf464 | ||
|
|
d3ffaebd17 | ||
|
|
bf01c549f5 | ||
|
|
6912d8c086 | ||
|
|
a89e1b5573 | ||
|
|
3a9c6088b2 | ||
|
|
fba629f385 | ||
|
|
90477825c2 | ||
|
|
1e56d39777 | ||
|
|
2829971412 | ||
|
|
c147fda6ca | ||
|
|
a27dfd8b7c | ||
|
|
a7c65efc45 | ||
|
|
646a524c8e | ||
|
|
f8d01d2c4f | ||
|
|
1d846f3744 | ||
|
|
6bbe58f876 | ||
|
|
36719fb086 | ||
|
|
78fd7a8458 | ||
|
|
f5976d0d63 | ||
|
|
b313fc3fbe | ||
|
|
cf667b8c47 | ||
|
|
7e3bd8586f | ||
|
|
d5df301317 | ||
|
|
628246c3da | ||
|
|
d228acd30a | ||
|
|
18cd97d914 | ||
|
|
5f13946bac | ||
|
|
8fb24fbc1e | ||
|
|
f0d4952055 | ||
|
|
7113580db2 | ||
|
|
163c211219 | ||
|
|
6993157b3a | ||
|
|
e2efdf674d | ||
|
|
9fcb8e5b6e | ||
|
|
a1a637c7f1 | ||
|
|
4f87f3e9f4 | ||
|
|
da6faa656c | ||
|
|
e1ad75e932 | ||
|
|
646bdbd002 | ||
|
|
9184de1997 | ||
|
|
eabc29ec4d | ||
|
|
e338cdab31 | ||
|
|
7001d609fa | ||
|
|
c8399ce3d2 | ||
|
|
3e1d701dce | ||
|
|
a385c39c2e | ||
|
|
5d1cf339a7 | ||
|
|
fafb8518e0 | ||
|
|
6807f4d4ea | ||
|
|
0ed7b2f3c0 | ||
|
|
116128ebb5 | ||
|
|
0c870c5fcd | ||
|
|
90bc3ab770 | ||
|
|
0021b0c242 | ||
|
|
6dc076f5d1 | ||
|
|
2dd1201c3f | ||
|
|
9be68966b4 | ||
|
|
47d482dca9 | ||
|
|
18dd13c872 | ||
|
|
c71bb877c3 | ||
|
|
eee8106f9e | ||
|
|
80ad596d3d | ||
|
|
9dbafcc368 | ||
|
|
9f5f113840 | ||
|
|
6afbdbe94f | ||
|
|
061a0c8cb6 |
8
.github/workflows/ci.py
vendored
8
.github/workflows/ci.py
vendored
@@ -31,7 +31,11 @@ def install_deps():
|
||||
sys.stdout.flush()
|
||||
if is_macos:
|
||||
items = (x.split()[1].strip('"') for x in open('Brewfile').readlines() if x.strip().startswith('brew '))
|
||||
run('brew', 'install', 'fish', *items)
|
||||
openssl = 'openssl'
|
||||
import ssl
|
||||
if ssl.OPENSSL_VERSION_INFO[0] == 1:
|
||||
openssl += '@1.1'
|
||||
run('brew', 'install', 'fish', openssl, *items)
|
||||
else:
|
||||
run('sudo apt-get update')
|
||||
run('sudo apt-get install -y libgl1-mesa-dev libxi-dev libxrandr-dev libxinerama-dev ca-certificates'
|
||||
@@ -59,7 +63,7 @@ def build_kitty():
|
||||
|
||||
|
||||
def test_kitty():
|
||||
run('./kitty/launcher/kitty +launch test.py')
|
||||
run('./test.py')
|
||||
|
||||
|
||||
def package_kitty():
|
||||
|
||||
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
@@ -73,7 +73,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install build-only deps
|
||||
run: pip install -r docs/requirements.txt flake8 mypy types-requests types-docutils
|
||||
@@ -84,8 +84,11 @@ jobs:
|
||||
- name: Build kitty package
|
||||
run: python .github/workflows/ci.py package
|
||||
|
||||
- name: Build kitty
|
||||
run: python setup.py build --debug
|
||||
|
||||
- name: Run mypy
|
||||
run: python test.py mypy
|
||||
run: ./test.py mypy
|
||||
|
||||
- name: Build man page
|
||||
run: make FAIL_WARN=1 man
|
||||
@@ -120,7 +123,7 @@ jobs:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@master
|
||||
with:
|
||||
fetch-depth: 10
|
||||
fetch-depth: 0 # needed for :commit: docs role
|
||||
|
||||
- name: Build kitty
|
||||
run: python3 .github/workflows/ci.py build
|
||||
@@ -128,5 +131,11 @@ jobs:
|
||||
- name: Test kitty
|
||||
run: python3 .github/workflows/ci.py test
|
||||
|
||||
- name: Install deps for docs
|
||||
run: python3 -m pip install -r docs/requirements.txt
|
||||
|
||||
- name: Builds docs
|
||||
run: make FAIL_WARN=1 docs
|
||||
|
||||
- name: Build kitty package
|
||||
run: python3 .github/workflows/ci.py package
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,6 +2,8 @@
|
||||
*.pyc
|
||||
*.pyo
|
||||
*_stub.pyi
|
||||
*_generated.go
|
||||
*_generated.h
|
||||
/.dmypy.json
|
||||
/tags
|
||||
/build/
|
||||
@@ -10,7 +12,7 @@
|
||||
/compile_commands.json
|
||||
/link_commands.json
|
||||
/glad/out/
|
||||
/kitty/launcher/
|
||||
/kitty/launcher/kitty*
|
||||
/*.dSYM/
|
||||
__pycache__/
|
||||
/glfw/wayland-*-client-protocol.[ch]
|
||||
|
||||
6
Makefile
6
Makefile
@@ -35,6 +35,12 @@ profile:
|
||||
app:
|
||||
python3 setup.py kitty.app $(VVAL)
|
||||
|
||||
linux-package: FORCE
|
||||
rm -rf linux-package
|
||||
python3 setup.py linux-package
|
||||
|
||||
FORCE:
|
||||
|
||||
man:
|
||||
$(MAKE) -C docs man
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@ def copy_python(env):
|
||||
iv['sanitize_source_folder'](os.path.join(env.py_dir, x))
|
||||
py_compile(env.py_dir)
|
||||
freeze_python(env.py_dir, pdir, env.obj_dir, ext_map, develop_mode_env_var='KITTY_DEVELOP_FROM', remove_pyc_files=True)
|
||||
shutil.rmtree(env.py_dir)
|
||||
|
||||
|
||||
def build_launcher(env):
|
||||
|
||||
@@ -369,6 +369,7 @@ class Freeze(object):
|
||||
iv['sanitize_source_folder'](join(self.python_stdlib, x))
|
||||
self.compile_py_modules()
|
||||
freeze_python(self.python_stdlib, pdir, self.obj_dir, ext_map, develop_mode_env_var='KITTY_DEVELOP_FROM', remove_pyc_files=True)
|
||||
shutil.rmtree(self.python_stdlib)
|
||||
iv['build_frozen_launcher']([path_to_freeze_dir(), self.obj_dir])
|
||||
os.rename(join(dirname(self.contents_dir), 'bin', 'kitty'), join(self.contents_dir, 'MacOS', 'kitty'))
|
||||
shutil.rmtree(join(dirname(self.contents_dir), 'bin'))
|
||||
|
||||
18
docs/_templates/base.html
vendored
18
docs/_templates/base.html
vendored
@@ -4,18 +4,14 @@
|
||||
{{ super() }}
|
||||
|
||||
{%- if analytics_id %}
|
||||
<script type="text/javascript">
|
||||
var _gaq = _gaq || [];
|
||||
_gaq.push(['_setAccount', '{{ analytics_id }}']);
|
||||
_gaq.push(['_setDomainName', 'none']);
|
||||
_gaq.push(['_setAllowLinker', true]);
|
||||
_gaq.push(['_trackPageview']);
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id={{ analytics_id }}"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
(function() {
|
||||
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
gtag('config', '{{ analytics_id }}');
|
||||
</script>
|
||||
{% endif -%}
|
||||
|
||||
|
||||
@@ -123,6 +123,7 @@ The full list of actions that can be mapped to key presses is available
|
||||
================================== =======================
|
||||
Action Shortcut
|
||||
================================== =======================
|
||||
Show this help :sc:`show_kitty_doc`
|
||||
Copy to clipboard :sc:`copy_to_clipboard` (also :kbd:`⌘+c` on macOS)
|
||||
Paste from clipboard :sc:`paste_from_clipboard` (also :kbd:`⌘+v` on macOS)
|
||||
Paste from selection :sc:`paste_from_selection`
|
||||
|
||||
@@ -29,12 +29,13 @@ Dependencies
|
||||
|
||||
Run-time dependencies:
|
||||
|
||||
* ``python`` >= 3.6
|
||||
* ``python`` >= 3.8
|
||||
* ``harfbuzz`` >= 2.2.0
|
||||
* ``zlib``
|
||||
* ``libpng``
|
||||
* ``liblcms2``
|
||||
* ``librsync``
|
||||
* ``openssl``
|
||||
* ``freetype`` (not needed on macOS)
|
||||
* ``fontconfig`` (not needed on macOS)
|
||||
* ``libcanberra`` (not needed on macOS)
|
||||
@@ -82,14 +83,21 @@ You can run |kitty|, as::
|
||||
If that works, you can create a symlink to the launcher in :file:`~/bin` or some
|
||||
other directory on your PATH so that you can run |kitty| using just ``kitty``.
|
||||
|
||||
To have the kitty documentation available locally, run::
|
||||
|
||||
python3 -m pip install -r docs/requirements.txt && make docs
|
||||
|
||||
|
||||
Building kitty.app on macOS from source
|
||||
-------------------------------------------
|
||||
|
||||
Run::
|
||||
|
||||
python3 -m pip install -r docs/requirements.txt && make docs
|
||||
make app
|
||||
|
||||
Building the docs needs to be done only once.
|
||||
|
||||
This :file:`kitty.app` unlike the released one does not include its own copy of
|
||||
Python and the other dependencies. So if you ever un-install/upgrade those
|
||||
dependencies you might have to rebuild the app.
|
||||
@@ -175,8 +183,11 @@ You should probably split |kitty| into three packages:
|
||||
:code:`kitty`
|
||||
Installs the main program
|
||||
|
||||
This allows users to install the terminfo and shell integration files on servers
|
||||
into which they ssh, without needing to install all of |kitty|.
|
||||
This allows users to install the terminfo and shell integration files on
|
||||
servers into which they ssh, without needing to install all of |kitty|. The
|
||||
shell integration files **must** still be present in
|
||||
:file:`lib/kitty/shell-integration` when installing the kitty main package as
|
||||
the kitty program expects to find them there.
|
||||
|
||||
.. note::
|
||||
You need a couple of extra dependencies to build linux-package. :file:`tic`
|
||||
|
||||
@@ -35,6 +35,83 @@ mouse anywhere in the current command to move the cursor there. See
|
||||
Detailed list of changes
|
||||
-------------------------------------
|
||||
|
||||
0.26.0 [2022-08-29]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- A new option :opt:`remote_control_password` to use fine grained permissions for what can be remote controlled (:disc:`5320`)
|
||||
|
||||
- Reduce startup latency by ~30 milliseconds when running kittens via key bindings inside kitty (:iss:`5159`)
|
||||
|
||||
- A new option :opt:`modify_font` to adjust various font metrics like underlines, cell sizes etc. (:pull:`5265`)
|
||||
|
||||
- A new shortcut :sc:`show_kitty_doc` to display the kitty docs in a browser
|
||||
|
||||
- Graphics protocol: Only delete temp files if they have the string
|
||||
:code:`tty-graphics-protocol` in their file paths. This prevents deletion of arbitrary files in :file:`/tmp`.
|
||||
|
||||
- Deprecate the ``adjust_baseline``, ``adjust_line_height`` and ``adjust_column_width`` options in favor of :opt:`modify_font`
|
||||
|
||||
- Wayland: Fix a regression in the previous release that caused mouse cursor
|
||||
animation and keyboard repeat to stop working when switching seats (:iss:`5188`)
|
||||
|
||||
- Allow resizing windows created in session files (:pull:`5196`)
|
||||
|
||||
- Fix horizontal wheel events not being reported to client programs when they grab the mouse (:iss:`2819`)
|
||||
|
||||
- macOS: Remote control: Fix unable to launch a new OS window or background process when there is no OS window (:iss:`5210`)
|
||||
|
||||
- macOS: Fix unable to open new tab or new window when there is no OS window (:iss:`5276`)
|
||||
|
||||
- kitty @ set-colors: Fix changing inactive_tab_foreground not working (:iss:`5214`)
|
||||
|
||||
- macOS: Fix a regression that caused switching keyboard input using Eisu and
|
||||
Kana keys not working (:iss:`5232`)
|
||||
|
||||
- Add a mappable action to toggle the mirrored setting for the tall and fat
|
||||
layouts (:pull:`5344`)
|
||||
|
||||
- Add a mappable action to switch between predefined bias values for the tall and fat
|
||||
layouts (:pull:`5352`)
|
||||
|
||||
- Wayland: Reduce flicker at startup by not using render frames immediately after a resize (:iss:`5235`)
|
||||
|
||||
- Linux: Update cursor position after all key presses not just pre-edit text
|
||||
changes (:iss:`5241`)
|
||||
|
||||
- ssh kitten: Allow ssh kitten to work from inside tmux, provided the tmux
|
||||
session inherits the correct KITTY env vars (:iss:`5227`)
|
||||
|
||||
- ssh kitten: A new option :code:`--symlink-strategy` to control how symlinks
|
||||
are copied to the remote machine (:iss:`5249`)
|
||||
|
||||
- ssh kitten: Allow pressing :kbd:`Ctrl+C` to abort ssh before the connection is
|
||||
completed (:iss:`5271`)
|
||||
|
||||
- Bash integration: Fix declare not creating global variables in .bashrc (:iss:`5254`)
|
||||
|
||||
- Bash integration: Fix the inherit_errexit option being set by shell integration (:iss:`5349`)
|
||||
|
||||
- :command:`kitty @ scroll-window` allow scrolling by fractions of a screen
|
||||
(:iss:`5294`)
|
||||
|
||||
- remote files kitten: Fix working with files whose names have characters that
|
||||
need to be quoted in shell scripts (:iss:`5313`)
|
||||
|
||||
- Expand ~ in paths configured in :opt:`editor` and :opt:`exe_search_path` (:disc:`5298`)
|
||||
|
||||
- Allow showing the working directory of the active window in tab titles
|
||||
(:pull:`5314`)
|
||||
|
||||
- ssh kitten: Allow completion of ssh options between the destination and command (:iss:`5322`)
|
||||
|
||||
- macOS: Fix speaking selected text not working (:iss:`5357`)
|
||||
|
||||
- Allow ignoring failure to close windows/tabs via rc commands (:disc:`5406`)
|
||||
|
||||
- Fix hyperlinks not present when fetching text from the history buffer
|
||||
(:iss:`5427`)
|
||||
|
||||
|
||||
0.25.2 [2022-06-07]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -687,7 +764,7 @@ Detailed list of changes
|
||||
0.21.2 [2021-06-28]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- A new :opt:`adjust_baseline` option to adjust the vertical alignment of text
|
||||
- A new ``adjust_baseline`` option to adjust the vertical alignment of text
|
||||
inside a line (:pull:`3734`)
|
||||
|
||||
- A new :opt:`url_excluded_characters` option to exclude additional characters
|
||||
@@ -861,7 +938,7 @@ Detailed list of changes
|
||||
scrollback buffer onto the screen. Useful, for example, to restore
|
||||
the screen after showing completions below the shell prompt.
|
||||
|
||||
- A new remote control command :ref:`at_env` to change the default
|
||||
- A new remote control command :ref:`at-env` to change the default
|
||||
environment passed to newly created windows (:iss:`3529`)
|
||||
|
||||
- Linux: Fix binary kitty builds not able to load fonts in WOFF2 format
|
||||
@@ -886,7 +963,7 @@ Detailed list of changes
|
||||
kitty window title bar
|
||||
|
||||
- Fix reading :option:`kitty --session` from ``STDIN`` not working when the
|
||||
:option:`kitty --detach` option is used (:iss:`3523`)
|
||||
:code:`kitty --detach` option is used (:iss:`3523`)
|
||||
|
||||
- Special case rendering of the few remaining Powerline box drawing chars
|
||||
(:iss:`3535`)
|
||||
@@ -2029,7 +2106,7 @@ Detailed list of changes
|
||||
- Allow the user to control the resize debounce time via
|
||||
:opt:`resize_debounce_time`.
|
||||
|
||||
- Remote control: Make the :ref:`at_set-font-size` command more capable.
|
||||
- Remote control: Make the :ref:`at-set-font-size` command more capable.
|
||||
It can now increment font size and reset it. It also only acts on the
|
||||
active top-level window, by default (:iss:`1581`)
|
||||
|
||||
@@ -2338,7 +2415,7 @@ Detailed list of changes
|
||||
|
||||
- Fix a regression in 0.12.0 that broke dynamic resizing of layouts (:iss:`860`)
|
||||
|
||||
- Wayland: Allow using the :option:`kitty --class` command line flag
|
||||
- Wayland: Allow using the :code:`kitty --class` command line flag
|
||||
to set the app id (:iss:`862`)
|
||||
|
||||
- Add completion of the kitty command for the fish shell (:pull:`829`)
|
||||
@@ -2381,7 +2458,7 @@ Detailed list of changes
|
||||
window is not the active window (:iss:`729`)
|
||||
|
||||
- Remote control: Allow changing the current window layout with a new
|
||||
:ref:`at_goto-layout` command (:iss:`845`)
|
||||
:ref:`at-goto-layout` command (:iss:`845`)
|
||||
|
||||
- Remote control: Allow matching windows by the environment variables of their
|
||||
child process as well
|
||||
|
||||
21
docs/conf.py
21
docs/conf.py
@@ -120,10 +120,23 @@ html_title = 'kitty'
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
github_icon_path = 'M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z' # noqa
|
||||
|
||||
html_theme_options: Dict[str, Any] = {
|
||||
'sidebar_hide_name': True,
|
||||
'navigation_with_keys': True,
|
||||
'footer_icons': [
|
||||
{
|
||||
"name": "GitHub",
|
||||
"url": "https://github.com/kovidgoyal/kitty",
|
||||
"html": f"""
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="{github_icon_path}"></path>
|
||||
</svg>
|
||||
""",
|
||||
"class": "",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -236,7 +249,7 @@ if you specify a program-to-run you can use the special placeholder
|
||||
from kitty.rc.base import cli_params_for
|
||||
for cmd_name in sorted(all_command_names()):
|
||||
func = command_for_name(cmd_name)
|
||||
p(f'.. _at_{func.name}:\n')
|
||||
p(f'.. _at-{func.name}:\n')
|
||||
p('kitty @', func.name)
|
||||
p('-' * 120)
|
||||
p('.. program::', 'kitty @', func.name)
|
||||
@@ -264,7 +277,7 @@ def write_remote_control_protocol_docs() -> None: # {{{
|
||||
from kitty.rc.base import (
|
||||
RemoteCommand, all_command_names, command_for_name
|
||||
)
|
||||
field_pat = re.compile(r'\s*([a-zA-Z0-9_+]+)\s*:\s*(.+)')
|
||||
field_pat = re.compile(r'\s*([a-zA-Z0-9_+/]+)\s*:\s*(.+)')
|
||||
|
||||
def format_cmd(p: Callable[..., None], name: str, cmd: RemoteCommand) -> None:
|
||||
p(name)
|
||||
@@ -276,7 +289,7 @@ def write_remote_control_protocol_docs() -> None: # {{{
|
||||
if m is None:
|
||||
p(line)
|
||||
else:
|
||||
fields.append((m.group(1), m.group(2)))
|
||||
fields.append((m.group(1).split('/')[0], m.group(2)))
|
||||
if fields:
|
||||
p('\nFields are:\n')
|
||||
for (name, desc) in fields:
|
||||
|
||||
54
docs/extract-rst-targets.py
Executable file
54
docs/extract-rst-targets.py
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import os
|
||||
import re
|
||||
from typing import Dict, Iterator
|
||||
|
||||
tgt_pat = re.compile(r'^.. _(\S+?):$', re.MULTILINE)
|
||||
title_pat = re.compile('^(.+)\n[-=^#*]{5,}$', re.MULTILINE)
|
||||
|
||||
|
||||
def find_explicit_targets(text: str) -> Iterator[str]:
|
||||
for m in tgt_pat.finditer(text):
|
||||
yield m.group(1)
|
||||
|
||||
|
||||
def find_page_title(text: str) -> str:
|
||||
for m in title_pat.finditer(text):
|
||||
return m.group(1)
|
||||
return ''
|
||||
|
||||
|
||||
def main() -> Dict[str, Dict[str, str]]:
|
||||
refs = {}
|
||||
docs = {}
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
for dirpath, dirnames, filenames in os.walk(base):
|
||||
if 'generated' in dirnames:
|
||||
dirnames.remove('generated')
|
||||
for f in filenames:
|
||||
if f.endswith('.rst'):
|
||||
with open(os.path.join(dirpath, f)) as stream:
|
||||
raw = stream.read()
|
||||
href = os.path.relpath(stream.name, base).replace(os.sep, '/')
|
||||
href = href.rpartition('.')[0] + '/'
|
||||
docs[href.rstrip('/')] = find_page_title(raw)
|
||||
first_line = raw.lstrip('\n').partition('\n')[0]
|
||||
first_target_added = False
|
||||
for explicit_target in find_explicit_targets(raw):
|
||||
# Shorten the reference link to the top of the page.
|
||||
# Note that anchor links should still be used in HTML docs
|
||||
# to allow jumping within the same page.
|
||||
if not first_target_added:
|
||||
first_target_added = True
|
||||
if first_line.startswith(f'.. _{explicit_target}:'):
|
||||
refs[explicit_target] = href
|
||||
continue
|
||||
refs[explicit_target] = href + f'#{explicit_target.replace("_", "-")}'
|
||||
return {'ref': refs, 'doc': docs}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import json
|
||||
print(json.dumps(main(), indent=2))
|
||||
@@ -132,8 +132,8 @@ You can also define keyboard shortcuts to set colors, for example::
|
||||
map f1 set_colors --configured /path/to/some/config/file/colors.conf
|
||||
|
||||
Or you can enable :doc:`remote control <remote-control>` for |kitty| and use
|
||||
:ref:`at_set-colors`. The shortcut mapping technique has the same syntax as the
|
||||
remote control command, for details, see :ref:`at_set-colors`.
|
||||
:ref:`at-set-colors`. The shortcut mapping technique has the same syntax as the
|
||||
remote control command, for details, see :ref:`at-set-colors`.
|
||||
|
||||
To change colors when SSHing into a remote host, use the :opt:`color_scheme
|
||||
<kitten-ssh.color_scheme>` setting for the :doc:`ssh kitten <kittens/ssh>`.
|
||||
@@ -283,6 +283,10 @@ homepage:
|
||||
:target: https://github.com/igrmk/whiskers
|
||||
:width: 256
|
||||
|
||||
.. image:: https://github.com/samholmes/whiskers/raw/main/whiskers.png
|
||||
:target: https://github.com/samholmes/whiskers
|
||||
:width: 256
|
||||
|
||||
On macOS you can change the icon by following the steps:
|
||||
|
||||
#. Find :file:`kitty.app` in the Applications folder, select it and press :kbd:`⌘+I`
|
||||
|
||||
@@ -30,7 +30,7 @@ Glossary
|
||||
overlay
|
||||
An *overlay window* is a :term:`kitty window <window>` that is placed on
|
||||
top of an existing kitty window, entirely covering it. Overlays are used
|
||||
throught kitty, for example, to display the :ref:`the scrollback buffer <scrollback>`,
|
||||
throughout kitty, for example, to display the :ref:`the scrollback buffer <scrollback>`,
|
||||
to display :doc:`hints </kittens/hints>`, for :doc:`unicode input
|
||||
</kittens/unicode_input>` etc.
|
||||
|
||||
@@ -107,6 +107,11 @@ Variables that influence kitty behavior
|
||||
Set this to the directory path of the kitty source code and its Python code
|
||||
will be loaded from there. Only works with official binary builds.
|
||||
|
||||
.. envvar:: KITTY_RC_PASSWORD
|
||||
|
||||
Set this to a pass phrase to use the ``kitty @`` remote control command with
|
||||
:opt:`remote_control_password`.
|
||||
|
||||
|
||||
Variables that kitty sets when running child programs
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -116,53 +121,49 @@ Variables that kitty sets when running child programs
|
||||
This is only set on macOS. If the country and language from the macOS user
|
||||
settings form an invalid locale, it will be set to :code:`en_US.UTF-8`.
|
||||
|
||||
|
||||
.. envvar:: PATH
|
||||
|
||||
kitty prepends itself to the PATH of its own environment to ensure the
|
||||
functions calling :program:`kitty` will work properly.
|
||||
|
||||
|
||||
.. envvar:: KITTY_WINDOW_ID
|
||||
|
||||
An integer that is the id for the kitty :term:`window` the program is running in.
|
||||
Can be used with the :doc:`kitty remote control facility <remote-control>`.
|
||||
|
||||
|
||||
.. envvar:: KITTY_PID
|
||||
|
||||
An integer that is the process id for the kitty process in which the program
|
||||
is running. Allows programs to tell kitty to reload its config by sending it
|
||||
the SIGUSR1 signal.
|
||||
|
||||
.. envvar:: KITTY_PUBLIC_KEY
|
||||
|
||||
A public key that programs can use to communicate securely with kitty using
|
||||
the remote control protocol. The format is: :code:`protocol:key data`.
|
||||
|
||||
.. envvar:: WINDOWID
|
||||
|
||||
The id for the :term:`OS Window <os_window>` the program is running in. Only available
|
||||
on platforms that have ids for their windows, such as X11 and macOS.
|
||||
|
||||
|
||||
.. envvar:: TERM
|
||||
|
||||
The name of the terminal, defaults to ``xterm-kitty``. See :opt:`term`.
|
||||
|
||||
|
||||
.. envvar:: TERMINFO
|
||||
|
||||
Path to a directory containing the kitty terminfo database.
|
||||
|
||||
|
||||
.. envvar:: KITTY_INSTALLATION_DIR
|
||||
|
||||
Path to the kitty installation directory.
|
||||
|
||||
|
||||
.. envvar:: COLORTERM
|
||||
|
||||
Set to the value ``truecolor`` to indicate that kitty supports 16 million
|
||||
colors.
|
||||
|
||||
|
||||
.. envvar:: KITTY_LISTEN_ON
|
||||
|
||||
Set when the :doc:`remote control <remote-control>` facility is enabled and
|
||||
@@ -170,45 +171,38 @@ Variables that kitty sets when running child programs
|
||||
Contains the path to the socket. Avoid the need to use :option:`kitty @ --to` when
|
||||
issuing remote control commands.
|
||||
|
||||
|
||||
.. envvar:: KITTY_PIPE_DATA
|
||||
|
||||
Set to data describing the layout of the screen when running child
|
||||
programs using :option:`launch --stdin-source` with the contents of the
|
||||
screen/scrollback piped to them.
|
||||
|
||||
|
||||
.. envvar:: KITTY_CHILD_CMDLINE
|
||||
|
||||
Set to the command line of the child process running in the kitty
|
||||
window when calling the notification callback program on terminal bell, see
|
||||
:opt:`command_on_bell`.
|
||||
|
||||
|
||||
.. envvar:: KITTY_COMMON_OPTS
|
||||
|
||||
Set with the values of some common kitty options when running
|
||||
kittens, so kittens can use them without needing to load :file:`kitty.conf`.
|
||||
|
||||
|
||||
.. envvar:: KITTY_SHELL_INTEGRATION
|
||||
|
||||
Set when enabling :ref:`shell_integration`. It is automatically removed by
|
||||
the shell integration scripts.
|
||||
|
||||
|
||||
.. envvar:: ZDOTDIR
|
||||
|
||||
Set when enabling :ref:`shell_integration` with :program:`zsh`, allowing
|
||||
:program:`zsh` to automatically load the integration script.
|
||||
|
||||
|
||||
.. envvar:: XDG_DATA_DIRS
|
||||
|
||||
Set when enabling :ref:`shell_integration` with :program:`fish`, allowing
|
||||
:program:`fish` to automatically load the integration script.
|
||||
|
||||
|
||||
.. envvar:: ENV
|
||||
|
||||
Set when enabling :ref:`shell_integration` with :program:`bash`, allowing
|
||||
|
||||
@@ -31,7 +31,7 @@ Some programs and libraries that use the kitty graphics protocol:
|
||||
* `ranger <https://github.com/ranger/ranger>`_ - a terminal file manager, with
|
||||
image previews, see this `PR <https://github.com/ranger/ranger/pull/1077>`_
|
||||
* :doc:`kitty-diff <kittens/diff>` - a side-by-side terminal diff program with support for images
|
||||
* `tpix <https://github.com/jesvedberg/tpix>` - a statically compiled binary that can be used to display images and easily installed on remote servers without root access
|
||||
* `tpix <https://github.com/jesvedberg/tpix>`_ - a statically compiled binary that can be used to display images and easily installed on remote servers without root access
|
||||
* `pixcat <https://github.com/mirukana/pixcat>`_ - a third party CLI and python library that wraps the graphics protocol
|
||||
* `neofetch <https://github.com/dylanaraps/neofetch>`_ - A command line system
|
||||
information tool
|
||||
@@ -43,6 +43,7 @@ Some programs and libraries that use the kitty graphics protocol:
|
||||
* `rasterm <https://github.com/BourgeoisBear/rasterm>`_ - Go library to display images in the terminal
|
||||
* `chafa <https://github.com/hpjansson/chafa>`_ - a terminal image viewer
|
||||
* `hologram.nvim <https://github.com/edluffy/hologram.nvim>`_ - view images inside nvim
|
||||
* `term-image <https://github.com/AnonymouX47/term-image>`_ - A Python library, CLI and TUI to display and browse images in the terminal
|
||||
|
||||
Other terminals that have implemented the graphics protocol:
|
||||
|
||||
@@ -236,7 +237,8 @@ Value of `t` Meaning
|
||||
the terminal emulator should only delete the file if it
|
||||
is in a known temporary directory, such as :file:`/tmp`,
|
||||
:file:`/dev/shm`, :file:`TMPDIR env var if present` and any platform
|
||||
specific temporary directories.
|
||||
specific temporary directories and the file has the
|
||||
string :code:`tty-graphics-protocol` in its full file path.
|
||||
``s`` A *shared memory object*, which on POSIX systems is a
|
||||
`POSIX shared memory object <https://pubs.opengroup.org/onlinepubs/9699919799/functions/shm_open.html>`_
|
||||
and on Windows is a
|
||||
@@ -417,7 +419,9 @@ they are semi-transparent. You can specify z-index values using the ``z`` key.
|
||||
Negative z-index values mean that the images will be drawn under the text. This
|
||||
allows rendering of text on top of images. Negative z-index values below
|
||||
INT32_MIN/2 (-1,073,741,824) will be drawn under cells with non-default background
|
||||
colors.
|
||||
colors. If two images with the same z-index overlap then the image with the
|
||||
lower id is considered to have the lower z-index. If the images have the same
|
||||
z-index and the same id, then the behavior is undefined.
|
||||
|
||||
.. note:: After placing an image on the screen the cursor must be moved to the
|
||||
right by the number of cols in the image placement rectangle and down by the
|
||||
|
||||
@@ -47,6 +47,12 @@ graphics protocol.
|
||||
Another terminal file manager, with previews of file contents powered by kitty's
|
||||
graphics protocol.
|
||||
|
||||
.. _tool_term_image:
|
||||
|
||||
`term-image <https://github.com/AnonymouX47/term-image>`__
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Tool to browse images in a terminal using kitty's graphics protocol.
|
||||
|
||||
.. _tool_koneko:
|
||||
|
||||
`koneko <https://github.com/twenty5151/koneko>`_
|
||||
|
||||
@@ -35,8 +35,11 @@ inside the kitty terminal to report key events.
|
||||
In addition to kitty, this protocol is also implemented in:
|
||||
|
||||
* The `foot terminal <https://codeberg.org/dnkl/foot/issues/319>`__
|
||||
* The `WezTerm terminal <https://wezfurlong.org/wezterm/config/lua/config/enable_kitty_keyboard.html>`__
|
||||
* The `notcurses library
|
||||
<https://github.com/dankamongmen/notcurses/issues/2131>`__
|
||||
* The `crossterm library
|
||||
<https://github.com/crossterm-rs/crossterm/pull/688>`__
|
||||
* The `neovim text editor <https://github.com/neovim/neovim/pull/18181>`__
|
||||
* The `kakoune text editor <https://github.com/mawww/kakoune/issues/4103>`__
|
||||
* The `dte text editor <https://gitlab.com/craigbarnes/dte/-/issues/138>`__
|
||||
|
||||
@@ -49,6 +49,24 @@ kittens. Look in the `kittens sub-directory
|
||||
code for those. Or see below for a list of :ref:`third-party kittens
|
||||
<external_kittens>`, that other kitty users have created.
|
||||
|
||||
kitty API to use with kittens
|
||||
-------------------------------
|
||||
|
||||
Kittens have full access to internal kitty APIs. However these are neither
|
||||
entirely stable nor documented. You can instead use the kitty
|
||||
:doc:`Remote control API </remote-control>`. Simply call
|
||||
:code:`boss.call_remote_control()`, with the same arguments you
|
||||
would pass to ``kitty @``. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def handle_result(args: List[str], answer: str, target_window_id: int, boss: Boss) -> None:
|
||||
# get the kitty window to which to send text
|
||||
w = boss.window_id_map.get(target_window_id)
|
||||
if w is not None:
|
||||
boss.call_remote_control(w, ('send-text', 'hello world'))
|
||||
|
||||
|
||||
|
||||
Passing arguments to kittens
|
||||
------------------------------
|
||||
|
||||
@@ -143,6 +143,16 @@ transmitted instantly without any roundtrip delay.
|
||||
is something POSIX sh compliant, and use :code:`python` as the
|
||||
:opt:`interpreter <kitten-ssh.interpreter>` in :file:`ssh.conf`.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
This may or may not work when using terminal multiplexers, depending on
|
||||
whether they passthrough the escape codes and if the values of the
|
||||
environment variables :envvar:`KITTY_PID` and :envvar:`KITTY_WINDOW_ID` are
|
||||
correct in the current session (they can be wrong when connecting to a tmux
|
||||
session running in a different window) and the ssh kitten is run in the
|
||||
currently active multiplexer window.
|
||||
|
||||
.. include:: /generated/conf-kitten-ssh.rst
|
||||
|
||||
|
||||
|
||||
295
docs/kitty_at_template.py
Normal file
295
docs/kitty_at_template.py
Normal file
@@ -0,0 +1,295 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import termios
|
||||
import time
|
||||
import tty
|
||||
from contextlib import contextmanager
|
||||
from ctypes import (
|
||||
CDLL, POINTER, byref, c_char_p, c_int, c_size_t, c_void_p,
|
||||
create_string_buffer
|
||||
)
|
||||
from ctypes.util import find_library
|
||||
|
||||
_plat = sys.platform.lower()
|
||||
is_macos: bool = 'darwin' in _plat
|
||||
|
||||
|
||||
def build_crypto_tools(): # {{{
|
||||
class EVP_PKEY_POINTER(c_void_p):
|
||||
|
||||
algorithm = 0
|
||||
|
||||
def __del__(self):
|
||||
EVP_PKEY_free(self)
|
||||
|
||||
@property
|
||||
def public(self):
|
||||
sz = c_size_t(0)
|
||||
EVP_PKEY_get_raw_public_key(self, None, byref(sz))
|
||||
buf = create_string_buffer(sz.value)
|
||||
EVP_PKEY_get_raw_public_key(self, buf, byref(sz))
|
||||
return buf.raw
|
||||
|
||||
def derive_secret(self, pubkey):
|
||||
pubkey = EVP_PKEY_new_raw_public_key(self.algorithm, None, pubkey, len(pubkey))
|
||||
ctx = EVP_PKEY_CTX_new(self, None)
|
||||
EVP_PKEY_derive_init(ctx)
|
||||
EVP_PKEY_derive_set_peer(ctx, pubkey)
|
||||
sz = c_size_t(0)
|
||||
EVP_PKEY_derive(ctx, None, byref(sz))
|
||||
buf = create_string_buffer(sz.value)
|
||||
EVP_PKEY_derive(ctx, buf, byref(sz))
|
||||
return hashlib.sha256(buf.raw).digest()
|
||||
|
||||
class EVP_PKEY_CTX_POINTER(c_void_p):
|
||||
|
||||
def __del__(self):
|
||||
EVP_PKEY_CTX_free(self)
|
||||
|
||||
class EVP_CIPHER_CTX_POINTER(c_void_p):
|
||||
|
||||
def __del__(self):
|
||||
EVP_CIPHER_CTX_free(self)
|
||||
|
||||
class EVP_CIPHER_POINTER(c_void_p):
|
||||
pass
|
||||
|
||||
cl = find_library('crypto')
|
||||
if not cl:
|
||||
raise SystemExit('Failed to find libcrypto on your system, make sure OpenSSL is installed')
|
||||
crypto = CDLL(cl)
|
||||
libc = CDLL(None)
|
||||
|
||||
def create_crypto_func(name, *argtypes, restype=c_int, int_return_ok=lambda x: x == 1):
|
||||
|
||||
impl = getattr(crypto, name)
|
||||
impl.restype = restype
|
||||
impl.argtypes = argtypes
|
||||
|
||||
def func(*a):
|
||||
res = impl(*a)
|
||||
if restype is c_int:
|
||||
if not int_return_ok(res):
|
||||
print('Call to', name, 'failed with return code:', res, file=sys.stderr)
|
||||
abort_on_openssl_error()
|
||||
elif restype is not None and issubclass(restype, c_void_p):
|
||||
if res.value is None:
|
||||
print('Call to', name, 'failed with NULL return', file=sys.stderr)
|
||||
abort_on_openssl_error()
|
||||
return res
|
||||
return func
|
||||
|
||||
OBJ_txt2nid = create_crypto_func('OBJ_txt2nid', c_char_p, int_return_ok=bool)
|
||||
EVP_PKEY_CTX_new_id = create_crypto_func('EVP_PKEY_CTX_new_id', c_int, c_void_p, restype=EVP_PKEY_CTX_POINTER)
|
||||
EVP_PKEY_CTX_new = create_crypto_func('EVP_PKEY_CTX_new', EVP_PKEY_POINTER, c_void_p, restype=EVP_PKEY_CTX_POINTER)
|
||||
EVP_PKEY_keygen_init = create_crypto_func('EVP_PKEY_keygen_init', EVP_PKEY_CTX_POINTER)
|
||||
EVP_PKEY_keygen = create_crypto_func('EVP_PKEY_keygen', EVP_PKEY_CTX_POINTER, POINTER(EVP_PKEY_POINTER))
|
||||
ERR_print_errors_fp = create_crypto_func('ERR_print_errors_fp', c_void_p, restype=None)
|
||||
EVP_PKEY_free = create_crypto_func('EVP_PKEY_free', EVP_PKEY_POINTER, restype=None)
|
||||
EVP_PKEY_CTX_free = create_crypto_func('EVP_PKEY_CTX_free', EVP_PKEY_CTX_POINTER, restype=None)
|
||||
EVP_PKEY_get_raw_public_key = create_crypto_func('EVP_PKEY_get_raw_public_key', EVP_PKEY_POINTER, c_char_p, POINTER(c_size_t))
|
||||
EVP_PKEY_new_raw_public_key = create_crypto_func('EVP_PKEY_new_raw_public_key', c_int, c_void_p, c_char_p, c_size_t, restype=EVP_PKEY_POINTER)
|
||||
EVP_PKEY_derive_init = create_crypto_func('EVP_PKEY_derive_init', EVP_PKEY_CTX_POINTER)
|
||||
EVP_PKEY_derive_set_peer = create_crypto_func('EVP_PKEY_derive_set_peer', EVP_PKEY_CTX_POINTER, EVP_PKEY_POINTER)
|
||||
EVP_PKEY_derive = create_crypto_func('EVP_PKEY_derive', EVP_PKEY_CTX_POINTER, c_char_p, POINTER(c_size_t))
|
||||
EVP_CIPHER_CTX_free = create_crypto_func('EVP_CIPHER_CTX_free', EVP_CIPHER_CTX_POINTER, restype=None)
|
||||
EVP_get_cipherbyname = create_crypto_func('EVP_get_cipherbyname', c_char_p, restype=EVP_CIPHER_POINTER)
|
||||
EVP_CIPHER_key_length = create_crypto_func('EVP_CIPHER_key_length', EVP_CIPHER_POINTER, int_return_ok=bool)
|
||||
EVP_CIPHER_iv_length = create_crypto_func('EVP_CIPHER_iv_length', EVP_CIPHER_POINTER, int_return_ok=bool)
|
||||
EVP_CIPHER_CTX_block_size = create_crypto_func('EVP_CIPHER_CTX_block_size', EVP_CIPHER_CTX_POINTER, int_return_ok=bool)
|
||||
EVP_CIPHER_CTX_new = create_crypto_func('EVP_CIPHER_CTX_new', restype=EVP_CIPHER_CTX_POINTER)
|
||||
EVP_EncryptInit_ex = create_crypto_func('EVP_EncryptInit_ex', EVP_CIPHER_CTX_POINTER, EVP_CIPHER_POINTER, c_void_p, c_char_p, c_char_p)
|
||||
EVP_EncryptUpdate = create_crypto_func('EVP_EncryptUpdate', EVP_CIPHER_CTX_POINTER, c_char_p, POINTER(c_int), c_char_p, c_int)
|
||||
EVP_EncryptFinal_ex = create_crypto_func('EVP_EncryptFinal_ex', EVP_CIPHER_CTX_POINTER, c_char_p, POINTER(c_int))
|
||||
EVP_CIPHER_CTX_ctrl = create_crypto_func('EVP_CIPHER_CTX_ctrl', EVP_CIPHER_CTX_POINTER, c_int, c_int, c_char_p)
|
||||
try:
|
||||
EVP_CIPHER_CTX_tag_length = create_crypto_func('EVP_CIPHER_CTX_tag_length', EVP_CIPHER_CTX_POINTER, int_return_ok=bool)
|
||||
except AttributeError: # need openssl >= 3
|
||||
def EVP_CIPHER_CTX_tag_length(cipher):
|
||||
return 16
|
||||
EVP_CTRL_AEAD_GET_TAG, EVP_CTRL_AEAD_SET_TAG = 0x10, 0x11 # these are defines in the header dont know how to get them programmatically
|
||||
EVP_CTRL_AEAD_SET_TAG
|
||||
|
||||
def abort_on_openssl_error():
|
||||
stderr = c_void_p.in_dll(libc, 'stderr')
|
||||
ERR_print_errors_fp(stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
def elliptic_curve_keypair(algorithm='X25519'):
|
||||
nid = OBJ_txt2nid(algorithm.encode())
|
||||
pctx = EVP_PKEY_CTX_new_id(nid, None)
|
||||
EVP_PKEY_keygen_init(pctx)
|
||||
key = EVP_PKEY_POINTER()
|
||||
EVP_PKEY_keygen(pctx, byref(key))
|
||||
key.algorithm = nid
|
||||
return key
|
||||
|
||||
def encrypt(plaintext, symmetric_key, algorithm='aes-256-gcm'):
|
||||
cipher = EVP_get_cipherbyname(algorithm.encode())
|
||||
if len(symmetric_key) != EVP_CIPHER_key_length(cipher):
|
||||
raise KeyError(f'The symmetric key has length {len(symmetric_key)} != {EVP_CIPHER_key_length(cipher)} needed for {algorithm}')
|
||||
ctx = EVP_CIPHER_CTX_new()
|
||||
iv = os.urandom(EVP_CIPHER_iv_length(cipher))
|
||||
EVP_EncryptInit_ex(ctx, cipher, None, symmetric_key, iv)
|
||||
bs = EVP_CIPHER_CTX_block_size(ctx)
|
||||
ciphertext = create_string_buffer(len(plaintext) + 2 * bs)
|
||||
outlen = c_int(len(ciphertext))
|
||||
EVP_EncryptUpdate(ctx, ciphertext, byref(outlen), plaintext, len(plaintext))
|
||||
ans = ciphertext[:outlen.value]
|
||||
outlen = c_int(len(ciphertext))
|
||||
EVP_EncryptFinal_ex(ctx, ciphertext, byref(outlen))
|
||||
if outlen.value:
|
||||
ans += ciphertext[:outlen.value]
|
||||
tag = create_string_buffer(EVP_CIPHER_CTX_tag_length(cipher))
|
||||
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, len(tag), tag)
|
||||
return iv, ans, tag.raw
|
||||
return elliptic_curve_keypair, encrypt
|
||||
# }}}
|
||||
|
||||
|
||||
# utils {{{
|
||||
def encrypt_cmd(cmd, password, pubkey=None):
|
||||
elliptic_curve_keypair, encrypt = build_crypto_tools()
|
||||
if pubkey is None:
|
||||
pubkey = os.environ['KITTY_PUBLIC_KEY']
|
||||
v, d = pubkey.split(':', 1)
|
||||
if v != '1':
|
||||
raise SystemExit(f'Unsupported encryption protocol: {v}')
|
||||
pubkey = base64.b85decode(d)
|
||||
k = elliptic_curve_keypair()
|
||||
sk = k.derive_secret(pubkey)
|
||||
cmd['timestamp'] = time.time_ns()
|
||||
cmd['password'] = password
|
||||
data = json.dumps(cmd).encode()
|
||||
iv, encrypted, tag = encrypt(data, sk)
|
||||
|
||||
def e(x):
|
||||
return base64.b85encode(x).decode('ascii')
|
||||
|
||||
return {
|
||||
'encrypted': e(encrypted), 'iv': e(iv), 'tag': e(tag), 'pubkey': e(k.public), 'version': cmd['version']
|
||||
}
|
||||
|
||||
|
||||
@contextmanager
|
||||
def raw_mode(fd):
|
||||
old = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(fd)
|
||||
yield
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
||||
|
||||
|
||||
def config_dir():
|
||||
if 'KITTY_CONFIG_DIRECTORY' in os.environ:
|
||||
return os.path.abspath(os.path.expanduser(os.environ['KITTY_CONFIG_DIRECTORY']))
|
||||
locations = []
|
||||
if 'XDG_CONFIG_HOME' in os.environ:
|
||||
locations.append(os.path.abspath(os.path.expanduser(os.environ['XDG_CONFIG_HOME'])))
|
||||
locations.append(os.path.expanduser('~/.config'))
|
||||
if is_macos:
|
||||
locations.append(os.path.expanduser('~/Library/Preferences'))
|
||||
for loc in filter(None, os.environ.get('XDG_CONFIG_DIRS', '').split(os.pathsep)):
|
||||
locations.append(os.path.abspath(os.path.expanduser(loc)))
|
||||
for loc in locations:
|
||||
if loc:
|
||||
q = os.path.join(loc, 'kitty')
|
||||
if os.access(q, os.W_OK) and os.path.exists(os.path.join(q, 'kitty.conf')):
|
||||
return q
|
||||
for loc in locations:
|
||||
if loc:
|
||||
q = os.path.join(loc, 'kitty')
|
||||
if os.path.isdir(q) and os.access(q, os.W_OK):
|
||||
return q
|
||||
return ''
|
||||
|
||||
|
||||
def resolve_custom_file(path):
|
||||
path = os.path.expanduser(path)
|
||||
path = os.path.expandvars(path)
|
||||
if not os.path.isabs(path):
|
||||
cdir = config_dir()
|
||||
if cdir:
|
||||
path = os.path.join(cdir, path)
|
||||
return path
|
||||
|
||||
|
||||
def get_password(opts):
|
||||
if opts.use_password == 'never':
|
||||
return ''
|
||||
ans = ''
|
||||
if opts.password:
|
||||
ans = opts.password
|
||||
if not ans and opts.password_file:
|
||||
if opts.password_file == '-':
|
||||
if sys.stdin.isatty():
|
||||
from getpass import getpass
|
||||
ans = getpass()
|
||||
else:
|
||||
ans = sys.stdin.read().rstrip()
|
||||
try:
|
||||
tty_fd = os.open(os.ctermid(), os.O_RDONLY | os.O_CLOEXEC)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
with open(tty_fd, closefd=True):
|
||||
os.dup2(tty_fd, sys.stdin.fileno())
|
||||
else:
|
||||
try:
|
||||
with open(resolve_custom_file(opts.password_file)) as f:
|
||||
ans = f.read().rstrip()
|
||||
except OSError:
|
||||
pass
|
||||
if not ans and opts.password_env:
|
||||
ans = os.environ.get(opts.password_env, '')
|
||||
if not ans and opts.use_password == 'always':
|
||||
raise SystemExit('No password was found')
|
||||
if ans and len(ans) > 1024:
|
||||
raise SystemExit('Specified password is too long')
|
||||
return ans
|
||||
# }}}
|
||||
|
||||
|
||||
arg_parser = argparse.ArgumentParser(prog='kitty@', description='Control kitty remotely.')
|
||||
arg_parser.add_argument('--password', default='', help='''\
|
||||
A password to use when contacting kitty. This will cause kitty to ask the user
|
||||
for permission to perform the specified action, unless the password has been
|
||||
accepted before or is pre-configured in kitty.conf''')
|
||||
arg_parser.add_argument('--password-file', default='rc-pass', help='''\
|
||||
A file from which to read the password. Trailing whitespace is ignored. Relative
|
||||
paths are resolved from the kitty configuration directory. Use - to read from STDIN.
|
||||
Used if no --password is supplied. Defaults to checking for the
|
||||
rc-pass file in the kitty configuration directory.''')
|
||||
arg_parser.add_argument('--password-env', default='KITTY_RC_PASSWORD', help='''\
|
||||
The name of an environment variable to read the password from.
|
||||
Used if no --password-file is supplied. Defaults to checking the KITTY_RC_PASSWORD.''')
|
||||
arg_parser.add_argument('--use-password', default='if-available', choices=('if-available', 'always', 'never'), help='''\
|
||||
If no password is available, kitty will usually just send the remote control command
|
||||
without a password. This option can be used to force it to always or never use
|
||||
the supplied password.''')
|
||||
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
|
||||
def populate_cmd(cmd):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
password = get_password(args)
|
||||
cmd = {'version': [0, 20, 0]} # use a random version that's fairly old
|
||||
populate_cmd(cmd)
|
||||
if password:
|
||||
encrypt_cmd(cmd, password)
|
||||
|
||||
# cmd = {'version': [0, 14, 2], 'cmd': 'ls'}
|
||||
# cmd = encrypt_cmd(cmd, 'test')
|
||||
# with open(os.open(os.ctermid(), os.O_RDWR | os.O_CLOEXEC), 'w') as tty_file, raw_mode(tty_file.fileno()):
|
||||
# print(end=f'\x1bP@kitty-cmd{json.dumps(cmd)}\x1b\\', flush=True, file=tty_file)
|
||||
# os.read(tty_file.fileno(), 4096)
|
||||
@@ -68,11 +68,20 @@ for the options is::
|
||||
└──────────────┴───────────────┘
|
||||
|
||||
In addition, you can map keys to increase or decrease the number of full-height
|
||||
windows, for example::
|
||||
windows, or toggle the mirrored setting, for example::
|
||||
|
||||
map ctrl+[ layout_action decrease_num_full_size_windows
|
||||
map ctrl+] layout_action increase_num_full_size_windows
|
||||
map ctrl+/ layout_action mirror toggle
|
||||
map ctrl+y layout_action mirror true
|
||||
map ctrl+n layout_action mirror false
|
||||
|
||||
You can also map a key to change the bias by providing a list of percentages
|
||||
and it will rotate through the list as you press the key. If you only provide
|
||||
one number it'll toggle between that percentage and 50, for example::
|
||||
|
||||
map ctrl+. layout_action bias 50 62 70
|
||||
map ctrl+, layout_action bias 62
|
||||
|
||||
The Fat Layout
|
||||
----------------
|
||||
@@ -101,8 +110,7 @@ on the bottom of the screen instead of the top. The syntax for the options is::
|
||||
└─────────┴──────────┴─────────┘
|
||||
|
||||
|
||||
This layout also supports ``decrease_num_full_size_windows`` layout action like
|
||||
the *Tall* layout, shown above.
|
||||
This layout also supports the same layout actions as the *Tall* layout, shown above.
|
||||
|
||||
|
||||
The Grid Layout
|
||||
|
||||
@@ -64,6 +64,10 @@ some special variables, documented below:
|
||||
``FRAGMENT``
|
||||
The fragment (unquoted), if any of the URL or the empty string.
|
||||
|
||||
``URL_PATH``
|
||||
The path, query and fragment portions of the URL, without any
|
||||
unquoting.
|
||||
|
||||
|
||||
.. note::
|
||||
You can use the :opt:`action_alias` option just as in :file:`kitty.conf` to
|
||||
|
||||
@@ -161,6 +161,8 @@ option in :file:`kitty.conf`. For example:
|
||||
# Set the --class for the new OS window
|
||||
os_window_class mywindow
|
||||
launch sh
|
||||
# Resize the current window (see the resize_window action for details)
|
||||
resize_window wider 2
|
||||
# Make the current window the active (focused) window
|
||||
focus
|
||||
launch emacs
|
||||
|
||||
@@ -7,13 +7,15 @@ form::
|
||||
|
||||
<ESC>P@kitty-cmd<JSON object><ESC>\
|
||||
|
||||
Where ``<ESC>`` is the byte ``0x1b``. The JSON object has the form::
|
||||
Where ``<ESC>`` is the byte ``0x1b``. The JSON object has the form:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"cmd": "command name",
|
||||
"version": <kitty version>,
|
||||
"no_response": <Optional Boolean>,
|
||||
"payload": <Optional JSON object>,
|
||||
"version": "<kitty version>",
|
||||
"no_response": "<Optional Boolean>",
|
||||
"payload": "<Optional JSON object>"
|
||||
}
|
||||
|
||||
The ``version`` above is an array of the form :code:`[0, 14, 2]`. If you are
|
||||
@@ -38,4 +40,41 @@ with the following command line::
|
||||
|
||||
echo -en '\eP@kitty-cmd{"cmd":"ls","version":[0,14,2]}\e\\' | socat - unix:/tmp/test | awk '{ print substr($0, 13, length($0) - 14) }' | jq -c '.data | fromjson' | jq .
|
||||
|
||||
.. _rc_crypto:
|
||||
|
||||
Encrypted communication
|
||||
--------------------------
|
||||
|
||||
.. versionadded:: 0.26.0
|
||||
|
||||
When using the :opt:`remote_control_password` option communication to the
|
||||
terminal is encrypted to keep the password secure. A public key is used from
|
||||
the :envvar:`KITTY_PUBLIC_KEY` environment variable. Currently, only one
|
||||
encryption protocol is supported. The protocol number is present in
|
||||
:envvar:`KITTY_PUBLIC_KEY` as ``1``. The key data in this environment variable
|
||||
is :rfc:`Base-85 <1924>` encoded. The algorithm used is `Elliptic Curve Diffie
|
||||
Helman <https://en.wikipedia.org/wiki/Elliptic-curve_Diffie–Hellman>`__ with
|
||||
the `X25519 curve <https://en.wikipedia.org/wiki/Curve25519>`__. A time based
|
||||
nonce is used to minimise replay attacks. The original JSON command has the
|
||||
fields: ``password`` and ``timestamp`` added. The timestamp is the number of
|
||||
nanoseconds since the epoch, excluding leap seconds. Commands with a timestamp
|
||||
more than 5 minutes from the current time are rejected. The command is then
|
||||
encrypted using AES-256-GCM in authenticated encryption mode, with a symmetric
|
||||
key that is derived from the ECDH key-pair by running the shared secret through
|
||||
SHA-256 hashing, once. An IV of at least 96 bits of CSPRNG data is used. The
|
||||
tag for authenticated encryption **must** be at least 128 bits long. The tag
|
||||
**must** authenticate only the value of the ``encrypted`` field. A new command
|
||||
is created and transmitted that contains the fields:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"version": "<kitty version>",
|
||||
"iv": "base85 encoded IV",
|
||||
"tag": "base85 encoded AEAD tag",
|
||||
"pubkey": "base85 encoded ECDH public key of sender",
|
||||
"encrypted": "The original command encrypted and base85 encoded"
|
||||
}
|
||||
|
||||
|
||||
.. include:: generated/rc.rst
|
||||
|
||||
@@ -17,8 +17,9 @@ Start by running |kitty| as::
|
||||
|
||||
kitty -o allow_remote_control=yes -o enabled_layouts=tall
|
||||
|
||||
In order for control to work, :opt:`allow_remote_control` must be enabled in
|
||||
:file:`kitty.conf`. Here we turn it on explicitly at the command line.
|
||||
In order for control to work, :opt:`allow_remote_control` or
|
||||
:opt:`remote_control_password` must be enabled in :file:`kitty.conf`. Here we
|
||||
turn it on explicitly at the command line.
|
||||
|
||||
Now, in the new |kitty| window, enter the command::
|
||||
|
||||
@@ -35,12 +36,12 @@ This will make ``Hello, World`` show up in the window running the :program:`cat`
|
||||
program. The :option:`kitty @ send-text --match` option is very powerful, it
|
||||
allows selecting windows by their titles, the command line of the program
|
||||
running in the window, the working directory of the program running in the
|
||||
window, etc. See :ref:`kitty @ send-text --help <at_send-text>` for details.
|
||||
window, etc. See :ref:`kitty @ send-text --help <at-send-text>` for details.
|
||||
|
||||
More usefully, you can pipe the output of a command running in one window to
|
||||
another window, for example::
|
||||
|
||||
ls | kitty @ send-text --match title:^Output --stdin
|
||||
ls | kitty @ send-text --match 'title:^Output' --stdin
|
||||
|
||||
This will show the output of :program:`ls` in the output window instead of the
|
||||
current window. You can use this technique to, for example, show the output of
|
||||
@@ -49,7 +50,7 @@ are endless.
|
||||
|
||||
You can even have things you type show up in a different window. Run::
|
||||
|
||||
kitty @ send-text --match title:^Output --stdin
|
||||
kitty @ send-text --match 'title:^Output' --stdin
|
||||
|
||||
And type some text, it will show up in the output window, instead of the current
|
||||
window. Type :kbd:`Ctrl+D` when you are ready to stop.
|
||||
@@ -61,7 +62,7 @@ Now, let's open a new tab::
|
||||
This will open a new tab running the bash shell with the title "My Tab".
|
||||
We can change the title of the tab to "New Title" with::
|
||||
|
||||
kitty @ set-tab-title --match title:^My New Title
|
||||
kitty @ set-tab-title --match 'title:^My' New Title
|
||||
|
||||
Let's change the title of the current tab::
|
||||
|
||||
@@ -69,12 +70,12 @@ Let's change the title of the current tab::
|
||||
|
||||
Now lets switch to the newly opened tab::
|
||||
|
||||
kitty @ focus-tab --match title:^New
|
||||
kitty @ focus-tab --match 'title:^New'
|
||||
|
||||
Similarly, to focus the previously opened output window (which will also switch
|
||||
back to the old tab, automatically)::
|
||||
|
||||
kitty @ focus-window --match title:^Output
|
||||
kitty @ focus-window --match 'title:^Output'
|
||||
|
||||
You can get a listing of available tabs and windows, by running::
|
||||
|
||||
@@ -92,11 +93,23 @@ As you can see, it is very easy to control |kitty| using the ``kitty @``
|
||||
messaging system. This tutorial touches only the surface of what is possible.
|
||||
See ``kitty @ --help`` for more details.
|
||||
|
||||
Note that in the example's above, ``kitty @`` messaging works only when run
|
||||
In the example's above, ``kitty @`` messaging works only when run
|
||||
inside a |kitty| window, not anywhere. But, within a |kitty| window it even
|
||||
works over SSH. If you want to control |kitty| from programs/scripts not running
|
||||
inside a |kitty| window, you have to implement a couple of extra steps. First
|
||||
start |kitty| as::
|
||||
inside a |kitty| window, see the section on :ref:`using a socket for remote control <rc_via_socket>`
|
||||
below.
|
||||
|
||||
|
||||
Note that if all you want to do is run a single |kitty| "daemon" and have
|
||||
subsequent |kitty| invocations appear as new top-level windows, you can use the
|
||||
simpler :option:`kitty --single-instance` option, see ``kitty --help`` for that.
|
||||
|
||||
|
||||
.. _rc_via_socket:
|
||||
|
||||
Remote control via a socket
|
||||
--------------------------------
|
||||
First, start |kitty| as::
|
||||
|
||||
kitty -o allow_remote_control=yes --listen-on unix:/tmp/mykitty
|
||||
|
||||
@@ -108,11 +121,6 @@ command line argument to ``kitty @``. For example::
|
||||
kitty @ --to unix:/tmp/mykitty ls
|
||||
|
||||
|
||||
Note that if all you want to do is run a single |kitty| "daemon" and have
|
||||
subsequent |kitty| invocations appear as new top-level windows, you can use the
|
||||
simpler :option:`kitty --single-instance` option, see ``kitty --help`` for that.
|
||||
|
||||
|
||||
The builtin kitty shell
|
||||
--------------------------
|
||||
|
||||
@@ -147,6 +155,112 @@ other computers (for example, over SSH) or as other users.
|
||||
what programs you run in such windows, since they can effectively control
|
||||
kitty, as if you were running with :opt:`allow_remote_control` turned on.
|
||||
|
||||
You can further restrict what is allowed in these windows by using
|
||||
:option:`kitty @ launch --remote-control-password`.
|
||||
|
||||
|
||||
Fine grained permissions for remote control
|
||||
----------------------------------------------
|
||||
|
||||
.. versionadded:: 0.26.0
|
||||
|
||||
The :opt:`allow_remote_control` option discussed so far is a blunt
|
||||
instrument, granting the ability to any program running on your computer
|
||||
or even on remote computers via SSH the ability to use remote control.
|
||||
|
||||
You can instead define remote control passwords that can be used to grant
|
||||
different levels of control to different places. You can even write your
|
||||
own script to decide which remote control requests are allowed. This is
|
||||
done using the :opt:`remote_control_password` option in :file:`kitty.conf`.
|
||||
Set :opt:`allow_remote_control` to :code:`password` to use this feature.
|
||||
Let's see some examples:
|
||||
|
||||
.. code-block:: conf
|
||||
|
||||
remote_control_password "control colors" get-colors set-colors
|
||||
|
||||
Now, using this password, you can, in scripts run the command::
|
||||
|
||||
kitty @ --password="control colors" set-colors background=red
|
||||
|
||||
Any script with access to the password can now change colors in kitty using
|
||||
remote control, but only that and nothing else. You can even supply the
|
||||
password via the :envvar:`KITTY_RC_PASSWORD` environment variable, or the
|
||||
file :file:`~/.config/kitty/rc-password` to avoid having to type it repeatedly.
|
||||
See :option:`kitty @ --password-file` and :option:`kitty @ --password-env`.
|
||||
|
||||
The :opt:`remote_control_password` can be specified multiple times to create
|
||||
different passwords with different capabilities. Run the following to get a
|
||||
list of all action names::
|
||||
|
||||
kitty @ --help
|
||||
|
||||
You can even use glob patterns to match action names, for example:
|
||||
|
||||
.. code-block:: conf
|
||||
|
||||
remote_control_password "control colors" *-colors
|
||||
|
||||
If no action names are specified, all actions are allowed.
|
||||
|
||||
If ``kitty @`` is run with a password that is not present in
|
||||
:file:`kitty.conf`, then kitty will interactively prompt the user to allow or
|
||||
disallow the remote control request. The user can choose to allow or disallow
|
||||
either just that request or all requests using that password. The user's
|
||||
decision is remembered for the duration of that kitty instance.
|
||||
|
||||
.. note::
|
||||
For password based authentication to work over SSH, you must pass the
|
||||
:envvar:`KITTY_PUBLIC_KEY` environment variable to the remote host. The
|
||||
:doc:`ssh kitten <kittens/ssh>` does this for you automatically. When
|
||||
using a password, :ref:`rc_crypto` is used to ensure the password
|
||||
is kept secure. This does mean that using password based authentication
|
||||
is slower as the entire command is encrypted before transmission. This
|
||||
can be noticeable when using a command like ``kitty @ set-background-image``
|
||||
which transmits large amounts of image data. Also, the clock on the remote
|
||||
system must match (within a few minutes) the clock on the local system.
|
||||
kitty uses a time based nonce to minimise the potential for replay attacks.
|
||||
|
||||
.. _rc_custom_auth:
|
||||
|
||||
Customizing authorization with your own program
|
||||
____________________________________________________________
|
||||
|
||||
If the ability to control access by action names is not fine grained enough,
|
||||
you can define your own Python script to examine every remote control command
|
||||
and allow/disallow it. To do so create a file in the kitty configuration
|
||||
directory, :file:`~/.config/kitty/my_rc_auth.py` and add the following
|
||||
to :file:`kitty.conf`:
|
||||
|
||||
.. code-block:: conf
|
||||
|
||||
remote_control_password "testing custom auth" my_rc_auth.py
|
||||
|
||||
:file:`my_rc_auth.py` should define a :code:`is_cmd_allowed` function
|
||||
as shown below:
|
||||
|
||||
.. code-block:: py
|
||||
|
||||
def is_cmd_allowed(pcmd, window, from_socket, extra_data):
|
||||
cmd_name = pcmd['cmd'] # the name of the command
|
||||
cmd_payload = pcmd['payload'] # the arguments to the command
|
||||
# examine the cmd_name and cmd_payload and return True to allow
|
||||
# the command or False to disallow it. Return None to have no
|
||||
# effect on the command.
|
||||
|
||||
# The command payload will vary from command to command, see
|
||||
# the rc protocol docs for details. Below is an example of
|
||||
# restricting the launch command to allow only running the
|
||||
# default shell.
|
||||
|
||||
if cmd_name != 'launch':
|
||||
return None
|
||||
if cmd_payload.get('args') or cmd_payload.get('env') or cmd_payload.get('copy_cmdline') or cmd_payload.get('copy_env'):
|
||||
return False
|
||||
# prints in this function go to the parent kitty process STDOUT
|
||||
print('Allowing launch command:', cmd_payload)
|
||||
return True
|
||||
|
||||
|
||||
.. _rc_mapping:
|
||||
|
||||
@@ -162,6 +276,12 @@ Then pressing the :kbd:`F1` key will set the active window margins to
|
||||
:code:`30`. The syntax for what follows :ac:`remote_control` is exactly the same
|
||||
as the syntax for what follows :code:`kitty @` above.
|
||||
|
||||
If you wish to ignore errors from the command, prefix the command with an
|
||||
``!``. For example, the following will not return an error when no windows
|
||||
are matched::
|
||||
|
||||
map f1 remote_control !focus-window --match XXXXXX
|
||||
|
||||
.. note:: You do not need :opt:`allow_remote_control` to use these mappings,
|
||||
as they are not actual remote programs, but are simply a way to resuse the
|
||||
remote control infrastructure via keybings.
|
||||
|
||||
@@ -41,7 +41,7 @@ Features
|
||||
* Sophisticated completion for the :program:`kitty` command in the shell.
|
||||
|
||||
* When confirming a quit command if a window is sitting at a shell prompt,
|
||||
it is optionally, not counted (see :opt:`confirm_os_window_close`)
|
||||
it is not counted (for details, see :opt:`confirm_os_window_close`)
|
||||
|
||||
|
||||
Configuration
|
||||
@@ -274,8 +274,8 @@ Edit files in new kitty windows even over SSH
|
||||
|
||||
edit-in-kitty myfile.txt
|
||||
edit-in-kitty --type tab --title "Editing My File" myfile.txt
|
||||
# open file.txt at line 75 (works with vim, emacs, nano, micro)
|
||||
edit-in-kitty +75 file.txt
|
||||
# open myfile.txt at line 75 (works with vim, neovim, emacs, nano, micro)
|
||||
edit-in-kitty +75 myfile.txt
|
||||
|
||||
The :command:`edit-in-kitty` command allows you to seamlessly edit files
|
||||
in your default :opt:`editor` in new kitty windows. This works even over
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!./kitty/launcher/kitty +launch
|
||||
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
|
||||
@@ -770,6 +770,11 @@ is_useful_apple_global_shortcut(int sc) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
is_apple_jis_layout_function_key(NSEvent *event) {
|
||||
return [event keyCode] == 0x66 /* kVK_JIS_Eisu */ || [event keyCode] == 0x68 /* kVK_JIS_Kana */;
|
||||
}
|
||||
|
||||
GLFWAPI GLFWapplicationshouldhandlereopenfun glfwSetApplicationShouldHandleReopen(GLFWapplicationshouldhandlereopenfun callback) {
|
||||
GLFWapplicationshouldhandlereopenfun previous = handle_reopen_callback;
|
||||
handle_reopen_callback = callback;
|
||||
@@ -837,6 +842,14 @@ int _glfwPlatformInit(void)
|
||||
last_keydown_shortcut_event.timestamp = [event timestamp];
|
||||
return event;
|
||||
}
|
||||
// check for JIS keyboard layout function keys
|
||||
if (is_apple_jis_layout_function_key(event)) {
|
||||
debug_key("keyDown triggerred JIS layout function key ignoring\n");
|
||||
last_keydown_shortcut_event.virtual_key_code = [event keyCode];
|
||||
last_keydown_shortcut_event.input_source_switch_modifiers = 0;
|
||||
last_keydown_shortcut_event.timestamp = [event timestamp];
|
||||
return event;
|
||||
}
|
||||
}
|
||||
last_keydown_shortcut_event.virtual_key_code = 0xffff;
|
||||
NSWindow *kw = [NSApp keyWindow];
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "internal.h"
|
||||
#include "../kitty/monotonic.h"
|
||||
|
||||
#include <Availability.h>
|
||||
#include <float.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -1540,6 +1541,39 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
|
||||
debug_key("\n\tdoCommandBySelector: (%s)\n", [NSStringFromSelector(selector) UTF8String]);
|
||||
}
|
||||
|
||||
- (BOOL)isAccessibilityElement
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isAccessibilitySelectorAllowed:(SEL)selector
|
||||
{
|
||||
if (selector == @selector(accessibilityRole) || selector == @selector(accessibilitySelectedText)) return YES;
|
||||
return NO;
|
||||
}
|
||||
|
||||
#if (TARGET_OS_OSX && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101400)
|
||||
|
||||
- (NSAccessibilityRole)accessibilityRole
|
||||
{
|
||||
return NSAccessibilityTextAreaRole;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
- (NSString *)accessibilitySelectedText
|
||||
{
|
||||
NSString *text = nil;
|
||||
if (_glfw.callbacks.get_current_selection) {
|
||||
char *s = _glfw.callbacks.get_current_selection();
|
||||
if (s) {
|
||||
text = [NSString stringWithUTF8String:s];
|
||||
free(s);
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
@end
|
||||
// }}}
|
||||
|
||||
@@ -2479,6 +2513,13 @@ void _glfwPlatformSetClipboardString(const char* string)
|
||||
[pasteboard setString:@(string) forType:NSPasteboardTypeString];
|
||||
}
|
||||
|
||||
void _glfwPlatformSetPrimarySelectionString(const char* string) {
|
||||
(void)string;
|
||||
// Apple doesnt have a primary selection
|
||||
}
|
||||
|
||||
const char* _glfwPlatformGetPrimarySelectionString(void) { return ""; }
|
||||
|
||||
const char* _glfwPlatformGetClipboardString(void)
|
||||
{
|
||||
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
|
||||
|
||||
9
glfw/context.c
vendored
9
glfw/context.c
vendored
@@ -396,15 +396,6 @@ bool _glfwRefreshContextAttribs(_GLFWwindow* window,
|
||||
window->context.release = GLFW_RELEASE_BEHAVIOR_FLUSH;
|
||||
}
|
||||
|
||||
// Clearing the front buffer to black to avoid garbage pixels left over from
|
||||
// previous uses of our bit of VRAM
|
||||
{
|
||||
PFNGLCLEARPROC glClear = (PFNGLCLEARPROC)
|
||||
window->context.getProcAddress("glClear");
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
window->context.swapBuffers(window);
|
||||
}
|
||||
|
||||
glfwMakeContextCurrent((GLFWwindow*) previous);
|
||||
return true;
|
||||
}
|
||||
|
||||
4
glfw/egl_context.c
vendored
4
glfw/egl_context.c
vendored
@@ -428,6 +428,8 @@ bool _glfwInitEGL(void)
|
||||
extensionSupportedEGL("EGL_KHR_get_all_proc_addresses");
|
||||
_glfw.egl.KHR_context_flush_control =
|
||||
extensionSupportedEGL("EGL_KHR_context_flush_control");
|
||||
_glfw.egl.EXT_present_opaque =
|
||||
extensionSupportedEGL("EGL_EXT_present_opaque");
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -598,6 +600,8 @@ bool _glfwCreateContextEGL(_GLFWwindow* window,
|
||||
if (_glfw.egl.KHR_gl_colorspace)
|
||||
setAttrib(EGL_GL_COLORSPACE_KHR, EGL_GL_COLORSPACE_SRGB_KHR);
|
||||
}
|
||||
if (_glfw.egl.EXT_present_opaque)
|
||||
setAttrib(EGL_PRESENT_OPAQUE_EXT, !fbconfig->transparent);
|
||||
|
||||
setAttrib(EGL_NONE, EGL_NONE);
|
||||
|
||||
|
||||
9
glfw/egl_context.h
vendored
9
glfw/egl_context.h
vendored
@@ -160,11 +160,11 @@ typedef EGLSurface (EGLAPIENTRY * PFNEGLCREATEPLATFORMWINDOWSURFACEEXTPROC)(EGLD
|
||||
//
|
||||
typedef struct _GLFWcontextEGL
|
||||
{
|
||||
EGLConfig config;
|
||||
EGLContext handle;
|
||||
EGLSurface surface;
|
||||
EGLConfig config;
|
||||
EGLContext handle;
|
||||
EGLSurface surface;
|
||||
|
||||
void* client;
|
||||
void* client;
|
||||
|
||||
} _GLFWcontextEGL;
|
||||
|
||||
@@ -186,6 +186,7 @@ typedef struct _GLFWlibraryEGL
|
||||
bool EXT_platform_base;
|
||||
bool EXT_platform_x11;
|
||||
bool EXT_platform_wayland;
|
||||
bool EXT_present_opaque;
|
||||
bool ANGLE_platform_angle;
|
||||
bool ANGLE_platform_angle_opengl;
|
||||
bool ANGLE_platform_angle_d3d;
|
||||
|
||||
4
glfw/glfw3.h
vendored
4
glfw/glfw3.h
vendored
@@ -512,6 +512,7 @@ typedef enum {
|
||||
* GLFW_LOCK_KEY_MODS input mode is set.
|
||||
*/
|
||||
#define GLFW_MOD_NUM_LOCK 0x0080
|
||||
#define GLFW_MOD_LAST GLFW_MOD_NUM_LOCK
|
||||
#define GLFW_LOCK_MASK (GLFW_MOD_NUM_LOCK | GLFW_MOD_CAPS_LOCK)
|
||||
|
||||
/*! @} */
|
||||
@@ -1716,6 +1717,7 @@ typedef void (* GLFWjoystickfun)(int,int);
|
||||
typedef void (* GLFWuserdatafun)(unsigned long long, void*);
|
||||
typedef void (* GLFWtickcallback)(void*);
|
||||
typedef bool (* GLFWdrawtextfun)(GLFWwindow *window, const char *text, uint32_t fg, uint32_t bg, uint8_t *output_buf, size_t width, size_t height, float x_offset, float y_offset, size_t right_margin);
|
||||
typedef char* (* GLFWcurrentselectionfun)(void);
|
||||
|
||||
/*! @brief Video mode type.
|
||||
*
|
||||
@@ -1873,6 +1875,7 @@ GLFWAPI unsigned long long glfwAddTimer(monotonic_t interval, bool repeats, GLFW
|
||||
GLFWAPI void glfwUpdateTimer(unsigned long long timer_id, monotonic_t interval, bool enabled);
|
||||
GLFWAPI void glfwRemoveTimer(unsigned long long);
|
||||
GLFWAPI GLFWdrawtextfun glfwSetDrawTextFunction(GLFWdrawtextfun function);
|
||||
GLFWAPI GLFWcurrentselectionfun glfwSetCurrentSelectionCallback(GLFWcurrentselectionfun callback);
|
||||
|
||||
/*! @brief Terminates the GLFW library.
|
||||
*
|
||||
@@ -2768,6 +2771,7 @@ GLFWAPI void glfwWindowHintString(int hint, const char* value);
|
||||
GLFWAPI GLFWwindow* glfwCreateWindow(int width, int height, const char* title, GLFWmonitor* monitor, GLFWwindow* share);
|
||||
GLFWAPI bool glfwToggleFullscreen(GLFWwindow *window, unsigned int flags);
|
||||
GLFWAPI bool glfwIsFullscreen(GLFWwindow *window, unsigned int flags);
|
||||
GLFWAPI bool glfwAreSwapsAllowed(const GLFWwindow* window);
|
||||
|
||||
/*! @brief Destroys the specified window and its context.
|
||||
*
|
||||
|
||||
7
glfw/init.c
vendored
7
glfw/init.c
vendored
@@ -386,3 +386,10 @@ GLFWAPI GLFWdrawtextfun glfwSetDrawTextFunction(GLFWdrawtextfun cbfun)
|
||||
_GLFW_SWAP_POINTERS(_glfw.callbacks.draw_text, cbfun);
|
||||
return cbfun;
|
||||
}
|
||||
|
||||
GLFWAPI GLFWcurrentselectionfun glfwSetCurrentSelectionCallback(GLFWcurrentselectionfun cbfun)
|
||||
{
|
||||
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
|
||||
_GLFW_SWAP_POINTERS(_glfw.callbacks.get_current_selection, cbfun);
|
||||
return cbfun;
|
||||
}
|
||||
|
||||
2
glfw/input.c
vendored
2
glfw/input.c
vendored
@@ -1540,7 +1540,7 @@ GLFWAPI const char* glfwGetClipboardString(GLFWwindow* handle UNUSED)
|
||||
return _glfwPlatformGetClipboardString();
|
||||
}
|
||||
|
||||
#if defined(_GLFW_X11) || defined(_GLFW_WAYLAND)
|
||||
#if defined(_GLFW_X11) || defined(_GLFW_WAYLAND) || defined(__APPLE__)
|
||||
GLFWAPI void glfwSetPrimarySelectionString(GLFWwindow* handle UNUSED, const char* string)
|
||||
{
|
||||
assert(string != NULL);
|
||||
|
||||
10
glfw/internal.h
vendored
10
glfw/internal.h
vendored
@@ -52,6 +52,8 @@
|
||||
#define GLFW_INCLUDE_NONE
|
||||
#include "glfw3.h"
|
||||
|
||||
#define EGL_PRESENT_OPAQUE_EXT 0x31df
|
||||
|
||||
#define _GLFW_INSERT_FIRST 0
|
||||
#define _GLFW_INSERT_LAST 1
|
||||
|
||||
@@ -439,6 +441,11 @@ struct _GLFWwindow
|
||||
bool rawMouseMotion;
|
||||
|
||||
_GLFWcontext context;
|
||||
#ifdef _GLFW_WAYLAND
|
||||
bool swaps_disallowed;
|
||||
#else
|
||||
const bool swaps_disallowed;
|
||||
#endif
|
||||
|
||||
struct {
|
||||
GLFWwindowposfun pos;
|
||||
@@ -614,6 +621,7 @@ struct _GLFWlibrary
|
||||
GLFWjoystickfun joystick;
|
||||
GLFWapplicationclosefun application_close;
|
||||
GLFWdrawtextfun draw_text;
|
||||
GLFWcurrentselectionfun get_current_selection;
|
||||
} callbacks;
|
||||
|
||||
|
||||
@@ -668,7 +676,7 @@ void _glfwPlatformSetGammaRamp(_GLFWmonitor* monitor, const GLFWgammaramp* ramp)
|
||||
|
||||
void _glfwPlatformSetClipboardString(const char* string);
|
||||
const char* _glfwPlatformGetClipboardString(void);
|
||||
#if defined(_GLFW_X11) || defined(_GLFW_WAYLAND)
|
||||
#if defined(_GLFW_X11) || defined(_GLFW_WAYLAND) || defined(__APPLE__)
|
||||
void _glfwPlatformSetPrimarySelectionString(const char* string);
|
||||
const char* _glfwPlatformGetPrimarySelectionString(void);
|
||||
#endif
|
||||
|
||||
4
glfw/linux_desktop_settings.c
vendored
4
glfw/linux_desktop_settings.c
vendored
@@ -83,8 +83,8 @@ process_settings_dict(DBusMessageIter *array_iter, void(process_setting)(const c
|
||||
|
||||
HANDLER(process_desktop_settings)
|
||||
cursor_theme_changed = false;
|
||||
DBusMessageIter root, array, item, settings;
|
||||
dbus_message_iter_init(msg, &root);
|
||||
DBusMessageIter root, array, item, settings;
|
||||
dbus_message_iter_init(msg, &root);
|
||||
#define die(...) { _glfwInputError(GLFW_PLATFORM_ERROR, __VA_ARGS__); return; }
|
||||
if (dbus_message_iter_get_arg_type(&root) != DBUS_TYPE_ARRAY) die("Reply to request for desktop settings is not an array");
|
||||
dbus_message_iter_recurse(&root, &array);
|
||||
|
||||
4
glfw/window.c
vendored
4
glfw/window.c
vendored
@@ -1067,6 +1067,10 @@ GLFWAPI bool glfwIsFullscreen(GLFWwindow* wh, unsigned int flags) {
|
||||
return _glfwPlatformIsFullscreen((_GLFWwindow*)wh, flags);
|
||||
}
|
||||
|
||||
GLFWAPI bool glfwAreSwapsAllowed(const GLFWwindow* wh) {
|
||||
return !(((_GLFWwindow*)wh)->swaps_disallowed);
|
||||
}
|
||||
|
||||
GLFWAPI void glfwSetWindowUserPointer(GLFWwindow* handle, void* pointer)
|
||||
{
|
||||
_GLFWwindow* window = (_GLFWwindow*) handle;
|
||||
|
||||
14
glfw/wl_client_side_decorations.c
vendored
14
glfw/wl_client_side_decorations.c
vendored
@@ -31,13 +31,13 @@ static void
|
||||
build_blur_kernel(kernel_type *blur_kernel, const size_t size, kernel_type sigma) {
|
||||
// 1D Normalized Gaussian
|
||||
const kernel_type half = size / (kernel_type)2;
|
||||
kernel_type sum = 0;
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
kernel_type f = (i - half);
|
||||
blur_kernel[i] = (kernel_type)exp(- f * f / sigma);
|
||||
sum += blur_kernel[i];
|
||||
}
|
||||
for (size_t i = 0; i < size; i++) blur_kernel[i] /= sum;
|
||||
kernel_type sum = 0;
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
kernel_type f = (i - half);
|
||||
blur_kernel[i] = (kernel_type)exp(- f * f / sigma);
|
||||
sum += blur_kernel[i];
|
||||
}
|
||||
for (size_t i = 0; i < size; i++) blur_kernel[i] /= sum;
|
||||
}
|
||||
|
||||
static void
|
||||
|
||||
10
glfw/wl_init.c
vendored
10
glfw/wl_init.c
vendored
@@ -573,10 +573,7 @@ static void seatHandleCapabilities(void* data UNUSED,
|
||||
{
|
||||
wl_pointer_destroy(_glfw.wl.pointer);
|
||||
_glfw.wl.pointer = NULL;
|
||||
if (_glfw.wl.cursorAnimationTimer) {
|
||||
removeTimer(&_glfw.wl.eventLoopData, _glfw.wl.cursorAnimationTimer);
|
||||
_glfw.wl.cursorAnimationTimer = 0;
|
||||
}
|
||||
if (_glfw.wl.cursorAnimationTimer) toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.cursorAnimationTimer, 0);
|
||||
}
|
||||
|
||||
if ((caps & WL_SEAT_CAPABILITY_KEYBOARD) && !_glfw.wl.keyboard)
|
||||
@@ -589,10 +586,7 @@ static void seatHandleCapabilities(void* data UNUSED,
|
||||
wl_keyboard_destroy(_glfw.wl.keyboard);
|
||||
_glfw.wl.keyboard = NULL;
|
||||
_glfw.wl.keyboardFocusId = 0;
|
||||
if (_glfw.wl.keyRepeatInfo.keyRepeatTimer) {
|
||||
removeTimer(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer);
|
||||
_glfw.wl.keyRepeatInfo.keyRepeatTimer = 0;
|
||||
}
|
||||
if (_glfw.wl.keyRepeatInfo.keyRepeatTimer) toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
12
glfw/wl_platform.h
vendored
12
glfw/wl_platform.h
vendored
@@ -116,12 +116,12 @@ typedef enum WaylandWindowState {
|
||||
TOPLEVEL_STATE_NONE = 0,
|
||||
TOPLEVEL_STATE_MAXIMIZED = 1,
|
||||
TOPLEVEL_STATE_FULLSCREEN = 2,
|
||||
TOPLEVEL_STATE_RESIZING = 4,
|
||||
TOPLEVEL_STATE_ACTIVATED = 8,
|
||||
TOPLEVEL_STATE_TILED_LEFT = 16,
|
||||
TOPLEVEL_STATE_TILED_RIGHT = 32,
|
||||
TOPLEVEL_STATE_TILED_TOP = 64,
|
||||
TOPLEVEL_STATE_TILED_BOTTOM = 128,
|
||||
TOPLEVEL_STATE_RESIZING = 4,
|
||||
TOPLEVEL_STATE_ACTIVATED = 8,
|
||||
TOPLEVEL_STATE_TILED_LEFT = 16,
|
||||
TOPLEVEL_STATE_TILED_RIGHT = 32,
|
||||
TOPLEVEL_STATE_TILED_TOP = 64,
|
||||
TOPLEVEL_STATE_TILED_BOTTOM = 128,
|
||||
} WaylandWindowState;
|
||||
|
||||
|
||||
|
||||
2
glfw/wl_text_input.c
vendored
2
glfw/wl_text_input.c
vendored
@@ -87,7 +87,7 @@ static void
|
||||
text_input_done(void *data UNUSED, struct zwp_text_input_v3 *txt_input UNUSED, uint32_t serial) {
|
||||
debug("text-input: done event: serial: %u current_commit_serial: %u\n", serial, commit_serial);
|
||||
if (serial != commit_serial) {
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: text_input_done serial mismatch, expected=%u got=%u\n", commit_serial, serial);
|
||||
if (serial > commit_serial) _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: text_input_done serial mismatch, expected=%u got=%u\n", commit_serial, serial);
|
||||
return;
|
||||
}
|
||||
if (pending_pre_edit) {
|
||||
|
||||
7
glfw/wl_window.c
vendored
7
glfw/wl_window.c
vendored
@@ -496,6 +496,10 @@ static void xdgSurfaceHandleConfigure(void* data,
|
||||
int height = window->wl.pending.height;
|
||||
if (!window->wl.surface_configured_once) {
|
||||
window->wl.surface_configured_once = true;
|
||||
// this will attach the buffer to the surface, the client is responsible for clearing the buffer to an appropriate blank
|
||||
window->swaps_disallowed = false;
|
||||
window->context.swapBuffers(window);
|
||||
|
||||
if (!width && !height && !new_states && !window->wl.decorations.serverSide && getenv("XAUTHORITY") && strstr(getenv("XAUTHORITY"), "mutter")) {
|
||||
// https://github.com/kovidgoyal/kitty/issues/4802
|
||||
debug("Ignoring first empty surface configure event on mutter.\n");
|
||||
@@ -784,6 +788,7 @@ int _glfwPlatformCreateWindow(_GLFWwindow* window,
|
||||
initialize_csd_metrics(window);
|
||||
window->wl.transparent = fbconfig->transparent;
|
||||
strncpy(window->wl.appId, wndconfig->wl.appId, sizeof(window->wl.appId));
|
||||
window->swaps_disallowed = true;
|
||||
|
||||
if (!createSurface(window, wndconfig))
|
||||
return false;
|
||||
@@ -1062,6 +1067,8 @@ void _glfwPlatformHideWindow(_GLFWwindow* window)
|
||||
xdg_surface_destroy(window->wl.xdg.surface);
|
||||
window->wl.xdg.toplevel = NULL;
|
||||
window->wl.xdg.surface = NULL;
|
||||
window->wl.surface_configured_once = false;
|
||||
window->swaps_disallowed = true;
|
||||
}
|
||||
window->wl.visible = false;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
from typing import (
|
||||
TYPE_CHECKING, Callable, Dict, List, NamedTuple, Optional, Tuple
|
||||
TYPE_CHECKING, Callable, Dict, Iterator, List, NamedTuple, Optional, Tuple
|
||||
)
|
||||
|
||||
from kitty.cli import parse_args
|
||||
@@ -16,11 +17,12 @@ from kitty.typing import BossType, KeyEventType, TypedDict
|
||||
from kitty.utils import ScreenSize
|
||||
|
||||
from ..tui.handler import Handler, result_handler
|
||||
from ..tui.loop import Loop, MouseEvent
|
||||
from ..tui.loop import Loop, MouseEvent, debug
|
||||
from ..tui.operations import MouseTracking, alternate_screen, styled
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import readline
|
||||
debug
|
||||
else:
|
||||
readline = None
|
||||
|
||||
@@ -108,6 +110,15 @@ The default choice is selected when the user presses the :kbd:`Enter` key.
|
||||
--prompt -p
|
||||
default="> "
|
||||
The prompt to use when inputting a line of text or a password.
|
||||
|
||||
|
||||
--unhide-key
|
||||
default=u
|
||||
The key to be pressed to unhide hidden text
|
||||
|
||||
|
||||
--hidden-text-placeholder
|
||||
The text in the message to be replaced by hidden text. The hidden text is read via STDIN.
|
||||
'''
|
||||
|
||||
|
||||
@@ -198,6 +209,7 @@ class Choose(Handler):
|
||||
mouse_tracking = MouseTracking.buttons_only
|
||||
|
||||
def __init__(self, cli_opts: AskCLIOptions) -> None:
|
||||
self.prefix_style_pat = re.compile(r'(?:\x1b\[[^m]*?m)+')
|
||||
self.cli_opts = cli_opts
|
||||
self.choices: Dict[str, Choice] = {}
|
||||
self.clickable_ranges: Dict[str, List[Range]] = {}
|
||||
@@ -219,6 +231,18 @@ class Choose(Handler):
|
||||
self.response_on_accept = cli_opts.default or ''
|
||||
if cli_opts.type in ('yesno', 'choices') and self.response_on_accept not in self.allowed:
|
||||
self.response_on_accept = 'y' if cli_opts.type == 'yesno' else tuple(self.choices.keys())[0]
|
||||
self.message = cli_opts.message
|
||||
self.hidden_text_start_pos = self.hidden_text_end_pos = -1
|
||||
self.hidden_text = ''
|
||||
self.replacement_text = t = f'Press {styled(self.cli_opts.unhide_key, fg="green")} or click to show'
|
||||
self.replacement_range = Range(-1, -1, -1)
|
||||
if self.message and self.cli_opts.hidden_text_placeholder:
|
||||
self.hidden_text_start_pos = self.message.find(self.cli_opts.hidden_text_placeholder)
|
||||
if self.hidden_text_start_pos > -1:
|
||||
self.hidden_text = sys.stdin.read().rstrip()
|
||||
self.hidden_text_end_pos = self.hidden_text_start_pos + len(t)
|
||||
suffix = self.message[self.hidden_text_start_pos + len(self.cli_opts.hidden_text_placeholder):]
|
||||
self.message = self.message[:self.hidden_text_start_pos] + t + suffix
|
||||
|
||||
def initialize(self) -> None:
|
||||
self.cmd.set_cursor_visible(False)
|
||||
@@ -227,24 +251,35 @@ class Choose(Handler):
|
||||
def finalize(self) -> None:
|
||||
self.cmd.set_cursor_visible(True)
|
||||
|
||||
def draw_long_text(self, text: str) -> int:
|
||||
y = 0
|
||||
def draw_long_text(self, text: str) -> Iterator[str]:
|
||||
if not text:
|
||||
yield ''
|
||||
return
|
||||
width = self.screen_size.cols - 2
|
||||
m = self.prefix_style_pat.match(text)
|
||||
prefix = m.group() if m else ''
|
||||
while text:
|
||||
t, text = truncate_at_space(text, width)
|
||||
t = t.strip()
|
||||
self.print(' ' * extra_for(wcswidth(t), width), styled(t, bold=True), sep='')
|
||||
y += 1
|
||||
return y
|
||||
yield ' ' * extra_for(wcswidth(t), width) + styled(prefix + t, bold=True)
|
||||
|
||||
@Handler.atomic_update
|
||||
def draw_screen(self) -> None:
|
||||
self.cmd.clear_screen()
|
||||
y = max(0, self.screen_size.rows // 2 - 2)
|
||||
msg_lines: List[str] = []
|
||||
if self.message:
|
||||
for line in self.message.splitlines():
|
||||
msg_lines.extend(self.draw_long_text(line))
|
||||
y = self.screen_size.rows - len(msg_lines)
|
||||
y = max(0, (y // 2) - 2)
|
||||
self.print(end='\r\n'*y)
|
||||
if self.cli_opts.message:
|
||||
for line in self.cli_opts.message.splitlines():
|
||||
y += self.draw_long_text(line)
|
||||
for line in msg_lines:
|
||||
if self.replacement_text in line:
|
||||
idx = line.find(self.replacement_text)
|
||||
x = wcswidth(line[:idx])
|
||||
self.replacement_range = Range(x, x + wcswidth(self.replacement_text), y)
|
||||
self.print(line)
|
||||
y += 1
|
||||
if self.screen_size.rows > 2:
|
||||
self.print()
|
||||
y += 1
|
||||
@@ -259,7 +294,8 @@ class Choose(Handler):
|
||||
current_line_length = 0
|
||||
current_line: List[Tuple[str, str]] = []
|
||||
lines: List[List[Tuple[str, str]]] = []
|
||||
sep, sep_sz = ' ', 2
|
||||
sep = ' '
|
||||
sep_sz = len(sep) + 2 # for the borders
|
||||
|
||||
for choice in choices:
|
||||
self.clickable_ranges[choice.letter] = []
|
||||
@@ -272,7 +308,7 @@ class Choose(Handler):
|
||||
current_line = []
|
||||
current_line_length = 0
|
||||
current_line.append((choice.letter, text))
|
||||
current_line_length += sz
|
||||
current_line_length += sz + sep_sz
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
|
||||
@@ -311,10 +347,12 @@ class Choose(Handler):
|
||||
self.print(' ' * offset, line, sep='', end='' if is_last else '\r\n')
|
||||
y += 1
|
||||
|
||||
self.cmd.set_line_wrapping(False)
|
||||
for boxed_line in lines:
|
||||
print_line(top, *boxed_line)
|
||||
print_line(middle, *boxed_line)
|
||||
print_line(bottom, *boxed_line, is_last=boxed_line is lines[-1])
|
||||
self.cmd.set_line_wrapping(True)
|
||||
|
||||
def draw_choice(self, y: int) -> None:
|
||||
if y + 3 <= self.screen_size.rows:
|
||||
@@ -370,6 +408,14 @@ class Choose(Handler):
|
||||
self.quit_loop(0)
|
||||
elif self.cli_opts.type == 'yesno':
|
||||
self.on_interrupt()
|
||||
elif self.hidden_text and text == self.cli_opts.unhide_key:
|
||||
self.unhide()
|
||||
|
||||
def unhide(self) -> None:
|
||||
if self.hidden_text and self.message:
|
||||
self.message = self.message[:self.hidden_text_start_pos] + self.hidden_text + self.message[self.hidden_text_end_pos:]
|
||||
self.hidden_text = ''
|
||||
self.draw_screen()
|
||||
|
||||
def on_key(self, key_event: KeyEventType) -> None:
|
||||
if key_event.matches('esc'):
|
||||
@@ -385,6 +431,8 @@ class Choose(Handler):
|
||||
self.response = letter
|
||||
self.quit_loop(0)
|
||||
return
|
||||
if self.hidden_text and self.replacement_range.has_point(ev.cell_x, ev.cell_y):
|
||||
self.unhide()
|
||||
|
||||
def on_resize(self, screen_size: ScreenSize) -> None:
|
||||
self.screen_size = screen_size
|
||||
|
||||
@@ -231,11 +231,11 @@ static PyMethodDef module_methods[] = {
|
||||
};
|
||||
|
||||
static struct PyModuleDef module = {
|
||||
.m_base = PyModuleDef_HEAD_INIT,
|
||||
.m_name = "subseq_matcher", /* name of module */
|
||||
.m_doc = NULL,
|
||||
.m_size = -1,
|
||||
.m_methods = module_methods
|
||||
.m_base = PyModuleDef_HEAD_INIT,
|
||||
.m_name = "subseq_matcher", /* name of module */
|
||||
.m_doc = NULL,
|
||||
.m_size = -1,
|
||||
.m_methods = module_methods
|
||||
};
|
||||
|
||||
EXPORTED PyMODINIT_FUNC
|
||||
|
||||
@@ -1,56 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import codecs
|
||||
import io
|
||||
import os
|
||||
import select
|
||||
import sys
|
||||
from typing import List, NoReturn, Optional
|
||||
|
||||
from kitty.cli import parse_args
|
||||
from kitty.cli_stub import ClipboardCLIOptions
|
||||
from kitty.fast_data_types import parse_input_from_terminal
|
||||
|
||||
from ..tui.handler import Handler
|
||||
from ..tui.loop import Loop
|
||||
|
||||
|
||||
class Clipboard(Handler):
|
||||
|
||||
def __init__(self, data_to_send: Optional[bytes], args: ClipboardCLIOptions):
|
||||
self.args = args
|
||||
self.clipboard_contents: Optional[str] = None
|
||||
self.data_to_send = data_to_send
|
||||
self.quit_on_write = False
|
||||
|
||||
def initialize(self) -> None:
|
||||
if self.data_to_send is not None:
|
||||
self.cmd.write_to_clipboard(self.data_to_send, self.args.use_primary)
|
||||
if not self.args.get_clipboard:
|
||||
if self.args.wait_for_completion:
|
||||
# ask kitty for the TN terminfo capability and
|
||||
# only quit after a response is received
|
||||
self.print('\x1bP+q544e\x1b\\', end='')
|
||||
self.print('Waiting for completion...')
|
||||
return
|
||||
self.quit_on_write = True
|
||||
return
|
||||
self.cmd.request_from_clipboard(self.args.use_primary)
|
||||
|
||||
def on_writing_finished(self) -> None:
|
||||
if self.quit_on_write:
|
||||
self.quit_loop(0)
|
||||
|
||||
def on_clipboard_response(self, text: str, from_primary: bool = False) -> None:
|
||||
self.clipboard_contents = text
|
||||
self.quit_loop(0)
|
||||
|
||||
def on_capability_response(self, name: str, val: str) -> None:
|
||||
self.quit_loop(0)
|
||||
|
||||
def on_interrupt(self) -> None:
|
||||
self.quit_loop(1)
|
||||
|
||||
def on_eot(self) -> None:
|
||||
self.quit_loop(1)
|
||||
|
||||
from ..tui.operations import (
|
||||
raw_mode, request_from_clipboard, write_to_clipboard
|
||||
)
|
||||
|
||||
OPTIONS = r'''
|
||||
--get-clipboard
|
||||
@@ -84,6 +48,62 @@ popup, see :opt:`clipboard_control` for details.
|
||||
'''
|
||||
|
||||
usage = ''
|
||||
got_capability_response = False
|
||||
got_clipboard_response = False
|
||||
clipboard_contents = ''
|
||||
clipboard_from_primary = False
|
||||
|
||||
|
||||
def ignore(x: str) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def on_text(x: str) -> None:
|
||||
if '\x03' in x:
|
||||
raise KeyboardInterrupt()
|
||||
if '\x04' in x:
|
||||
raise EOFError()
|
||||
|
||||
|
||||
def on_dcs(dcs: str) -> None:
|
||||
global got_capability_response
|
||||
if dcs.startswith('1+r'):
|
||||
got_capability_response = True
|
||||
|
||||
|
||||
def on_osc(osc: str) -> None:
|
||||
global clipboard_contents, clipboard_from_primary, got_clipboard_response
|
||||
idx = osc.find(';')
|
||||
if idx <= 0:
|
||||
return
|
||||
q = osc[:idx]
|
||||
if q == '52':
|
||||
got_clipboard_response = True
|
||||
widx = osc.find(';', idx + 1)
|
||||
if widx < idx:
|
||||
clipboard_from_primary = osc.find('p', idx + 1) > -1
|
||||
clipboard_contents = ''
|
||||
else:
|
||||
from base64 import standard_b64decode
|
||||
clipboard_from_primary = osc.find('p', idx+1, widx) > -1
|
||||
data = memoryview(osc.encode('ascii'))
|
||||
clipboard_contents = standard_b64decode(data[widx+1:]).decode('utf-8')
|
||||
|
||||
|
||||
def wait_loop(tty_fd: int) -> None:
|
||||
os.set_blocking(tty_fd, False)
|
||||
decoder = codecs.getincrementaldecoder('utf-8')('ignore')
|
||||
with raw_mode(tty_fd):
|
||||
buf = ''
|
||||
while not got_capability_response and not got_clipboard_response:
|
||||
rd = select.select([tty_fd], [], [])[0]
|
||||
if rd:
|
||||
raw = os.read(tty_fd, io.DEFAULT_BUFFER_SIZE)
|
||||
if not raw:
|
||||
raise EOFError()
|
||||
data = decoder.decode(raw)
|
||||
buf = (buf + data) if buf else data
|
||||
buf = parse_input_from_terminal(on_text, on_dcs, ignore, on_osc, ignore, ignore, buf, False)
|
||||
|
||||
|
||||
def main(args: List[str]) -> NoReturn:
|
||||
@@ -93,17 +113,34 @@ def main(args: List[str]) -> NoReturn:
|
||||
data: Optional[bytes] = None
|
||||
if not sys.stdin.isatty():
|
||||
data = sys.stdin.buffer.read()
|
||||
try:
|
||||
sys.stdin = open(os.ctermid())
|
||||
except FileNotFoundError:
|
||||
raise SystemExit('Not connected to a controlling terminal device, no /dev/tty')
|
||||
loop = Loop()
|
||||
handler = Clipboard(data, cli_opts)
|
||||
loop.loop(handler)
|
||||
if loop.return_code == 0 and handler.clipboard_contents:
|
||||
sys.stdout.write(handler.clipboard_contents)
|
||||
sys.stdout.flush()
|
||||
raise SystemExit(loop.return_code)
|
||||
wait_for_capability_response = False
|
||||
data_to_write = []
|
||||
if data:
|
||||
data_to_write.append(write_to_clipboard(data, cli_opts.use_primary).encode('ascii'))
|
||||
if not cli_opts.get_clipboard and cli_opts.wait_for_completion:
|
||||
data_to_write.append(b'\x1bP+q544e\x1b\\')
|
||||
wait_for_capability_response = True
|
||||
if cli_opts.get_clipboard:
|
||||
data_to_write.append(request_from_clipboard(cli_opts.use_primary).encode('ascii'))
|
||||
wait_for_capability_response = True
|
||||
tty_fd = os.open(os.ctermid(), os.O_RDWR | os.O_CLOEXEC)
|
||||
retcode = 0
|
||||
with open(tty_fd, 'wb', closefd=True) as ttyf:
|
||||
for x in data_to_write:
|
||||
ttyf.write(x)
|
||||
ttyf.flush()
|
||||
if wait_for_capability_response:
|
||||
try:
|
||||
wait_loop(tty_fd)
|
||||
except KeyboardInterrupt:
|
||||
sys.excepthook = lambda *a: None
|
||||
raise
|
||||
except EOFError:
|
||||
retcode = 1
|
||||
if clipboard_contents:
|
||||
print(end=clipboard_contents)
|
||||
|
||||
raise SystemExit(retcode)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
0
kittens/diff/collect.py
Executable file → Normal file
0
kittens/diff/collect.py
Executable file → Normal file
0
kittens/diff/main.py
Executable file → Normal file
0
kittens/diff/main.py
Executable file → Normal file
6
kittens/diff/options/definition.py
Executable file → Normal file
6
kittens/diff/options/definition.py
Executable file → Normal file
@@ -56,9 +56,9 @@ A glob pattern that is matched against only the filename of files and directorie
|
||||
files and directories are ignored when scanning the filesystem to look for files to diff.
|
||||
Can be specified multiple times to use multiple patterns. For example::
|
||||
|
||||
ignore_name .git
|
||||
ignore_name *~
|
||||
ignore_name *.pyc
|
||||
ignore_name .git
|
||||
ignore_name *~
|
||||
ignore_name *.pyc
|
||||
''',
|
||||
)
|
||||
|
||||
|
||||
0
kittens/diff/options/utils.py
Executable file → Normal file
0
kittens/diff/options/utils.py
Executable file → Normal file
@@ -187,11 +187,11 @@ static PyMethodDef module_methods[] = {
|
||||
};
|
||||
|
||||
static struct PyModuleDef module = {
|
||||
.m_base = PyModuleDef_HEAD_INIT,
|
||||
.m_name = "diff_speedup", /* name of module */
|
||||
.m_doc = NULL,
|
||||
.m_size = -1,
|
||||
.m_methods = module_methods
|
||||
.m_base = PyModuleDef_HEAD_INIT,
|
||||
.m_name = "diff_speedup", /* name of module */
|
||||
.m_doc = NULL,
|
||||
.m_size = -1,
|
||||
.m_methods = module_methods
|
||||
};
|
||||
|
||||
EXPORTED PyMODINIT_FUNC
|
||||
|
||||
8
kittens/icat/main.py
Executable file → Normal file
8
kittens/icat/main.py
Executable file → Normal file
@@ -353,7 +353,7 @@ def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfil
|
||||
else:
|
||||
import struct
|
||||
use_number = max(1, struct.unpack('@I', os.urandom(4))[0])
|
||||
with NamedTemporaryFile() as f:
|
||||
with NamedTemporaryFile(prefix='tty-graphics-protocol-') as f:
|
||||
prefix = f.name
|
||||
frame_data = render_image(
|
||||
path, prefix, m, available_width, available_height, args.scale_up,
|
||||
@@ -405,7 +405,7 @@ def detect_support(wait_for: float = 10, silent: bool = False) -> bool:
|
||||
parse_responses()
|
||||
return 1 not in responses or 2 not in responses
|
||||
|
||||
with NamedTemporaryFile() as f:
|
||||
with NamedTemporaryFile(prefix='tty-graphics-protocol') as f:
|
||||
f.write(b'abcd')
|
||||
f.flush()
|
||||
gc = GraphicsCommand()
|
||||
@@ -472,13 +472,13 @@ def process_single_item(
|
||||
file_removed = False
|
||||
try:
|
||||
if isinstance(item, bytes):
|
||||
with NamedTemporaryFile(prefix='stdin-image-data-', delete=False) as tf:
|
||||
with NamedTemporaryFile(prefix='tty-graphics-protocol-', delete=False) as tf:
|
||||
tf.write(item)
|
||||
item = tf.name
|
||||
is_tempfile = True
|
||||
if url_pat is not None and url_pat.match(item) is not None:
|
||||
from urllib.request import urlretrieve
|
||||
with NamedTemporaryFile(prefix='url-image-data-', delete=False) as tf:
|
||||
with NamedTemporaryFile(prefix='tty-graphics-protocol-', delete=False) as tf:
|
||||
try:
|
||||
with socket_timeout(30):
|
||||
urlretrieve(item, filename=tf.name)
|
||||
|
||||
@@ -1,453 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import select
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from importlib import import_module
|
||||
from typing import (
|
||||
IO, TYPE_CHECKING, Any, Dict, List, NoReturn, Optional, Union, cast
|
||||
)
|
||||
|
||||
from kitty.child import remove_cloexec
|
||||
from kitty.constants import kitty_exe
|
||||
from kitty.entry_points import main as main_entry_point
|
||||
from kitty.fast_data_types import establish_controlling_tty, safe_pipe
|
||||
from kitty.shm import SharedMemory
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _typeshed import ReadableBuffer, WriteableBuffer
|
||||
|
||||
|
||||
hangup_events = select.POLLHUP
|
||||
error_events = select.POLLERR | select.POLLNVAL
|
||||
basic_events = hangup_events | error_events
|
||||
|
||||
|
||||
class PrewarmProcessFailed(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Child:
|
||||
child_id: int
|
||||
child_process_pid: int
|
||||
|
||||
|
||||
class PrewarmProcess:
|
||||
|
||||
def __init__(self, create_file_to_read_from_worker: bool = False) -> None:
|
||||
self.from_worker_fd, self.in_worker_fd = safe_pipe()
|
||||
self.children: Dict[int, Child] = {}
|
||||
if create_file_to_read_from_worker:
|
||||
os.set_blocking(self.from_worker_fd, True)
|
||||
self.from_worker = open(self.from_worker_fd, mode='r', closefd=True)
|
||||
self.from_worker_fd = -1
|
||||
|
||||
def take_from_worker_fd(self) -> int:
|
||||
ans, self.from_worker_fd = self.from_worker_fd, -1
|
||||
return ans
|
||||
|
||||
def __del__(self) -> None:
|
||||
if self.from_worker_fd > -1:
|
||||
os.close(self.from_worker_fd)
|
||||
self.from_worker_fd = -1
|
||||
if hasattr(self, 'from_worker'):
|
||||
self.from_worker.close()
|
||||
del self.from_worker
|
||||
if self.worker_started:
|
||||
import subprocess
|
||||
self.process.stdin and self.process.stdin.close()
|
||||
self.process.stdout and self.process.stdout.close()
|
||||
try:
|
||||
self.process.wait(timeout=1.0)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
del self.process
|
||||
|
||||
@property
|
||||
def worker_started(self) -> bool:
|
||||
return self.in_worker_fd == -1
|
||||
|
||||
def ensure_worker(self) -> None:
|
||||
if not self.worker_started:
|
||||
import subprocess
|
||||
self.process = subprocess.Popen(
|
||||
[kitty_exe(), '+kitten', 'prewarm', str(self.in_worker_fd)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, pass_fds=(self.in_worker_fd,))
|
||||
os.close(self.in_worker_fd)
|
||||
self.in_worker_fd = -1
|
||||
assert self.process.stdin is not None and self.process.stdout is not None
|
||||
self.write_to_process_fd = self.process.stdin.fileno()
|
||||
self.read_from_process_fd = self.process.stdout.fileno()
|
||||
os.set_blocking(self.write_to_process_fd, False)
|
||||
os.set_blocking(self.read_from_process_fd, False)
|
||||
self.poll = select.poll()
|
||||
self.poll.register(self.process.stdout.fileno(), select.POLLIN | basic_events)
|
||||
|
||||
def poll_to_send(self, yes: bool = True) -> None:
|
||||
if yes:
|
||||
self.poll.register(self.write_to_process_fd, select.POLLOUT | basic_events)
|
||||
else:
|
||||
self.poll.unregister(self.write_to_process_fd)
|
||||
|
||||
def reload_kitty_config(self) -> None:
|
||||
if self.worker_started:
|
||||
self.send_to_prewarm_process('reload_kitty_config:\n')
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
tty_fd: int,
|
||||
argv: List[str],
|
||||
cwd: str = '',
|
||||
env: Optional[Dict[str, str]] = None,
|
||||
stdin_data: Optional[Union[str, bytes]] = None
|
||||
) -> Child:
|
||||
self.ensure_worker()
|
||||
tty_name = os.ttyname(tty_fd)
|
||||
if isinstance(stdin_data, str):
|
||||
stdin_data = stdin_data.encode()
|
||||
if env is None:
|
||||
env = dict(os.environ)
|
||||
cmd: Dict[str, Union[int, List[str], str, Dict[str, str]]] = {
|
||||
'tty_name': tty_name, 'cwd': cwd or os.getcwd(), 'argv': argv, 'env': env,
|
||||
}
|
||||
total_size = 0
|
||||
if stdin_data is not None:
|
||||
cmd['stdin_size'] = len(stdin_data)
|
||||
total_size += len(stdin_data)
|
||||
data = json.dumps(cmd).encode()
|
||||
total_size += len(data) + SharedMemory.num_bytes_for_size
|
||||
with SharedMemory(size=total_size, unlink_on_exit=True) as shm:
|
||||
shm.write_data_with_size(data)
|
||||
if stdin_data:
|
||||
shm.write(stdin_data)
|
||||
shm.flush()
|
||||
self.send_to_prewarm_process(f'fork:{shm.name}\n')
|
||||
input_buf = b''
|
||||
st = time.monotonic()
|
||||
while time.monotonic() - st < 2:
|
||||
for (fd, event) in self.poll.poll(0.2):
|
||||
if event & basic_events:
|
||||
raise PrewarmProcessFailed('Failed doing I/O with prewarm process')
|
||||
if fd == self.read_from_process_fd and event & select.POLLIN:
|
||||
d = os.read(self.read_from_process_fd, io.DEFAULT_BUFFER_SIZE)
|
||||
input_buf += d
|
||||
while (idx := input_buf.find(b'\n')) > -1:
|
||||
line = input_buf[:idx].decode()
|
||||
input_buf = input_buf[idx+1:]
|
||||
if line.startswith('CHILD:'):
|
||||
_, cid, pid = line.split(':')
|
||||
child = self.add_child(int(cid), int(pid))
|
||||
shm.unlink_on_exit = False
|
||||
return child
|
||||
if line.startswith('ERR:'):
|
||||
raise PrewarmProcessFailed(line.split(':', 1)[-1])
|
||||
raise PrewarmProcessFailed('Timed out waiting for I/O with prewarm process')
|
||||
|
||||
def add_child(self, child_id: int, pid: int) -> Child:
|
||||
self.children[child_id] = c = Child(child_id, pid)
|
||||
return c
|
||||
|
||||
def send_to_prewarm_process(self, output_buf: Union[str, bytes] = b'', timeout: float = 2) -> None:
|
||||
if isinstance(output_buf, str):
|
||||
output_buf = output_buf.encode()
|
||||
st = time.monotonic()
|
||||
while time.monotonic() - st < timeout and output_buf:
|
||||
self.poll_to_send(bool(output_buf))
|
||||
for (fd, event) in self.poll.poll(0.2):
|
||||
if event & basic_events:
|
||||
raise PrewarmProcessFailed('Failed doing I/O with prewarm process')
|
||||
if fd == self.write_to_process_fd and event & select.POLLOUT:
|
||||
n = os.write(self.write_to_process_fd, output_buf)
|
||||
output_buf = output_buf[n:]
|
||||
self.poll_to_send(False)
|
||||
if output_buf:
|
||||
raise PrewarmProcessFailed('Timed out waiting to write to prewarm process')
|
||||
|
||||
def mark_child_as_ready(self, child_id: int) -> bool:
|
||||
c = self.children.pop(child_id, None)
|
||||
if c is None:
|
||||
return False
|
||||
self.send_to_prewarm_process(f'ready:{child_id}\n')
|
||||
return True
|
||||
|
||||
|
||||
def reload_kitty_config() -> None:
|
||||
from kittens.tui.utils import kitty_opts
|
||||
kitty_opts.clear_cached()
|
||||
kitty_opts()
|
||||
|
||||
|
||||
def prewarm() -> None:
|
||||
reload_kitty_config()
|
||||
for kitten in ('hints', 'ssh', 'unicode_input', 'ask', 'show_error'):
|
||||
import_module(f'kittens.{kitten}.main')
|
||||
|
||||
|
||||
class MemoryViewReadWrapperBytes(io.BufferedIOBase):
|
||||
|
||||
def __init__(self, mw: memoryview):
|
||||
self.mw = mw
|
||||
self.pos = 0
|
||||
|
||||
def detach(self) -> io.RawIOBase:
|
||||
raise io.UnsupportedOperation('detach() not supported')
|
||||
|
||||
def read(self, size: Optional[int] = -1) -> bytes:
|
||||
if size is None or size < 0:
|
||||
size = max(0, len(self.mw) - self.pos)
|
||||
oldpos = self.pos
|
||||
self.pos = min(len(self.mw), self.pos + size)
|
||||
if self.pos <= oldpos:
|
||||
return b''
|
||||
return bytes(self.mw[oldpos:self.pos])
|
||||
|
||||
def readinto(self, b: 'WriteableBuffer') -> int:
|
||||
if not isinstance(b, memoryview):
|
||||
b = memoryview(b)
|
||||
b = b.cast('B')
|
||||
data = self.read(len(b))
|
||||
n = len(data)
|
||||
b[:n] = data
|
||||
return n
|
||||
readinto1 = readinto
|
||||
|
||||
def readall(self) -> bytes:
|
||||
return self.read()
|
||||
|
||||
def write(self, b: 'ReadableBuffer') -> int:
|
||||
raise io.UnsupportedOperation('readonly stream')
|
||||
|
||||
def readable(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class MemoryViewReadWrapper(io.TextIOWrapper):
|
||||
|
||||
def __init__(self, mw: memoryview):
|
||||
super().__init__(cast(IO[bytes], MemoryViewReadWrapperBytes(mw)), encoding='utf-8', errors='replace')
|
||||
|
||||
|
||||
def child_main(cmd: Dict[str, Any], ready_fd: int) -> NoReturn:
|
||||
cwd = cmd.get('cwd')
|
||||
if cwd:
|
||||
try:
|
||||
os.chdir(cwd)
|
||||
except OSError:
|
||||
with suppress(OSError):
|
||||
os.chdir('/')
|
||||
os.setsid()
|
||||
env = cmd.get('env')
|
||||
if env is not None:
|
||||
os.environ.clear()
|
||||
os.environ.update(env)
|
||||
argv = cmd.get('argv')
|
||||
if argv:
|
||||
sys.argv = list(argv)
|
||||
poll = select.poll()
|
||||
poll.register(ready_fd, select.POLLIN | select.POLLERR | select.POLLHUP)
|
||||
poll.poll()
|
||||
os.close(ready_fd)
|
||||
tty_name = cmd.get('tty_name')
|
||||
if tty_name:
|
||||
sys.__stdout__.flush()
|
||||
sys.__stderr__.flush()
|
||||
establish_controlling_tty(tty_name, sys.__stdin__.fileno(), sys.__stdout__.fileno(), sys.__stderr__.fileno())
|
||||
main_entry_point()
|
||||
raise SystemExit(0)
|
||||
|
||||
|
||||
def fork(shm_address: str, ready_fd: int) -> int:
|
||||
sz = pos = 0
|
||||
with SharedMemory(name=shm_address, unlink_on_exit=True) as shm:
|
||||
data = shm.read_data_with_size()
|
||||
cmd = json.loads(data)
|
||||
sz = cmd.get('stdin_size', 0)
|
||||
if sz:
|
||||
pos = shm.tell()
|
||||
shm.unlink_on_exit = False
|
||||
|
||||
try:
|
||||
child_pid = os.fork()
|
||||
except OSError:
|
||||
if sz:
|
||||
with SharedMemory(shm_address, unlink_on_exit=True):
|
||||
pass
|
||||
if child_pid:
|
||||
# master process
|
||||
return child_pid
|
||||
# child process
|
||||
if shm.unlink_on_exit:
|
||||
child_main(cmd, ready_fd)
|
||||
else:
|
||||
with SharedMemory(shm_address, unlink_on_exit=True) as shm:
|
||||
stdin_data = memoryview(shm.mmap)[pos:pos + sz]
|
||||
if stdin_data:
|
||||
sys.stdin = MemoryViewReadWrapper(stdin_data)
|
||||
try:
|
||||
child_main(cmd, ready_fd)
|
||||
finally:
|
||||
stdin_data.release()
|
||||
sys.stdin = sys.__stdin__
|
||||
|
||||
|
||||
def main(args: List[str] = sys.argv) -> None:
|
||||
read_signal_fd, write_signal_fd = safe_pipe()
|
||||
notify_child_death_fd = int(sys.argv[-1])
|
||||
os.set_blocking(notify_child_death_fd, False)
|
||||
signal.set_wakeup_fd(write_signal_fd)
|
||||
signal.signal(signal.SIGCHLD, lambda *a: None)
|
||||
signal.siginterrupt(signal.SIGCHLD, False)
|
||||
prewarm()
|
||||
stdin_fd = sys.__stdin__.fileno()
|
||||
os.set_blocking(stdin_fd, False)
|
||||
stdout_fd = sys.__stdout__.fileno()
|
||||
os.set_blocking(stdout_fd, False)
|
||||
poll = select.poll()
|
||||
poll.register(stdin_fd, select.POLLIN | basic_events)
|
||||
poll.register(read_signal_fd, select.POLLIN | basic_events)
|
||||
input_buf = output_buf = child_death_buf = b''
|
||||
child_ready_fds: Dict[int, int] = {}
|
||||
child_id_map: Dict[int, int] = {}
|
||||
self_pid = os.getpid()
|
||||
|
||||
def check_event(event: int, err_msg: str) -> None:
|
||||
if event & hangup_events:
|
||||
raise SystemExit(0)
|
||||
if event & error_events:
|
||||
raise SystemExit(err_msg)
|
||||
|
||||
def handle_input(event: int) -> None:
|
||||
nonlocal input_buf, output_buf
|
||||
check_event(event, 'Polling of STDIN failed')
|
||||
if not (event & select.POLLIN):
|
||||
return
|
||||
d = os.read(stdin_fd, io.DEFAULT_BUFFER_SIZE)
|
||||
if not d:
|
||||
raise SystemExit(0)
|
||||
input_buf += d
|
||||
while (idx := input_buf.find(b'\n')) > -1:
|
||||
line = input_buf[:idx].decode()
|
||||
input_buf = input_buf[idx+1:]
|
||||
cmd, _, payload = line.partition(':')
|
||||
if cmd == 'reload_kitty_config':
|
||||
reload_kitty_config()
|
||||
elif cmd == 'ready':
|
||||
child_id = int(payload)
|
||||
cfd = child_ready_fds.pop(child_id)
|
||||
if cfd is not None:
|
||||
os.write(cfd, b'1')
|
||||
os.close(cfd)
|
||||
elif cmd == 'fork':
|
||||
read_fd, write_fd = safe_pipe(False)
|
||||
remove_cloexec(read_fd)
|
||||
try:
|
||||
child_pid = fork(payload, read_fd)
|
||||
except Exception as e:
|
||||
es = str(e).replace('\n', ' ')
|
||||
output_buf += f'ERR:{es}\n'.encode()
|
||||
else:
|
||||
if os.getpid() == self_pid:
|
||||
child_id = len(child_id_map) + 1
|
||||
child_id_map[child_id] = child_pid
|
||||
child_ready_fds[child_id] = write_fd
|
||||
output_buf += f'CHILD:{child_id}:{child_pid}\n'.encode()
|
||||
finally:
|
||||
if os.getpid() == self_pid:
|
||||
os.close(read_fd)
|
||||
elif cmd == 'echo':
|
||||
output_buf += f'{payload}\n'.encode()
|
||||
|
||||
def handle_output(event: int) -> None:
|
||||
nonlocal output_buf
|
||||
check_event(event, 'Polling of STDOUT failed')
|
||||
if not (event & select.POLLOUT):
|
||||
return
|
||||
if output_buf:
|
||||
n = os.write(stdout_fd, output_buf)
|
||||
if not n:
|
||||
raise SystemExit(0)
|
||||
output_buf = output_buf[n:]
|
||||
if not output_buf:
|
||||
poll.unregister(stdout_fd)
|
||||
|
||||
def handle_notify_child_death(event: int) -> None:
|
||||
nonlocal child_death_buf
|
||||
check_event(event, 'Polling of notify child death fd failed')
|
||||
if not (event & select.POLLOUT):
|
||||
return
|
||||
if child_death_buf:
|
||||
n = os.write(notify_child_death_fd, child_death_buf)
|
||||
if not n:
|
||||
raise SystemExit(0)
|
||||
child_death_buf = child_death_buf[n:]
|
||||
if not child_death_buf:
|
||||
poll.unregister(notify_child_death_fd)
|
||||
|
||||
def handle_signal(event: int) -> None:
|
||||
nonlocal child_death_buf
|
||||
check_event(event, 'Polling of signal fd failed')
|
||||
if not (event & select.POLLIN):
|
||||
return
|
||||
d = os.read(read_signal_fd, io.DEFAULT_BUFFER_SIZE)
|
||||
if not d:
|
||||
raise SystemExit(0)
|
||||
signals = set(bytearray(d))
|
||||
if signal.SIGCHLD in signals:
|
||||
while True:
|
||||
try:
|
||||
pid, exit_status = os.waitpid(-1, os.WNOHANG)
|
||||
except ChildProcessError:
|
||||
break
|
||||
# a zero return means there is at least one child process
|
||||
# existing and no exit status is available
|
||||
if pid == 0:
|
||||
break
|
||||
matched_child_id = -1
|
||||
for child_id, child_pid in child_id_map.items():
|
||||
if child_pid == pid:
|
||||
matched_child_id = child_id
|
||||
break
|
||||
if matched_child_id > -1:
|
||||
del child_id_map[matched_child_id]
|
||||
child_ready_fds.pop(matched_child_id, None)
|
||||
child_death_buf += f'{pid}\n'.encode()
|
||||
try:
|
||||
while True:
|
||||
if output_buf:
|
||||
poll.register(stdout_fd, select.POLLOUT | basic_events)
|
||||
if child_death_buf:
|
||||
poll.register(notify_child_death_fd, select.POLLOUT | basic_events)
|
||||
for (q, event) in poll.poll():
|
||||
if q == stdin_fd:
|
||||
handle_input(event)
|
||||
elif q == stdout_fd:
|
||||
handle_output(event)
|
||||
elif q == read_signal_fd:
|
||||
handle_signal(event)
|
||||
elif q == notify_child_death_fd:
|
||||
handle_notify_child_death(event)
|
||||
except (KeyboardInterrupt, EOFError, BrokenPipeError):
|
||||
if os.getpid() == self_pid:
|
||||
raise SystemExit(1)
|
||||
raise
|
||||
except Exception:
|
||||
if os.getpid() == self_pid:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise
|
||||
finally:
|
||||
if os.getpid() == self_pid:
|
||||
for fmd in child_ready_fds.values():
|
||||
with suppress(OSError):
|
||||
os.close(fmd)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -206,7 +206,7 @@ class ControlMaster:
|
||||
show_error(msg)
|
||||
|
||||
def download(self) -> bool:
|
||||
cmdline = self.batch_cmd_prefix + [self.conn_data.hostname, 'cat', self.remote_path]
|
||||
cmdline = self.batch_cmd_prefix + [self.conn_data.hostname, 'cat', shlex.quote(self.remote_path)]
|
||||
with open(self.dest, 'wb') as f:
|
||||
cp = subprocess.run(cmdline, stdout=f, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL)
|
||||
if cp.returncode != 0:
|
||||
@@ -216,7 +216,7 @@ class ControlMaster:
|
||||
|
||||
def upload(self, suppress_output: bool = True) -> bool:
|
||||
cmd_prefix = self.cmd_prefix if suppress_output else self.batch_cmd_prefix
|
||||
cmd = cmd_prefix + [self.conn_data.hostname, 'cat', '>', self.remote_path]
|
||||
cmd = cmd_prefix + [self.conn_data.hostname, 'cat', '>', shlex.quote(self.remote_path)]
|
||||
if not suppress_output:
|
||||
print(shlex.join(cmd))
|
||||
with open(self.dest, 'rb') as f:
|
||||
|
||||
@@ -122,7 +122,11 @@ def run_kitten(kitten: str, run_name: str = '__main__') -> None:
|
||||
print(kitten, file=sys.stderr)
|
||||
raise SystemExit(f'No kitten named {original_kitten_name}')
|
||||
m = runpy.run_path(path, init_globals={'sys': sys, 'os': os}, run_name='__run_kitten__')
|
||||
m['main'](sys.argv)
|
||||
from kitty.fast_data_types import set_options
|
||||
try:
|
||||
m['main'](sys.argv)
|
||||
finally:
|
||||
set_options(None)
|
||||
|
||||
|
||||
@run_once
|
||||
|
||||
@@ -34,7 +34,7 @@ def format_mods(mods: int) -> str:
|
||||
class KeysHandler(Handler):
|
||||
|
||||
def initialize(self) -> None:
|
||||
self.cmd.set_window_title('Kitty extended keyboard protocol demo')
|
||||
self.cmd.set_window_title('kitty extended keyboard protocol demo')
|
||||
self.cmd.set_cursor_visible(False)
|
||||
self.print('Press any keys - Ctrl+C or Ctrl+D will terminate')
|
||||
|
||||
@@ -47,7 +47,10 @@ class KeysHandler(Handler):
|
||||
mods = format_mods(key_event.mods)
|
||||
if mods:
|
||||
mods += '+'
|
||||
key = f'{mods}{key_event.key} '
|
||||
kk = key_event.key
|
||||
if kk == ' ':
|
||||
kk = 'SPACE'
|
||||
key = f'{mods}{kk} '
|
||||
self.cmd.colored(key, 'green')
|
||||
self.cmd.colored(etype + ' ', 'yellow')
|
||||
self.cmd.styled(key_event.text, italic=True)
|
||||
|
||||
@@ -45,7 +45,7 @@ def read_keys() -> None:
|
||||
|
||||
|
||||
def legacy_main() -> None:
|
||||
print('Press any keys - Ctrl-D will terminate this program', end='\r\n', flush=True)
|
||||
print('Press any keys - Ctrl+D will terminate this program', end='\r\n', flush=True)
|
||||
print(styled('UNIX', italic=True, fg='green'), styled('send_text', italic=True, fg='green'), sep='\t\t', end='\r\n')
|
||||
|
||||
with raw_mode():
|
||||
@@ -62,6 +62,7 @@ reset and :code:`application` mode is with DECCKM set. :code:`kitty` is the full
|
||||
kitty extended keyboard protocol.
|
||||
'''.format
|
||||
help_text = 'Show the codes generated by the terminal for key presses in various keyboard modes'
|
||||
usage = ''
|
||||
|
||||
|
||||
def main(args: List[str]) -> None:
|
||||
@@ -80,3 +81,8 @@ def main(args: List[str]) -> None:
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
elif __name__ == '__doc__':
|
||||
cd = sys.cli_docs # type: ignore
|
||||
cd['usage'] = usage
|
||||
cd['options'] = OPTIONS
|
||||
cd['help_text'] = help_text
|
||||
|
||||
@@ -291,6 +291,7 @@ def complete_option(ans: Completions, prefix: str = '-') -> None:
|
||||
def complete(ans: Completions, words: Sequence[str], new_word: bool) -> None:
|
||||
options = ssh_options()
|
||||
expecting_arg = False
|
||||
seen_destination = False
|
||||
types = ['' for i in range(len(words))]
|
||||
for i, word in enumerate(words):
|
||||
if expecting_arg:
|
||||
@@ -302,8 +303,10 @@ def complete(ans: Completions, words: Sequence[str], new_word: bool) -> None:
|
||||
if len(word) == 2 and options.get(word[1]):
|
||||
expecting_arg = True
|
||||
continue
|
||||
if seen_destination:
|
||||
break
|
||||
types[i] = 'destination'
|
||||
break
|
||||
seen_destination = True
|
||||
if new_word:
|
||||
if words:
|
||||
if expecting_arg:
|
||||
|
||||
@@ -38,7 +38,18 @@ type=list
|
||||
A glob pattern. Files with names matching this pattern are excluded from being
|
||||
transferred. Useful when adding directories. Can
|
||||
be specified multiple times, if any of the patterns match the file will be
|
||||
excluded.
|
||||
excluded. To exclude a directory use a pattern like */directory_name/*.
|
||||
|
||||
|
||||
--symlink-strategy
|
||||
default=preserve
|
||||
choices=preserve,resolve,keep-path
|
||||
Control what happens if the specified path is a symlink. The default is to preserve
|
||||
the symlink, re-creating it on the remote machine. Setting this to :code:`resolve`
|
||||
will cause the symlink to be followed and its target used as the file/directory to copy.
|
||||
The value of :code:`keep-path` is the same as :code:`resolve` except that the remote
|
||||
file path is derived from the symlink's path instead of the path of the symlink's target.
|
||||
Note that this option does not apply to symlinks encountered while recursively copying directories.
|
||||
'''
|
||||
|
||||
|
||||
@@ -100,5 +111,9 @@ def parse_copy_instructions(val: str, current_val: Dict[str, str]) -> Iterable[T
|
||||
raise CopyCLIError('Specifying a remote location with more than one file is not supported')
|
||||
home = home_path()
|
||||
for loc in locations:
|
||||
arcname = get_arcname(loc, opts.dest, home)
|
||||
yield str(uuid.uuid4()), CopyInstruction(loc, arcname, tuple(opts.exclude))
|
||||
if opts.symlink_strategy != 'preserve':
|
||||
rp = os.path.realpath(loc)
|
||||
else:
|
||||
rp = loc
|
||||
arcname = get_arcname(rp if opts.symlink_strategy == 'resolve' else loc, opts.dest, home)
|
||||
yield str(uuid.uuid4()), CopyInstruction(rp, arcname, tuple(opts.exclude))
|
||||
|
||||
@@ -27,7 +27,6 @@ from typing import (
|
||||
Tuple, Union, cast
|
||||
)
|
||||
|
||||
from kittens.tui.operations import restore_colors, save_colors
|
||||
from kitty.constants import (
|
||||
cache_dir, runtime_dir, shell_integration_dir, ssh_control_master_template,
|
||||
str_version, terminfo_dir
|
||||
@@ -40,7 +39,11 @@ from kitty.utils import (
|
||||
set_echo as turn_off_echo
|
||||
)
|
||||
|
||||
from ..tui.utils import kitty_opts
|
||||
from ..tui.operations import (
|
||||
RESTORE_PRIVATE_MODE_VALUES, SAVE_PRIVATE_MODE_VALUES, Mode,
|
||||
restore_colors, save_colors, set_mode
|
||||
)
|
||||
from ..tui.utils import kitty_opts, running_in_tmux
|
||||
from .config import init_config
|
||||
from .copy import CopyInstruction
|
||||
from .options.types import Options as SSHOptions
|
||||
@@ -168,6 +171,8 @@ def make_tarfile(ssh_opts: SSHOptions, base_env: Dict[str, str], compression: st
|
||||
env['KITTY_LOGIN_CWD'] = ssh_opts.cwd
|
||||
if ssh_opts.remote_kitty != 'no':
|
||||
env['KITTY_REMOTE'] = ssh_opts.remote_kitty
|
||||
if os.environ.get('KITTY_PUBLIC_KEY'):
|
||||
env['KITTY_PUBLIC_KEY'] = os.environ['KITTY_PUBLIC_KEY']
|
||||
env_script = serialize_env(literal_env, env, base_env, for_python=compression != 'gz')
|
||||
buf = io.BytesIO()
|
||||
with tarfile.open(mode=f'w:{compression}', fileobj=buf, encoding='utf-8') as tf:
|
||||
@@ -209,7 +214,7 @@ def get_ssh_data(msg: str, request_id: str) -> Iterator[bytes]:
|
||||
if pw != env_data['pw']:
|
||||
raise ValueError('Incorrect password')
|
||||
if rq_id != request_id:
|
||||
raise ValueError('Incorrect request id')
|
||||
raise ValueError(f'Incorrect request id: {rq_id!r} expecting the KITTY_PID-KITTY_WINDOW_ID for the current kitty window')
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
yield f'{e}\n'.encode('utf-8')
|
||||
@@ -409,9 +414,11 @@ class InvalidSSHArgs(ValueError):
|
||||
os.execlp(ssh_exe(), 'ssh')
|
||||
|
||||
|
||||
passthrough_args = {f'-{x}' for x in 'NnfGT'}
|
||||
|
||||
|
||||
def parse_ssh_args(args: List[str], extra_args: Tuple[str, ...] = ()) -> Tuple[List[str], List[str], bool, Tuple[str, ...]]:
|
||||
boolean_ssh_args, other_ssh_args = get_ssh_cli()
|
||||
passthrough_args = {f'-{x}' for x in 'NnfG'}
|
||||
ssh_args = []
|
||||
server_args: List[str] = []
|
||||
expecting_option_val = False
|
||||
@@ -508,7 +515,7 @@ def get_remote_command(
|
||||
return wrap_bootstrap_script(sh_script, interpreter), replacements, shm_name
|
||||
|
||||
|
||||
def connection_sharing_args(opts: SSHOptions, kitty_pid: int) -> List[str]:
|
||||
def connection_sharing_args(kitty_pid: int) -> List[str]:
|
||||
rd = runtime_dir()
|
||||
# Bloody OpenSSH generates a 40 char hash and in creating the socket
|
||||
# appends a 27 char temp suffix to it. Socket max path length is approx
|
||||
@@ -547,17 +554,29 @@ def connection_sharing_args(opts: SSHOptions, kitty_pid: int) -> List[str]:
|
||||
def restore_terminal_state() -> Iterator[bool]:
|
||||
with open(os.ctermid()) as f:
|
||||
val = termios.tcgetattr(f.fileno())
|
||||
print(end=SAVE_PRIVATE_MODE_VALUES)
|
||||
print(end=set_mode(Mode.HANDLE_TERMIOS_SIGNALS), flush=True)
|
||||
try:
|
||||
yield bool(val[3] & termios.ECHO)
|
||||
finally:
|
||||
termios.tcsetattr(f.fileno(), termios.TCSAFLUSH, val)
|
||||
print(end=RESTORE_PRIVATE_MODE_VALUES, flush=True)
|
||||
|
||||
|
||||
def dcs_to_kitty(payload: Union[bytes, str], type: str = 'ssh') -> bytes:
|
||||
if isinstance(payload, str):
|
||||
payload = payload.encode('utf-8')
|
||||
payload = standard_b64encode(payload)
|
||||
return b'\033P@kitty-' + type.encode('ascii') + b'|' + payload + b'\033\\'
|
||||
ans = b'\033P@kitty-' + type.encode('ascii') + b'|' + payload
|
||||
tmux = running_in_tmux()
|
||||
if tmux:
|
||||
cp = subprocess.run([tmux, 'set', '-p', 'allow-passthrough', 'on'])
|
||||
if cp.returncode != 0:
|
||||
raise SystemExit(cp.returncode)
|
||||
ans = b'\033Ptmux;\033' + ans + b'\033\033\\\033\\'
|
||||
else:
|
||||
ans += b'\033\\'
|
||||
return ans
|
||||
|
||||
|
||||
@run_once
|
||||
@@ -571,7 +590,6 @@ def ssh_version() -> Tuple[int, int]:
|
||||
|
||||
@contextmanager
|
||||
def drain_potential_tty_garbage(p: 'subprocess.Popen[bytes]', data_request: str) -> Iterator[None]:
|
||||
ssh_started_at = time.monotonic()
|
||||
with open(os.open(os.ctermid(), os.O_CLOEXEC | os.O_RDWR | os.O_NOCTTY), 'wb') as tty:
|
||||
if data_request:
|
||||
turn_off_echo(tty.fileno())
|
||||
@@ -580,14 +598,18 @@ def drain_potential_tty_garbage(p: 'subprocess.Popen[bytes]', data_request: str)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if p.returncode and time.monotonic() - ssh_started_at < 30:
|
||||
# discard queued input data on tty in case data transmission was
|
||||
# interrupted due to SSH failure, avoids spewing garbage to
|
||||
# screen
|
||||
data = b''
|
||||
give_up_at = time.monotonic() + 1
|
||||
tty_fd = tty.fileno()
|
||||
while time.monotonic() < give_up_at and b'KITTY_DATA_END' not in data:
|
||||
# discard queued input data on tty in case data transmission was
|
||||
# interrupted due to SSH failure, avoids spewing garbage to screen
|
||||
from uuid import uuid4
|
||||
canary = uuid4().hex.encode('ascii')
|
||||
turn_off_echo(tty.fileno())
|
||||
tty.write(dcs_to_kitty(canary + b'\n\r', type='echo'))
|
||||
tty.flush()
|
||||
data = b''
|
||||
give_up_at = time.monotonic() + 2
|
||||
tty_fd = tty.fileno()
|
||||
while time.monotonic() < give_up_at and canary not in data:
|
||||
with suppress(KeyboardInterrupt):
|
||||
rd, wr, err = select([tty_fd], [], [tty_fd], max(0, give_up_at - time.monotonic()))
|
||||
if err or not rd:
|
||||
break
|
||||
@@ -669,7 +691,7 @@ def run_ssh(ssh_args: List[str], server_args: List[str], found_extra_args: Tuple
|
||||
overrides.insert(0, f'hostname {uname}@{hostname_for_match}')
|
||||
host_opts = init_config(hostname_for_match, uname, overrides)
|
||||
if host_opts.share_connections:
|
||||
cmd[insertion_point:insertion_point] = connection_sharing_args(host_opts, int(os.environ['KITTY_PID']))
|
||||
cmd[insertion_point:insertion_point] = connection_sharing_args(int(os.environ['KITTY_PID']))
|
||||
use_kitty_askpass = host_opts.askpass == 'native' or (host_opts.askpass == 'unless-set' and 'SSH_ASKPASS' not in os.environ)
|
||||
need_to_request_data = True
|
||||
if use_kitty_askpass:
|
||||
@@ -699,16 +721,13 @@ def run_ssh(ssh_args: List[str], server_args: List[str], found_extra_args: Tuple
|
||||
else:
|
||||
rq = '' if need_to_request_data else 'id={REQUEST_ID}:pwfile={PASSWORD_FILENAME}:pw={DATA_PASSWORD}'.format(**replacements)
|
||||
with drain_potential_tty_garbage(p, rq):
|
||||
try:
|
||||
raise SystemExit(p.wait())
|
||||
except KeyboardInterrupt:
|
||||
raise SystemExit(1)
|
||||
raise SystemExit(p.wait())
|
||||
finally:
|
||||
if colors_changed:
|
||||
print(end=restore_colors(), flush=True)
|
||||
|
||||
|
||||
def main(args: List[str]) -> NoReturn:
|
||||
def main(args: List[str]) -> None:
|
||||
args = args[1:]
|
||||
if args and args[0] == 'use-python':
|
||||
args = args[1:] # backwards compat from when we had a python implementation
|
||||
@@ -716,13 +735,20 @@ def main(args: List[str]) -> NoReturn:
|
||||
ssh_args, server_args, passthrough, found_extra_args = parse_ssh_args(args, extra_args=('--kitten',))
|
||||
except InvalidSSHArgs as e:
|
||||
e.system_exit()
|
||||
if passthrough:
|
||||
if found_extra_args:
|
||||
raise SystemExit(f'The SSH kitten cannot work with the options: {", ".join(passthrough_args)}')
|
||||
os.execlp(ssh_exe(), 'ssh', *args)
|
||||
|
||||
if not os.environ.get('KITTY_WINDOW_ID') or not os.environ.get('KITTY_PID'):
|
||||
raise SystemExit('The SSH kitten is meant to run inside a kitty window')
|
||||
if passthrough:
|
||||
raise SystemExit('The SSH kitten is meant for interactive use via SSH only')
|
||||
if not sys.stdin.isatty():
|
||||
raise SystemExit('The SSH kitten is meant for interactive use only, STDIN must be a terminal')
|
||||
run_ssh(ssh_args, server_args, found_extra_args)
|
||||
try:
|
||||
run_ssh(ssh_args, server_args, found_extra_args)
|
||||
except KeyboardInterrupt:
|
||||
sys.excepthook = lambda *a: None
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Sequence
|
||||
|
||||
from kitty.types import run_once
|
||||
|
||||
@@ -50,12 +50,14 @@ def ssh_options() -> Dict[str, str]:
|
||||
return ans
|
||||
|
||||
|
||||
def is_kitten_cmdline(q: List[str]) -> bool:
|
||||
def is_kitten_cmdline(q: Sequence[str]) -> bool:
|
||||
if len(q) < 4:
|
||||
return False
|
||||
if os.path.basename(q[0]).lower() != 'kitty':
|
||||
return False
|
||||
return q[1:3] == ['+kitten', 'ssh'] or q[1:4] == ['+', 'kitten', 'ssh']
|
||||
if q[1:3] == ['+kitten', 'ssh'] or q[1:4] == ['+', 'kitten', 'ssh']:
|
||||
return True
|
||||
return q[1:3] == ['+runpy', 'from kittens.runner import main; main()'] and len(q) >= 6 and q[5] == 'ssh'
|
||||
|
||||
|
||||
def patch_cmdline(key: str, val: str, argv: List[str]) -> None:
|
||||
|
||||
@@ -243,14 +243,14 @@ static PyModuleDef_Slot slots[] = { {Py_mod_exec, (void*)exec_module}, {0, NULL}
|
||||
END_IGNORE_PEDANTIC_WARNINGS
|
||||
|
||||
static struct PyModuleDef module = {
|
||||
.m_base = PyModuleDef_HEAD_INIT,
|
||||
.m_name = "rsync", /* name of module */
|
||||
.m_doc = NULL,
|
||||
.m_slots = slots,
|
||||
.m_methods = module_methods
|
||||
.m_base = PyModuleDef_HEAD_INIT,
|
||||
.m_name = "rsync", /* name of module */
|
||||
.m_doc = NULL,
|
||||
.m_slots = slots,
|
||||
.m_methods = module_methods
|
||||
};
|
||||
|
||||
EXPORTED PyMODINIT_FUNC
|
||||
PyInit_rsync(void) {
|
||||
return PyModuleDef_Init(&module);
|
||||
return PyModuleDef_Init(&module);
|
||||
}
|
||||
|
||||
@@ -301,7 +301,7 @@ def render_as_single_image(
|
||||
remove_alpha: str = '', flip: bool = False, flop: bool = False,
|
||||
) -> Tuple[str, int, int]:
|
||||
import tempfile
|
||||
fd, output = tempfile.mkstemp(prefix='icat-', suffix=f'.{m.mode}', dir=tdir)
|
||||
fd, output = tempfile.mkstemp(prefix='tty-graphics-protocol-', suffix=f'.{m.mode}', dir=tdir)
|
||||
os.close(fd)
|
||||
result = render_image(
|
||||
path, output, m, available_width, available_height, scale_up,
|
||||
|
||||
@@ -109,17 +109,17 @@ class TermManager:
|
||||
|
||||
|
||||
class MouseButton(IntFlag):
|
||||
NONE, LEFT, MIDDLE, RIGHT, FOURTH, FIFTH = 0, 1, 2, 4, 8, 16
|
||||
WHEEL_UP, WHEEL_DOWN = -1, -2
|
||||
NONE, LEFT, MIDDLE, RIGHT, FOURTH, FIFTH, SIXTH, SEVENTH = 0, 1, 2, 4, 8, 16, 32, 64
|
||||
WHEEL_UP, WHEEL_DOWN, WHEEL_LEFT, WHEEL_RIGHT = -1, -2, -4, -8
|
||||
|
||||
|
||||
bmap = {0: MouseButton.LEFT, 1: MouseButton.MIDDLE, 2: MouseButton.RIGHT}
|
||||
bmap = MouseButton.LEFT, MouseButton.MIDDLE, MouseButton.RIGHT
|
||||
ebmap = MouseButton.FOURTH, MouseButton.FIFTH, MouseButton.SIXTH, MouseButton.SEVENTH
|
||||
wbmap = MouseButton.WHEEL_UP, MouseButton.WHEEL_DOWN, MouseButton.WHEEL_LEFT, MouseButton.WHEEL_RIGHT
|
||||
SHIFT_INDICATOR = 1 << 2
|
||||
ALT_INDICATOR = 1 << 3
|
||||
CTRL_INDICATOR = 1 << 4
|
||||
MOTION_INDICATOR = 1 << 5
|
||||
SCROLL_BUTTON_INDICATOR = 1 << 6
|
||||
EXTRA_BUTTON_INDICATOR = 1 << 7
|
||||
|
||||
|
||||
class EventType(Enum):
|
||||
@@ -150,13 +150,12 @@ def decode_sgr_mouse(text: str, screen_size: ScreenSize) -> MouseEvent:
|
||||
typ = EventType.RELEASE if m == 'm' else (EventType.MOVE if cb & MOTION_INDICATOR else EventType.PRESS)
|
||||
buttons: MouseButton = MouseButton.NONE
|
||||
cb3 = cb & 3
|
||||
if cb3 != 3:
|
||||
if cb & SCROLL_BUTTON_INDICATOR:
|
||||
buttons = MouseButton.WHEEL_DOWN if cb3 & 1 else MouseButton.WHEEL_UP
|
||||
elif cb & EXTRA_BUTTON_INDICATOR:
|
||||
buttons |= MouseButton.FIFTH if cb3 & 1 else MouseButton.FOURTH
|
||||
else:
|
||||
buttons |= bmap[cb3]
|
||||
if cb >= 128:
|
||||
buttons |= ebmap[cb3]
|
||||
elif cb >= 64:
|
||||
buttons |= wbmap[cb3]
|
||||
elif cb3 < 3:
|
||||
buttons |= bmap[cb3]
|
||||
mods = 0
|
||||
if cb & SHIFT_INDICATOR:
|
||||
mods |= SHIFT
|
||||
|
||||
@@ -47,6 +47,7 @@ class Mode(Enum):
|
||||
ALTERNATE_SCREEN = 1049, '?'
|
||||
BRACKETED_PASTE = 2004, '?'
|
||||
PENDING_UPDATE = 2026, '?'
|
||||
HANDLE_TERMIOS_SIGNALS = 19997, '?'
|
||||
|
||||
|
||||
def cmd(f: F) -> F:
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import os
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
from typing import TYPE_CHECKING, Tuple
|
||||
from typing import TYPE_CHECKING, Optional, Sequence, Tuple, cast
|
||||
|
||||
from kitty.types import run_once
|
||||
|
||||
@@ -55,12 +56,29 @@ def human_size(
|
||||
return format_number(size / 1024**exponent, max_num_of_decimals) + sep + unit_list[exponent]
|
||||
|
||||
|
||||
@run_once
|
||||
def kitty_opts() -> 'Options':
|
||||
from kitty.cli import create_default_opts
|
||||
from kitty.fast_data_types import get_options, set_options
|
||||
try:
|
||||
ans = cast(Optional['Options'], get_options())
|
||||
except RuntimeError:
|
||||
ans = None
|
||||
if ans is None:
|
||||
from kitty.cli import create_default_opts
|
||||
from kitty.utils import suppress_error_logging
|
||||
with suppress_error_logging():
|
||||
ans = create_default_opts()
|
||||
set_options(ans)
|
||||
return ans
|
||||
|
||||
|
||||
def set_kitty_opts(paths: Sequence[str], overrides: Sequence[str] = ()) -> 'Options':
|
||||
from kitty.config import load_config
|
||||
from kitty.fast_data_types import set_options
|
||||
from kitty.utils import suppress_error_logging
|
||||
with suppress_error_logging():
|
||||
return create_default_opts()
|
||||
opts = load_config(*paths, overrides=overrides or None)
|
||||
set_options(opts)
|
||||
return opts
|
||||
|
||||
|
||||
def report_error(msg: str = '', return_code: int = 1, print_exc: bool = False) -> None:
|
||||
@@ -82,3 +100,26 @@ def report_error(msg: str = '', return_code: int = 1, print_exc: bool = False) -
|
||||
def report_unhandled_error(msg: str = '') -> None:
|
||||
' Report an unhandled exception with the overlay ready message '
|
||||
return report_error(msg, print_exc=True)
|
||||
|
||||
|
||||
@run_once
|
||||
def running_in_tmux() -> str:
|
||||
socket = os.environ.get('TMUX')
|
||||
if not socket:
|
||||
return ''
|
||||
parts = socket.split(',')
|
||||
if len(parts) < 2:
|
||||
return ''
|
||||
try:
|
||||
if not os.access(parts[0], os.R_OK | os.W_OK):
|
||||
return ''
|
||||
except OSError:
|
||||
return ''
|
||||
from kitty.child import cmdline_of_pid
|
||||
c = cmdline_of_pid(int(parts[1]))
|
||||
if not c:
|
||||
return ''
|
||||
exe = os.path.basename(c[0])
|
||||
if exe.lower() == 'tmux':
|
||||
return exe
|
||||
return ''
|
||||
|
||||
@@ -101,14 +101,14 @@ static PyModuleDef_Slot slots[] = { {Py_mod_exec, (void*)exec_module}, {0, NULL}
|
||||
END_IGNORE_PEDANTIC_WARNINGS
|
||||
|
||||
static struct PyModuleDef module = {
|
||||
.m_base = PyModuleDef_HEAD_INIT,
|
||||
.m_name = "unicode_names", /* name of module */
|
||||
.m_doc = NULL,
|
||||
.m_slots = slots,
|
||||
.m_methods = module_methods
|
||||
.m_base = PyModuleDef_HEAD_INIT,
|
||||
.m_name = "unicode_names", /* name of module */
|
||||
.m_doc = NULL,
|
||||
.m_slots = slots,
|
||||
.m_methods = module_methods
|
||||
};
|
||||
|
||||
EXPORTED PyMODINIT_FUNC
|
||||
PyInit_unicode_names(void) {
|
||||
return PyModuleDef_Init(&module);
|
||||
return PyModuleDef_Init(&module);
|
||||
}
|
||||
|
||||
280
kitty/boss.py
280
kitty/boss.py
@@ -2,6 +2,7 @@
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import atexit
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -11,8 +12,8 @@ from functools import partial
|
||||
from gettext import gettext as _
|
||||
from time import monotonic
|
||||
from typing import (
|
||||
Any, Callable, Container, Dict, Iterable, Iterator, List, Optional, Set,
|
||||
Tuple, Union
|
||||
TYPE_CHECKING, Any, Callable, Container, Dict, Iterable, Iterator, List,
|
||||
Optional, Set, Tuple, Union
|
||||
)
|
||||
from weakref import WeakValueDictionary
|
||||
|
||||
@@ -22,24 +23,25 @@ from .cli_stub import CLIOptions
|
||||
from .conf.utils import BadLine, KeyAction, to_cmdline
|
||||
from .config import common_opts_as_dict, prepare_config_file_for_editing
|
||||
from .constants import (
|
||||
appname, cache_dir, config_dir, is_macos, is_wayland, kitty_exe,
|
||||
RC_ENCRYPTION_PROTOCOL_VERSION, appname, cache_dir, clear_handled_signals,
|
||||
config_dir, handled_signals, is_macos, is_wayland, kitty_exe,
|
||||
logo_png_file, supports_primary_selection, website_url
|
||||
)
|
||||
from .fast_data_types import (
|
||||
CLOSE_BEING_CONFIRMED, GLFW_MOD_ALT, GLFW_MOD_CONTROL, GLFW_MOD_SHIFT,
|
||||
GLFW_MOD_SUPER, GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS,
|
||||
IMPERATIVE_CLOSE_REQUESTED, NO_CLOSE_REQUESTED, ChildMonitor, Color,
|
||||
KeyEvent, add_timer, apply_options_update, background_opacity_of,
|
||||
change_background_opacity, change_os_window_state, cocoa_set_menubar_title,
|
||||
create_os_window, current_application_quit_request, current_os_window,
|
||||
destroy_global_data, focus_os_window, get_boss, get_clipboard_string,
|
||||
get_options, get_os_window_size, global_font_size,
|
||||
mark_os_window_for_close, os_window_font_size, patch_global_colors,
|
||||
redirect_mouse_handling, ring_bell, safe_pipe,
|
||||
set_application_quit_request, set_background_image, set_boss,
|
||||
set_clipboard_string, set_in_sequence_mode, set_options,
|
||||
set_os_window_size, set_os_window_title, thread_write, toggle_fullscreen,
|
||||
toggle_maximized, toggle_secure_input
|
||||
EllipticCurveKey, KeyEvent, SingleKey, add_timer, apply_options_update,
|
||||
background_opacity_of, change_background_opacity, change_os_window_state,
|
||||
cocoa_set_menubar_title, create_os_window,
|
||||
current_application_quit_request, current_os_window, destroy_global_data,
|
||||
focus_os_window, get_boss, get_clipboard_string, get_options,
|
||||
get_os_window_size, global_font_size, mark_os_window_for_close,
|
||||
os_window_font_size, patch_global_colors, redirect_mouse_handling,
|
||||
ring_bell, safe_pipe, send_data_to_peer, set_application_quit_request,
|
||||
set_background_image, set_boss, set_clipboard_string, set_in_sequence_mode,
|
||||
set_options, set_os_window_size, set_os_window_title, thread_write,
|
||||
toggle_fullscreen, toggle_maximized, toggle_secure_input
|
||||
)
|
||||
from .key_encoding import get_name_to_functional_number_map
|
||||
from .keys import get_shortcut, shortcut_matches
|
||||
@@ -48,12 +50,13 @@ from .notify import notification_activated
|
||||
from .options.types import Options
|
||||
from .options.utils import MINIMUM_FONT_SIZE, KeyMap, SubSequenceMap
|
||||
from .os_window_size import initial_window_size_func
|
||||
from .prewarm import PrewarmProcess
|
||||
from .rgb import color_from_int
|
||||
from .session import Session, create_sessions, get_os_window_sizing_data
|
||||
from .tabs import (
|
||||
SpecialWindow, SpecialWindowInstance, Tab, TabDict, TabManager
|
||||
)
|
||||
from .types import _T, AsyncResponse, SingleKey, WindowSystemMouseEvent, ac
|
||||
from .types import _T, AsyncResponse, WindowSystemMouseEvent, ac
|
||||
from .typing import PopenType, TypedDict
|
||||
from .utils import (
|
||||
cleanup_ssh_control_masters, func_name, get_editor, get_new_os_window_size,
|
||||
@@ -64,6 +67,11 @@ from .utils import (
|
||||
)
|
||||
from .window import CommandOutput, CwdRequest, Window
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .rc.base import ResponseType
|
||||
|
||||
RCResponse = Union[Dict[str, Any], None, AsyncResponse]
|
||||
|
||||
|
||||
class OSWindowDict(TypedDict):
|
||||
id: int
|
||||
@@ -227,10 +235,13 @@ class Boss:
|
||||
opts: Options,
|
||||
args: CLIOptions,
|
||||
cached_values: Dict[str, Any],
|
||||
global_shortcuts: Dict[str, SingleKey]
|
||||
global_shortcuts: Dict[str, SingleKey],
|
||||
prewarm: PrewarmProcess,
|
||||
):
|
||||
set_layout_options(opts)
|
||||
self.update_check_started = False
|
||||
self.encryption_key = EllipticCurveKey()
|
||||
self.encryption_public_key = f'{RC_ENCRYPTION_PROTOCOL_VERSION}:{base64.b85encode(self.encryption_key.public).decode("ascii")}'
|
||||
self.clipboard_buffers: Dict[str, str] = {}
|
||||
self.update_check_process: Optional['PopenType[bytes]'] = None
|
||||
self.window_id_map: WeakValueDictionary[int, Window] = WeakValueDictionary()
|
||||
@@ -250,12 +261,19 @@ class Boss:
|
||||
# we dont allow reloading the config file to change
|
||||
# allow_remote_control
|
||||
self.allow_remote_control = opts.allow_remote_control
|
||||
if args.listen_on and (self.allow_remote_control in ('y', 'socket-only')):
|
||||
if self.allow_remote_control in ('y', 'yes', 'true'):
|
||||
self.allow_remote_control = 'y'
|
||||
elif self.allow_remote_control in ('n', 'no', 'false'):
|
||||
self.allow_remote_control = 'n'
|
||||
self.listening_on = ''
|
||||
if args.listen_on and self.allow_remote_control in ('y', 'socket', 'socket-only', 'password'):
|
||||
listen_fd = listen_on(args.listen_on)
|
||||
self.listening_on = args.listen_on
|
||||
self.prewarm = prewarm
|
||||
self.child_monitor = ChildMonitor(
|
||||
self.on_child_death,
|
||||
DumpCommands(args) if args.dump_commands or args.dump_bytes else None,
|
||||
talk_fd, listen_fd
|
||||
talk_fd, listen_fd, self.prewarm.take_from_worker_fd()
|
||||
)
|
||||
set_boss(self)
|
||||
self.args = args
|
||||
@@ -434,26 +452,108 @@ class Boss:
|
||||
self.child_monitor.add_child(window.id, window.child.pid, window.child.child_fd, window.screen)
|
||||
self.window_id_map[window.id] = window
|
||||
|
||||
def _handle_remote_command(self, cmd: str, window: Optional[Window] = None, peer_id: int = 0) -> Union[Dict[str, Any], None, AsyncResponse]:
|
||||
from .remote_control import handle_cmd
|
||||
def _handle_remote_command(self, cmd: str, window: Optional[Window] = None, peer_id: int = 0) -> RCResponse:
|
||||
from .remote_control import is_cmd_allowed, parse_cmd
|
||||
response = None
|
||||
window = window or None
|
||||
if self.allow_remote_control == 'y' or peer_id > 0 or getattr(window, 'allow_remote_control', False):
|
||||
try:
|
||||
response = handle_cmd(self, window, cmd, peer_id)
|
||||
except Exception as err:
|
||||
import traceback
|
||||
response = {'ok': False, 'error': str(err)}
|
||||
if not getattr(err, 'hide_traceback', False):
|
||||
response['tb'] = traceback.format_exc()
|
||||
else:
|
||||
no_response = False
|
||||
try:
|
||||
no_response = json.loads(cmd).get('no_response')
|
||||
except Exception:
|
||||
pass
|
||||
window_has_remote_control = bool(window and window.allow_remote_control)
|
||||
if not window_has_remote_control:
|
||||
if self.allow_remote_control == 'n':
|
||||
return {'ok': False, 'error': 'Remote control is disabled'}
|
||||
if self.allow_remote_control == 'socket-only' and peer_id == 0:
|
||||
return {'ok': False, 'error': 'Remote control is allowed over a socket only'}
|
||||
try:
|
||||
pcmd = parse_cmd(cmd, self.encryption_key)
|
||||
except Exception as e:
|
||||
log_error(f'Failed to parse remote command with error: {e}')
|
||||
return response
|
||||
if not pcmd:
|
||||
return response
|
||||
extra_data: Dict[str, Any] = {}
|
||||
try:
|
||||
allowed_unconditionally = (
|
||||
self.allow_remote_control == 'y' or (peer_id > 0 and self.allow_remote_control in ('socket-only', 'socket')) or
|
||||
(window and window.remote_control_allowed(pcmd, extra_data)))
|
||||
except PermissionError:
|
||||
return {'ok': False, 'error': 'Remote control disallowed by window specific password'}
|
||||
if allowed_unconditionally:
|
||||
return self._execute_remote_command(pcmd, window, peer_id)
|
||||
q = is_cmd_allowed(pcmd, window, peer_id > 0, extra_data)
|
||||
if q is True:
|
||||
return self._execute_remote_command(pcmd, window, peer_id)
|
||||
if q is None:
|
||||
if self.ask_if_remote_cmd_is_allowed(pcmd, window, peer_id):
|
||||
return AsyncResponse()
|
||||
response = {'ok': False, 'error': 'Remote control is disabled. Add allow_remote_control to your kitty.conf'}
|
||||
if q is False and pcmd.get('password'):
|
||||
response['error'] = 'The user rejected this password or it is disallowed by remote_control_password in kitty.conf'
|
||||
no_response = pcmd.get('no_response') or False
|
||||
if no_response:
|
||||
return None
|
||||
return response
|
||||
|
||||
def ask_if_remote_cmd_is_allowed(self, pcmd: Dict[str, Any], window: Optional[Window] = None, peer_id: int = 0) -> bool:
|
||||
from kittens.tui.operations import styled
|
||||
in_flight = 0
|
||||
for w in self.window_id_map.values():
|
||||
if w.window_custom_type == 'remote_command_permission_dialog':
|
||||
in_flight += 1
|
||||
if in_flight > 4:
|
||||
log_error('Denying remote command permission as there are too many existing permission requests')
|
||||
return False
|
||||
wid = 0 if window is None else window.id
|
||||
hidden_text = styled(pcmd['password'], fg='yellow')
|
||||
overlay_window = self.choose(
|
||||
_('A program wishes to control kitty.\n'
|
||||
'Action: {1}\n' 'Password: {0}\n\n' '{2}'
|
||||
).format(
|
||||
hidden_text, styled(pcmd['cmd'], fg='magenta'),
|
||||
'\x1b[m' + styled(_(
|
||||
'Note that allowing the password will allow all future actions using the same password, in this kitty instance.'
|
||||
), dim=True, italic=True)),
|
||||
partial(self.remote_cmd_permission_received, pcmd, wid, peer_id),
|
||||
'a;green:Allow request', 'p;yellow:Allow password', 'r;magenta:Deny request', 'd;red:Deny password',
|
||||
window=window, default='a', hidden_text=hidden_text
|
||||
)
|
||||
if overlay_window is None:
|
||||
return False
|
||||
overlay_window.window_custom_type = 'remote_command_permission_dialog'
|
||||
return True
|
||||
|
||||
def remote_cmd_permission_received(self, pcmd: Dict[str, Any], window_id: int, peer_id: int, choice: str) -> None:
|
||||
from .remote_control import (
|
||||
encode_response_for_peer, set_user_password_allowed
|
||||
)
|
||||
response: RCResponse = None
|
||||
window = self.window_id_map.get(window_id)
|
||||
choice = choice or 'r'
|
||||
if choice in ('r', 'd'):
|
||||
if choice == 'd':
|
||||
set_user_password_allowed(pcmd['password'], False)
|
||||
no_response = pcmd.get('no_response') or False
|
||||
if not no_response:
|
||||
response = {'ok': False, 'error': 'Remote control is disabled. Add allow_remote_control to your kitty.conf'}
|
||||
response = {'ok': False, 'error': 'The user rejected this ' + ('request' if choice == 'r' else 'password')}
|
||||
elif choice in ('a', 'p'):
|
||||
if choice == 'p':
|
||||
set_user_password_allowed(pcmd['password'], True)
|
||||
response = self._execute_remote_command(pcmd, window, peer_id)
|
||||
if window is not None and response is not None and not isinstance(response, AsyncResponse):
|
||||
window.send_cmd_response(response)
|
||||
if peer_id > 0:
|
||||
if response is None:
|
||||
send_data_to_peer(peer_id, b'')
|
||||
elif not isinstance(response, AsyncResponse):
|
||||
send_data_to_peer(peer_id, encode_response_for_peer(response))
|
||||
|
||||
def _execute_remote_command(self, pcmd: Dict[str, Any], window: Optional[Window] = None, peer_id: int = 0) -> RCResponse:
|
||||
from .remote_control import handle_cmd
|
||||
try:
|
||||
response = handle_cmd(self, window, pcmd, peer_id)
|
||||
except Exception as err:
|
||||
import traceback
|
||||
response = {'ok': False, 'error': str(err)}
|
||||
if not getattr(err, 'hide_traceback', False):
|
||||
response['tb'] = traceback.format_exc()
|
||||
return response
|
||||
|
||||
@ac('misc', '''
|
||||
@@ -461,34 +561,51 @@ class Boss:
|
||||
|
||||
For example::
|
||||
|
||||
map F1 remote_control set-spacing margin=30
|
||||
map f1 remote_control set-spacing margin=30
|
||||
|
||||
See :ref:`rc_mapping` for details.
|
||||
''')
|
||||
def remote_control(self, *args: str) -> None:
|
||||
from .rc.base import (
|
||||
PayloadGetter, command_for_name, parse_subcommand_cli
|
||||
)
|
||||
from .remote_control import parse_rc_args
|
||||
try:
|
||||
global_opts, items = parse_rc_args(['@'] + list(args))
|
||||
if not items:
|
||||
return
|
||||
cmd = items[0]
|
||||
c = command_for_name(cmd)
|
||||
opts, items = parse_subcommand_cli(c, items)
|
||||
payload = c.message_to_kitty(global_opts, opts, items)
|
||||
import types
|
||||
if isinstance(payload, types.GeneratorType):
|
||||
for x in payload:
|
||||
c.response_from_kitty(self, self.active_window, PayloadGetter(c, x if isinstance(x, dict) else {}))
|
||||
else:
|
||||
c.response_from_kitty(self, self.active_window, PayloadGetter(c, payload if isinstance(payload, dict) else {}))
|
||||
self.call_remote_control(self.active_window, args)
|
||||
except (Exception, SystemExit):
|
||||
import traceback
|
||||
tb = traceback.format_exc()
|
||||
self.show_error(_('remote_control mapping failed'), tb)
|
||||
|
||||
def call_remote_control(self, active_window: Optional[Window], args: Tuple[str, ...]) -> 'ResponseType':
|
||||
from .rc.base import (
|
||||
PayloadGetter, command_for_name, parse_subcommand_cli
|
||||
)
|
||||
from .remote_control import parse_rc_args
|
||||
aa = list(args)
|
||||
silent = False
|
||||
if aa and aa[0].startswith('!'):
|
||||
aa[0] = aa[0][1:]
|
||||
silent = True
|
||||
try:
|
||||
global_opts, items = parse_rc_args(['@'] + aa)
|
||||
if not items:
|
||||
return None
|
||||
cmd = items[0]
|
||||
c = command_for_name(cmd)
|
||||
opts, items = parse_subcommand_cli(c, items)
|
||||
payload = c.message_to_kitty(global_opts, opts, items)
|
||||
except SystemExit as e:
|
||||
raise Exception(str(e)) from e
|
||||
import types
|
||||
try:
|
||||
if isinstance(payload, types.GeneratorType):
|
||||
for x in payload:
|
||||
c.response_from_kitty(self, active_window, PayloadGetter(c, x if isinstance(x, dict) else {}))
|
||||
return None
|
||||
return c.response_from_kitty(self, active_window, PayloadGetter(c, payload if isinstance(payload, dict) else {}))
|
||||
except Exception as e:
|
||||
if silent:
|
||||
log_error(f'Failed to run remote_control mapping: {aa} with error: {e}')
|
||||
return None
|
||||
raise
|
||||
|
||||
def peer_message_received(self, msg_bytes: bytes, peer_id: int) -> Union[bytes, bool, None]:
|
||||
cmd_prefix = b'\x1bP@kitty-cmd'
|
||||
terminator = b'\x1b\\'
|
||||
@@ -661,15 +778,28 @@ class Boss:
|
||||
*choices: str, # The choices, see the help for the ask kitten for format of a choice
|
||||
window: Optional[Window] = None, # the window associated with the confirmation
|
||||
default: str = '', # the default choice when the user presses Enter
|
||||
) -> None:
|
||||
hidden_text: str = '', # text to hide in the message
|
||||
hidden_text_placeholder: str = 'HIDDEN_TEXT_PLACEHOLDER', # placeholder text to insert in to message
|
||||
unhide_key: str = 'u', # key to press to unhide hidden text
|
||||
) -> Optional[Window]:
|
||||
def callback_(res: Dict[str, Any], x: int, boss: Boss) -> None:
|
||||
callback(res.get('response') or '')
|
||||
if hidden_text:
|
||||
msg = msg.replace(hidden_text, hidden_text_placeholder)
|
||||
cmd = ['--type=choices', '--message', msg]
|
||||
if default:
|
||||
cmd += ['-d', default]
|
||||
for c in choices:
|
||||
cmd += ['-c', c]
|
||||
self.run_kitten_with_metadata('ask', cmd, window=window, custom_callback=callback_, default_data={'response': ''})
|
||||
if hidden_text:
|
||||
cmd += ['--hidden-text-placeholder', hidden_text_placeholder, '--unhide-key', unhide_key]
|
||||
input_data = hidden_text
|
||||
else:
|
||||
input_data = None
|
||||
ans = self.run_kitten_with_metadata('ask', cmd, window=window, custom_callback=callback_, input_data=input_data, default_data={'response': ''})
|
||||
if isinstance(ans, Window):
|
||||
return ans
|
||||
return None
|
||||
|
||||
def get_line(
|
||||
self, msg: str, # can contain newlines and ANSI formatting
|
||||
@@ -733,6 +863,8 @@ class Boss:
|
||||
if not getattr(self, 'io_thread_started', False):
|
||||
self.child_monitor.start()
|
||||
self.io_thread_started = True
|
||||
for signum in self.child_monitor.handled_signals():
|
||||
handled_signals.add(signum)
|
||||
urls: List[str] = getattr(sys, 'cmdline_args_for_open', [])
|
||||
if urls:
|
||||
delattr(sys, 'cmdline_args_for_open')
|
||||
@@ -1100,6 +1232,13 @@ class Boss:
|
||||
w, h = get_new_os_window_size(metrics, width, height, unit, incremental, has_window_scaling)
|
||||
set_os_window_size(os_window_id, w, h)
|
||||
|
||||
def tab_for_id(self, tab_id: int) -> Optional[Tab]:
|
||||
for tm in self.os_window_map.values():
|
||||
tab = tm.tab_for_id(tab_id)
|
||||
if tab is not None:
|
||||
return tab
|
||||
return None
|
||||
|
||||
def default_bg_changed_for(self, window_id: int) -> None:
|
||||
w = self.window_id_map.get(window_id)
|
||||
if w is not None:
|
||||
@@ -1501,26 +1640,25 @@ class Boss:
|
||||
tab = self._new_tab(SpecialWindow(cmd, **kw))
|
||||
if tab is not None:
|
||||
for w in tab:
|
||||
w.allow_remote_control = True
|
||||
window = w
|
||||
elif window_type == 'os_window':
|
||||
os_window_id = self._new_os_window(SpecialWindow(cmd, **kw))
|
||||
for tab in self.os_window_map[os_window_id]:
|
||||
for w in tab:
|
||||
w.allow_remote_control = True
|
||||
window = w
|
||||
elif window_type == 'overlay':
|
||||
tab = self.active_tab
|
||||
if aw is not None and tab is not None:
|
||||
kw['overlay_for'] = aw.id
|
||||
window = tab.new_special_window(SpecialWindow(cmd, **kw), allow_remote_control=True)
|
||||
window = tab.new_special_window(SpecialWindow(cmd, **kw))
|
||||
else:
|
||||
tab = self.active_tab
|
||||
if tab is not None:
|
||||
window = tab.new_special_window(SpecialWindow(cmd, **kw), allow_remote_control=True)
|
||||
window = tab.new_special_window(SpecialWindow(cmd, **kw))
|
||||
|
||||
path, ext = os.path.splitext(logo_png_file)
|
||||
window.set_logo(f'{path}-128{ext}', position='bottom-right', alpha=0.25)
|
||||
window.allow_remote_control = True
|
||||
|
||||
def switch_focus_to(self, window_id: int) -> None:
|
||||
tab = self.active_tab
|
||||
@@ -1602,6 +1740,12 @@ class Boss:
|
||||
if get_options().copy_on_select:
|
||||
self.copy_to_buffer(get_options().copy_on_select)
|
||||
|
||||
def get_active_selection(self) -> Optional[str]:
|
||||
w = self.active_window
|
||||
if w is not None and not w.destroyed:
|
||||
return w.text_for_selection()
|
||||
return None
|
||||
|
||||
@ac('cp', '''
|
||||
Copy the selection from the active window to the specified buffer
|
||||
|
||||
@@ -1737,7 +1881,7 @@ class Boss:
|
||||
if stdin:
|
||||
r, w = safe_pipe(False)
|
||||
try:
|
||||
subprocess.Popen(cmd, env=env, stdin=r, cwd=cwd)
|
||||
subprocess.Popen(cmd, env=env, stdin=r, cwd=cwd, preexec_fn=clear_handled_signals)
|
||||
except Exception:
|
||||
os.close(w)
|
||||
else:
|
||||
@@ -1745,7 +1889,7 @@ class Boss:
|
||||
finally:
|
||||
os.close(r)
|
||||
else:
|
||||
subprocess.Popen(cmd, env=env, cwd=cwd)
|
||||
subprocess.Popen(cmd, env=env, cwd=cwd, preexec_fn=clear_handled_signals)
|
||||
|
||||
def pipe(self, source: str, dest: str, exe: str, *args: str) -> Optional[Window]:
|
||||
cmd = [exe] + list(args)
|
||||
@@ -1806,6 +1950,8 @@ class Boss:
|
||||
special_window = args
|
||||
else:
|
||||
special_window = self.args_to_special_window(args, cwd_from=cwd_from)
|
||||
if not self.os_window_map:
|
||||
self.add_os_window()
|
||||
tm = self.active_tab_manager
|
||||
if tm is not None:
|
||||
return tm.new_tab(special_window=special_window, cwd_from=cwd_from, as_neighbor=as_neighbor)
|
||||
@@ -1827,8 +1973,6 @@ class Boss:
|
||||
self._create_tab(list(args), cwd_from=CwdRequest(self.active_window_for_cwd))
|
||||
|
||||
def new_tab_with_wd(self, wd: Union[str, List[str]], str_is_multiple_paths: bool = False) -> None:
|
||||
if not self.os_window_map:
|
||||
self.add_os_window()
|
||||
if isinstance(wd, str):
|
||||
wd = wd.split(os.pathsep) if str_is_multiple_paths else [wd]
|
||||
for path in wd:
|
||||
@@ -1836,6 +1980,11 @@ class Boss:
|
||||
self._new_tab(special_window)
|
||||
|
||||
def _new_window(self, args: List[str], cwd_from: Optional[CwdRequest] = None) -> Optional[Window]:
|
||||
if not self.os_window_map:
|
||||
os_window_id = self.add_os_window()
|
||||
tm = self.os_window_map.get(os_window_id)
|
||||
if tm is not None and not tm.active_tab:
|
||||
tm.new_tab(empty_tab=True)
|
||||
tab = self.active_tab
|
||||
if tab is None:
|
||||
return None
|
||||
@@ -1954,6 +2103,7 @@ class Boss:
|
||||
for w in self.all_windows:
|
||||
self.default_bg_changed_for(w.id)
|
||||
w.refresh()
|
||||
self.prewarm.reload_kitty_config()
|
||||
|
||||
@ac('misc', '''
|
||||
Reload the config file
|
||||
@@ -1965,8 +2115,8 @@ class Boss:
|
||||
map f5 load_config_file /path/to/some/kitty.conf
|
||||
''')
|
||||
def load_config_file(self, *paths: str, apply_overrides: bool = True) -> None:
|
||||
from .config import load_config
|
||||
from .cli import default_config_paths
|
||||
from .config import load_config
|
||||
old_opts = get_options()
|
||||
prev_paths = old_opts.config_paths or default_config_paths(self.args.config)
|
||||
paths = paths or prev_paths
|
||||
@@ -2038,7 +2188,7 @@ class Boss:
|
||||
@ac('misc', '''
|
||||
Change colors in the specified windows
|
||||
|
||||
For details, see :ref:`at_set-colors`. For example::
|
||||
For details, see :ref:`at-set-colors`. For example::
|
||||
|
||||
map f5 set_colors --configured /path/to/some/config/file/colors.conf
|
||||
''')
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <signal.h>
|
||||
extern PyTypeObject Screen_Type;
|
||||
|
||||
@@ -33,7 +34,7 @@ extern PyTypeObject Screen_Type;
|
||||
#define EVDBG(...)
|
||||
#endif
|
||||
|
||||
#define EXTRA_FDS 2
|
||||
#define EXTRA_FDS 3
|
||||
#ifndef MSG_NOSIGNAL
|
||||
// Apple does not implement MSG_NOSIGNAL
|
||||
#define MSG_NOSIGNAL 0
|
||||
@@ -56,7 +57,7 @@ typedef struct {
|
||||
bool shutting_down;
|
||||
pthread_t io_thread, talk_thread;
|
||||
|
||||
int talk_fd, listen_fd;
|
||||
int talk_fd, listen_fd, prewarm_fd;
|
||||
Message *messages;
|
||||
size_t messages_capacity, messages_count;
|
||||
LoopData io_loop_data;
|
||||
@@ -123,11 +124,11 @@ static PyObject *
|
||||
new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
|
||||
ChildMonitor *self;
|
||||
PyObject *dump_callback, *death_notify;
|
||||
int talk_fd = -1, listen_fd = -1;
|
||||
int talk_fd = -1, listen_fd = -1, prewarm_fd = -1;
|
||||
int ret;
|
||||
|
||||
if (the_monitor) { PyErr_SetString(PyExc_RuntimeError, "Can have only a single ChildMonitor instance"); return NULL; }
|
||||
if (!PyArg_ParseTuple(args, "OO|ii", &death_notify, &dump_callback, &talk_fd, &listen_fd)) return NULL;
|
||||
if (!PyArg_ParseTuple(args, "OO|iii", &death_notify, &dump_callback, &talk_fd, &listen_fd, &prewarm_fd)) return NULL;
|
||||
if ((ret = pthread_mutex_init(&children_lock, NULL)) != 0) {
|
||||
PyErr_Format(PyExc_RuntimeError, "Failed to create children_lock mutex: %s", strerror(ret));
|
||||
return NULL;
|
||||
@@ -137,9 +138,10 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
|
||||
return NULL;
|
||||
}
|
||||
self = (ChildMonitor *)type->tp_alloc(type, 0);
|
||||
if (!init_loop_data(&self->io_loop_data, 5, SIGINT, SIGTERM, SIGCHLD, SIGUSR1, SIGUSR2)) return PyErr_SetFromErrno(PyExc_OSError);
|
||||
if (!init_loop_data(&self->io_loop_data, SIGINT, SIGHUP, SIGTERM, SIGCHLD, SIGUSR1, SIGUSR2, 0)) return PyErr_SetFromErrno(PyExc_OSError);
|
||||
self->talk_fd = talk_fd;
|
||||
self->listen_fd = listen_fd;
|
||||
self->prewarm_fd = prewarm_fd;
|
||||
if (self == NULL) return PyErr_NoMemory();
|
||||
self->death_notify = death_notify; Py_INCREF(death_notify);
|
||||
if (dump_callback != Py_None) {
|
||||
@@ -148,7 +150,8 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
|
||||
} else parse_func = parse_worker;
|
||||
self->count = 0;
|
||||
children_fds[0].fd = self->io_loop_data.wakeup_read_fd; children_fds[1].fd = self->io_loop_data.signal_read_fd;
|
||||
children_fds[0].events = POLLIN; children_fds[1].events = POLLIN;
|
||||
children_fds[2].fd = self->prewarm_fd;
|
||||
children_fds[0].events = POLLIN; children_fds[1].events = POLLIN; children_fds[2].events = POLLIN;
|
||||
the_monitor = self;
|
||||
|
||||
return (PyObject*) self;
|
||||
@@ -174,9 +177,21 @@ dealloc(ChildMonitor* self) {
|
||||
FREE_CHILD(add_queue[add_queue_count]);
|
||||
}
|
||||
free_loop_data(&self->io_loop_data);
|
||||
safe_close(self->prewarm_fd, __FILE__, __LINE__); self->prewarm_fd = -1;
|
||||
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
handled_signals(ChildMonitor *self, PyObject *args UNUSED) {
|
||||
PyObject *ans = PyTuple_New(self->io_loop_data.num_handled_signals);
|
||||
if (ans) {
|
||||
for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(ans); i++) {
|
||||
PyTuple_SET_ITEM(ans, i, PyLong_FromLong((long)self->io_loop_data.handled_signals[i]));
|
||||
}
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
static void
|
||||
wakeup_io_loop(ChildMonitor *self, bool in_signal_handler) {
|
||||
wakeup_loop(&self->io_loop_data, in_signal_handler, "io_loop");
|
||||
@@ -427,7 +442,7 @@ parse_input(ChildMonitor *self) {
|
||||
}
|
||||
if (resp) {
|
||||
if (PyBytes_Check(resp)) send_response_to_peer(msg->peer_id, PyBytes_AS_STRING(resp), PyBytes_GET_SIZE(resp));
|
||||
else if (resp == Py_None || resp == Py_True) send_response_to_peer(msg->peer_id, NULL, 0);
|
||||
else if (resp == Py_None) send_response_to_peer(msg->peer_id, NULL, 0);
|
||||
Py_CLEAR(resp);
|
||||
} else send_response_to_peer(msg->peer_id, NULL, 0);
|
||||
}
|
||||
@@ -743,7 +758,6 @@ render(monotonic_t now, bool input_read) {
|
||||
|
||||
for (size_t i = 0; i < global_state.num_os_windows; i++) {
|
||||
OSWindow *w = global_state.os_windows + i;
|
||||
w->render_calls++;
|
||||
if (!w->num_tabs) continue;
|
||||
if (!should_os_window_be_rendered(w)) {
|
||||
update_os_window_title(w);
|
||||
@@ -752,8 +766,11 @@ render(monotonic_t now, bool input_read) {
|
||||
}
|
||||
if (USE_RENDER_FRAMES && w->render_state != RENDER_FRAME_READY) {
|
||||
if (w->render_state == RENDER_FRAME_NOT_REQUESTED || no_render_frame_received_recently(w, now, ms_to_monotonic_t(250ll))) request_frame_render(w);
|
||||
continue;
|
||||
// dont respect render frames soon after a resize on Wayland as they cause flicker because
|
||||
// we want to fill the newly resized buffer ASAP, not at compositors convenience
|
||||
if (global_state.is_wayland && (monotonic() - w->viewport_resized_at) > s_double_to_monotonic_t(1)) continue;
|
||||
}
|
||||
w->render_calls++;
|
||||
make_os_window_context_current(w);
|
||||
if (w->live_resize.in_progress && OPT(resize_draw_strategy) >= RESIZE_DRAW_BLANK) {
|
||||
blank_os_window(w);
|
||||
@@ -1240,12 +1257,13 @@ read_bytes(int fd, Screen *screen) {
|
||||
|
||||
typedef struct { bool kill_signal, child_died, reload_config; } SignalSet;
|
||||
|
||||
static void
|
||||
static bool
|
||||
handle_signal(const siginfo_t *siginfo, void *data) {
|
||||
SignalSet *ss = data;
|
||||
switch(siginfo->si_signo) {
|
||||
case SIGINT:
|
||||
case SIGTERM:
|
||||
case SIGHUP:
|
||||
ss->kill_signal = true;
|
||||
break;
|
||||
case SIGCHLD:
|
||||
@@ -1260,6 +1278,7 @@ handle_signal(const siginfo_t *siginfo, void *data) {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -1289,6 +1308,34 @@ mark_monitored_pids(pid_t pid, int status) {
|
||||
children_mutex(unlock);
|
||||
}
|
||||
|
||||
static void
|
||||
reap_prewarmed_children(ChildMonitor *self, int fd, bool enable_close_on_child_death) {
|
||||
static char buf[256];
|
||||
static size_t buf_pos = 0;
|
||||
while(true) {
|
||||
ssize_t len = read(fd, buf + buf_pos, sizeof(buf) - buf_pos);
|
||||
if (len < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
if (errno != EIO && errno != EAGAIN) log_error("Call to read() from reap_prewarmed_children() failed with error: %s", strerror(errno));
|
||||
break;
|
||||
}
|
||||
buf_pos += len;
|
||||
char *nl;
|
||||
while (buf_pos > 1 && (nl = memchr(buf, '\n', buf_pos)) != NULL) {
|
||||
size_t sz = nl - buf + 1;
|
||||
if (enable_close_on_child_death) {
|
||||
*nl = 0;
|
||||
int pid = atoi(buf);
|
||||
if (pid) mark_child_for_removal(self, pid);
|
||||
}
|
||||
memmove(buf, buf + sz, sz);
|
||||
buf_pos -= sz;
|
||||
}
|
||||
if (len == 0) break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void
|
||||
reap_children(ChildMonitor *self, bool enable_close_on_child_death) {
|
||||
int status;
|
||||
@@ -1379,6 +1426,9 @@ io_loop(void *data) {
|
||||
}
|
||||
if (ss.child_died) reap_children(self, OPT(close_on_child_death));
|
||||
}
|
||||
if (children_fds[2].revents && POLLIN) {
|
||||
reap_prewarmed_children(self, children_fds[2].fd, OPT(close_on_child_death));
|
||||
}
|
||||
for (i = 0; i < self->count; i++) {
|
||||
if (children_fds[EXTRA_FDS + i].revents & (POLLIN | POLLHUP)) {
|
||||
data_received = true;
|
||||
@@ -1682,8 +1732,10 @@ send_response_to_peer(id_type peer_id, const char *msg, size_t msg_sz) {
|
||||
peer->write.capacity += msg_sz;
|
||||
} else fatal("Out of memory");
|
||||
}
|
||||
if (msg) memcpy(peer->write.data + peer->write.used, msg, msg_sz);
|
||||
peer->write.used += msg_sz;
|
||||
if (msg_sz && msg) {
|
||||
memcpy(peer->write.data + peer->write.used, msg, msg_sz);
|
||||
peer->write.used += msg_sz;
|
||||
}
|
||||
}
|
||||
wakeup = true;
|
||||
break;
|
||||
@@ -1705,6 +1757,7 @@ static PyMethodDef methods[] = {
|
||||
METHOD(main_loop, METH_NOARGS)
|
||||
METHOD(mark_for_close, METH_VARARGS)
|
||||
METHOD(resize_pty, METH_VARARGS)
|
||||
METHODB(handled_signals, METH_NOARGS),
|
||||
{"set_iutf8_winid", (PyCFunction)pyset_iutf8, METH_VARARGS, ""},
|
||||
{NULL} /* Sentinel */
|
||||
};
|
||||
@@ -1743,7 +1796,6 @@ cocoa_set_menubar_title(PyObject *self UNUSED, PyObject *args UNUSED) {
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
|
||||
send_data_to_peer(PyObject *self UNUSED, PyObject *args) {
|
||||
char * msg; Py_ssize_t sz;
|
||||
unsigned long long peer_id;
|
||||
@@ -1752,8 +1804,28 @@ send_data_to_peer(PyObject *self UNUSED, PyObject *args) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
random_unix_socket(PyObject *self UNUSED, PyObject *args UNUSED) {
|
||||
#ifndef SO_PASSCRED
|
||||
errno = ENOTSUP;
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
#else
|
||||
int fd, optval = 1;
|
||||
struct sockaddr_un bind_addr = {.sun_family=AF_UNIX};
|
||||
fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) return PyErr_SetFromErrno(PyExc_OSError);
|
||||
if (setsockopt(fd, SOL_SOCKET, SO_PASSCRED, &optval, sizeof optval) != 0) goto fail;
|
||||
if (bind(fd, (struct sockaddr *)&bind_addr, sizeof(sa_family_t)) != 0) goto fail;
|
||||
return PyLong_FromLong((long)fd);
|
||||
fail:
|
||||
safe_close(fd, __FILE__, __LINE__);
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
#endif
|
||||
}
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
METHODB(safe_pipe, METH_VARARGS),
|
||||
METHODB(random_unix_socket, METH_NOARGS),
|
||||
{"add_timer", (PyCFunction)add_python_timer, METH_VARARGS, ""},
|
||||
{"remove_timer", (PyCFunction)remove_python_timer, METH_VARARGS, ""},
|
||||
METHODB(monitor_pid, METH_VARARGS),
|
||||
|
||||
@@ -79,14 +79,16 @@ wait_for_terminal_ready(int fd) {
|
||||
|
||||
static PyObject*
|
||||
spawn(PyObject *self UNUSED, PyObject *args) {
|
||||
PyObject *argv_p, *env_p;
|
||||
PyObject *argv_p, *env_p, *handled_signals_p;
|
||||
int master, slave, stdin_read_fd, stdin_write_fd, ready_read_fd, ready_write_fd;
|
||||
char *cwd, *exe;
|
||||
if (!PyArg_ParseTuple(args, "ssO!O!iiiiii", &exe, &cwd, &PyTuple_Type, &argv_p, &PyTuple_Type, &env_p, &master, &slave, &stdin_read_fd, &stdin_write_fd, &ready_read_fd, &ready_write_fd)) return NULL;
|
||||
if (!PyArg_ParseTuple(args, "ssO!O!iiiiiiO!", &exe, &cwd, &PyTuple_Type, &argv_p, &PyTuple_Type, &env_p, &master, &slave, &stdin_read_fd, &stdin_write_fd, &ready_read_fd, &ready_write_fd, &PyTuple_Type, &handled_signals_p)) return NULL;
|
||||
char name[2048] = {0};
|
||||
if (ttyname_r(slave, name, sizeof(name) - 1) != 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; }
|
||||
char **argv = serialize_string_tuple(argv_p);
|
||||
char **env = serialize_string_tuple(env_p);
|
||||
int handled_signals[16] = {0}, num_handled_signals = MIN((int)arraysz(handled_signals), PyTuple_GET_SIZE(handled_signals_p));
|
||||
for (Py_ssize_t i = 0; i < num_handled_signals; i++) handled_signals[i] = PyLong_AsLong(PyTuple_GET_ITEM(handled_signals_p, i));
|
||||
|
||||
#if PY_VERSION_HEX >= 0x03070000
|
||||
PyOS_BeforeFork();
|
||||
@@ -99,10 +101,10 @@ spawn(PyObject *self UNUSED, PyObject *args) {
|
||||
PyOS_AfterFork_Child();
|
||||
#endif
|
||||
// See _Py_RestoreSignals in signalmodule.c for a list of signals python nukes
|
||||
sigset_t signals = {0};
|
||||
struct sigaction act = {.sa_handler=SIG_DFL};
|
||||
#define SA(which) { if (sigaction(which, &act, NULL) != 0) exit_on_err("sigaction() in child process failed"); }
|
||||
SA(SIGINT); SA(SIGTERM); SA(SIGCHLD); SA(SIGPIPE); SA(SIGUSR1); SA(SIGUSR2);
|
||||
const struct sigaction act = {.sa_handler=SIG_DFL};
|
||||
|
||||
#define SA(which) if (sigaction(which, &act, NULL) != 0) exit_on_err("sigaction() in child process failed");
|
||||
for (int si = 0; si < num_handled_signals; si++) { SA(handled_signals[si]); }
|
||||
#ifdef SIGXFSZ
|
||||
SA(SIGXFSZ);
|
||||
#endif
|
||||
@@ -110,29 +112,28 @@ spawn(PyObject *self UNUSED, PyObject *args) {
|
||||
SA(SIGXFZ);
|
||||
#endif
|
||||
#undef SA
|
||||
sigset_t signals; sigemptyset(&signals);
|
||||
if (sigprocmask(SIG_SETMASK, &signals, NULL) != 0) exit_on_err("sigprocmask() in child process failed");
|
||||
// Use only signal-safe functions (man 7 signal-safety)
|
||||
if (chdir(cwd) != 0) { if (chdir("/") != 0) {} }; // ignore failure to chdir to /
|
||||
if (setsid() == -1) exit_on_err("setsid() in child process failed");
|
||||
|
||||
// Establish the controlling terminal (see man 7 credentials)
|
||||
int tfd = safe_open(name, O_RDWR, 0);
|
||||
int tfd = safe_open(name, O_RDWR | O_CLOEXEC, 0);
|
||||
if (tfd == -1) exit_on_err("Failed to open controlling terminal");
|
||||
#ifdef TIOCSCTTY
|
||||
// On BSD open() does not establish the controlling terminal
|
||||
if (ioctl(tfd, TIOCSCTTY, 0) == -1) exit_on_err("Failed to set controlling terminal with TIOCSCTTY");
|
||||
#endif
|
||||
safe_close(tfd, __FILE__, __LINE__);
|
||||
|
||||
// Redirect stdin/stdout/stderr to the pty
|
||||
if (dup2(slave, 1) == -1) exit_on_err("dup2() failed for fd number 1");
|
||||
if (dup2(slave, 2) == -1) exit_on_err("dup2() failed for fd number 2");
|
||||
if (safe_dup2(slave, 1) == -1) exit_on_err("dup2() failed for fd number 1");
|
||||
if (safe_dup2(slave, 2) == -1) exit_on_err("dup2() failed for fd number 2");
|
||||
if (stdin_read_fd > -1) {
|
||||
if (dup2(stdin_read_fd, 0) == -1) exit_on_err("dup2() failed for fd number 0");
|
||||
if (safe_dup2(stdin_read_fd, 0) == -1) exit_on_err("dup2() failed for fd number 0");
|
||||
safe_close(stdin_read_fd, __FILE__, __LINE__);
|
||||
safe_close(stdin_write_fd, __FILE__, __LINE__);
|
||||
} else {
|
||||
if (dup2(slave, 0) == -1) exit_on_err("dup2() failed for fd number 0");
|
||||
if (safe_dup2(slave, 0) == -1) exit_on_err("dup2() failed for fd number 0");
|
||||
}
|
||||
safe_close(slave, __FILE__, __LINE__);
|
||||
safe_close(master, __FILE__, __LINE__);
|
||||
@@ -180,33 +181,18 @@ spawn(PyObject *self UNUSED, PyObject *args) {
|
||||
return PyLong_FromLong(pid);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
establish_controlling_tty(PyObject *self UNUSED, PyObject *args) {
|
||||
const char *ttyname;
|
||||
int stdin_fd, stdout_fd, stderr_fd;
|
||||
if (!PyArg_ParseTuple(args, "siii", &ttyname, &stdin_fd, &stdout_fd, &stderr_fd)) return NULL;
|
||||
int tfd = safe_open(ttyname, O_RDWR, 0);
|
||||
if (tfd == -1) return PyErr_SetFromErrnoWithFilename(PyExc_OSError, ttyname);
|
||||
#ifdef TIOCSCTTY
|
||||
// On BSD open() does not establish the controlling terminal
|
||||
if (ioctl(tfd, TIOCSCTTY, 0) == -1) return PyErr_SetFromErrno(PyExc_OSError);
|
||||
#endif
|
||||
if (dup2(tfd, stdin_fd) == -1) return PyErr_SetFromErrno(PyExc_OSError);
|
||||
if (dup2(tfd, stdout_fd) == -1) return PyErr_SetFromErrno(PyExc_OSError);
|
||||
if (dup2(tfd, stderr_fd) == -1) return PyErr_SetFromErrno(PyExc_OSError);
|
||||
safe_close(tfd, __FILE__, __LINE__);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
METHODB(spawn, METH_VARARGS),
|
||||
METHODB(establish_controlling_tty, METH_VARARGS),
|
||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
|
||||
bool
|
||||
init_child(PyObject *module) {
|
||||
PyModule_AddIntMacro(module, CLD_KILLED);
|
||||
PyModule_AddIntMacro(module, CLD_STOPPED);
|
||||
PyModule_AddIntMacro(module, CLD_EXITED);
|
||||
PyModule_AddIntMacro(module, CLD_CONTINUED);
|
||||
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
126
kitty/child.py
126
kitty/child.py
@@ -13,7 +13,8 @@ from typing import (
|
||||
import kitty.fast_data_types as fast_data_types
|
||||
|
||||
from .constants import (
|
||||
is_freebsd, is_macos, kitty_base_dir, shell_path, terminfo_dir
|
||||
handled_signals, is_freebsd, is_macos, kitty_base_dir, shell_path,
|
||||
terminfo_dir
|
||||
)
|
||||
from .types import run_once
|
||||
from .utils import log_error, which
|
||||
@@ -142,14 +143,6 @@ def environ_of_process(pid: int) -> Dict[str, str]:
|
||||
return parse_environ_block(_environ_of_process(pid))
|
||||
|
||||
|
||||
def remove_cloexec(fd: int) -> None:
|
||||
os.set_inheritable(fd, True)
|
||||
|
||||
|
||||
def remove_blocking(fd: int) -> None:
|
||||
os.set_blocking(fd, False)
|
||||
|
||||
|
||||
def process_env() -> Dict[str, str]:
|
||||
ans = dict(os.environ)
|
||||
ssl_env_var = getattr(sys, 'kitty_ssl_env_var', None)
|
||||
@@ -177,7 +170,8 @@ def set_default_env(val: Optional[Dict[str, str]] = None) -> None:
|
||||
|
||||
def openpty() -> Tuple[int, int]:
|
||||
master, slave = os.openpty() # Note that master and slave are in blocking mode
|
||||
remove_cloexec(slave)
|
||||
os.set_inheritable(slave, True)
|
||||
os.set_inheritable(master, False)
|
||||
fast_data_types.set_iutf8_fd(master, True)
|
||||
return master, slave
|
||||
|
||||
@@ -193,11 +187,33 @@ class ProcessDesc(TypedDict):
|
||||
cmdline: Optional[Sequence[str]]
|
||||
|
||||
|
||||
def is_prewarmable(argv: Sequence[str]) -> bool:
|
||||
if len(argv) < 3 or os.path.basename(argv[0]) != 'kitty':
|
||||
return False
|
||||
if argv[1][:1] not in '@+':
|
||||
return False
|
||||
if argv[1][0] == '@':
|
||||
return True
|
||||
if argv[1] == '+':
|
||||
return argv[2] != 'open'
|
||||
return argv[1] != '+open'
|
||||
|
||||
|
||||
@run_once
|
||||
def cmdline_of_prewarmer() -> List[str]:
|
||||
# we need this check in case the prewarmed process has done an exec and
|
||||
# changed its cmdline
|
||||
with suppress(Exception):
|
||||
return cmdline_of_pid(fast_data_types.get_boss().prewarm.worker_pid)
|
||||
return ['']
|
||||
|
||||
|
||||
class Child:
|
||||
|
||||
child_fd: Optional[int] = None
|
||||
pid: Optional[int] = None
|
||||
forked = False
|
||||
is_prewarmed = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -206,11 +222,11 @@ class Child:
|
||||
stdin: Optional[bytes] = None,
|
||||
env: Optional[Dict[str, str]] = None,
|
||||
cwd_from: Optional['CwdRequest'] = None,
|
||||
allow_remote_control: bool = False,
|
||||
is_clone_launch: str = '',
|
||||
add_listen_on_env_var: bool = True,
|
||||
):
|
||||
self.allow_remote_control = allow_remote_control
|
||||
self.is_clone_launch = is_clone_launch
|
||||
self.add_listen_on_env_var = add_listen_on_env_var
|
||||
self.argv = list(argv)
|
||||
if cwd_from:
|
||||
try:
|
||||
@@ -226,13 +242,19 @@ class Child:
|
||||
def final_env(self) -> Dict[str, str]:
|
||||
from kitty.options.utils import DELETE_ENV_VAR
|
||||
env = default_env().copy()
|
||||
if is_macos and env.get('LC_CTYPE') == 'UTF-8' and not sys._xoptions.get(
|
||||
boss = fast_data_types.get_boss()
|
||||
if is_macos and env.get('LC_CTYPE') == 'UTF-8' and not getattr(sys, 'kitty_run_data').get(
|
||||
'lc_ctype_before_python') and not getattr(default_env, 'lc_ctype_set_by_user', False):
|
||||
del env['LC_CTYPE']
|
||||
env.update(self.env)
|
||||
env['TERM'] = fast_data_types.get_options().term
|
||||
env['COLORTERM'] = 'truecolor'
|
||||
env['KITTY_PID'] = getpid()
|
||||
env['KITTY_PUBLIC_KEY'] = boss.encryption_public_key
|
||||
if self.add_listen_on_env_var and boss.listening_on:
|
||||
env['KITTY_LISTEN_ON'] = boss.listening_on
|
||||
else:
|
||||
env.pop('KITTY_LISTEN_ON', None)
|
||||
if self.cwd:
|
||||
# needed in case cwd is a symlink, in which case shells
|
||||
# can use it to display the current directory name rather
|
||||
@@ -261,14 +283,18 @@ class Child:
|
||||
self.forked = True
|
||||
master, slave = openpty()
|
||||
stdin, self.stdin = self.stdin, None
|
||||
ready_read_fd, ready_write_fd = os.pipe()
|
||||
remove_cloexec(ready_read_fd)
|
||||
if stdin is not None:
|
||||
stdin_read_fd, stdin_write_fd = os.pipe()
|
||||
remove_cloexec(stdin_read_fd)
|
||||
else:
|
||||
stdin_read_fd = stdin_write_fd = -1
|
||||
env = tuple(f'{k}={v}' for k, v in self.final_env().items())
|
||||
self.is_prewarmed = is_prewarmable(self.argv)
|
||||
if not self.is_prewarmed:
|
||||
ready_read_fd, ready_write_fd = os.pipe()
|
||||
os.set_inheritable(ready_write_fd, False)
|
||||
os.set_inheritable(ready_read_fd, True)
|
||||
if stdin is not None:
|
||||
stdin_read_fd, stdin_write_fd = os.pipe()
|
||||
os.set_inheritable(stdin_write_fd, False)
|
||||
os.set_inheritable(stdin_read_fd, True)
|
||||
else:
|
||||
stdin_read_fd = stdin_write_fd = -1
|
||||
env = tuple(f'{k}={v}' for k, v in self.final_env().items())
|
||||
argv = list(self.argv)
|
||||
exe = argv[0]
|
||||
if is_macos and exe == shell_path:
|
||||
@@ -289,29 +315,46 @@ class Child:
|
||||
argv[0] = (f'-{exe.split("/")[-1]}')
|
||||
self.final_exe = which(exe) or exe
|
||||
self.final_argv0 = argv[0]
|
||||
pid = fast_data_types.spawn(self.final_exe, self.cwd, tuple(argv), env, master, slave, stdin_read_fd, stdin_write_fd, ready_read_fd, ready_write_fd)
|
||||
if self.is_prewarmed:
|
||||
fe = self.final_env()
|
||||
self.prewarmed_child = fast_data_types.get_boss().prewarm(slave, self.argv, self.cwd, fe, stdin)
|
||||
pid = self.prewarmed_child.child_process_pid
|
||||
else:
|
||||
pid = fast_data_types.spawn(
|
||||
self.final_exe, self.cwd, tuple(argv), env, master, slave, stdin_read_fd, stdin_write_fd,
|
||||
ready_read_fd, ready_write_fd, tuple(handled_signals))
|
||||
os.close(slave)
|
||||
self.pid = pid
|
||||
self.child_fd = master
|
||||
if stdin is not None:
|
||||
os.close(stdin_read_fd)
|
||||
fast_data_types.thread_write(stdin_write_fd, stdin)
|
||||
os.close(ready_read_fd)
|
||||
self.terminal_ready_fd = ready_write_fd
|
||||
if not self.is_prewarmed:
|
||||
if stdin is not None:
|
||||
os.close(stdin_read_fd)
|
||||
fast_data_types.thread_write(stdin_write_fd, stdin)
|
||||
os.close(ready_read_fd)
|
||||
self.terminal_ready_fd = ready_write_fd
|
||||
if self.child_fd is not None:
|
||||
remove_blocking(self.child_fd)
|
||||
os.set_blocking(self.child_fd, False)
|
||||
return pid
|
||||
|
||||
def mark_terminal_ready(self) -> None:
|
||||
os.close(self.terminal_ready_fd)
|
||||
def __del__(self) -> None:
|
||||
fd = getattr(self, 'terminal_ready_fd', -1)
|
||||
if fd > -1:
|
||||
os.close(fd)
|
||||
self.terminal_ready_fd = -1
|
||||
|
||||
def mark_terminal_ready(self) -> None:
|
||||
if self.is_prewarmed:
|
||||
fast_data_types.get_boss().prewarm.mark_child_as_ready(self.prewarmed_child.child_id)
|
||||
else:
|
||||
os.close(self.terminal_ready_fd)
|
||||
self.terminal_ready_fd = -1
|
||||
|
||||
def cmdline_of_pid(self, pid: int) -> List[str]:
|
||||
try:
|
||||
ans = cmdline_of_pid(pid)
|
||||
except Exception:
|
||||
ans = []
|
||||
if not ans and pid == self.pid:
|
||||
if pid == self.pid and (not ans or (self.is_prewarmed and ans == cmdline_of_prewarmer())):
|
||||
ans = list(self.argv)
|
||||
return ans
|
||||
|
||||
@@ -411,3 +454,24 @@ class Child:
|
||||
with suppress(Exception):
|
||||
return environ_of_process(pid)
|
||||
return {}
|
||||
|
||||
def send_signal_for_key(self, key_num: int) -> bool:
|
||||
import signal
|
||||
import termios
|
||||
if self.child_fd is None:
|
||||
return False
|
||||
t = termios.tcgetattr(self.child_fd)
|
||||
if not t[3] & termios.ISIG:
|
||||
return False
|
||||
cc = t[-1]
|
||||
if key_num == cc[termios.VINTR]:
|
||||
s = signal.SIGINT
|
||||
elif key_num == cc[termios.VSUSP]:
|
||||
s = signal.SIGTSTP
|
||||
elif key_num == cc[termios.VQUIT]:
|
||||
s = signal.SIGQUIT
|
||||
else:
|
||||
return False
|
||||
pgrp = os.tcgetpgrp(self.child_fd)
|
||||
os.killpg(pgrp, s)
|
||||
return True
|
||||
|
||||
227
kitty/cli.py
227
kitty/cli.py
@@ -2,6 +2,7 @@
|
||||
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
from collections import deque
|
||||
from typing import (
|
||||
@@ -11,8 +12,12 @@ from typing import (
|
||||
|
||||
from .cli_stub import CLIOptions
|
||||
from .conf.utils import resolve_config
|
||||
from .constants import appname, defconf, is_macos, str_version, website_url
|
||||
from .constants import (
|
||||
appname, clear_handled_signals, defconf, is_macos, str_version, website_url
|
||||
)
|
||||
from .fast_data_types import wcswidth
|
||||
from .options.types import Options as KittyOpts
|
||||
from .types import run_once
|
||||
from .typing import BadLineType, TypedDict
|
||||
|
||||
|
||||
@@ -24,6 +29,7 @@ class OptionDict(TypedDict):
|
||||
type: str
|
||||
default: Optional[str]
|
||||
condition: bool
|
||||
completion: Dict[str, str]
|
||||
|
||||
|
||||
CONFIG_HELP = '''\
|
||||
@@ -118,8 +124,7 @@ def option(x: str) -> str:
|
||||
idx = x.find('-')
|
||||
if idx > -1:
|
||||
x = x[idx:]
|
||||
parts = map(bold, x.split())
|
||||
return ' '.join(parts)
|
||||
return bold(x.rstrip('>'))
|
||||
|
||||
|
||||
@role
|
||||
@@ -127,6 +132,16 @@ def code(x: str) -> str:
|
||||
return cyan(x)
|
||||
|
||||
|
||||
def text_and_target(x: str) -> Tuple[str, str]:
|
||||
parts = x.split('<', 1)
|
||||
return parts[0].strip(), parts[-1].rstrip('>')
|
||||
|
||||
|
||||
@role
|
||||
def term(x: str) -> str:
|
||||
return ref_hyperlink(x, 'term-')
|
||||
|
||||
|
||||
@role
|
||||
def kbd(x: str) -> str:
|
||||
return x
|
||||
@@ -134,12 +149,18 @@ def kbd(x: str) -> str:
|
||||
|
||||
@role
|
||||
def env(x: str) -> str:
|
||||
return italic(x)
|
||||
return ref_hyperlink(x, 'envvar-')
|
||||
|
||||
|
||||
role_map['envvar'] = role_map['env']
|
||||
|
||||
|
||||
def hyperlink_for_url(url: str, text: str) -> str:
|
||||
if sys.stdout.isatty():
|
||||
return f'\x1b]8;;{url}\x1b\\\x1b[4:3;58:5:4m{text}\x1b[4:0;59m\x1b]8;;\x1b\\'
|
||||
return text
|
||||
|
||||
|
||||
@role
|
||||
def file(x: str) -> str:
|
||||
return italic(x)
|
||||
@@ -147,12 +168,51 @@ def file(x: str) -> str:
|
||||
|
||||
@role
|
||||
def doc(x: str) -> str:
|
||||
return website_url(x)
|
||||
t, q = text_and_target(x)
|
||||
if t == q:
|
||||
from .conf.types import ref_map
|
||||
m = ref_map()['doc']
|
||||
q = q.strip('/')
|
||||
if q in m:
|
||||
x = f'{m[q]} <{t}>'
|
||||
return ref_hyperlink(x, 'doc-')
|
||||
|
||||
|
||||
@run_once
|
||||
def hostname() -> str:
|
||||
import socket
|
||||
return socket.gethostname() or 'localhost'
|
||||
|
||||
|
||||
def ref_hyperlink(x: str, prefix: str = '') -> str:
|
||||
t, q = text_and_target(x)
|
||||
url = f'kitty+doc://{hostname()}/#ref={prefix}{q}'
|
||||
return hyperlink_for_url(url, t)
|
||||
|
||||
|
||||
@role
|
||||
def ref(x: str) -> str:
|
||||
return re.sub(r'\s*<\S+?>', '', x)
|
||||
return ref_hyperlink(x)
|
||||
|
||||
|
||||
@role
|
||||
def ac(x: str) -> str:
|
||||
return ref_hyperlink(x, 'action-')
|
||||
|
||||
|
||||
@role
|
||||
def iss(x: str) -> str:
|
||||
return ref_hyperlink(x, 'issues-')
|
||||
|
||||
|
||||
@role
|
||||
def pull(x: str) -> str:
|
||||
return ref_hyperlink(x, 'pull-')
|
||||
|
||||
|
||||
@role
|
||||
def disc(x: str) -> str:
|
||||
return ref_hyperlink(x, 'discussions-')
|
||||
|
||||
|
||||
OptionSpecSeq = List[Union[str, OptionDict]]
|
||||
@@ -165,15 +225,19 @@ def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, Option
|
||||
state = NORMAL
|
||||
lines = spec.splitlines()
|
||||
prev_line = ''
|
||||
prev_indent = 0
|
||||
seq: OptionSpecSeq = []
|
||||
disabled: OptionSpecSeq = []
|
||||
mpat = re.compile('([a-z]+)=(.+)')
|
||||
current_cmd: OptionDict = {
|
||||
'dest': '', 'aliases': frozenset(), 'help': '', 'choices': frozenset(),
|
||||
'type': '', 'condition': False, 'default': None
|
||||
'type': '', 'condition': False, 'default': None, 'completion': {},
|
||||
}
|
||||
empty_cmd = current_cmd
|
||||
|
||||
def indent_of_line(x: str) -> int:
|
||||
return len(x) - len(x.lstrip())
|
||||
|
||||
for line in lines:
|
||||
line = line.rstrip()
|
||||
if state is NORMAL:
|
||||
@@ -187,7 +251,7 @@ def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, Option
|
||||
current_cmd = {
|
||||
'dest': parts[0][2:].replace('-', '_'), 'aliases': frozenset(parts), 'help': '',
|
||||
'choices': frozenset(), 'type': '',
|
||||
'default': None, 'condition': True
|
||||
'default': None, 'condition': True, 'completion': {}
|
||||
}
|
||||
state = METADATA
|
||||
continue
|
||||
@@ -210,11 +274,24 @@ def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, Option
|
||||
current_cmd['dest'] = v
|
||||
elif k == 'condition':
|
||||
current_cmd['condition'] = bool(eval(v))
|
||||
elif k == 'completion':
|
||||
cv = current_cmd['completion'] = {}
|
||||
for x in shlex.split(v):
|
||||
ck, vv = x.split(':', 1)
|
||||
cv[ck] = vv
|
||||
elif state is HELP:
|
||||
if line:
|
||||
current_indent = indent_of_line(line)
|
||||
if current_indent > 1:
|
||||
if prev_indent == 0:
|
||||
current_cmd['help'] += '\n'
|
||||
else:
|
||||
line = line.strip()
|
||||
prev_indent = current_indent
|
||||
spc = '' if current_cmd['help'].endswith('\n') else ' '
|
||||
current_cmd['help'] += spc + line
|
||||
else:
|
||||
prev_indent = 0
|
||||
if prev_line:
|
||||
current_cmd['help'] += '\n\n'
|
||||
else:
|
||||
@@ -254,36 +331,56 @@ def version(add_rev: bool = False) -> str:
|
||||
|
||||
|
||||
def wrap(text: str, limit: int = 80) -> Iterator[str]:
|
||||
NORMAL, IN_FORMAT = 'NORMAL', 'IN_FORMAT'
|
||||
state = NORMAL
|
||||
last_space_at = None
|
||||
chars_in_line = 0
|
||||
breaks = []
|
||||
for i, ch in enumerate(text):
|
||||
if state is IN_FORMAT:
|
||||
if ch == 'm':
|
||||
state = NORMAL
|
||||
continue
|
||||
if ch == '\033':
|
||||
state = IN_FORMAT
|
||||
continue
|
||||
if ch == ' ':
|
||||
last_space_at = i
|
||||
if chars_in_line < limit:
|
||||
chars_in_line += 1
|
||||
continue
|
||||
if last_space_at is not None:
|
||||
breaks.append(last_space_at)
|
||||
last_space_at = None
|
||||
chars_in_line = i - breaks[-1]
|
||||
if not text.strip():
|
||||
yield ''
|
||||
return
|
||||
in_escape = 0
|
||||
current_line: List[str] = []
|
||||
escapes: List[str] = []
|
||||
current_word: List[str] = []
|
||||
current_line_length = 0
|
||||
|
||||
lines: List[str] = []
|
||||
for b in reversed(breaks):
|
||||
lines.append(text[b:].lstrip())
|
||||
text = text[:b]
|
||||
if text:
|
||||
lines.append(text)
|
||||
return reversed(lines)
|
||||
def print_word(ch: str = '') -> Iterator[str]:
|
||||
nonlocal current_word, current_line, escapes, current_line_length
|
||||
cw = ''.join(current_word)
|
||||
w = wcswidth(cw)
|
||||
if current_line_length + w > limit:
|
||||
yield ''.join(current_line)
|
||||
current_line = []
|
||||
current_line_length = 0
|
||||
cw = cw.strip()
|
||||
current_word = [cw]
|
||||
if escapes:
|
||||
current_line.append(''.join(escapes))
|
||||
escapes = []
|
||||
if current_word:
|
||||
current_line.append(cw)
|
||||
current_line_length += w
|
||||
current_word = []
|
||||
if ch:
|
||||
current_word.append(ch)
|
||||
|
||||
for i, ch in enumerate(text):
|
||||
if in_escape > 0:
|
||||
if in_escape == 1 and ch in '[]':
|
||||
in_escape = 2 if ch == '[' else 3
|
||||
if (in_escape == 2 and ch == 'm') or (in_escape == 3 and ch == '\\' and text[i-1] == '\x1b'):
|
||||
in_escape = 0
|
||||
escapes.append(ch)
|
||||
continue
|
||||
if ch == '\x1b':
|
||||
in_escape = 1
|
||||
if current_word:
|
||||
yield from print_word()
|
||||
escapes.append(ch)
|
||||
continue
|
||||
if current_word and ch.isspace() and ch != '\xa0':
|
||||
yield from print_word(ch)
|
||||
else:
|
||||
current_word.append(ch)
|
||||
yield from print_word()
|
||||
if current_line:
|
||||
yield ''.join(current_line)
|
||||
|
||||
|
||||
def get_defaults_from_seq(seq: OptionSpecSeq) -> Dict[str, Any]:
|
||||
@@ -324,10 +421,7 @@ class PrintHelpForSeq:
|
||||
j = '\n' + (' ' * indent)
|
||||
lines: List[str] = []
|
||||
for ln in text.splitlines():
|
||||
if ln:
|
||||
lines.extend(wrap(ln, limit=linesz - indent))
|
||||
else:
|
||||
lines.append('')
|
||||
lines.extend(wrap(ln, limit=linesz - indent))
|
||||
a((' ' * leading_indent) + j.join(lines))
|
||||
|
||||
usage = '[program-to-run ...]' if usage is None else usage
|
||||
@@ -346,15 +440,16 @@ class PrintHelpForSeq:
|
||||
help_text = opt['help']
|
||||
if help_text == '!':
|
||||
continue # hidden option
|
||||
a(' ' + ', '.join(map(green, sorted(opt['aliases']))))
|
||||
a(' ' + ', '.join(map(green, sorted(opt['aliases'], reverse=True))))
|
||||
defval = opt.get('default')
|
||||
if not opt.get('type', '').startswith('bool-'):
|
||||
blocks[-1] += '={}'.format(italic(opt['dest'].upper()))
|
||||
if defval:
|
||||
dt = '=[{}]'.format(italic(defval))
|
||||
blocks[-1] += dt
|
||||
if opt.get('help'):
|
||||
defval = opt.get('default')
|
||||
t = help_text.replace('%default', str(defval))
|
||||
wa(prettify(t.strip()), indent=4)
|
||||
if defval is not None:
|
||||
wa(f'Default: {defval}', indent=4)
|
||||
t = help_text.replace('%default', str(defval)).strip()
|
||||
t = t.replace('#placeholder_for_formatting#', '')
|
||||
wa(prettify(t), indent=4)
|
||||
if opt.get('choices'):
|
||||
wa('Choices: {}'.format(', '.join(opt['choices'])), indent=4)
|
||||
a('')
|
||||
@@ -362,7 +457,7 @@ class PrintHelpForSeq:
|
||||
text = '\n'.join(blocks) + '\n\n' + version()
|
||||
if print_help_for_seq.allow_pager and sys.stdout.isatty():
|
||||
import subprocess
|
||||
p = subprocess.Popen(['less', '-isRXF'], stdin=subprocess.PIPE)
|
||||
p = subprocess.Popen(['less', '-isRXF'], stdin=subprocess.PIPE, preexec_fn=clear_handled_signals)
|
||||
try:
|
||||
p.communicate(text.encode('utf-8'))
|
||||
except KeyboardInterrupt:
|
||||
@@ -416,6 +511,7 @@ def seq_as_rst(
|
||||
if opt.get('help'):
|
||||
defval = opt.get('default')
|
||||
t = help_text.replace('%default', str(defval)).strip()
|
||||
t = t.replace('#placeholder_for_formatting#', '')
|
||||
a('')
|
||||
a(textwrap.indent(prettify_rst(t), ' ' * 4))
|
||||
if defval is not None:
|
||||
@@ -603,6 +699,7 @@ are running a program that does not set titles.
|
||||
|
||||
--config -c
|
||||
type=list
|
||||
completion=type:file ext:conf group:"Config files" kwds:none,NONE
|
||||
{config_help}
|
||||
|
||||
|
||||
@@ -614,6 +711,7 @@ Syntax: :italic:`name=value`. For example: :option:`{appname} -o` font_size=20
|
||||
|
||||
--directory --working-directory -d
|
||||
default=.
|
||||
completion=type:directory
|
||||
Change to the specified directory when launching.
|
||||
|
||||
|
||||
@@ -624,9 +722,11 @@ Detach from the controlling terminal, if any.
|
||||
|
||||
|
||||
--session
|
||||
completion=type:file ext:session relative:conf group:"Session files"
|
||||
Path to a file containing the startup :italic:`session` (tabs, windows, layout,
|
||||
programs). Use - to read from STDIN. See the :file:`README` file for details and
|
||||
an example.
|
||||
an example. Environment variables are expanded, relative paths are resolved relative
|
||||
to the kitty configuration directory.
|
||||
|
||||
|
||||
--hold
|
||||
@@ -664,19 +764,18 @@ regardless of this option.
|
||||
|
||||
--listen-on
|
||||
Listen on the specified socket address for control messages. For example,
|
||||
:option:`{appname} --listen-on`=unix:/tmp/mykitty or
|
||||
:option:`{appname} --listen-on`=tcp:localhost:12345. On Linux systems, you can
|
||||
also use abstract UNIX sockets, not associated with a file, like this:
|
||||
:option:`{appname} --listen-on`=unix:@mykitty. Environment variables are
|
||||
expanded and relative paths are resolved with respect to the temporary
|
||||
directory. To control kitty, you can send commands to it with
|
||||
:italic:`{appname} @` using the :option:`{appname} @ --to` option to specify
|
||||
this address. Unless you enabled :opt:`allow_remote_control` in
|
||||
:file:`{conf_name}.conf`, this option will be ignored. Note that if you run
|
||||
:italic:`{appname} @` within a kitty window, there is
|
||||
no need to specify the :option:`{appname} @ --to` option as it will
|
||||
automatically read from the environment. For UNIX sockets, this can also be
|
||||
specified in :file:`{conf_name}.conf`.
|
||||
:option:`{appname} --listen-on`=unix:/tmp/mykitty or :option:`{appname}
|
||||
--listen-on`=tcp:localhost:12345. On Linux systems, you can also use abstract
|
||||
UNIX sockets, not associated with a file, like this: :option:`{appname}
|
||||
--listen-on`=unix:@mykitty. Environment variables are expanded and relative
|
||||
paths are resolved with respect to the temporary directory. To control kitty,
|
||||
you can send commands to it with :italic:`{appname} @` using the
|
||||
:option:`{appname} @ --to` option to specify this address. Note that if you run
|
||||
:italic:`{appname} @` within a kitty window, there is no need to specify the
|
||||
:option:`{appname} @ --to` option as it will automatically read from the
|
||||
environment. Note that this will be ignored unless :opt:`allow_remote_control`
|
||||
is set to either: :code:`yes`, :code:`socket` or :code:`socket-only`. For UNIX
|
||||
sockets, this can also be specified in :file:`{conf_name}.conf`.
|
||||
|
||||
|
||||
--start-as
|
||||
@@ -702,7 +801,6 @@ Output commands received from child process to STDOUT.
|
||||
Replay previously dumped commands. Specify the path to a dump file previously
|
||||
created by :option:`{appname} --dump-commands`. You
|
||||
can open a new kitty window to replay the commands with::
|
||||
|
||||
{appname} sh -c "{appname} --replay-commands /path/to/dump/file; read"
|
||||
|
||||
|
||||
@@ -730,6 +828,7 @@ present in the main font.
|
||||
|
||||
|
||||
--watcher
|
||||
completion=type:file ext:py relative:conf group:"Watcher files"
|
||||
This option is deprecated in favor of the :opt:`watcher` option in
|
||||
:file:`{conf_name}.conf` and should not be used.
|
||||
|
||||
|
||||
@@ -906,6 +906,17 @@ cocoa_system_beep(const char *path) {
|
||||
else NSBeep();
|
||||
}
|
||||
|
||||
static void
|
||||
uncaughtExceptionHandler(NSException *exception) {
|
||||
log_error("Unhandled exception in Cocoa: %s", [[exception description] UTF8String]);
|
||||
log_error("Stack trace:\n%s", [[exception.callStackSymbols description] UTF8String]);
|
||||
}
|
||||
|
||||
void
|
||||
cocoa_set_uncaught_exception_handler(void) {
|
||||
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
|
||||
}
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
{"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""},
|
||||
{"cocoa_set_global_shortcut", (PyCFunction)cocoa_set_global_shortcut, METH_VARARGS, ""},
|
||||
@@ -915,17 +926,10 @@ static PyMethodDef module_methods[] = {
|
||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
static void
|
||||
uncaughtExceptionHandler(NSException *exception) {
|
||||
log_error("Unhandled exception in Cocoa: %s", [[exception description] UTF8String]);
|
||||
log_error("Stack trace:\n%s", [[exception.callStackSymbols description] UTF8String]);
|
||||
}
|
||||
|
||||
bool
|
||||
init_cocoa(PyObject *module) {
|
||||
memset(&global_shortcuts, 0, sizeof(global_shortcuts));
|
||||
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
||||
register_at_exit_cleanup_func(COCOA_CLEANUP_FUNC, cleanup);
|
||||
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ from kittens.runner import (
|
||||
)
|
||||
|
||||
from .cli import (
|
||||
OptionDict, OptionSpecSeq, options_for_completion, parse_option_spec,
|
||||
prettify
|
||||
OptionDict, options_for_completion, parse_option_spec, prettify
|
||||
)
|
||||
from .constants import shell_integration_dir
|
||||
from .remote_control import global_options_spec
|
||||
from .constants import config_dir, shell_integration_dir
|
||||
from .fast_data_types import truncate_point_for_length, wcswidth
|
||||
from .rc.base import all_command_names, command_for_name
|
||||
from .shell import options_for_cmd
|
||||
@@ -278,8 +278,8 @@ def zsh_output_serializer(ans: Completions) -> str:
|
||||
allm = tuple(matches)
|
||||
if len(allm) > 1:
|
||||
common_prefix = os.path.commonprefix(allm)
|
||||
if common_prefix and '/' in common_prefix:
|
||||
common_prefix = os.path.dirname(common_prefix) + '/'
|
||||
if common_prefix and os.sep in common_prefix:
|
||||
common_prefix = os.path.dirname(common_prefix).rstrip(os.sep) + os.sep
|
||||
cmd.extend(('-p', shlex.quote(common_prefix)))
|
||||
matches = MatchGroup({k[len(common_prefix):]: v for k, v in matches.items()})
|
||||
has_descriptions = any(matches.values())
|
||||
@@ -387,20 +387,6 @@ def complete_kitty_cli_arg(ans: Completions, opt: Optional[OptionDict], prefix:
|
||||
from kitty.config import option_names_for_completion
|
||||
k = 'Config directives'
|
||||
ans.add_match_group(k, {k+'=': '' for k in option_names_for_completion() if k.startswith(prefix)}, trailing_space=False)
|
||||
elif dest == 'config':
|
||||
|
||||
def is_conf_file(x: str) -> bool:
|
||||
if os.path.isdir(x):
|
||||
return True
|
||||
return x.lower().endswith('.conf')
|
||||
|
||||
complete_files_and_dirs(ans, prefix, files_group_name='Config files', predicate=is_conf_file)
|
||||
elif dest == 'session':
|
||||
complete_files_and_dirs(ans, prefix, files_group_name='Session files')
|
||||
elif dest == 'watcher':
|
||||
complete_files_and_dirs(ans, prefix, files_group_name='Watcher files')
|
||||
elif dest == 'directory':
|
||||
complete_dirs(ans, prefix)
|
||||
elif dest == 'listen_on':
|
||||
if ':' not in prefix:
|
||||
k = 'Address type'
|
||||
@@ -411,6 +397,15 @@ def complete_kitty_cli_arg(ans: Completions, opt: Optional[OptionDict], prefix:
|
||||
complete_basic_option_args(ans, opt, prefix)
|
||||
|
||||
|
||||
def basic_option_arg_completer(ans: Completions, opt: Optional[OptionDict], prefix: str, unknown_args: Delegate) -> None:
|
||||
prefix = prefix or ''
|
||||
if not opt:
|
||||
if unknown_args.num_of_unknown_args > 0:
|
||||
ans.delegate = unknown_args
|
||||
return
|
||||
complete_basic_option_args(ans, opt, prefix)
|
||||
|
||||
|
||||
CompleteArgsFunc = Callable[[Completions, Optional[OptionDict], str, Delegate], None]
|
||||
|
||||
|
||||
@@ -419,7 +414,7 @@ def complete_alias_map(
|
||||
words: Sequence[str],
|
||||
new_word: bool,
|
||||
option_map: Dict[str, OptionDict],
|
||||
complete_args: Optional[CompleteArgsFunc] = None
|
||||
complete_args: CompleteArgsFunc = basic_option_arg_completer,
|
||||
) -> None:
|
||||
expecting_arg = False
|
||||
opt: Optional[OptionDict] = None
|
||||
@@ -431,11 +426,11 @@ def complete_alias_map(
|
||||
if w is not last_word:
|
||||
continue
|
||||
long_opt = option_map.get(prev_word)
|
||||
if long_opt is not None and complete_args is not None:
|
||||
if long_opt is not None:
|
||||
complete_args(ans, long_opt, '', Delegate())
|
||||
return
|
||||
if w is last_word and not new_word:
|
||||
if opt is not None and complete_args is not None:
|
||||
if opt is not None:
|
||||
complete_args(ans, opt, w, Delegate())
|
||||
return
|
||||
expecting_arg = False
|
||||
@@ -444,7 +439,7 @@ def complete_alias_map(
|
||||
parts = w.split('=', 1)
|
||||
if len(parts) == 2:
|
||||
long_opt = option_map.get(parts[0])
|
||||
if long_opt is not None and complete_args is not None:
|
||||
if long_opt is not None:
|
||||
complete_args(ans, long_opt, parts[1], Delegate())
|
||||
ans.add_prefix(f'{parts[0]}=')
|
||||
return
|
||||
@@ -453,23 +448,20 @@ def complete_alias_map(
|
||||
if w.startswith('-'):
|
||||
ans.add_match_group('Options', {k: opt['help'] for k, opt in option_map.items() if k.startswith(last_word)})
|
||||
else:
|
||||
if complete_args is not None:
|
||||
complete_args(ans, None, last_word, Delegate(words, i))
|
||||
complete_args(ans, None, last_word, Delegate(words, i))
|
||||
return
|
||||
if opt is None:
|
||||
if complete_args is not None:
|
||||
complete_args(ans, None, '' if new_word else last_word, Delegate(words, i, new_word))
|
||||
complete_args(ans, None, '' if new_word else last_word, Delegate(words, i, new_word))
|
||||
if w.startswith('--') and '=' in w:
|
||||
continue
|
||||
return # some non-option word encountered
|
||||
expecting_arg = not opt.get('type', '').startswith('bool-')
|
||||
if expecting_arg:
|
||||
if opt is not None and complete_args is not None:
|
||||
if opt is not None:
|
||||
complete_args(ans, opt, '' if new_word else last_word, Delegate())
|
||||
else:
|
||||
prefix = '' if new_word else last_word
|
||||
if complete_args is not None:
|
||||
complete_args(ans, None, prefix, Delegate())
|
||||
complete_args(ans, None, prefix, Delegate())
|
||||
ans.add_match_group('Options', {k: opt['help'] for k, opt in option_map.items() if k.startswith(prefix)})
|
||||
|
||||
|
||||
@@ -477,23 +469,33 @@ def complete_cli(
|
||||
ans: Completions,
|
||||
words: Sequence[str],
|
||||
new_word: bool,
|
||||
seq: OptionSpecSeq,
|
||||
complete_args: Optional[CompleteArgsFunc] = None
|
||||
) -> None:
|
||||
option_map = {}
|
||||
for opt in seq:
|
||||
for opt in options_for_completion():
|
||||
if not isinstance(opt, str):
|
||||
for alias in opt['aliases']:
|
||||
option_map[alias] = opt
|
||||
complete_alias_map(ans, words, new_word, option_map, complete_args)
|
||||
complete_alias_map(ans, words, new_word, option_map, complete_kitty_cli_arg)
|
||||
|
||||
|
||||
def global_options_for_remote_cmd() -> Dict[str, OptionDict]:
|
||||
seq, disabled = parse_option_spec(global_options_spec())
|
||||
ans: Dict[str, OptionDict] = {}
|
||||
for opt in seq:
|
||||
if isinstance(opt, str):
|
||||
continue
|
||||
for alias in opt['aliases']:
|
||||
ans[alias] = opt
|
||||
return ans
|
||||
|
||||
|
||||
def complete_remote_command(ans: Completions, cmd_name: str, words: Sequence[str], new_word: bool) -> None:
|
||||
aliases, alias_map = options_for_cmd(cmd_name)
|
||||
if not alias_map:
|
||||
try:
|
||||
args_completion = command_for_name(cmd_name).args_completion
|
||||
except KeyError:
|
||||
return
|
||||
args_completer: Optional[CompleteArgsFunc] = None
|
||||
args_completion = command_for_name(cmd_name).args_completion
|
||||
args_completer: CompleteArgsFunc = basic_option_arg_completer
|
||||
if args_completion:
|
||||
if 'files' in args_completion:
|
||||
title, matchers = args_completion['files']
|
||||
@@ -509,7 +511,7 @@ def complete_launch_wrapper(ans: Completions, words: Sequence[str], new_word: bo
|
||||
from kitty.launch import clone_safe_opts
|
||||
aliases, alias_map = options_for_cmd('launch')
|
||||
alias_map = {k: v for k, v in alias_map.items() if v['dest'] in clone_safe_opts()}
|
||||
args_completer: Optional[CompleteArgsFunc] = None
|
||||
args_completer: CompleteArgsFunc = basic_option_arg_completer
|
||||
if allow_files:
|
||||
args_completer = remote_files_completer('Files', ('*',))
|
||||
complete_alias_map(ans, words, new_word, alias_map, complete_args=args_completer)
|
||||
@@ -564,9 +566,102 @@ def complete_files_and_dirs(
|
||||
ans.add_match_group(files_group_name, files, is_files=True)
|
||||
|
||||
|
||||
def filter_files_from_completion_spec(spec: Dict[str, str]) -> Callable[['os.DirEntry[str]', str], bool]:
|
||||
|
||||
if 'ext' in spec:
|
||||
extensions = frozenset(os.extsep + x.lower() for x in spec['ext'].split(','))
|
||||
else:
|
||||
extensions = frozenset()
|
||||
|
||||
if 'mime' in spec:
|
||||
import re
|
||||
from fnmatch import translate
|
||||
mimes = tuple(re.compile(translate(x)) for x in spec['mime'].split(','))
|
||||
from .guess_mime_type import guess_type
|
||||
else:
|
||||
mimes = ()
|
||||
|
||||
if mimes or extensions:
|
||||
def check_file(x: 'os.DirEntry[str]', result: str) -> bool:
|
||||
if extensions:
|
||||
q = result.lower()
|
||||
for ext in extensions:
|
||||
if q.endswith(ext):
|
||||
return True
|
||||
if mimes:
|
||||
mq = guess_type(result)
|
||||
if mq:
|
||||
for mime in mimes:
|
||||
if mime.match(mq):
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
def check_file(x: 'os.DirEntry[str]', result: str) -> bool:
|
||||
return True
|
||||
|
||||
return check_file
|
||||
|
||||
|
||||
def complete_file_path(ans: Completions, spec: Dict[str, str], prefix: str, only_dirs: bool = False) -> None:
|
||||
prefix = prefix.replace(r'\ ', ' ')
|
||||
relative_to = spec.get('relative', '')
|
||||
if relative_to:
|
||||
if relative_to == 'conf':
|
||||
relative_to = config_dir
|
||||
else:
|
||||
relative_to = os.getcwd()
|
||||
src_dir = relative_to
|
||||
check_against = prefix
|
||||
prefix_result_with = prefix
|
||||
files, dirs = [], []
|
||||
if prefix:
|
||||
expanded_prefix = os.path.expandvars(os.path.expanduser(prefix))
|
||||
check_against = os.path.basename(expanded_prefix)
|
||||
prefix_result_with = os.path.dirname(expanded_prefix).rstrip(os.sep) + os.sep
|
||||
if os.path.isabs(expanded_prefix):
|
||||
src_dir = os.path.dirname(expanded_prefix)
|
||||
elif os.sep in expanded_prefix or (os.altsep and os.altsep in expanded_prefix):
|
||||
src_dir = os.path.join(relative_to, os.path.dirname(expanded_prefix))
|
||||
else:
|
||||
prefix_result_with = ''
|
||||
try:
|
||||
items: Iterable['os.DirEntry[str]'] = os.scandir(src_dir)
|
||||
except OSError:
|
||||
items = ()
|
||||
check_file = filter_files_from_completion_spec(spec)
|
||||
for x in items:
|
||||
if not x.name.startswith(check_against):
|
||||
continue
|
||||
result = prefix_result_with + x.name
|
||||
if x.is_dir():
|
||||
dirs.append(result.rstrip(os.sep) + os.sep)
|
||||
else:
|
||||
if check_file(x, result):
|
||||
files.append(result)
|
||||
if dirs:
|
||||
ans.add_match_group('Directories', dirs, trailing_space=False, is_files=True)
|
||||
if not only_dirs and files:
|
||||
ans.add_match_group(spec.get('group') or 'Files', files, is_files=True)
|
||||
|
||||
|
||||
def complete_path(ans: Completions, opt: OptionDict, prefix: str) -> None:
|
||||
spec = opt['completion']
|
||||
t = spec['type']
|
||||
if 'kwds' in spec:
|
||||
kwds = [x for x in spec['kwds'].split(',') if x.startswith(prefix)]
|
||||
if kwds:
|
||||
ans.add_match_group('Keywords', kwds)
|
||||
if t == 'file':
|
||||
complete_file_path(ans, spec, prefix)
|
||||
elif t == 'directory':
|
||||
complete_file_path(ans, spec, prefix, only_dirs=True)
|
||||
|
||||
|
||||
def complete_basic_option_args(ans: Completions, opt: OptionDict, prefix: str) -> None:
|
||||
if opt['choices']:
|
||||
ans.add_match_group(f'Choices for {opt["dest"]}', tuple(k for k in opt['choices'] if k.startswith(prefix)))
|
||||
elif opt['completion'].get('type') in ('file', 'directory'):
|
||||
complete_path(ans, opt, prefix)
|
||||
|
||||
|
||||
def complete_dirs(ans: Completions, prefix: str = '') -> None:
|
||||
@@ -630,6 +725,19 @@ def remote_args_completer(title: str, words: Iterable[str]) -> CompleteArgsFunc:
|
||||
return complete_names_for_arg
|
||||
|
||||
|
||||
def remote_command_completer(ans: Completions, opt: Optional[OptionDict], prefix: str, unknown_args: Delegate) -> None:
|
||||
if opt is None:
|
||||
words = unknown_args.words[unknown_args.pos:]
|
||||
new_word = unknown_args.new_word
|
||||
if not words or (len(words) == 1 and not new_word):
|
||||
prefix = (words or ('',))[0]
|
||||
ans.add_match_group('Remote control commands', {c: '' for c in remote_control_command_names() if c.startswith(prefix)})
|
||||
else:
|
||||
complete_remote_command(ans, words[0], words[1:], new_word)
|
||||
else:
|
||||
basic_option_arg_completer(ans, opt, prefix, unknown_args)
|
||||
|
||||
|
||||
def config_file_predicate(filename: str) -> bool:
|
||||
return filename.endswith('.conf')
|
||||
|
||||
@@ -660,42 +768,38 @@ def complete_kitten(ans: Completions, kitten: str, words: Sequence[str], new_wor
|
||||
options = cd['options']()
|
||||
seq = parse_option_spec(options)[0]
|
||||
option_map = {}
|
||||
if not new_word:
|
||||
for opt in seq:
|
||||
if not isinstance(opt, str):
|
||||
for alias in opt['aliases']:
|
||||
option_map[alias] = opt
|
||||
for opt in seq:
|
||||
if not isinstance(opt, str):
|
||||
for alias in opt['aliases']:
|
||||
option_map[alias] = opt
|
||||
complete_alias_map(ans, words, new_word, option_map, {
|
||||
'icat': complete_icat_args,
|
||||
'diff': complete_diff_args,
|
||||
'themes': complete_themes_args,
|
||||
}.get(kitten))
|
||||
}.get(kitten, basic_option_arg_completer))
|
||||
|
||||
|
||||
def find_completions(words: Sequence[str], new_word: bool, entry_points: Iterable[str], namespaced_entry_points: Iterable[str]) -> Completions:
|
||||
ans = Completions()
|
||||
if not words:
|
||||
return ans
|
||||
if words[0] in ('edit-in-kitty', 'clone-in-kitty'):
|
||||
exe = os.path.basename(words[0])
|
||||
if exe in ('edit-in-kitty', 'clone-in-kitty'):
|
||||
complete_launch_wrapper(ans, words[1:], new_word, allow_files=words[0] != 'clone-in-kitty')
|
||||
return ans
|
||||
if words[0] != 'kitty':
|
||||
if exe != 'kitty':
|
||||
return ans
|
||||
words = words[1:]
|
||||
if not words or (len(words) == 1 and not new_word):
|
||||
if words and words[0].startswith('--') and '=' in words[0]:
|
||||
complete_cli(ans, words, new_word, options_for_completion(), complete_kitty_cli_arg)
|
||||
complete_cli(ans, words, new_word)
|
||||
return ans
|
||||
prefix = words[0] if words else ''
|
||||
completions_for_first_word(ans, prefix, entry_points, namespaced_entry_points)
|
||||
kitty_cli_opts(ans, prefix)
|
||||
return ans
|
||||
if words[0] == '@':
|
||||
if len(words) == 1 or (len(words) == 2 and not new_word):
|
||||
prefix = words[1] if len(words) > 1 else ''
|
||||
ans.add_match_group('Remote control commands', {c: '' for c in remote_control_command_names() if c.startswith(prefix)})
|
||||
else:
|
||||
complete_remote_command(ans, words[1], words[2:], new_word)
|
||||
complete_alias_map(ans, words[1:], new_word, global_options_for_remote_cmd(), remote_command_completer)
|
||||
return ans
|
||||
if words[0].startswith('@'):
|
||||
if len(words) == 1 and not new_word:
|
||||
@@ -703,6 +807,7 @@ def find_completions(words: Sequence[str], new_word: bool, entry_points: Iterabl
|
||||
ans.add_match_group('Remote control commands', {f'@{c}': '' for c in remote_control_command_names() if c.startswith(prefix)})
|
||||
else:
|
||||
complete_remote_command(ans, words[0][1:], words[1:], new_word)
|
||||
return ans
|
||||
if words[0] == '+':
|
||||
if len(words) == 1 or (len(words) == 2 and not new_word):
|
||||
prefix = words[1] if len(words) > 1 else ''
|
||||
@@ -714,7 +819,7 @@ def find_completions(words: Sequence[str], new_word: bool, entry_points: Iterabl
|
||||
else:
|
||||
complete_kitten(ans, words[2], words[3:], new_word)
|
||||
elif words[1] == 'open':
|
||||
complete_cli(ans, words[2:], new_word, options_for_completion(), complete_kitty_cli_arg)
|
||||
complete_cli(ans, words[2:], new_word)
|
||||
return ans
|
||||
if words[0].startswith('+'):
|
||||
if len(words) == 1:
|
||||
@@ -722,7 +827,7 @@ def find_completions(words: Sequence[str], new_word: bool, entry_points: Iterabl
|
||||
if words[0] == '+kitten':
|
||||
ans.add_match_group('Kittens', all_kitten_names())
|
||||
elif words[0] == '+open':
|
||||
complete_cli(ans, words[1:], new_word, options_for_completion(), complete_kitty_cli_arg)
|
||||
complete_cli(ans, words[1:], new_word)
|
||||
else:
|
||||
prefix = words[0]
|
||||
ans.add_match_group('Entry points', (c for c in namespaced_entry_points if c.startswith(prefix)))
|
||||
@@ -733,9 +838,9 @@ def find_completions(words: Sequence[str], new_word: bool, entry_points: Iterabl
|
||||
else:
|
||||
complete_kitten(ans, words[1], words[2:], new_word)
|
||||
elif words[0] == '+open':
|
||||
complete_cli(ans, words[1:], new_word, options_for_completion(), complete_kitty_cli_arg)
|
||||
complete_cli(ans, words[1:], new_word)
|
||||
else:
|
||||
complete_cli(ans, words, new_word, options_for_completion(), complete_kitty_cli_arg)
|
||||
complete_cli(ans, words, new_word)
|
||||
|
||||
return ans
|
||||
|
||||
|
||||
@@ -390,7 +390,7 @@ def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
|
||||
return class_def, '\n'.join(preamble + ['', ''] + tc_lines)
|
||||
|
||||
|
||||
def generate_c_conversion(loc: str, ctypes: List[Option]) -> str:
|
||||
def generate_c_conversion(loc: str, ctypes: List[Union[Option, MultiOption]]) -> str:
|
||||
lines: List[str] = []
|
||||
basic_converters = {
|
||||
'int': 'PyLong_AsLong', 'uint': 'PyLong_AsUnsignedLong', 'bool': 'PyObject_IsTrue',
|
||||
@@ -437,7 +437,7 @@ def write_output(loc: str, defn: Definition) -> None:
|
||||
f.write(f'{tc}\n')
|
||||
ctypes = []
|
||||
for opt in defn.root_group.iter_all_non_groups():
|
||||
if isinstance(opt, Option) and opt.ctype:
|
||||
if isinstance(opt, (Option, MultiOption)) and opt.ctype:
|
||||
ctypes.append(opt)
|
||||
if ctypes:
|
||||
c = generate_c_conversion(loc, ctypes)
|
||||
|
||||
@@ -45,27 +45,50 @@ def expand_opt_references(conf_name: str, text: str) -> str:
|
||||
|
||||
|
||||
@run_once
|
||||
def ref_map() -> Dict[str, str]:
|
||||
from kitty.actions import get_all_actions
|
||||
ref_map = {
|
||||
'layouts': f'{website_url("overview")}#layouts',
|
||||
'include': f'{website_url("conf")}#include',
|
||||
'watchers': f'{website_url("launch")}#watchers',
|
||||
'sessions': f'{website_url("overview")}#startup-sessions',
|
||||
'functional': f'{website_url("keyboard-protocol")}#functional-key-definitions',
|
||||
'ssh_copy_command': f'{website_url("kittens/ssh")}#ssh-copy-command',
|
||||
'shell_integration': website_url("shell-integration"),
|
||||
'clone_shell': f'{website_url("shell-integration")}#clone-shell',
|
||||
'github_discussions': 'https://github.com/kovidgoyal/kitty/discussions',
|
||||
}
|
||||
for actions in get_all_actions().values():
|
||||
for ac in actions:
|
||||
ref_map[f'action-{ac.name}'] = f'{website_url("actions")}#' + ac.name.replace('_', '-')
|
||||
return ref_map
|
||||
def ref_map() -> Dict[str, Dict[str, str]]:
|
||||
import json
|
||||
from ..fast_data_types import get_docs_ref_map
|
||||
ans: Dict[str, Dict[str, str]] = json.loads(get_docs_ref_map())
|
||||
return ans
|
||||
|
||||
|
||||
def resolve_ref(ref: str, website_url: Callable[[str], str] = website_url) -> str:
|
||||
m = ref_map()
|
||||
href = m['ref'].get(ref, '')
|
||||
prefix, rest = ref.partition('-')[::2]
|
||||
if href:
|
||||
pass
|
||||
elif ref.startswith('conf-kitty-'):
|
||||
href = f'conf#{ref}'
|
||||
elif ref.startswith('conf-kitten-'):
|
||||
parts = ref.split('-')
|
||||
href = "kittens/" + parts[2] + f'/#{ref}'
|
||||
elif ref.startswith('at_'):
|
||||
base = ref.split('_', 1)[1]
|
||||
href = "remote-control/#at-" + base.replace('_', '-')
|
||||
elif ref.startswith('at-'):
|
||||
base = ref.split('-', 1)[1]
|
||||
href = "remote-control/#at-" + base.replace('_', '-')
|
||||
elif ref.startswith('action-group-'):
|
||||
href = f'actions/#{ref}'
|
||||
elif prefix == 'action':
|
||||
href = f'actions/#{rest.replace("_", "-")}'
|
||||
elif prefix in ('term', 'envvar'):
|
||||
href = 'glossary/#' + ref
|
||||
elif prefix == 'doc':
|
||||
href = rest.lstrip('/')
|
||||
elif prefix in ('issues', 'pull', 'discussions'):
|
||||
t, num = ref.partition(':')[::2]
|
||||
href = f'https://github.com/kovidgoyal/kitty/{prefix}/{rest}'
|
||||
if not (href.startswith('https://') or href.startswith('http://')):
|
||||
href = website_url(href)
|
||||
return href
|
||||
|
||||
|
||||
def remove_markup(text: str) -> str:
|
||||
|
||||
imap = {'iss': 'issues-', 'pull': 'pull-', 'disc': 'discussions-'}
|
||||
|
||||
def extract(m: 'Match[str]') -> Tuple[str, str]:
|
||||
parts = m.group(2).split('<')
|
||||
t = parts[0].strip()
|
||||
@@ -73,24 +96,24 @@ def remove_markup(text: str) -> str:
|
||||
return t, q
|
||||
|
||||
def sub(m: 'Match[str]') -> str:
|
||||
if m.group(1) == 'ref':
|
||||
key = m.group(1)
|
||||
if key in ('ref', 'iss', 'pull', 'disc'):
|
||||
t, q = extract(m)
|
||||
return f'{t} <{ref_map()[q]}>'
|
||||
if m.group(1) == 'doc':
|
||||
q = imap.get(key, '') + q
|
||||
url = resolve_ref(q)
|
||||
if not url:
|
||||
raise KeyError(f'Failed to resolve :{m.group(1)}: {q}')
|
||||
return f'{t} <{url}>'
|
||||
if key == 'doc':
|
||||
t, q = extract(m)
|
||||
return f'{t} <{website_url(q.lstrip("/"))}>'
|
||||
if m.group(1) == 'ac':
|
||||
q = m.group(2).split('<')[-1].rstrip('>')
|
||||
return q
|
||||
if m.group(1) == 'term':
|
||||
return f'{t} <{website_url(q)}>'
|
||||
if key in ('term', 'option'):
|
||||
t, _ = extract(m)
|
||||
return t
|
||||
if m.group(1) == 'option':
|
||||
t, _ = extract(m)
|
||||
return t
|
||||
if m.group(1) == 'disc':
|
||||
if key in ('ac', 'opt'):
|
||||
t, q = extract(m)
|
||||
return f'{t} {ref_map()["github_discussions"]}/{q}'
|
||||
return f'{t} {q}' if q and q != t else t
|
||||
|
||||
return str(m.group(2))
|
||||
|
||||
return re.sub(r':([a-zA-Z0-9]+):`(.+?)`', sub, text, flags=re.DOTALL)
|
||||
|
||||
@@ -79,24 +79,27 @@ class ToCmdline:
|
||||
self.override_env.update(override)
|
||||
return self
|
||||
|
||||
def __call__(self, x: str) -> List[str]:
|
||||
return list(
|
||||
map(
|
||||
lambda y: expandvars(
|
||||
os.path.expanduser(y),
|
||||
os.environ if self.override_env is None else self.override_env,
|
||||
fallback_to_os_env=False
|
||||
),
|
||||
shlex.split(x)
|
||||
def __call__(self, x: str, expand: bool = True) -> List[str]:
|
||||
ans = shlex.split(x)
|
||||
if expand:
|
||||
ans = list(
|
||||
map(
|
||||
lambda y: expandvars(
|
||||
os.path.expanduser(y),
|
||||
os.environ if self.override_env is None else self.override_env,
|
||||
fallback_to_os_env=False
|
||||
),
|
||||
ans
|
||||
)
|
||||
)
|
||||
)
|
||||
return ans
|
||||
|
||||
|
||||
to_cmdline_implementation = ToCmdline()
|
||||
|
||||
|
||||
def to_cmdline(x: str) -> List[str]:
|
||||
return to_cmdline_implementation(x)
|
||||
def to_cmdline(x: str, expand: bool = True) -> List[str]:
|
||||
return to_cmdline_implementation(x, expand)
|
||||
|
||||
|
||||
def python_string(text: str) -> str:
|
||||
@@ -185,7 +188,7 @@ def parse_line(
|
||||
val = os.path.expandvars(os.path.expanduser(val.strip()))
|
||||
if key == 'globinclude':
|
||||
from pathlib import Path
|
||||
vals = tuple(map(lambda x: str(os.fspath(x)), Path(base_path_for_includes).glob(val)))
|
||||
vals = tuple(map(lambda x: str(os.fspath(x)), sorted(Path(base_path_for_includes).glob(val))))
|
||||
elif key == 'envinclude':
|
||||
from fnmatch import fnmatchcase
|
||||
for x in os.environ:
|
||||
|
||||
@@ -6,7 +6,7 @@ import os
|
||||
import pwd
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
from typing import TYPE_CHECKING, Iterable, NamedTuple, Optional, Set
|
||||
from typing import TYPE_CHECKING, Any, Iterable, NamedTuple, Optional, Set
|
||||
|
||||
from .types import run_once
|
||||
|
||||
@@ -22,14 +22,15 @@ class Version(NamedTuple):
|
||||
|
||||
appname: str = 'kitty'
|
||||
kitty_face = '🐱'
|
||||
version: Version = Version(0, 25, 2)
|
||||
version: Version = Version(0, 26, 0)
|
||||
str_version: str = '.'.join(map(str, version))
|
||||
_plat = sys.platform.lower()
|
||||
is_macos: bool = 'darwin' in _plat
|
||||
is_freebsd: bool = 'freebsd' in _plat
|
||||
is_running_from_develop: bool = False
|
||||
RC_ENCRYPTION_PROTOCOL_VERSION = '1'
|
||||
if getattr(sys, 'frozen', False):
|
||||
extensions_dir: str = getattr(sys, 'kitty_extensions_dir')
|
||||
extensions_dir: str = getattr(sys, 'kitty_run_data')['extensions_dir']
|
||||
|
||||
def get_frozen_base() -> str:
|
||||
global is_running_from_develop
|
||||
@@ -61,7 +62,7 @@ else:
|
||||
|
||||
@run_once
|
||||
def kitty_exe() -> str:
|
||||
rpath = sys._xoptions.get('bundle_exe_dir')
|
||||
rpath = getattr(sys, 'kitty_run_data').get('bundle_exe_dir')
|
||||
if not rpath:
|
||||
items = os.environ.get('PATH', '').split(os.pathsep) + [os.path.join(kitty_base_dir, 'kitty', 'launcher')]
|
||||
seen: Set[str] = set()
|
||||
@@ -158,7 +159,7 @@ def runtime_dir() -> str:
|
||||
return candidate
|
||||
|
||||
|
||||
def wakeup() -> None:
|
||||
def wakeup_io_loop() -> None:
|
||||
from .fast_data_types import get_boss
|
||||
b = get_boss()
|
||||
if b is not None:
|
||||
@@ -229,9 +230,50 @@ def read_kitty_resource(name: str, package_name: str = 'kitty') -> bytes:
|
||||
return read_binary(package_name, name)
|
||||
|
||||
|
||||
def website_url(doc_name: str = '') -> str:
|
||||
def website_url(doc_name: str = '', website: str = 'https://sw.kovidgoyal.net/kitty/') -> str:
|
||||
if doc_name:
|
||||
doc_name = doc_name.rstrip('/')
|
||||
if doc_name:
|
||||
doc_name += '/'
|
||||
return f'https://sw.kovidgoyal.net/kitty/{doc_name}'
|
||||
base, _, frag = doc_name.partition('#')
|
||||
base = base.rstrip('/')
|
||||
if base:
|
||||
base += '/'
|
||||
doc_name = base + (f'#{frag}' if frag else '')
|
||||
return website + doc_name.lstrip('/')
|
||||
|
||||
|
||||
handled_signals: Set[int] = set()
|
||||
|
||||
|
||||
def clear_handled_signals(*a: Any) -> None:
|
||||
if not handled_signals:
|
||||
return
|
||||
import signal
|
||||
if hasattr(signal, 'pthread_sigmask'):
|
||||
signal.pthread_sigmask(signal.SIG_UNBLOCK, handled_signals)
|
||||
for s in handled_signals:
|
||||
signal.signal(s, signal.SIG_DFL)
|
||||
|
||||
|
||||
@run_once
|
||||
def local_docs() -> str:
|
||||
d = os.path.dirname
|
||||
base = d(d(kitty_exe()))
|
||||
from_source = getattr(sys, 'kitty_run_data').get('from_source')
|
||||
if is_macos and from_source and '/kitty.app/Contents/' in kitty_exe():
|
||||
base = d(d(d(base)))
|
||||
subdir = os.path.join('doc', 'kitty', 'html')
|
||||
linux_ans = os.path.join(base, 'share', subdir)
|
||||
if getattr(sys, 'frozen', False):
|
||||
if is_macos:
|
||||
return os.path.join(d(d(d(extensions_dir))), subdir)
|
||||
return linux_ans
|
||||
if os.path.isdir(linux_ans):
|
||||
return linux_ans
|
||||
if from_source:
|
||||
sq = os.path.join(d(base), 'docs', '_build', 'html')
|
||||
if os.path.isdir(sq):
|
||||
return sq
|
||||
for candidate in ('/usr', '/usr/local', '/opt/homebrew'):
|
||||
q = os.path.join(candidate, 'share', subdir)
|
||||
if os.path.isdir(q):
|
||||
return q
|
||||
return ''
|
||||
|
||||
@@ -324,13 +324,6 @@ harfbuzz_font_for_face(PyObject* s) {
|
||||
return self->hb_font;
|
||||
}
|
||||
|
||||
static unsigned int
|
||||
adjust_ypos(unsigned int pos, unsigned int cell_height, int adjustment) {
|
||||
if (adjustment >= 0) adjustment = MIN(adjustment, (int)pos - 1);
|
||||
else adjustment = MAX(adjustment, (int)pos - (int)cell_height + 1);
|
||||
return pos - adjustment;
|
||||
}
|
||||
|
||||
void
|
||||
cell_metrics(PyObject *s, unsigned int* cell_width, unsigned int* cell_height, unsigned int* baseline, unsigned int* underline_position, unsigned int* underline_thickness, unsigned int* strikethrough_position, unsigned int* strikethrough_thickness) {
|
||||
// See https://developer.apple.com/library/content/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/TypoFeatures/TextSystemFeatures.html
|
||||
@@ -376,9 +369,6 @@ cell_metrics(PyObject *s, unsigned int* cell_width, unsigned int* cell_height, u
|
||||
CTLineGetTypographicBounds(line, &typographic_ascent, &typographic_descent, &typographic_leading);
|
||||
*cell_height = MAX(4u, (unsigned int)ceilf(line_height));
|
||||
CGFloat bounds_ascent = bounds_without_leading.size.height + bounds_without_leading.origin.y;
|
||||
int baseline_offset = 0;
|
||||
if (OPT(adjust_baseline_px) != 0) baseline_offset = OPT(adjust_baseline_px);
|
||||
else if (OPT(adjust_baseline_frac) != 0) baseline_offset = (int)(*cell_height * OPT(adjust_baseline_frac));
|
||||
*baseline = (unsigned int)floor(bounds_ascent + 0.5);
|
||||
// Not sure if we should add this to bounds ascent and then round it or add
|
||||
// it to already rounded baseline and round again.
|
||||
@@ -393,12 +383,6 @@ cell_metrics(PyObject *s, unsigned int* cell_width, unsigned int* cell_height, u
|
||||
debug("\tline metrics: ascent: %f descent: %f leading: %f\n", typographic_ascent, typographic_descent, typographic_leading);
|
||||
debug("\tfont metrics: ascent: %f descent: %f leading: %f underline_position: %f\n", self->ascent, self->descent, self->leading, self->underline_position);
|
||||
debug("\tcell_height: %u baseline: %u underline_position: %u strikethrough_position: %u\n", *cell_height, *baseline, *underline_position, *strikethrough_position);
|
||||
if (baseline_offset) {
|
||||
*baseline = adjust_ypos(*baseline, *cell_height, baseline_offset);
|
||||
*underline_position = adjust_ypos(*underline_position, *cell_height, baseline_offset);
|
||||
*strikethrough_position = adjust_ypos(*strikethrough_position, *cell_height, baseline_offset);
|
||||
}
|
||||
|
||||
CFRelease(test_frame); CFRelease(path); CFRelease(framesetter);
|
||||
|
||||
#undef count
|
||||
|
||||
468
kitty/crypto.c
Normal file
468
kitty/crypto.c
Normal file
@@ -0,0 +1,468 @@
|
||||
/*
|
||||
* crypto.c
|
||||
* Copyright (C) 2022 Kovid Goyal <kovid at kovidgoyal.net>
|
||||
*
|
||||
* Distributed under terms of the GPL3 license.
|
||||
*/
|
||||
|
||||
#include "data-types.h"
|
||||
#include "cross-platform-random.h"
|
||||
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/ec.h>
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <sys/mman.h>
|
||||
#include <structmember.h>
|
||||
|
||||
#define SHA1_DIGEST_LENGTH SHA_DIGEST_LENGTH
|
||||
|
||||
typedef enum HASH_ALGORITHM { SHA1_HASH, SHA224_HASH, SHA256_HASH, SHA384_HASH, SHA512_HASH } HASH_ALGORITHM;
|
||||
static PyObject* Crypto_Exception = NULL;
|
||||
|
||||
static PyObject*
|
||||
set_error_from_openssl(const char *prefix) {
|
||||
BIO *bio = BIO_new(BIO_s_mem());
|
||||
ERR_print_errors(bio);
|
||||
char *buf = NULL;
|
||||
size_t len = BIO_get_mem_data(bio, &buf);
|
||||
PyObject *msg = PyUnicode_FromStringAndSize(buf, len);
|
||||
if (msg) PyErr_Format(Crypto_Exception, "%s: %U", prefix, msg);
|
||||
BIO_free(bio);
|
||||
Py_CLEAR(msg);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Secret {{{
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
|
||||
void *secret;
|
||||
size_t secret_len;
|
||||
} Secret;
|
||||
|
||||
static PyObject *
|
||||
new_secret(PyTypeObject *type UNUSED, PyObject *args UNUSED, PyObject *kwds UNUSED) {
|
||||
PyErr_SetString(PyExc_TypeError, "Cannot create Secret objects directly"); return NULL;
|
||||
}
|
||||
|
||||
static Secret* alloc_secret(size_t len);
|
||||
|
||||
static void
|
||||
dealloc_secret(Secret *self) {
|
||||
if (self->secret) OPENSSL_clear_free(self->secret, self->secret_len);
|
||||
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||
}
|
||||
|
||||
static int
|
||||
__eq__(Secret *a, Secret *b) {
|
||||
const size_t l = a->secret_len < b->secret_len ? a->secret_len : b->secret_len;
|
||||
return memcmp(a->secret, b->secret, l) == 0;
|
||||
}
|
||||
|
||||
static Py_ssize_t
|
||||
__len__(PyObject *self) {
|
||||
return (Py_ssize_t)(((Secret*)self)->secret_len);
|
||||
}
|
||||
|
||||
|
||||
static PySequenceMethods sequence_methods = {
|
||||
.sq_length = __len__,
|
||||
};
|
||||
|
||||
|
||||
static PyObject *
|
||||
richcmp(PyObject *obj1, PyObject *obj2, int op);
|
||||
|
||||
static PyTypeObject Secret_Type = {
|
||||
PyVarObject_HEAD_INIT(NULL, 0)
|
||||
.tp_name = "fast_data_types.Secret",
|
||||
.tp_basicsize = sizeof(Secret),
|
||||
.tp_dealloc = (destructor)dealloc_secret,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = "Secure storage for secrets",
|
||||
.tp_new = new_secret,
|
||||
.tp_richcompare = richcmp,
|
||||
.tp_as_sequence = &sequence_methods,
|
||||
};
|
||||
|
||||
RICHCMP(Secret)
|
||||
|
||||
static Secret*
|
||||
alloc_secret(size_t len) {
|
||||
Secret *self = (Secret*)Secret_Type.tp_alloc(&Secret_Type, 0);
|
||||
if (self) {
|
||||
self->secret_len = len;
|
||||
if (NULL == (self->secret = OPENSSL_malloc(len))) { Py_CLEAR(self); return (Secret*)set_error_from_openssl("Failed to malloc"); }
|
||||
if (0 != mlock(self->secret, self->secret_len)) { Py_CLEAR(self); return (Secret*)PyErr_SetFromErrno(PyExc_OSError); }
|
||||
}
|
||||
return self;
|
||||
}
|
||||
// }}}
|
||||
|
||||
// EllipticCurveKey {{{
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
|
||||
EVP_PKEY *key;
|
||||
int algorithm, nid;
|
||||
} EllipticCurveKey;
|
||||
|
||||
|
||||
static PyObject *
|
||||
new_ec_key(PyTypeObject *type, PyObject *args, PyObject *kwds) {
|
||||
EllipticCurveKey *self;
|
||||
static const char* kwlist[] = {"algorithm", NULL};
|
||||
int algorithm = EVP_PKEY_X25519, nid = NID_X25519;
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|i", (char**)kwlist, &algorithm)) return NULL;
|
||||
switch(algorithm) {
|
||||
case EVP_PKEY_X25519: break;
|
||||
default: PyErr_SetString(PyExc_KeyError, "Unknown algorithm"); return NULL;
|
||||
}
|
||||
EVP_PKEY *key = NULL;
|
||||
EVP_PKEY_CTX *pctx = NULL;
|
||||
#define cleanup() { if (key) EVP_PKEY_free(key); key = NULL; if (pctx) EVP_PKEY_CTX_free(pctx); pctx = NULL; }
|
||||
#define ssl_error(text) { cleanup(); return set_error_from_openssl(text); }
|
||||
|
||||
if (NULL == (pctx = EVP_PKEY_CTX_new_id(nid, NULL))) ssl_error("Failed to create context for key generation");
|
||||
if(1 != EVP_PKEY_keygen_init(pctx)) ssl_error("Failed to initialize keygen context");
|
||||
if (1 != EVP_PKEY_keygen(pctx, &key)) ssl_error("Failed to generate key");
|
||||
|
||||
self = (EllipticCurveKey *)type->tp_alloc(type, 0);
|
||||
if (self) {
|
||||
self->key = key; key = NULL;
|
||||
self->nid = nid; self->algorithm = algorithm;
|
||||
}
|
||||
cleanup();
|
||||
return (PyObject*) self;
|
||||
#undef cleanup
|
||||
#undef ssl_error
|
||||
}
|
||||
|
||||
static void
|
||||
dealloc_ec_key(EllipticCurveKey* self) {
|
||||
if (self->key) EVP_PKEY_free(self->key);
|
||||
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
hash_data_to_secret(const unsigned char *data, size_t len, int hash_algorithm) {
|
||||
size_t hash_size;
|
||||
#define H(which) case which##_HASH: hash_size = which##_DIGEST_LENGTH; break;
|
||||
switch (hash_algorithm) {
|
||||
H(SHA1) H(SHA224) H(SHA256) H(SHA384) H(SHA512)
|
||||
default: PyErr_Format(PyExc_KeyError, "Unknown hash algorithm: %d", hash_algorithm); return NULL;
|
||||
}
|
||||
#undef H
|
||||
Secret *ans = alloc_secret(hash_size);
|
||||
if (!ans) return NULL;
|
||||
#define H(which) case which##_HASH: if (which(data, len, ans->secret) == NULL) { Py_CLEAR(ans); return set_error_from_openssl("Failed to " #which); } break;
|
||||
switch ((HASH_ALGORITHM)hash_algorithm) { H(SHA1) H(SHA224) H(SHA256) H(SHA384) H(SHA512) }
|
||||
#undef H
|
||||
return (PyObject*)ans;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
derive_secret(EllipticCurveKey *self, PyObject *args) {
|
||||
const char *pubkey_raw;
|
||||
int hash_algorithm = SHA256_HASH;
|
||||
Py_ssize_t pubkey_len;
|
||||
if (!PyArg_ParseTuple(args, "y#|i", &pubkey_raw, &pubkey_len, &hash_algorithm)) return NULL;
|
||||
|
||||
EVP_PKEY_CTX *ctx = NULL;
|
||||
unsigned char *secret = NULL; size_t secret_len = 0;
|
||||
EVP_PKEY *public_key = EVP_PKEY_new_raw_public_key(self->algorithm, NULL, (const unsigned char*)pubkey_raw, pubkey_len);
|
||||
#define cleanup() { if (public_key) EVP_PKEY_free(public_key); public_key = NULL; if (ctx) EVP_PKEY_CTX_free(ctx); ctx = NULL; if (secret) OPENSSL_clear_free(secret, secret_len); secret = NULL; }
|
||||
#define ssl_error(text) { cleanup(); return set_error_from_openssl(text); }
|
||||
if (!public_key) ssl_error("Failed to create public key");
|
||||
|
||||
if (NULL == (ctx = EVP_PKEY_CTX_new(self->key, NULL))) ssl_error("Failed to create context for shared secret derivation");
|
||||
if (1 != EVP_PKEY_derive_init(ctx)) ssl_error("Failed to initialize derivation");
|
||||
if (1 != EVP_PKEY_derive_set_peer(ctx, public_key)) ssl_error("Failed to add public key");
|
||||
|
||||
if (1 != EVP_PKEY_derive(ctx, NULL, &secret_len)) ssl_error("Failed to get length for secret");
|
||||
if (NULL == (secret = OPENSSL_malloc(secret_len))) ssl_error("Failed to allocate secret key");
|
||||
if (mlock(secret, secret_len) != 0) { cleanup(); return PyErr_SetFromErrno(PyExc_OSError); }
|
||||
if (1 != (EVP_PKEY_derive(ctx, secret, &secret_len))) ssl_error("Failed to derive the secret");
|
||||
|
||||
PyObject *ans = hash_data_to_secret(secret, secret_len, hash_algorithm);
|
||||
cleanup();
|
||||
return ans;
|
||||
#undef cleanup
|
||||
#undef ssl_error
|
||||
}
|
||||
|
||||
|
||||
static PyObject*
|
||||
elliptic_curve_key_get_public(EllipticCurveKey *self, void UNUSED *closure) {
|
||||
/* PEM_write_PUBKEY(stdout, pkey); */
|
||||
size_t len = 0;
|
||||
if (1 != EVP_PKEY_get_raw_public_key(self->key, NULL, &len)) return set_error_from_openssl("Could not get public key from EVP_PKEY");
|
||||
PyObject *ans = PyBytes_FromStringAndSize(NULL, len);
|
||||
if (!ans) return NULL;
|
||||
if (1 != EVP_PKEY_get_raw_public_key(self->key, (unsigned char*)PyBytes_AS_STRING(ans), &len)) { Py_CLEAR(ans); return set_error_from_openssl("Could not get public key from EVP_PKEY"); }
|
||||
return ans;
|
||||
}
|
||||
|
||||
|
||||
static PyObject*
|
||||
elliptic_curve_key_get_private(EllipticCurveKey *self, void UNUSED *closure) {
|
||||
size_t len = 0;
|
||||
if (1 != EVP_PKEY_get_raw_private_key(self->key, NULL, &len)) return set_error_from_openssl("Could not get public key from EVP_PKEY");
|
||||
Secret *ans = alloc_secret(len);
|
||||
if (!ans) return NULL;
|
||||
if (mlock(PyBytes_AS_STRING(ans), len) != 0) { Py_CLEAR(ans); return PyErr_SetFromErrno(PyExc_OSError); }
|
||||
if (1 != EVP_PKEY_get_raw_private_key(self->key, (unsigned char*)ans->secret, &len)) { Py_CLEAR(ans); return set_error_from_openssl("Could not get public key from EVP_PKEY"); }
|
||||
return (PyObject*)ans;
|
||||
}
|
||||
|
||||
|
||||
static PyGetSetDef getsetters[] = {
|
||||
{"public", (getter)elliptic_curve_key_get_public, NULL, "Get the public key as raw bytes", NULL},
|
||||
{"private", (getter)elliptic_curve_key_get_private, NULL, "Get the private key as raw bytes", NULL},
|
||||
{NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
static PyMethodDef methods[] = {
|
||||
METHODB(derive_secret, METH_VARARGS),
|
||||
{NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
|
||||
static PyTypeObject EllipticCurveKey_Type = {
|
||||
PyVarObject_HEAD_INIT(NULL, 0)
|
||||
.tp_name = "fast_data_types.EllipticCurveKey",
|
||||
.tp_basicsize = sizeof(EllipticCurveKey),
|
||||
.tp_dealloc = (destructor)dealloc_ec_key,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = "Keys for use with Elliptic Curve crypto",
|
||||
.tp_new = new_ec_key,
|
||||
.tp_methods = methods,
|
||||
.tp_getset = getsetters,
|
||||
};
|
||||
// }}}
|
||||
|
||||
// AES256GCMEncrypt {{{
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
|
||||
EVP_CIPHER_CTX *ctx;
|
||||
PyObject *iv, *tag;
|
||||
int state;
|
||||
} AES256GCMEncrypt;
|
||||
|
||||
static PyObject *
|
||||
new_aes256gcmencrypt(PyTypeObject *type, PyObject *args, PyObject *kwds UNUSED) {
|
||||
Secret *key;
|
||||
if (!PyArg_ParseTuple(args, "O!", &Secret_Type, &key)) return NULL;
|
||||
const EVP_CIPHER *cipher = EVP_get_cipherbynid(NID_aes_256_gcm);
|
||||
if (key->secret_len != (size_t)EVP_CIPHER_key_length(cipher)) { PyErr_Format(PyExc_ValueError, "The key for AES 256 GCM must be %d bytes long", EVP_CIPHER_key_length(cipher)); return NULL; }
|
||||
AES256GCMEncrypt *self = (AES256GCMEncrypt *)type->tp_alloc(type, 0);
|
||||
if (!self) return NULL;
|
||||
if (!(self->ctx = EVP_CIPHER_CTX_new())) { Py_CLEAR(self); return set_error_from_openssl("Failed to allocate encryption context"); }
|
||||
if (!(self->iv = PyBytes_FromStringAndSize(NULL, EVP_CIPHER_iv_length(cipher)))) { Py_CLEAR(self); return NULL; }
|
||||
if (!secure_random_bytes((unsigned char*)PyBytes_AS_STRING(self->iv), PyBytes_GET_SIZE(self->iv))) { Py_CLEAR(self); return NULL; }
|
||||
if (!(self->tag = PyBytes_FromStringAndSize(NULL, 0))) { Py_CLEAR(self); return NULL; }
|
||||
if (1 != EVP_EncryptInit_ex(self->ctx, cipher, NULL, key->secret, (const unsigned char*)PyBytes_AS_STRING(self->iv))) {
|
||||
Py_CLEAR(self); return set_error_from_openssl("Failed to initialize encryption context"); }
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
static void
|
||||
dealloc_aes256gcmencrypt(AES256GCMEncrypt *self) {
|
||||
Py_CLEAR(self->iv); Py_CLEAR(self->tag);
|
||||
if (self->ctx) EVP_CIPHER_CTX_free(self->ctx);
|
||||
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
add_authenticated_but_unencrypted_data(AES256GCMEncrypt *self, PyObject *args) {
|
||||
if (self->state > 0) { PyErr_SetString(Crypto_Exception, "Cannot add data once encryption has started"); return NULL; }
|
||||
const char *aad; Py_ssize_t aad_len;
|
||||
if (!PyArg_ParseTuple(args, "y#", &aad, &aad_len)) return NULL;
|
||||
int len;
|
||||
if (aad_len > 0 && 1 != EVP_EncryptUpdate(self->ctx, NULL, &len, (const unsigned char*)aad, aad_len)) return set_error_from_openssl("Failed to add AAD data");
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static int
|
||||
cipher_ctx_tag_length(const EVP_CIPHER_CTX *ctx) {
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
|
||||
return EVP_CIPHER_CTX_tag_length(ctx);
|
||||
#else
|
||||
(void)ctx;
|
||||
return 16;
|
||||
#endif
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
add_data_to_be_encrypted(AES256GCMEncrypt *self, PyObject *args) {
|
||||
if (self->state > 1) { PyErr_SetString(Crypto_Exception, "Encryption has been finished"); return NULL; }
|
||||
const char *plaintext; Py_ssize_t plaintext_len;
|
||||
int finish_encryption = 0;
|
||||
if (!PyArg_ParseTuple(args, "y#|p", &plaintext, &plaintext_len, &finish_encryption)) return NULL;
|
||||
PyObject *ciphertext = PyBytes_FromStringAndSize(NULL, plaintext_len + 2 * EVP_CIPHER_CTX_block_size(self->ctx));
|
||||
if (!ciphertext) return NULL;
|
||||
self->state = 1;
|
||||
int offset = 0;
|
||||
if (plaintext_len) {
|
||||
int len = PyBytes_GET_SIZE(ciphertext);
|
||||
if (1 != EVP_EncryptUpdate(self->ctx, (unsigned char*)PyBytes_AS_STRING(ciphertext), &len, (const unsigned char*)plaintext, plaintext_len)
|
||||
) { Py_CLEAR(ciphertext); return set_error_from_openssl("Failed to encrypt"); }
|
||||
offset = len;
|
||||
}
|
||||
if (finish_encryption) {
|
||||
int len = PyBytes_GET_SIZE(ciphertext) - offset;
|
||||
if (1 != EVP_EncryptFinal_ex(self->ctx, (unsigned char*)PyBytes_AS_STRING(ciphertext) + offset, &len)) {
|
||||
Py_CLEAR(ciphertext); return set_error_from_openssl("Failed to finish encryption"); }
|
||||
offset += len;
|
||||
self->state = 2;
|
||||
|
||||
PyObject *tag = PyBytes_FromStringAndSize(NULL, cipher_ctx_tag_length(self->ctx));
|
||||
if (!tag) { Py_CLEAR(ciphertext); return NULL; }
|
||||
Py_CLEAR(self->tag); self->tag = tag;
|
||||
if (1 != EVP_CIPHER_CTX_ctrl(self->ctx, EVP_CTRL_AEAD_GET_TAG, PyBytes_GET_SIZE(self->tag), PyBytes_AS_STRING(tag))) {
|
||||
Py_CLEAR(ciphertext); return NULL;
|
||||
}
|
||||
}
|
||||
if (offset != PyBytes_GET_SIZE(ciphertext)) { _PyBytes_Resize(&ciphertext, offset); if (!ciphertext) return NULL; }
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
static PyMethodDef aes256gcmencrypt_methods[] = {
|
||||
METHODB(add_authenticated_but_unencrypted_data, METH_VARARGS),
|
||||
METHODB(add_data_to_be_encrypted, METH_VARARGS),
|
||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
static PyMemberDef aes256gcmencrypt_members[] = {
|
||||
{"iv", T_OBJECT_EX, offsetof(AES256GCMEncrypt, iv), READONLY, "IV"},
|
||||
{"tag", T_OBJECT_EX, offsetof(AES256GCMEncrypt, tag), READONLY, "The tag for authentication"},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
static PyTypeObject AES256GCMEncrypt_Type = {
|
||||
PyVarObject_HEAD_INIT(NULL, 0)
|
||||
.tp_name = "fast_data_types.AES256GCMEncrypt",
|
||||
.tp_basicsize = sizeof(AES256GCMEncrypt),
|
||||
.tp_dealloc = (destructor)dealloc_aes256gcmencrypt,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = "Encrypt using AES 256 GCM with authentication",
|
||||
.tp_new = new_aes256gcmencrypt,
|
||||
.tp_methods = aes256gcmencrypt_methods,
|
||||
.tp_members = aes256gcmencrypt_members,
|
||||
};
|
||||
|
||||
// }}}
|
||||
|
||||
// AES256GCMDecrypt {{{
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
|
||||
EVP_CIPHER_CTX *ctx;
|
||||
int state;
|
||||
} AES256GCMDecrypt;
|
||||
|
||||
static PyObject *
|
||||
new_aes256gcmdecrypt(PyTypeObject *type, PyObject *args, PyObject *kwds UNUSED) {
|
||||
Secret *key; unsigned char *iv, *tag; Py_ssize_t iv_len, tag_len;
|
||||
if (!PyArg_ParseTuple(args, "O!y#y#", &Secret_Type, &key, &iv, &iv_len, &tag, &tag_len)) return NULL;
|
||||
const EVP_CIPHER *cipher = EVP_get_cipherbynid(NID_aes_256_gcm);
|
||||
if (key->secret_len != (size_t)EVP_CIPHER_key_length(cipher)) { PyErr_Format(PyExc_ValueError, "The key for AES 256 GCM must be %d bytes long", EVP_CIPHER_key_length(cipher)); return NULL; }
|
||||
if (iv_len < EVP_CIPHER_iv_length(cipher)) { PyErr_Format(PyExc_ValueError, "The iv for AES 256 GCM must be at least %d bytes long", EVP_CIPHER_iv_length(cipher)); return NULL; }
|
||||
AES256GCMDecrypt *self = (AES256GCMDecrypt *)type->tp_alloc(type, 0);
|
||||
if (!self) return NULL;
|
||||
if (!(self->ctx = EVP_CIPHER_CTX_new())) { Py_CLEAR(self); return set_error_from_openssl("Failed to allocate decryption context"); }
|
||||
if (iv_len > EVP_CIPHER_iv_length(cipher)) {
|
||||
if (!EVP_CIPHER_CTX_ctrl(self->ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, NULL)) { Py_CLEAR(self); return set_error_from_openssl("Failed to set the IV length"); }
|
||||
}
|
||||
if (1 != EVP_DecryptInit_ex(self->ctx, cipher, NULL, key->secret, iv)) {
|
||||
Py_CLEAR(self); return set_error_from_openssl("Failed to initialize encryption context"); }
|
||||
// Ensure tag length is 16 because the OpenSSL verification routines will happily pass even if you set a truncated tag.
|
||||
if (tag_len < cipher_ctx_tag_length(self->ctx)) { PyErr_Format(PyExc_ValueError, "Tag length for AES 256 GCM must be at least %d", cipher_ctx_tag_length(self->ctx)); return NULL; }
|
||||
if (!EVP_CIPHER_CTX_ctrl(self->ctx, EVP_CTRL_AEAD_SET_TAG, tag_len, tag)) { Py_CLEAR(self); return set_error_from_openssl("Failed to set the tag"); }
|
||||
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
static void
|
||||
dealloc_aes256gcmdecrypt(AES256GCMDecrypt *self) {
|
||||
if (self->ctx) EVP_CIPHER_CTX_free(self->ctx);
|
||||
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
add_data_to_be_authenticated_but_not_decrypted(AES256GCMDecrypt *self, PyObject *args) {
|
||||
if (self->state > 0) { PyErr_SetString(Crypto_Exception, "Cannot add data once decryption has started"); return NULL; }
|
||||
const char *aad; Py_ssize_t aad_len;
|
||||
if (!PyArg_ParseTuple(args, "y#", &aad, &aad_len)) return NULL;
|
||||
int len;
|
||||
if (aad_len > 0 && 1 != EVP_DecryptUpdate(self->ctx, NULL, &len, (const unsigned char*)aad, aad_len)) return set_error_from_openssl("Failed to add AAD data");
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
add_data_to_be_decrypted(AES256GCMDecrypt *self, PyObject *args) {
|
||||
if (self->state > 1) { PyErr_SetString(Crypto_Exception, "Decryption has been finished"); return NULL; }
|
||||
const char *ciphertext; Py_ssize_t ciphertext_len;
|
||||
int finish_decryption = 0;
|
||||
if (!PyArg_ParseTuple(args, "y#|p", &ciphertext, &ciphertext_len, &finish_decryption)) return NULL;
|
||||
PyObject *plaintext = PyBytes_FromStringAndSize(NULL, ciphertext_len + 2 * EVP_CIPHER_CTX_block_size(self->ctx));
|
||||
if (!plaintext) return NULL;
|
||||
self->state = 1;
|
||||
int offset = 0;
|
||||
if (ciphertext_len) {
|
||||
int len = PyBytes_GET_SIZE(plaintext);
|
||||
if (1 != EVP_DecryptUpdate(self->ctx, (unsigned char*)PyBytes_AS_STRING(plaintext), &len, (const unsigned char*)ciphertext, ciphertext_len)
|
||||
) { Py_CLEAR(plaintext); return set_error_from_openssl("Failed to decrypt"); }
|
||||
offset = len;
|
||||
}
|
||||
if (finish_decryption) {
|
||||
int len = PyBytes_GET_SIZE(plaintext) - offset;
|
||||
int ret = EVP_DecryptFinal_ex(self->ctx, (unsigned char*)PyBytes_AS_STRING(plaintext) + offset, &len);
|
||||
self->state = 2;
|
||||
if (ret <= 0) { Py_CLEAR(plaintext); PyErr_SetString(Crypto_Exception, "Failed to finish decrypt"); return NULL; }
|
||||
offset += len;
|
||||
}
|
||||
if (offset != PyBytes_GET_SIZE(plaintext)) { _PyBytes_Resize(&plaintext, offset); if (!plaintext) return NULL; }
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
static PyMethodDef aes256gcmdecrypt_methods[] = {
|
||||
METHODB(add_data_to_be_authenticated_but_not_decrypted, METH_VARARGS),
|
||||
METHODB(add_data_to_be_decrypted, METH_VARARGS),
|
||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
static PyTypeObject AES256GCMDecrypt_Type = {
|
||||
PyVarObject_HEAD_INIT(NULL, 0)
|
||||
.tp_name = "fast_data_types.AES256GCMDecrypt",
|
||||
.tp_basicsize = sizeof(AES256GCMDecrypt),
|
||||
.tp_dealloc = (destructor)dealloc_aes256gcmdecrypt,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = "Decrypt using AES 256 GCM with authentication",
|
||||
.tp_new = new_aes256gcmdecrypt,
|
||||
.tp_methods = aes256gcmdecrypt_methods,
|
||||
};
|
||||
|
||||
// }}}
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
bool
|
||||
init_crypto_library(PyObject *module) {
|
||||
Crypto_Exception = PyErr_NewException("fast_data_types.CryptoError", NULL, NULL);
|
||||
if (Crypto_Exception == NULL) return false;
|
||||
if (PyModule_AddObject(module, "CryptoError", Crypto_Exception) != 0) return false;
|
||||
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
||||
ADD_TYPE(Secret); ADD_TYPE(EllipticCurveKey); ADD_TYPE(AES256GCMEncrypt); ADD_TYPE(AES256GCMDecrypt);
|
||||
if (PyModule_AddIntConstant(module, "X25519", EVP_PKEY_X25519) != 0) return false;
|
||||
#define AI(which) if (PyModule_AddIntMacro(module, which) != 0) return false;
|
||||
AI(SHA1_HASH); AI(SHA224_HASH); AI(SHA256_HASH); AI(SHA384_HASH); AI(SHA512_HASH);
|
||||
#undef AI
|
||||
return true;
|
||||
}
|
||||
@@ -13,6 +13,9 @@
|
||||
#endif
|
||||
|
||||
#include "data-types.h"
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
#include "cleanup.h"
|
||||
#include "safe-wrappers.h"
|
||||
#include "control-codes.h"
|
||||
@@ -189,8 +192,34 @@ locale_is_valid(PyObject *self UNUSED, PyObject *args) {
|
||||
Py_RETURN_TRUE;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
py_getpeereid(PyObject *self UNUSED, PyObject *args) {
|
||||
int fd;
|
||||
if (!PyArg_ParseTuple(args, "i", &fd)) return NULL;
|
||||
uid_t euid = 0; gid_t egid = 0;
|
||||
#ifdef __linux__
|
||||
struct ucred cr;
|
||||
socklen_t sz = sizeof(cr);
|
||||
if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cr, &sz) != 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; }
|
||||
euid = cr.uid; egid = cr.gid;
|
||||
#else
|
||||
if (getpeereid(fd, &euid, &egid) != 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; }
|
||||
#endif
|
||||
int u = euid, g = egid;
|
||||
return Py_BuildValue("ii", u, g);
|
||||
}
|
||||
|
||||
#include "docs_ref_map_generated.h"
|
||||
|
||||
static PyObject*
|
||||
get_docs_ref_map(PyObject *self UNUSED, PyObject *args UNUSED) {
|
||||
return PyBytes_FromStringAndSize(docs_ref_map, sizeof(docs_ref_map));
|
||||
}
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
{"wcwidth", (PyCFunction)wcwidth_wrap, METH_O, ""},
|
||||
{"get_docs_ref_map", (PyCFunction)get_docs_ref_map, METH_NOARGS, ""},
|
||||
{"getpeereid", (PyCFunction)py_getpeereid, METH_VARARGS, ""},
|
||||
{"wcswidth", (PyCFunction)wcswidth_std, METH_O, ""},
|
||||
{"open_tty", open_tty, METH_VARARGS, ""},
|
||||
{"normal_tty", normal_tty, METH_VARARGS, ""},
|
||||
@@ -217,11 +246,11 @@ static PyMethodDef module_methods[] = {
|
||||
|
||||
|
||||
static struct PyModuleDef module = {
|
||||
.m_base = PyModuleDef_HEAD_INIT,
|
||||
.m_name = "fast_data_types", /* name of module */
|
||||
.m_doc = NULL,
|
||||
.m_size = -1,
|
||||
.m_methods = module_methods
|
||||
.m_base = PyModuleDef_HEAD_INIT,
|
||||
.m_name = "fast_data_types", /* name of module */
|
||||
.m_doc = NULL,
|
||||
.m_size = -1,
|
||||
.m_methods = module_methods
|
||||
};
|
||||
|
||||
|
||||
@@ -234,6 +263,7 @@ extern int init_Line(PyObject *);
|
||||
extern int init_ColorProfile(PyObject *);
|
||||
extern int init_Screen(PyObject *);
|
||||
extern bool init_fontconfig_library(PyObject*);
|
||||
extern bool init_crypto_library(PyObject*);
|
||||
extern bool init_desktop(PyObject*);
|
||||
extern bool init_fonts(PyObject*);
|
||||
extern bool init_glfw(PyObject *m);
|
||||
@@ -247,6 +277,7 @@ extern bool init_kittens(PyObject *module);
|
||||
extern bool init_logging(PyObject *module);
|
||||
extern bool init_png_reader(PyObject *module);
|
||||
extern bool init_utmp(PyObject *module);
|
||||
extern bool init_loop_utils(PyObject *module);
|
||||
#ifdef __APPLE__
|
||||
extern int init_CoreText(PyObject *);
|
||||
extern bool init_cocoa(PyObject *module);
|
||||
@@ -268,7 +299,6 @@ shift_to_first_set_bit(CellAttrs x) {
|
||||
return ans;
|
||||
}
|
||||
|
||||
|
||||
EXPORTED PyMODINIT_FUNC
|
||||
PyInit_fast_data_types(void) {
|
||||
PyObject *m;
|
||||
@@ -315,6 +345,8 @@ PyInit_fast_data_types(void) {
|
||||
#endif
|
||||
if (!init_fonts(m)) return NULL;
|
||||
if (!init_utmp(m)) return NULL;
|
||||
if (!init_loop_utils(m)) return NULL;
|
||||
if (!init_crypto_library(m)) return NULL;
|
||||
|
||||
CellAttrs a;
|
||||
#define s(name, attr) { a.val = 0; a.attr = 1; PyModule_AddIntConstant(m, #name, shift_to_first_set_bit(a)); }
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
// Required minimum OpenGL version
|
||||
#define OPENGL_REQUIRED_VERSION_MAJOR 3
|
||||
#define OPENGL_REQUIRED_VERSION_MINOR 3
|
||||
#define GLFW_MOD_KITTY 1024
|
||||
#define GLFW_MOD_KITTY (GLFW_MOD_LAST * 2)
|
||||
#define UNUSED __attribute__ ((unused))
|
||||
#define PYNOARG PyObject *__a1 UNUSED, PyObject *__a2 UNUSED
|
||||
#define EXPORTED __attribute__ ((visibility ("default")))
|
||||
@@ -133,6 +133,14 @@ typedef struct ImageAnchorPosition {
|
||||
#endif
|
||||
#define START_ALLOW_CASE_RANGE IGNORE_PEDANTIC_WARNINGS
|
||||
#define END_ALLOW_CASE_RANGE END_IGNORE_PEDANTIC_WARNINGS
|
||||
#define BIT_MASK(__TYPE__, __ONE_COUNT__) \
|
||||
(((__TYPE__) (-((__ONE_COUNT__) != 0))) \
|
||||
& (((__TYPE__) -1) >> ((sizeof(__TYPE__) * CHAR_BIT) - (__ONE_COUNT__))))
|
||||
#define ADD_TYPE(which) \
|
||||
if (PyType_Ready(&which##_Type) < 0) return false; \
|
||||
if (PyModule_AddObject(module, #which, (PyObject *)&which##_Type) != 0) return false; \
|
||||
Py_INCREF(&which##_Type);
|
||||
|
||||
|
||||
typedef enum UTF8State { UTF8_ACCEPT = 0, UTF8_REJECT = 1} UTF8State;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ from .fast_data_types import Color, num_users
|
||||
from .options.types import Options as KittyOpts, defaults
|
||||
from .options.utils import SequenceMap
|
||||
from .rgb import color_as_sharp
|
||||
from .types import MouseEvent, Shortcut
|
||||
from .types import MouseEvent, Shortcut, mod_to_names
|
||||
|
||||
AnyEvent = TypeVar('AnyEvent', MouseEvent, Shortcut)
|
||||
Print = Callable[..., None]
|
||||
@@ -41,30 +41,27 @@ def title(x: str) -> str:
|
||||
return colored(x, 'blue', intense=True)
|
||||
|
||||
|
||||
def print_event(ev: AnyEvent, defn: str, print: Print) -> None:
|
||||
print(f'\t{ev.human_repr} → {defn}')
|
||||
def print_event(ev: str, defn: str, print: Print) -> None:
|
||||
print(f'\t{ev} → {defn}')
|
||||
|
||||
|
||||
def print_mapping_changes(defns: Dict[AnyEvent, str], changes: Set[AnyEvent], text: str, print: Print) -> None:
|
||||
def print_mapping_changes(defns: Dict[str, str], changes: Set[str], text: str, print: Print) -> None:
|
||||
if changes:
|
||||
print(title(text))
|
||||
for k in sorted(changes):
|
||||
print_event(k, defns[k], print)
|
||||
|
||||
|
||||
def compare_maps(final: Dict[AnyEvent, str], initial: Dict[AnyEvent, str], print: Print) -> None:
|
||||
is_mouse = False
|
||||
for k in initial:
|
||||
if isinstance(k, MouseEvent):
|
||||
is_mouse = True
|
||||
break
|
||||
added = set(final) - set(initial)
|
||||
removed = set(initial) - set(final)
|
||||
changed = {k for k in set(final) & set(initial) if final[k] != initial[k]}
|
||||
which = 'mouse actions' if is_mouse else 'shortcuts'
|
||||
print_mapping_changes(final, added, f'Added {which}:', print)
|
||||
print_mapping_changes(initial, removed, f'Removed {which}:', print)
|
||||
print_mapping_changes(final, changed, f'Changed {which}:', print)
|
||||
def compare_maps(final: Dict[AnyEvent, str], final_kitty_mod: int, initial: Dict[AnyEvent, str], initial_kitty_mod: int, print: Print) -> None:
|
||||
ei = {k.human_repr(initial_kitty_mod): v for k, v in initial.items()}
|
||||
ef = {k.human_repr(final_kitty_mod): v for k, v in final.items()}
|
||||
added = set(ef) - set(ei)
|
||||
removed = set(ei) - set(ef)
|
||||
changed = {k for k in set(ef) & set(ei) if ef[k] != ei[k]}
|
||||
which = 'shortcuts' if isinstance(next(iter(initial)), Shortcut) else 'mouse actions'
|
||||
print_mapping_changes(ef, added, f'Added {which}:', print)
|
||||
print_mapping_changes(ei, removed, f'Removed {which}:', print)
|
||||
print_mapping_changes(ef, changed, f'Changed {which}:', print)
|
||||
|
||||
|
||||
def flatten_sequence_map(m: SequenceMap) -> ShortcutMap:
|
||||
@@ -95,6 +92,9 @@ def compare_opts(opts: KittyOpts, print: Print) -> None:
|
||||
if f == 'symbol_map':
|
||||
for k in sorted(val):
|
||||
print(f'\tU+{k[0]:04x} - U+{k[1]:04x} → {val[k]}')
|
||||
elif f == 'modify_font':
|
||||
for k in sorted(val):
|
||||
print(' ', val[k])
|
||||
else:
|
||||
print(pformat(val))
|
||||
else:
|
||||
@@ -102,16 +102,19 @@ def compare_opts(opts: KittyOpts, print: Print) -> None:
|
||||
if isinstance(val, Color):
|
||||
colors.append(fmt.format(f) + ' ' + color_as_sharp(val) + ' ' + styled(' ', bg=val))
|
||||
else:
|
||||
print(fmt.format(f), str(getattr(opts, f)))
|
||||
if f == 'kitty_mod':
|
||||
print(fmt.format(f), '+'.join(mod_to_names(getattr(opts, f))))
|
||||
else:
|
||||
print(fmt.format(f), str(getattr(opts, f)))
|
||||
|
||||
compare_maps(opts.mousemap, default_opts.mousemap, print)
|
||||
compare_maps(opts.mousemap, opts.kitty_mod, default_opts.mousemap, default_opts.kitty_mod, print)
|
||||
final_, initial_ = opts.keymap, default_opts.keymap
|
||||
final: ShortcutMap = {Shortcut((k,)): v for k, v in final_.items()}
|
||||
initial: ShortcutMap = {Shortcut((k,)): v for k, v in initial_.items()}
|
||||
final_s, initial_s = map(flatten_sequence_map, (opts.sequence_map, default_opts.sequence_map))
|
||||
final.update(final_s)
|
||||
initial.update(initial_s)
|
||||
compare_maps(final, initial, print)
|
||||
compare_maps(final, opts.kitty_mod, initial, default_opts.kitty_mod, print)
|
||||
if colors:
|
||||
print(f'{title("Colors")}:', end='\n\t')
|
||||
print('\n\t'.join(sorted(colors)))
|
||||
|
||||
@@ -154,12 +154,11 @@ namespaced_entry_points['edit-config'] = edit_config_file
|
||||
namespaced_entry_points['shebang'] = shebang
|
||||
|
||||
|
||||
def setup_openssl_environment() -> None:
|
||||
def setup_openssl_environment(ext_dir: str) -> None:
|
||||
# Use our bundled CA certificates instead of the system ones, since
|
||||
# many systems come with no certificates in a useable form or have various
|
||||
# locations for the certificates.
|
||||
d = os.path.dirname
|
||||
ext_dir: str = getattr(sys, 'kitty_extensions_dir')
|
||||
if 'darwin' in sys.platform.lower():
|
||||
cert_file = os.path.join(d(d(d(ext_dir))), 'cacert.pem')
|
||||
else:
|
||||
@@ -169,8 +168,10 @@ def setup_openssl_environment() -> None:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if getattr(sys, 'frozen', False) and getattr(sys, 'kitty_extensions_dir', ''):
|
||||
setup_openssl_environment()
|
||||
if getattr(sys, 'frozen', False):
|
||||
ext_dir: str = getattr(sys, 'kitty_run_data').get('extensions_dir')
|
||||
if ext_dir:
|
||||
setup_openssl_environment(ext_dir)
|
||||
first_arg = '' if len(sys.argv) < 2 else sys.argv[1]
|
||||
func = entry_points.get(first_arg)
|
||||
if func is None:
|
||||
|
||||
@@ -2,7 +2,7 @@ import termios
|
||||
from ctypes import Array, c_ubyte
|
||||
from typing import (
|
||||
Any, AnyStr, Callable, Dict, List, NewType, Optional, Tuple, TypedDict,
|
||||
Union
|
||||
Union, Iterator
|
||||
)
|
||||
|
||||
from kitty.boss import Boss
|
||||
@@ -10,8 +10,13 @@ from kitty.fonts import FontFeature
|
||||
from kitty.fonts.render import FontObject
|
||||
from kitty.marks import MarkerFunc
|
||||
from kitty.options.types import Options
|
||||
from kitty.types import SignalInfo
|
||||
|
||||
# Constants {{{
|
||||
CLD_KILLED: int
|
||||
CLD_STOPPED: int
|
||||
CLD_CONTINUED: int
|
||||
CLD_EXITED: int
|
||||
SHM_NAME_MAX: int
|
||||
MOUSE_SELECTION_LINE: int
|
||||
MOUSE_SELECTION_EXTEND: int
|
||||
@@ -539,10 +544,6 @@ def get_options() -> Options:
|
||||
pass
|
||||
|
||||
|
||||
def resolve_key_mods(kitty_mod: int, mods: int) -> int:
|
||||
pass
|
||||
|
||||
|
||||
def parse_font_feature(ff: str) -> bytes:
|
||||
pass
|
||||
|
||||
@@ -887,6 +888,10 @@ def cell_size_for_window(os_window_id: int) -> Tuple[int, int]:
|
||||
pass
|
||||
|
||||
|
||||
def wakeup_main_loop() -> None:
|
||||
pass
|
||||
|
||||
|
||||
class Region:
|
||||
left: int
|
||||
top: int
|
||||
@@ -985,9 +990,6 @@ def create_test_font_group(sz: float, dpix: float,
|
||||
|
||||
class HistoryBuf:
|
||||
|
||||
def as_text(self, callback: Callable[[str], None], as_ansi: bool, insert_wrap_markers: bool) -> None:
|
||||
pass
|
||||
|
||||
def pagerhist_as_text(self, upto_output_start: bool = False) -> str:
|
||||
pass
|
||||
|
||||
@@ -1138,6 +1140,7 @@ class Screen:
|
||||
pass
|
||||
as_text_non_visual = as_text
|
||||
as_text_alternate = as_text
|
||||
as_text_for_history_buf = as_text
|
||||
|
||||
def cmd_output(self, which: int, callback: Callable[[str], None], as_ansi: bool, insert_wrap_markers: bool) -> bool:
|
||||
pass
|
||||
@@ -1193,13 +1196,17 @@ class ChildMonitor:
|
||||
death_notify: Callable[[int], None],
|
||||
dump_callback: Optional[Callable[[bytes], None]],
|
||||
talk_fd: int = -1,
|
||||
listen_fd: int = -1
|
||||
listen_fd: int = -1,
|
||||
prewarm_fd: int = -1,
|
||||
):
|
||||
pass
|
||||
|
||||
def wakeup(self) -> None:
|
||||
pass
|
||||
|
||||
def handled_signals(self) -> Tuple[int, ...]:
|
||||
pass
|
||||
|
||||
def main_loop(self) -> None:
|
||||
pass
|
||||
|
||||
@@ -1279,7 +1286,8 @@ def spawn(
|
||||
stdin_read_fd: int,
|
||||
stdin_write_fd: int,
|
||||
ready_read_fd: int,
|
||||
ready_write_fd: int
|
||||
ready_write_fd: int,
|
||||
handled_signals: Tuple[int, ...],
|
||||
) -> int:
|
||||
pass
|
||||
|
||||
@@ -1381,5 +1389,99 @@ def sigqueue(pid: int, signal: int, value: int) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def establish_controlling_tty(ttyname: str, stdin: int, stdout: int, stderr: int) -> None:
|
||||
def random_unix_socket() -> int:
|
||||
pass
|
||||
|
||||
|
||||
def read_signals(fd: int, callback: Callable[[SignalInfo], None]) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def install_signal_handlers(*signals: int) -> Tuple[int, int]:
|
||||
pass
|
||||
|
||||
|
||||
def remove_signal_handlers() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def getpeereid(fd: int) -> Tuple[int, int]:
|
||||
pass
|
||||
|
||||
|
||||
X25519: int
|
||||
SHA1_HASH: int
|
||||
SHA224_HASH: int
|
||||
SHA256_HASH: int
|
||||
SHA384_HASH: int
|
||||
SHA512_HASH: int
|
||||
|
||||
|
||||
class Secret:
|
||||
pass
|
||||
|
||||
|
||||
class EllipticCurveKey:
|
||||
|
||||
def __init__(
|
||||
self, algorithm: int = 0 # X25519
|
||||
): pass
|
||||
|
||||
def derive_secret(
|
||||
self, pubkey: bytes, hash_algorithm: int = 0 # SHA256_HASH
|
||||
) -> Secret: pass
|
||||
|
||||
@property
|
||||
def public(self) -> bytes: ...
|
||||
|
||||
@property
|
||||
def private(self) -> Secret: ...
|
||||
|
||||
|
||||
class AES256GCMEncrypt:
|
||||
|
||||
def __init__(self, key: Secret): ...
|
||||
|
||||
def add_authenticated_but_unencrypted_data(self, data: bytes) -> None: ...
|
||||
|
||||
def add_data_to_be_encrypted(self, data: bytes, finished: bool = False) -> bytes: ...
|
||||
|
||||
@property
|
||||
def iv(self) -> bytes: ...
|
||||
|
||||
@property
|
||||
def tag(self) -> bytes: ...
|
||||
|
||||
|
||||
class AES256GCMDecrypt:
|
||||
|
||||
def __init__(self, key: Secret, iv: bytes, tag: bytes): ...
|
||||
|
||||
def add_data_to_be_authenticated_but_not_decrypted(self, data: bytes) -> None: ...
|
||||
|
||||
def add_data_to_be_decrypted(self, data: bytes, finished: bool = False) -> bytes: ...
|
||||
|
||||
|
||||
class SingleKey:
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(self, mods: int = 0, is_native: object = False, key: int = -1): ...
|
||||
def __hash__(self) -> int: ...
|
||||
def __len__(self) -> int: ...
|
||||
def __getitem__(self, x: int) -> int: ...
|
||||
@property
|
||||
def mods(self) -> int: ...
|
||||
@property
|
||||
def is_native(self) -> bool: ...
|
||||
@property
|
||||
def key(self) -> int: ...
|
||||
@property
|
||||
def defined_with_kitty_mod(self) -> bool: ...
|
||||
def __iter__(self) -> Iterator[int]: ...
|
||||
def _replace(self, mods: int = 0, is_native: object = False, key: int = -1) -> 'SingleKey': ...
|
||||
def resolve_kitty_mod(self, mod: int) -> 'SingleKey': ...
|
||||
|
||||
|
||||
def set_use_os_log(yes: bool) -> None: ...
|
||||
def get_docs_ref_map() -> bytes: ...
|
||||
|
||||
@@ -331,6 +331,27 @@ python_send_to_gpu(FONTS_DATA_HANDLE fg, unsigned int x, unsigned int y, unsigne
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
adjust_metric(unsigned int *metric, float adj, AdjustmentUnit unit, double dpi) {
|
||||
if (adj == 0.f) return;
|
||||
int a = 0;
|
||||
switch (unit) {
|
||||
case POINT:
|
||||
a = ((long)round((adj * (dpi / 72.0)))); break;
|
||||
case PERCENT:
|
||||
*metric = (int)roundf((fabsf(adj) * (float)*metric) / 100.f); return;
|
||||
case PIXEL:
|
||||
a = (int)roundf(adj); break;
|
||||
}
|
||||
*metric = (a < 0 && -a > (int)*metric) ? 0 : *metric + a;
|
||||
}
|
||||
|
||||
static unsigned int
|
||||
adjust_ypos(unsigned int pos, unsigned int cell_height, int adjustment) {
|
||||
if (adjustment >= 0) adjustment = MIN(adjustment, (int)pos - 1);
|
||||
else adjustment = MAX(adjustment, (int)pos - (int)cell_height + 1);
|
||||
return pos - adjustment;
|
||||
}
|
||||
|
||||
static void
|
||||
calc_cell_metrics(FontGroup *fg) {
|
||||
@@ -338,18 +359,16 @@ calc_cell_metrics(FontGroup *fg) {
|
||||
cell_metrics(fg->fonts[fg->medium_font_idx].face, &cell_width, &cell_height, &baseline, &underline_position, &underline_thickness, &strikethrough_position, &strikethrough_thickness);
|
||||
if (!cell_width) fatal("Failed to calculate cell width for the specified font");
|
||||
unsigned int before_cell_height = cell_height;
|
||||
int cw = cell_width, ch = cell_height;
|
||||
if (OPT(adjust_line_height_px) != 0) ch += OPT(adjust_line_height_px);
|
||||
if (OPT(adjust_line_height_frac) != 0.f) ch = (int)(ch * OPT(adjust_line_height_frac));
|
||||
if (OPT(adjust_column_width_px != 0)) cw += OPT(adjust_column_width_px);
|
||||
if (OPT(adjust_column_width_frac) != 0.f) cw = (int)(cw * OPT(adjust_column_width_frac));
|
||||
unsigned int cw = cell_width, ch = cell_height;
|
||||
adjust_metric(&cw, OPT(cell_width).val, OPT(cell_width).unit, fg->logical_dpi_x);
|
||||
adjust_metric(&ch, OPT(cell_height).val, OPT(cell_height).unit, fg->logical_dpi_y);
|
||||
#define MAX_DIM 1000
|
||||
#define MIN_WIDTH 2
|
||||
#define MIN_HEIGHT 4
|
||||
if (cw >= MIN_WIDTH && cw <= MAX_DIM) cell_width = cw;
|
||||
else log_error("Cell width invalid after adjustment, ignoring adjust_column_width");
|
||||
else log_error("Cell width invalid after adjustment, ignoring modify_font cell_width");
|
||||
if (ch >= MIN_HEIGHT && ch <= MAX_DIM) cell_height = ch;
|
||||
else log_error("Cell height invalid after adjustment, ignoring adjust_line_height");
|
||||
else log_error("Cell height invalid after adjustment, ignoring modify_font cell_height");
|
||||
int line_height_adjustment = cell_height - before_cell_height;
|
||||
if (cell_height < MIN_HEIGHT) fatal("Line height too small: %u", cell_height);
|
||||
if (cell_height > MAX_DIM) fatal("Line height too large: %u", cell_height);
|
||||
@@ -358,9 +377,24 @@ calc_cell_metrics(FontGroup *fg) {
|
||||
#undef MIN_WIDTH
|
||||
#undef MIN_HEIGHT
|
||||
#undef MAX_DIM
|
||||
|
||||
unsigned int baseline_before = baseline;
|
||||
#define A(which, dpi) adjust_metric(&which, OPT(which).val, OPT(which).unit, fg->logical_dpi_##dpi);
|
||||
A(underline_thickness, y); A(underline_position, y); A(strikethrough_thickness, y); A(strikethrough_position, y); A(baseline, y);
|
||||
#undef A
|
||||
|
||||
if (baseline_before != baseline) {
|
||||
int adjustment = baseline - baseline_before;
|
||||
baseline = adjust_ypos(baseline_before, cell_height, adjustment);
|
||||
underline_position = adjust_ypos(underline_position, cell_height, adjustment);
|
||||
strikethrough_position = adjust_ypos(underline_position, cell_height, adjustment);
|
||||
}
|
||||
|
||||
underline_position = MIN(cell_height - 1, underline_position);
|
||||
// ensure there is at least a couple of pixels available to render styled underlines
|
||||
while (underline_position > baseline + 1 && cell_height - underline_position < 2) underline_position--;
|
||||
// there should be at least one pixel on either side of the underline_position
|
||||
if (underline_position > baseline + 1 && underline_position > cell_height - 1)
|
||||
underline_position = MAX(baseline + 1, cell_height - 1);
|
||||
if (line_height_adjustment > 1) {
|
||||
baseline += MIN(cell_height - 1, (unsigned)line_height_adjustment / 2);
|
||||
underline_position += MIN(cell_height - 1, (unsigned)line_height_adjustment / 2);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
try:
|
||||
from typing import TypedDict
|
||||
from typing import TypedDict, NamedTuple
|
||||
except ImportError:
|
||||
TypedDict = dict
|
||||
from enum import Enum, IntEnum, auto
|
||||
|
||||
|
||||
class ListedFont(TypedDict):
|
||||
@@ -21,3 +22,39 @@ class FontFeature:
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return repr(self.name)
|
||||
|
||||
|
||||
class ModificationType(Enum):
|
||||
underline_position = auto()
|
||||
underline_thickness = auto()
|
||||
strikethrough_position = auto()
|
||||
strikethrough_thickness = auto()
|
||||
cell_width = auto()
|
||||
cell_height = auto()
|
||||
baseline = auto()
|
||||
size = auto()
|
||||
|
||||
|
||||
class ModificationUnit(IntEnum):
|
||||
pt = 0
|
||||
percent = 1
|
||||
pixel = 2
|
||||
|
||||
|
||||
class ModificationValue(NamedTuple):
|
||||
val: float
|
||||
unit: ModificationUnit
|
||||
|
||||
def __repr__(self) -> str:
|
||||
u = '%' if self.unit is ModificationUnit.percent else ''
|
||||
return f'{self.val:g}{u}'
|
||||
|
||||
|
||||
class FontModification(NamedTuple):
|
||||
mod_type: ModificationType
|
||||
mod_value: ModificationValue
|
||||
font_name: str = ''
|
||||
|
||||
def __repr__(self) -> str:
|
||||
fn = f' {self.font_name}' if self.font_name else ''
|
||||
return f'{self.mod_type.name}{fn} {self.mod_value}'
|
||||
|
||||
@@ -314,7 +314,7 @@ def render_special(
|
||||
dpi_x: float = 96.,
|
||||
dpi_y: float = 96.,
|
||||
) -> CBufType:
|
||||
underline_position = min(underline_position, cell_height - underline_thickness)
|
||||
underline_position = min(underline_position, cell_height - sum(divmod(underline_thickness, 2)))
|
||||
CharTexture = ctypes.c_ubyte * (cell_width * cell_height)
|
||||
|
||||
if missing:
|
||||
|
||||
@@ -306,21 +306,11 @@ calc_cell_width(Face *self) {
|
||||
}
|
||||
|
||||
|
||||
static unsigned int
|
||||
adjust_ypos(unsigned int pos, unsigned int cell_height, int adjustment) {
|
||||
if (adjustment >= 0) adjustment = MIN(adjustment, (int)pos - 1);
|
||||
else adjustment = MAX(adjustment, (int)pos - (int)cell_height + 1);
|
||||
return pos - adjustment;
|
||||
}
|
||||
|
||||
void
|
||||
cell_metrics(PyObject *s, unsigned int* cell_width, unsigned int* cell_height, unsigned int* baseline, unsigned int* underline_position, unsigned int* underline_thickness, unsigned int* strikethrough_position, unsigned int* strikethrough_thickness) {
|
||||
Face *self = (Face*)s;
|
||||
*cell_width = calc_cell_width(self);
|
||||
*cell_height = calc_cell_height(self, true);
|
||||
int baseline_offset = 0;
|
||||
if (OPT(adjust_baseline_px) != 0) baseline_offset = OPT(adjust_baseline_px);
|
||||
else if (OPT(adjust_baseline_frac) != 0) baseline_offset = (int)(*cell_height * OPT(adjust_baseline_frac));
|
||||
*baseline = font_units_to_pixels_y(self, self->ascender);
|
||||
*underline_position = MIN(*cell_height - 1, (unsigned int)font_units_to_pixels_y(self, MAX(0, self->ascender - self->underline_position)));
|
||||
*underline_thickness = MAX(1, font_units_to_pixels_y(self, self->underline_thickness));
|
||||
@@ -335,11 +325,6 @@ cell_metrics(PyObject *s, unsigned int* cell_width, unsigned int* cell_height, u
|
||||
} else {
|
||||
*strikethrough_thickness = *underline_thickness;
|
||||
}
|
||||
if (baseline_offset) {
|
||||
*baseline = adjust_ypos(*baseline, *cell_height, baseline_offset);
|
||||
*underline_position = adjust_ypos(*underline_position, *cell_height, baseline_offset);
|
||||
*strikethrough_position = adjust_ypos(*strikethrough_position, *cell_height, baseline_offset);
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int
|
||||
|
||||
6
kitty/glfw-wrapper.c
generated
6
kitty/glfw-wrapper.c
generated
@@ -38,6 +38,9 @@ load_glfw(const char* path) {
|
||||
*(void **) (&glfwSetDrawTextFunction_impl) = dlsym(handle, "glfwSetDrawTextFunction");
|
||||
if (glfwSetDrawTextFunction_impl == NULL) fail("Failed to load glfw function glfwSetDrawTextFunction with error: %s", dlerror());
|
||||
|
||||
*(void **) (&glfwSetCurrentSelectionCallback_impl) = dlsym(handle, "glfwSetCurrentSelectionCallback");
|
||||
if (glfwSetCurrentSelectionCallback_impl == NULL) fail("Failed to load glfw function glfwSetCurrentSelectionCallback with error: %s", dlerror());
|
||||
|
||||
*(void **) (&glfwTerminate_impl) = dlsym(handle, "glfwTerminate");
|
||||
if (glfwTerminate_impl == NULL) fail("Failed to load glfw function glfwTerminate with error: %s", dlerror());
|
||||
|
||||
@@ -119,6 +122,9 @@ load_glfw(const char* path) {
|
||||
*(void **) (&glfwIsFullscreen_impl) = dlsym(handle, "glfwIsFullscreen");
|
||||
if (glfwIsFullscreen_impl == NULL) fail("Failed to load glfw function glfwIsFullscreen with error: %s", dlerror());
|
||||
|
||||
*(void **) (&glfwAreSwapsAllowed_impl) = dlsym(handle, "glfwAreSwapsAllowed");
|
||||
if (glfwAreSwapsAllowed_impl == NULL) fail("Failed to load glfw function glfwAreSwapsAllowed with error: %s", dlerror());
|
||||
|
||||
*(void **) (&glfwDestroyWindow_impl) = dlsym(handle, "glfwDestroyWindow");
|
||||
if (glfwDestroyWindow_impl == NULL) fail("Failed to load glfw function glfwDestroyWindow with error: %s", dlerror());
|
||||
|
||||
|
||||
10
kitty/glfw-wrapper.h
generated
10
kitty/glfw-wrapper.h
generated
@@ -250,6 +250,7 @@ typedef enum {
|
||||
* GLFW_LOCK_KEY_MODS input mode is set.
|
||||
*/
|
||||
#define GLFW_MOD_NUM_LOCK 0x0080
|
||||
#define GLFW_MOD_LAST GLFW_MOD_NUM_LOCK
|
||||
#define GLFW_LOCK_MASK (GLFW_MOD_NUM_LOCK | GLFW_MOD_CAPS_LOCK)
|
||||
|
||||
/*! @} */
|
||||
@@ -1454,6 +1455,7 @@ typedef void (* GLFWjoystickfun)(int,int);
|
||||
typedef void (* GLFWuserdatafun)(unsigned long long, void*);
|
||||
typedef void (* GLFWtickcallback)(void*);
|
||||
typedef bool (* GLFWdrawtextfun)(GLFWwindow *window, const char *text, uint32_t fg, uint32_t bg, uint8_t *output_buf, size_t width, size_t height, float x_offset, float y_offset, size_t right_margin);
|
||||
typedef char* (* GLFWcurrentselectionfun)(void);
|
||||
|
||||
/*! @brief Video mode type.
|
||||
*
|
||||
@@ -1643,6 +1645,10 @@ typedef GLFWdrawtextfun (*glfwSetDrawTextFunction_func)(GLFWdrawtextfun);
|
||||
GFW_EXTERN glfwSetDrawTextFunction_func glfwSetDrawTextFunction_impl;
|
||||
#define glfwSetDrawTextFunction glfwSetDrawTextFunction_impl
|
||||
|
||||
typedef GLFWcurrentselectionfun (*glfwSetCurrentSelectionCallback_func)(GLFWcurrentselectionfun);
|
||||
GFW_EXTERN glfwSetCurrentSelectionCallback_func glfwSetCurrentSelectionCallback_impl;
|
||||
#define glfwSetCurrentSelectionCallback glfwSetCurrentSelectionCallback_impl
|
||||
|
||||
typedef void (*glfwTerminate_func)(void);
|
||||
GFW_EXTERN glfwTerminate_func glfwTerminate_impl;
|
||||
#define glfwTerminate glfwTerminate_impl
|
||||
@@ -1751,6 +1757,10 @@ typedef bool (*glfwIsFullscreen_func)(GLFWwindow*, unsigned int);
|
||||
GFW_EXTERN glfwIsFullscreen_func glfwIsFullscreen_impl;
|
||||
#define glfwIsFullscreen glfwIsFullscreen_impl
|
||||
|
||||
typedef bool (*glfwAreSwapsAllowed_func)(const GLFWwindow*);
|
||||
GFW_EXTERN glfwAreSwapsAllowed_func glfwAreSwapsAllowed_impl;
|
||||
#define glfwAreSwapsAllowed glfwAreSwapsAllowed_impl
|
||||
|
||||
typedef void (*glfwDestroyWindow_func)(GLFWwindow*);
|
||||
GFW_EXTERN glfwDestroyWindow_func glfwDestroyWindow_impl;
|
||||
#define glfwDestroyWindow glfwDestroyWindow_impl
|
||||
|
||||
75
kitty/glfw.c
75
kitty/glfw.c
@@ -25,6 +25,7 @@ extern void cocoa_set_titlebar_appearance(void *w, unsigned int theme);
|
||||
extern void cocoa_set_titlebar_color(void *w, color_type color);
|
||||
extern bool cocoa_alt_option_key_pressed(unsigned long);
|
||||
extern void cocoa_toggle_secure_keyboard_entry(void);
|
||||
extern void cocoa_set_uncaught_exception_handler(void);
|
||||
extern void cocoa_update_menu_bar_title(PyObject*);
|
||||
extern size_t cocoa_get_workspace_ids(void *w, size_t *workspace_ids, size_t array_sz);
|
||||
extern monotonic_t cocoa_cursor_blink_interval(void);
|
||||
@@ -108,6 +109,7 @@ update_os_window_viewport(OSWindow *window, bool notify_boss) {
|
||||
return; // no change, ignore
|
||||
}
|
||||
int min_width, min_height; min_size_for_os_window(window, &min_width, &min_height);
|
||||
window->viewport_resized_at = monotonic();
|
||||
if (w <= 0 || h <= 0 || fw < min_width || fh < min_height || fw < w || fh < h) {
|
||||
log_error("Invalid geometry ignored: framebuffer: %dx%d window: %dx%d\n", fw, fh, w, h);
|
||||
if (!window->viewport_updated_at_least_once) {
|
||||
@@ -463,6 +465,18 @@ application_close_requested_callback(int flags) {
|
||||
}
|
||||
}
|
||||
|
||||
static char*
|
||||
get_current_selection(void) {
|
||||
if (!global_state.boss) return NULL;
|
||||
PyObject *ret = PyObject_CallMethod(global_state.boss, "get_active_selection", NULL);
|
||||
if (!ret) { PyErr_Print(); return NULL; }
|
||||
char* ans = NULL;
|
||||
if (PyUnicode_Check(ret)) ans = strdup(PyUnicode_AsUTF8(ret));
|
||||
Py_DECREF(ret);
|
||||
return ans;
|
||||
}
|
||||
|
||||
|
||||
static void get_window_dpi(GLFWwindow *w, double *x, double *y);
|
||||
|
||||
#ifdef __APPLE__
|
||||
@@ -763,6 +777,7 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) {
|
||||
glfwWindowHint(GLFW_STENCIL_BITS, 0);
|
||||
if (OPT(hide_window_decorations) & 1) glfwWindowHint(GLFW_DECORATED, false);
|
||||
glfwSetApplicationCloseCallback(application_close_requested_callback);
|
||||
glfwSetCurrentSelectionCallback(get_current_selection);
|
||||
#ifdef __APPLE__
|
||||
cocoa_set_activation_policy(OPT(macos_hide_from_tasks));
|
||||
glfwWindowHint(GLFW_COCOA_GRAPHICS_SWITCHING, true);
|
||||
@@ -771,11 +786,12 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) {
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifndef __APPLE__
|
||||
#ifdef __APPLE__
|
||||
glfwWindowHint(GLFW_COCOA_COLOR_SPACE, OPT(macos_colorspace));
|
||||
#else
|
||||
glfwWindowHintString(GLFW_X11_INSTANCE_NAME, wm_class_name);
|
||||
glfwWindowHintString(GLFW_X11_CLASS_NAME, wm_class_class);
|
||||
glfwWindowHintString(GLFW_WAYLAND_APP_ID, wm_class_class);
|
||||
glfwWindowHint(GLFW_COCOA_COLOR_SPACE, OPT(macos_colorspace));
|
||||
#endif
|
||||
|
||||
if (global_state.num_os_windows >= MAX_CHILDREN) {
|
||||
@@ -810,12 +826,6 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) {
|
||||
if (ret == NULL) return NULL;
|
||||
int width = PyLong_AsLong(PyTuple_GET_ITEM(ret, 0)), height = PyLong_AsLong(PyTuple_GET_ITEM(ret, 1));
|
||||
Py_CLEAR(ret);
|
||||
// The GLFW Wayland backend cannot create and show windows separately so we
|
||||
// cannot call the pre_show_callback. See
|
||||
// https://github.com/glfw/glfw/issues/1268 It doesn't matter since there
|
||||
// is no startup notification in Wayland anyway. It amazes me that anyone
|
||||
// uses Wayland as anything other than a butt for jokes.
|
||||
if (global_state.is_wayland) glfwWindowHint(GLFW_VISIBLE, true);
|
||||
GLFWwindow *glfw_window = glfwCreateWindow(width, height, title, NULL, temp_window ? temp_window : common_context);
|
||||
if (temp_window) { glfwDestroyWindow(temp_window); temp_window = NULL; }
|
||||
if (glfw_window == NULL) { PyErr_SetString(PyExc_ValueError, "Failed to create GLFWwindow"); return NULL; }
|
||||
@@ -830,25 +840,24 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) {
|
||||
#ifndef __APPLE__
|
||||
if (is_first_window) glfwSwapInterval(OPT(sync_to_monitor) && !global_state.is_wayland ? 1 : 0);
|
||||
#endif
|
||||
glfwSwapBuffers(glfw_window);
|
||||
// On Wayland the initial swap is allowed only after the first XDG configure event
|
||||
if (glfwAreSwapsAllowed(glfw_window)) glfwSwapBuffers(glfw_window);
|
||||
glfwSetInputMode(glfw_window, GLFW_LOCK_KEY_MODS, true);
|
||||
if (!global_state.is_wayland) {
|
||||
PyObject *pret = PyObject_CallFunction(pre_show_callback, "N", native_window_handle(glfw_window));
|
||||
if (pret == NULL) return NULL;
|
||||
Py_DECREF(pret);
|
||||
if (x != -1 && y != -1) glfwSetWindowPos(glfw_window, x, y);
|
||||
glfwShowWindow(glfw_window);
|
||||
PyObject *pret = PyObject_CallFunction(pre_show_callback, "N", native_window_handle(glfw_window));
|
||||
if (pret == NULL) return NULL;
|
||||
Py_DECREF(pret);
|
||||
if (x != -1 && y != -1) glfwSetWindowPos(glfw_window, x, y);
|
||||
glfwShowWindow(glfw_window);
|
||||
#ifdef __APPLE__
|
||||
float n_xscale, n_yscale;
|
||||
double n_xdpi, n_ydpi;
|
||||
get_window_content_scale(glfw_window, &n_xscale, &n_yscale, &n_xdpi, &n_ydpi);
|
||||
if (n_xdpi != xdpi || n_ydpi != ydpi) {
|
||||
// this can happen if the window is moved by the OS to a different monitor when shown
|
||||
xdpi = n_xdpi; ydpi = n_ydpi;
|
||||
fonts_data = load_fonts_data(OPT(font_size), xdpi, ydpi);
|
||||
}
|
||||
#endif
|
||||
float n_xscale, n_yscale;
|
||||
double n_xdpi, n_ydpi;
|
||||
get_window_content_scale(glfw_window, &n_xscale, &n_yscale, &n_xdpi, &n_ydpi);
|
||||
if (n_xdpi != xdpi || n_ydpi != ydpi) {
|
||||
// this can happen if the window is moved by the OS to a different monitor when shown
|
||||
xdpi = n_xdpi; ydpi = n_ydpi;
|
||||
fonts_data = load_fonts_data(OPT(font_size), xdpi, ydpi);
|
||||
}
|
||||
#endif
|
||||
if (is_first_window) {
|
||||
PyObject *ret = PyObject_CallFunction(load_programs, "O", is_semi_transparent ? Py_True : Py_False);
|
||||
if (ret == NULL) return NULL;
|
||||
@@ -1029,6 +1038,9 @@ glfw_init(PyObject UNUSED *self, PyObject *args) {
|
||||
const char* path;
|
||||
int debug_keyboard = 0, debug_rendering = 0;
|
||||
if (!PyArg_ParseTuple(args, "s|pp", &path, &debug_keyboard, &debug_rendering)) return NULL;
|
||||
#ifdef __APPLE__
|
||||
cocoa_set_uncaught_exception_handler();
|
||||
#endif
|
||||
const char* err = load_glfw(path);
|
||||
if (err) { PyErr_SetString(PyExc_RuntimeError, err); return NULL; }
|
||||
glfwSetErrorCallback(error_callback);
|
||||
@@ -1349,7 +1361,7 @@ is_mouse_hidden(OSWindow *w) {
|
||||
|
||||
void
|
||||
swap_window_buffers(OSWindow *os_window) {
|
||||
glfwSwapBuffers(os_window->handle);
|
||||
if (glfwAreSwapsAllowed(os_window->handle)) glfwSwapBuffers(os_window->handle);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1362,7 +1374,8 @@ should_os_window_be_rendered(OSWindow* w) {
|
||||
return (
|
||||
glfwGetWindowAttrib(w->handle, GLFW_ICONIFIED) ||
|
||||
!glfwGetWindowAttrib(w->handle, GLFW_VISIBLE) ||
|
||||
glfwGetWindowAttrib(w->handle, GLFW_OCCLUDED)
|
||||
glfwGetWindowAttrib(w->handle, GLFW_OCCLUDED) ||
|
||||
!glfwAreSwapsAllowed(w->handle)
|
||||
) ? false : true;
|
||||
}
|
||||
|
||||
@@ -1629,11 +1642,12 @@ void cleanup_glfw(void) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// constants {{{
|
||||
bool
|
||||
init_glfw(PyObject *m) {
|
||||
if (PyModule_AddFunctions(m, module_methods) != 0) return false;
|
||||
register_at_exit_cleanup_func(GLFW_CLEANUP_FUNC, cleanup_glfw);
|
||||
|
||||
// constants {{{
|
||||
#define ADDC(n) if(PyModule_AddIntConstant(m, #n, n) != 0) return false;
|
||||
ADDC(GLFW_RELEASE);
|
||||
ADDC(GLFW_PRESS);
|
||||
@@ -1875,9 +1889,8 @@ init_glfw(PyObject *m) {
|
||||
// ---
|
||||
ADDC(GLFW_CONNECTED);
|
||||
ADDC(GLFW_DISCONNECTED);
|
||||
|
||||
return true;
|
||||
#undef ADDC
|
||||
// }}}
|
||||
|
||||
return true;
|
||||
}
|
||||
// }}}
|
||||
// }}}
|
||||
|
||||
@@ -419,7 +419,7 @@ load_image_data(GraphicsManager *self, Image *img, const GraphicsCommand *g, con
|
||||
if (fd == -1) ABRT("EBADF", "Failed to open file for graphics transmission with error: [%d] %s", errno, strerror(errno));
|
||||
load_data->loading_completed_successfully = mmap_img_file(self, fd, g->data_sz, g->data_offset);
|
||||
safe_close(fd, __FILE__, __LINE__);
|
||||
if (transmission_type == 't') {
|
||||
if (transmission_type == 't' && strstr(fname, "tty-graphics-protocol") != NULL) {
|
||||
if (global_state.boss) { call_boss(safe_delete_temp_file, "s", fname); }
|
||||
else unlink(fname);
|
||||
}
|
||||
|
||||
@@ -480,13 +480,11 @@ get_line_wrapper(void *x, int y) {
|
||||
return &glw->line;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
as_text(HistoryBuf *self, PyObject *args) {
|
||||
PyObject*
|
||||
as_text_history_buf(HistoryBuf *self, PyObject *args, ANSIBuf *output) {
|
||||
GetLineWrapper glw = {.self=self};
|
||||
glw.line.xnum = self->xnum;
|
||||
ANSIBuf output = {0};
|
||||
PyObject *ans = as_text_generic(args, &glw, get_line_wrapper, self->count, &output);
|
||||
free(output.buf);
|
||||
PyObject *ans = as_text_generic(args, &glw, get_line_wrapper, self->count, output);
|
||||
return ans;
|
||||
}
|
||||
|
||||
@@ -523,7 +521,6 @@ static PyMethodDef methods[] = {
|
||||
METHODB(pagerhist_rewrap, METH_O),
|
||||
METHODB(pagerhist_as_text, METH_VARARGS),
|
||||
METHODB(pagerhist_as_bytes, METH_VARARGS),
|
||||
METHODB(as_text, METH_VARARGS),
|
||||
METHOD(dirty_lines, METH_NOARGS)
|
||||
METHOD(push, METH_VARARGS)
|
||||
METHOD(rewrap, METH_VARARGS)
|
||||
|
||||
10
kitty/key_encoding.py
generated
10
kitty/key_encoding.py
generated
@@ -292,14 +292,18 @@ enter_key = KeyEvent(key='ENTER')
|
||||
backspace_key = KeyEvent(key='BACKSPACE')
|
||||
config_mod_map = {
|
||||
'SHIFT': SHIFT,
|
||||
'⇧': SHIFT,
|
||||
'ALT': ALT,
|
||||
'OPTION': ALT,
|
||||
'OPT': ALT,
|
||||
'⌥': ALT,
|
||||
'⌘': SUPER,
|
||||
'CMD': SUPER,
|
||||
'SUPER': SUPER,
|
||||
'CTRL': CTRL,
|
||||
'COMMAND': SUPER,
|
||||
'CMD': SUPER,
|
||||
'⌘': SUPER,
|
||||
'CONTROL': CTRL,
|
||||
'CTRL': CTRL,
|
||||
'⌃': CTRL,
|
||||
'HYPER': HYPER,
|
||||
'META': META,
|
||||
'NUM_LOCK': NUM_LOCK,
|
||||
|
||||
195
kitty/keys.c
195
kitty/keys.c
@@ -157,9 +157,8 @@ on_key_input(GLFWkeyevent *ev) {
|
||||
case GLFW_IME_NONE:
|
||||
// for macOS, update ime position on every key input
|
||||
// because the position is required before next input
|
||||
#if defined(__APPLE__)
|
||||
// On Linux this is needed by Fig integration: https://github.com/kovidgoyal/kitty/issues/5241
|
||||
update_ime_position(w, screen);
|
||||
#endif
|
||||
break;
|
||||
default:
|
||||
debug("invalid state, ignoring\n");
|
||||
@@ -219,6 +218,9 @@ on_key_input(GLFWkeyevent *ev) {
|
||||
schedule_write_to_child(w->id, 1, text, strlen(text));
|
||||
debug("sent key as text to child\n");
|
||||
} else if (size > 0) {
|
||||
if (size == 1 && screen->modes.mHANDLE_TERMIOS_SIGNALS) {
|
||||
if (screen_send_signal_for_key(screen, *encoded_key)) return;
|
||||
}
|
||||
schedule_write_to_child(w->id, 1, encoded_key, size);
|
||||
debug("sent encoded key to child\n");
|
||||
} else {
|
||||
@@ -281,11 +283,200 @@ static PyMethodDef module_methods[] = {
|
||||
{0}
|
||||
};
|
||||
|
||||
// SingleKey {{{
|
||||
typedef uint32_t keybitfield;
|
||||
#define KEY_BITS 21
|
||||
#define MOD_BITS 10
|
||||
#if 1 << (MOD_BITS-1) < GLFW_MOD_KITTY
|
||||
#error "Not enough mod bits"
|
||||
#endif
|
||||
typedef union Key {
|
||||
struct {
|
||||
keybitfield mods : MOD_BITS;
|
||||
keybitfield is_native: 1;
|
||||
keybitfield key : KEY_BITS;
|
||||
};
|
||||
keybitfield val;
|
||||
} Key;
|
||||
|
||||
static PyTypeObject SingleKey_Type;
|
||||
static char *SingleKey_kwds[] = {"mods", "is_native", "key", NULL};
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
|
||||
Key key;
|
||||
bool defined_with_kitty_mod;
|
||||
} SingleKey;
|
||||
|
||||
static inline void
|
||||
SingleKey_set_vals(SingleKey *self, long key, unsigned short mods, int is_native) {
|
||||
if (key >= 0 && (unsigned long)key <= BIT_MASK(keybitfield, KEY_BITS)) {
|
||||
keybitfield k = (keybitfield)(unsigned long)key;
|
||||
self->key.key = k & BIT_MASK(keybitfield, KEY_BITS);
|
||||
}
|
||||
if (!(mods & 1 << (MOD_BITS + 1))) self->key.mods = mods & BIT_MASK(u_int32_t, MOD_BITS);
|
||||
if (is_native > -1) self->key.is_native = is_native ? 1 : 0;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
SingleKey_new(PyTypeObject *type, PyObject *args, PyObject *kw) {
|
||||
long key = -1; unsigned short mods = 1 << (MOD_BITS + 1); int is_native = -1;
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kw, "|Hpl", SingleKey_kwds, &mods, &is_native, &key)) return NULL;
|
||||
SingleKey *self = (SingleKey *)type->tp_alloc(type, 0);
|
||||
if (self) SingleKey_set_vals(self, key, mods, is_native);
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
static void
|
||||
SingleKey_dealloc(SingleKey* self) {
|
||||
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
SingleKey_repr(PyObject *s) {
|
||||
SingleKey *self = (SingleKey*)s;
|
||||
char buf[128];
|
||||
int pos = 0;
|
||||
pos += PyOS_snprintf(buf + pos, sizeof(buf) - pos, "SingleKey(");
|
||||
unsigned int mods = self->key.mods;
|
||||
if (mods) pos += PyOS_snprintf(buf + pos, sizeof(buf) - pos, "mods=%u, ", mods);
|
||||
if (self->key.is_native) pos += PyOS_snprintf(buf + pos, sizeof(buf) - pos, "is_native=True, ");
|
||||
unsigned long key = self->key.key;
|
||||
if (key) pos += PyOS_snprintf(buf + pos, sizeof(buf) - pos, "key=%lu, ", key);
|
||||
if (buf[pos-1] == ' ') pos -= 2;
|
||||
pos += PyOS_snprintf(buf + pos, sizeof(buf) - pos, ")");
|
||||
return PyUnicode_FromString(buf);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
SingleKey_get_key(SingleKey *self, void UNUSED *closure) {
|
||||
const unsigned long val = self->key.key;
|
||||
return PyLong_FromUnsignedLong(val);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
SingleKey_get_mods(SingleKey *self, void UNUSED *closure) {
|
||||
const unsigned long mods = self->key.mods;
|
||||
return PyLong_FromUnsignedLong(mods);
|
||||
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
SingleKey_get_is_native(SingleKey *self, void UNUSED *closure) {
|
||||
if (self->key.is_native) Py_RETURN_TRUE;
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
SingleKey_defined_with_kitty_mod(SingleKey *self, void UNUSED *closure) {
|
||||
if (self->defined_with_kitty_mod || (self->key.mods & GLFW_MOD_KITTY)) Py_RETURN_TRUE;
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
|
||||
static PyGetSetDef SingleKey_getsetters[] = {
|
||||
{"key", (getter)SingleKey_get_key, NULL, "The key as an integer", NULL},
|
||||
{"mods", (getter)SingleKey_get_mods, NULL, "The modifiers as an integer", NULL},
|
||||
{"is_native", (getter)SingleKey_get_is_native, NULL, "A bool", NULL},
|
||||
{"defined_with_kitty_mod", (getter)SingleKey_defined_with_kitty_mod, NULL, "A bool", NULL},
|
||||
{NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
static Py_hash_t
|
||||
SingleKey_hash(PyObject *self) {
|
||||
Py_hash_t ans = ((SingleKey*)self)->key.val;
|
||||
if (ans == -1) ans = -2;
|
||||
return ans;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
SingleKey_richcompare(PyObject *self, PyObject *other, int op) {
|
||||
if (!PyObject_TypeCheck(other, &SingleKey_Type)) { PyErr_SetString(PyExc_TypeError, "Cannot compare SingleKey to other objects"); return NULL; }
|
||||
SingleKey *a = (SingleKey*)self, *b = (SingleKey*)other;
|
||||
Py_RETURN_RICHCOMPARE(a->key.val, b->key.val, op);
|
||||
}
|
||||
|
||||
static Py_ssize_t
|
||||
SingleKey___len__(PyObject *self UNUSED) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
SingleKey_item(PyObject *o, Py_ssize_t i) {
|
||||
SingleKey *self = (SingleKey*)o;
|
||||
switch(i) {
|
||||
case 0:
|
||||
return SingleKey_get_mods(self, NULL);
|
||||
case 1:
|
||||
return SingleKey_get_is_native(self, NULL);
|
||||
case 2:
|
||||
return SingleKey_get_key(self, NULL);
|
||||
}
|
||||
PyErr_SetString(PyExc_IndexError, "tuple index out of range");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static PySequenceMethods SingleKey_sequence_methods = {
|
||||
.sq_length = SingleKey___len__,
|
||||
.sq_item = SingleKey_item,
|
||||
};
|
||||
|
||||
static PyObject*
|
||||
SingleKey_resolve_kitty_mod(SingleKey *self, PyObject *km) {
|
||||
if (!(self->key.mods & GLFW_MOD_KITTY)) { Py_INCREF(self); return (PyObject*)self; }
|
||||
unsigned long kitty_mod = PyLong_AsUnsignedLong(km);
|
||||
if (PyErr_Occurred()) return NULL;
|
||||
SingleKey *ans = (SingleKey*)SingleKey_Type.tp_alloc(&SingleKey_Type, 0);
|
||||
if (!ans) return NULL;
|
||||
ans->key.val = self->key.val;
|
||||
ans->key.mods = (ans->key.mods & ~GLFW_MOD_KITTY) | kitty_mod;
|
||||
ans->defined_with_kitty_mod = true;
|
||||
return (PyObject*)ans;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
SingleKey_replace(SingleKey *self, PyObject *args, PyObject *kw) {
|
||||
long key = -2; unsigned short mods = 1 << (MOD_BITS + 1); int is_native = -1;
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kw, "|Hpl", SingleKey_kwds, &mods, &is_native, &key)) return NULL;
|
||||
SingleKey *ans = (SingleKey*)SingleKey_Type.tp_alloc(&SingleKey_Type, 0);
|
||||
if (ans) {
|
||||
if (key == -1) key = 0;
|
||||
ans->key.val = self->key.val;
|
||||
SingleKey_set_vals(ans, key, mods, is_native);
|
||||
}
|
||||
return (PyObject*)ans;
|
||||
}
|
||||
|
||||
static PyMethodDef SingleKey_methods[] = {
|
||||
{"_replace", (PyCFunction)(void (*) (void))SingleKey_replace, METH_VARARGS | METH_KEYWORDS, ""},
|
||||
{"resolve_kitty_mod", (PyCFunction)SingleKey_resolve_kitty_mod, METH_O, ""},
|
||||
{NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
|
||||
static PyTypeObject SingleKey_Type = {
|
||||
PyVarObject_HEAD_INIT(NULL, 0)
|
||||
.tp_name = "fast_data_types.SingleKey",
|
||||
.tp_basicsize = sizeof(SingleKey),
|
||||
.tp_dealloc = (destructor)SingleKey_dealloc,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = "Compact and fast representation of a single key as defined in the config",
|
||||
.tp_new = SingleKey_new,
|
||||
.tp_hash = SingleKey_hash,
|
||||
.tp_richcompare = SingleKey_richcompare,
|
||||
.tp_as_sequence = &SingleKey_sequence_methods,
|
||||
.tp_repr = SingleKey_repr,
|
||||
.tp_methods = SingleKey_methods,
|
||||
.tp_getset = SingleKey_getsetters,
|
||||
}; // }}}
|
||||
|
||||
|
||||
bool
|
||||
init_keys(PyObject *module) {
|
||||
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
||||
if (PyType_Ready(&PyKeyEvent_Type) < 0) return false;
|
||||
if (PyModule_AddObject(module, "KeyEvent", (PyObject *)&PyKeyEvent_Type) != 0) return 0;
|
||||
Py_INCREF(&PyKeyEvent_Type);
|
||||
ADD_TYPE(SingleKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@ from typing import Union
|
||||
|
||||
from .fast_data_types import (
|
||||
GLFW_MOD_ALT, GLFW_MOD_CONTROL, GLFW_MOD_HYPER, GLFW_MOD_META,
|
||||
GLFW_MOD_SHIFT, GLFW_MOD_SUPER, KeyEvent
|
||||
GLFW_MOD_SHIFT, GLFW_MOD_SUPER, KeyEvent, SingleKey
|
||||
)
|
||||
from .options.utils import KeyMap, SequenceMap, SubSequenceMap
|
||||
from .types import SingleKey
|
||||
from .typing import ScreenType
|
||||
|
||||
mod_mask = GLFW_MOD_ALT | GLFW_MOD_CONTROL | GLFW_MOD_SHIFT | GLFW_MOD_SUPER | GLFW_MOD_META | GLFW_MOD_HYPER
|
||||
|
||||
@@ -54,6 +54,7 @@ read_response(int fd, monotonic_t timeout, PyObject *ans) {
|
||||
switch(state) {
|
||||
case START:
|
||||
if (ch == 0x1b) state = STARTING_ESC;
|
||||
if (ch == 0x03) { PyErr_SetString(PyExc_KeyboardInterrupt, "User pressed Ctrl+C"); return false; }
|
||||
break;
|
||||
#define CASE(curr, q, next) case curr: state = ch == q ? next : START; break;
|
||||
CASE(STARTING_ESC, 'P', P);
|
||||
|
||||
132
kitty/launch.py
132
kitty/launch.py
@@ -57,13 +57,28 @@ of the active window in the tab is used as the tab title. The special value
|
||||
type=choices
|
||||
default=window
|
||||
choices=window,tab,os-window,overlay,background,clipboard,primary
|
||||
Where to launch the child process, in a new kitty :code:`window` in the current
|
||||
tab, a new :code:`tab`, or a new :code:`os-window` or an :code:`overlay` over
|
||||
the current window. Note that if the current window already has an overlay, then
|
||||
it will open a new window. The value of :code:`background` means the process
|
||||
will be run in the background. The values :code:`clipboard` and :code:`primary`
|
||||
are meant to work with :option:`--stdin-source <launch --stdin-source>` to copy
|
||||
data to the system clipboard or primary selection.
|
||||
Where to launch the child process:
|
||||
|
||||
:code:`window`
|
||||
A new :term:`kitty window <window>` in the current tab
|
||||
|
||||
:code:`tab`
|
||||
A new :term:`tab` in the current OS window
|
||||
|
||||
:code:`os-window <os_window>`
|
||||
A new :term:`operating system window <os_window>`
|
||||
|
||||
:code:`overlay`
|
||||
An :term:`overlay window <overlay>` covering the current active kitty window
|
||||
|
||||
:code:`background`
|
||||
The process will be run in the :italic:`background`, without a kitty window.
|
||||
|
||||
:code:`clipboard`, :code:`primary`
|
||||
These two are meant to work with :option:`--stdin-source <launch --stdin-source>` to copy
|
||||
data to the :italic:`system clipboard` or :italic:`primary selection`.
|
||||
|
||||
#placeholder_for_formatting#
|
||||
|
||||
|
||||
--keep-focus --dont-take-focus
|
||||
@@ -73,6 +88,7 @@ opened window.
|
||||
|
||||
|
||||
--cwd
|
||||
completion=type:directory kwds:current,oldest,last_reported
|
||||
The working directory for the newly launched child. Use the special value
|
||||
:code:`current` to use the working directory of the currently active window.
|
||||
The special value :code:`last_reported` uses the last working directory reported
|
||||
@@ -110,10 +126,9 @@ currently active window.
|
||||
--copy-env
|
||||
type=bool-set
|
||||
Copy the environment variables from the currently active window into the newly
|
||||
launched child process. Note that most shells only set environment variables
|
||||
for child processes, so this will only copy the environment variables that the
|
||||
shell process itself has, not the environment variables child processes inside
|
||||
the shell see. To copy that enviroment, use either the :ref:`clone-in-kitty
|
||||
launched child process. Note that this only copies the environment when the
|
||||
window was first created, as it is not possible to get updated environment variables
|
||||
from arbitrary processes. To copy that environment, use either the :ref:`clone-in-kitty
|
||||
<clone_shell>` feature or the kitty remote control feature with :option:`kitty
|
||||
@ launch --copy-env`.
|
||||
|
||||
@@ -141,6 +156,26 @@ enabled in :file:`kitty.conf`). Note that any program with the right level of
|
||||
permissions can still write to the pipes of any other program on the same
|
||||
computer and therefore can control kitty. It can, however, be useful to block
|
||||
programs running on other computers (for example, over SSH) or as other users.
|
||||
See :option:`--remote-control-password` for ways to restrict actions allowed by
|
||||
remote control.
|
||||
|
||||
|
||||
--remote-control-password
|
||||
type=list
|
||||
Restrict the actions remote control is allowed to take. This works like
|
||||
:opt:`remote_control_password`. You can specify a password and list of actions
|
||||
just as for :opt:`remote_control_password`. For example::
|
||||
--remote-control-password '"my passphrase" get-* set-colors'
|
||||
|
||||
This password will be in effect for this window only.
|
||||
Note that any passwords you have defined for :opt:`remote_control_password`
|
||||
in :file:`kitty.conf` are also in effect. You can override them by using the same password here.
|
||||
You can also disable all :opt:`remote_control_password` global passwords for this window, by using::
|
||||
--remote-control-password '!'
|
||||
|
||||
This option only takes effect if :option:`--allow-remote-control`
|
||||
is also specified. Can be specified multiple times to create multiple passwords.
|
||||
This option was added to kitty in version 0.26.0
|
||||
|
||||
|
||||
--stdin-source
|
||||
@@ -148,17 +183,32 @@ type=choices
|
||||
default=none
|
||||
choices=none,@selection,@screen,@screen_scrollback,@alternate,@alternate_scrollback,@first_cmd_output_on_screen,@last_cmd_output,@last_visited_cmd_output
|
||||
Pass the screen contents as :file:`STDIN` to the child process.
|
||||
:code:`@selection` is the currently selected text. :code:`@screen` is the
|
||||
contents of the currently active window. :code:`@screen_scrollback` is the same
|
||||
as :code:`@screen`, but includes the scrollback buffer as well.
|
||||
:code:`@alternate` is the secondary screen of the current active window. For
|
||||
example if you run a full screen terminal application, the secondary screen will
|
||||
be the screen you return to when quitting the application.
|
||||
:code:`@first_cmd_output_on_screen` is the output from the first command run in
|
||||
the shell on screen, :code:`@last_cmd_output` is the output from the last
|
||||
command run in the shell, :code:`@last_visited_cmd_output` is the first output
|
||||
below the last scrolled position via :ac:`scroll_to_prompt`, this three needs
|
||||
:ref:`shell integration <shell_integration>` to work.
|
||||
|
||||
:code:`@selection`
|
||||
is the currently selected text.
|
||||
|
||||
:code:`@screen`
|
||||
is the contents of the currently active window.
|
||||
|
||||
:code:`@screen_scrollback`
|
||||
is the same as :code:`@screen`, but includes the scrollback buffer as well.
|
||||
|
||||
:code:`@alternate`
|
||||
is the secondary screen of the current active window. For example if you run
|
||||
a full screen terminal application, the secondary screen will
|
||||
be the screen you return to when quitting the application.
|
||||
|
||||
:code:`@first_cmd_output_on_screen`
|
||||
is the output from the first command run in the shell on screen.
|
||||
|
||||
:code:`@last_cmd_output`
|
||||
is the output from the last command run in the shell.
|
||||
|
||||
:code:`@last_visited_cmd_output`
|
||||
is the first output below the last scrolled position via :ac:`scroll_to_prompt`,
|
||||
this needs :ref:`shell integration <shell_integration>` to work.
|
||||
|
||||
#placeholder_for_formatting#
|
||||
|
||||
|
||||
--stdin-add-formatting
|
||||
@@ -181,15 +231,15 @@ the same as for the :ac:`toggle_marker` action (see :doc:`/marks`).
|
||||
|
||||
|
||||
--os-window-class
|
||||
Set the *WM_CLASS* property on X11 and the application id property on Wayland
|
||||
for the newly created OS window when using :option:`--type=os-window
|
||||
Set the :italic:`WM_CLASS` property on X11 and the application id property on
|
||||
Wayland for the newly created OS window when using :option:`--type=os-window
|
||||
<launch --type>`. Defaults to whatever is used by the parent kitty process,
|
||||
which in turn defaults to :code:`kitty`.
|
||||
|
||||
|
||||
--os-window-name
|
||||
Set the *WM_NAME* property on X11 for the newly created OS Window when using
|
||||
:option:`--type=os-window <launch --type>`. Defaults to
|
||||
Set the :italic:`WM_NAME` property on X11 for the newly created OS Window when
|
||||
using :option:`--type=os-window <launch --type>`. Defaults to
|
||||
:option:`--os-window-class <launch --os-window-class>`.
|
||||
|
||||
|
||||
@@ -200,8 +250,9 @@ use the title of the current OS window, if any.
|
||||
|
||||
|
||||
--logo
|
||||
completion=type:file ext:png group:"PNG images" relative:conf
|
||||
Path to a PNG image to use as the logo for the newly created window. See
|
||||
:opt:`window_logo_path`.
|
||||
:opt:`window_logo_path`. Relative paths are resolved from the kitty configuration directory.
|
||||
|
||||
|
||||
--logo-position
|
||||
@@ -213,19 +264,20 @@ specified. See :opt:`window_logo_position`.
|
||||
type=float
|
||||
default=-1
|
||||
The amount the window logo should be faded into the background. Only takes
|
||||
effect if :option:`--logo` is specified. See :opt:`window_logo_position`.
|
||||
effect if :option:`--logo` is specified. See :opt:`window_logo_alpha`.
|
||||
|
||||
|
||||
--color
|
||||
type=list
|
||||
Change colors in the newly launched window. You can either specify a path to a
|
||||
:file:`.conf` file with the same syntax as :file:`kitty.conf` to read the colors
|
||||
from, or specify them individually, for example: ``--color background=white``
|
||||
``--color foreground=red``
|
||||
from, or specify them individually, for example: :code:`--color background=white
|
||||
--color foreground=red`.
|
||||
|
||||
|
||||
--watcher -w
|
||||
type=list
|
||||
completion=type:file ext:py relative:conf group:"Python scripts"
|
||||
Path to a Python file. Appropriately named functions in this file will be called
|
||||
for various events, such as when the window is resized, focused or closed. See
|
||||
the section on watchers in the launch command documentation: :ref:`watchers`.
|
||||
@@ -317,6 +369,7 @@ def load_watch_modules(watchers: Iterable[str]) -> Optional[Watchers]:
|
||||
class LaunchKwds(TypedDict):
|
||||
|
||||
allow_remote_control: bool
|
||||
remote_control_passwords: Optional[Dict[str, Sequence[str]]]
|
||||
cwd_from: Optional[CwdRequest]
|
||||
cwd: Optional[str]
|
||||
location: Optional[str]
|
||||
@@ -381,8 +434,16 @@ def launch(
|
||||
tm = boss.active_tab_manager
|
||||
opts.os_window_title = get_os_window_title(tm.os_window_id) if tm else None
|
||||
env = get_env(opts, active_child)
|
||||
remote_control_restrictions: Optional[Dict[str, Sequence[str]]] = None
|
||||
if opts.allow_remote_control and opts.remote_control_password:
|
||||
from kitty.options.utils import remote_control_password
|
||||
remote_control_restrictions = {}
|
||||
for rcp in opts.remote_control_password:
|
||||
for pw, rcp_items in remote_control_password(rcp, {}):
|
||||
remote_control_restrictions[pw] = rcp_items
|
||||
kw: LaunchKwds = {
|
||||
'allow_remote_control': opts.allow_remote_control,
|
||||
'remote_control_passwords': remote_control_restrictions,
|
||||
'cwd_from': None,
|
||||
'cwd': None,
|
||||
'location': None,
|
||||
@@ -620,11 +681,10 @@ class EditCmd:
|
||||
self.file_localpath = os.path.join(self.tdir, self.file_name)
|
||||
with open(self.file_localpath, 'wb') as f:
|
||||
f.write(self.file_data)
|
||||
self.file_obj = open(self.file_localpath, 'rb')
|
||||
self.file_data = b''
|
||||
self.last_mod_time = self.file_mod_time
|
||||
if not self.opts.cwd:
|
||||
self.opts.cwd = os.path.dirname(self.file_obj.name)
|
||||
self.opts.cwd = os.path.dirname(self.file_localpath)
|
||||
|
||||
def __del__(self) -> None:
|
||||
if self.tdir:
|
||||
@@ -633,12 +693,12 @@ class EditCmd:
|
||||
self.tdir = ''
|
||||
|
||||
def read_data(self) -> bytes:
|
||||
self.file_obj.seek(0)
|
||||
return self.file_obj.read()
|
||||
with open(self.file_localpath, 'rb') as f:
|
||||
return f.read()
|
||||
|
||||
@property
|
||||
def file_mod_time(self) -> int:
|
||||
return os.stat(self.file_obj.fileno()).st_mtime_ns
|
||||
return os.stat(self.file_localpath).st_mtime_ns
|
||||
|
||||
def schedule_check(self) -> None:
|
||||
if not self.abort_signaled:
|
||||
@@ -733,7 +793,7 @@ def remote_edit(msg: str, window: Window) -> None:
|
||||
if q is not None:
|
||||
q.abort_signaled = c.abort_signaled
|
||||
return
|
||||
cmdline = get_editor(path_to_edit=c.file_obj.name, line_number=c.line_number)
|
||||
cmdline = get_editor(path_to_edit=c.file_localpath, line_number=c.line_number)
|
||||
w = launch(get_boss(), c.opts, cmdline, active=window)
|
||||
if w is not None:
|
||||
c.source_window_id = window.id
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user