Compare commits
464 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d8d669a43 | ||
|
|
f4d44f30b4 | ||
|
|
4c77b0c562 | ||
|
|
2f367fa90c | ||
|
|
970ec9c839 | ||
|
|
be52f29792 | ||
|
|
8ae2f06828 | ||
|
|
601b2a05ac | ||
|
|
b78fbb4521 | ||
|
|
c40ef01445 | ||
|
|
ab8a4c6b9f | ||
|
|
dd331ca12e | ||
|
|
c3b23679f3 | ||
|
|
d0398dca28 | ||
|
|
e66c732b41 | ||
|
|
e7b216ba68 | ||
|
|
838a85ee8b | ||
|
|
3ec93e0b4e | ||
|
|
e8a9bbb51d | ||
|
|
25c2a0e241 | ||
|
|
de12dddbef | ||
|
|
24eeeedb1f | ||
|
|
c2bc24de35 | ||
|
|
21446e36c1 | ||
|
|
0f734719e7 | ||
|
|
f3c96f308d | ||
|
|
8ff841f1ac | ||
|
|
709c4abb53 | ||
|
|
c1f06941f5 | ||
|
|
ecb74aed29 | ||
|
|
c72154782b | ||
|
|
9520006466 | ||
|
|
40f04284d4 | ||
|
|
832534aac9 | ||
|
|
6cc89416ce | ||
|
|
b3fc2cb118 | ||
|
|
69efd9801b | ||
|
|
c07f164154 | ||
|
|
a2d1140229 | ||
|
|
bae9b095b4 | ||
|
|
efe6ff6188 | ||
|
|
3257fdf24f | ||
|
|
3284d71300 | ||
|
|
cdb3e2c1cd | ||
|
|
57cffc71b7 | ||
|
|
a4f1caeb4e | ||
|
|
45de091ee6 | ||
|
|
00c1802319 | ||
|
|
02c2d24360 | ||
|
|
499c255e81 | ||
|
|
30cad2e0a6 | ||
|
|
6e838b83d8 | ||
|
|
5297223ee3 | ||
|
|
2340d5be8d | ||
|
|
7de1a145aa | ||
|
|
32426d61c3 | ||
|
|
3ce47660a0 | ||
|
|
0dff455ffa | ||
|
|
81739288e8 | ||
|
|
5641668bc1 | ||
|
|
5c03a52a0b | ||
|
|
087b52e3e0 | ||
|
|
733b8e7c1c | ||
|
|
aa83f42f2d | ||
|
|
4ed2854791 | ||
|
|
73cdd87d91 | ||
|
|
51c8e3b2c6 | ||
|
|
e522095fae | ||
|
|
6fc1226028 | ||
|
|
fb40280d3c | ||
|
|
746cb3684a | ||
|
|
16c6545e93 | ||
|
|
0c4b20aa4e | ||
|
|
15e3e8d8b1 | ||
|
|
6123de52c2 | ||
|
|
3db0aab24b | ||
|
|
ae77f696ce | ||
|
|
98369db7f5 | ||
|
|
bc9d6892d4 | ||
|
|
5c02c370d4 | ||
|
|
b944cdddeb | ||
|
|
a1d203d34a | ||
|
|
e151b8e604 | ||
|
|
3c67e991c2 | ||
|
|
cb7aed3234 | ||
|
|
ff80b906d0 | ||
|
|
8300436481 | ||
|
|
6b13454091 | ||
|
|
9eae4ad913 | ||
|
|
bfb8532c52 | ||
|
|
c41405fd57 | ||
|
|
276ba7754a | ||
|
|
8569be81ea | ||
|
|
a765d551e4 | ||
|
|
4309aa1ace | ||
|
|
6ef83a09d3 | ||
|
|
adcc616c92 | ||
|
|
6dc1617429 | ||
|
|
1837168b0b | ||
|
|
1be1864657 | ||
|
|
49c335972f | ||
|
|
aabc99c7bf | ||
|
|
7c91dc6183 | ||
|
|
ff8a99211d | ||
|
|
efda0ea455 | ||
|
|
906be21b8d | ||
|
|
5a36fbfe7b | ||
|
|
f9f69a0577 | ||
|
|
8ccbb96b8d | ||
|
|
8ae7256fd6 | ||
|
|
ba401c19c2 | ||
|
|
11343d42c3 | ||
|
|
6c628bc594 | ||
|
|
f6cbca1aeb | ||
|
|
51e7c8c136 | ||
|
|
f952694ffd | ||
|
|
59afdfc4e9 | ||
|
|
27ec1e4b3c | ||
|
|
e791a8a7bb | ||
|
|
66fe53ceb1 | ||
|
|
de3cc11423 | ||
|
|
901eae9604 | ||
|
|
1962519666 | ||
|
|
1e84cbe2ab | ||
|
|
06da2b88ff | ||
|
|
91b9110dcc | ||
|
|
d57abb56ce | ||
|
|
f6edb774fc | ||
|
|
ec0f3e9128 | ||
|
|
6f1404d17b | ||
|
|
3d9e47c295 | ||
|
|
afa6128155 | ||
|
|
72b1996423 | ||
|
|
6b1a9d635e | ||
|
|
dea79f16d0 | ||
|
|
2d772d7243 | ||
|
|
09fb246c8f | ||
|
|
0a5c16363c | ||
|
|
b444f4636e | ||
|
|
2b8acebd6e | ||
|
|
766010c292 | ||
|
|
0a2768e496 | ||
|
|
0fd7f3f7b5 | ||
|
|
d3472966d3 | ||
|
|
576ab24609 | ||
|
|
621453b068 | ||
|
|
6638546247 | ||
|
|
f99edbae3c | ||
|
|
150bf1a5b0 | ||
|
|
91a17e3f0c | ||
|
|
7160027c14 | ||
|
|
736d6cf7e6 | ||
|
|
82de6a1c56 | ||
|
|
1fbb4f763e | ||
|
|
20582431d6 | ||
|
|
7d9cf0b064 | ||
|
|
a834f221f1 | ||
|
|
c95dca4023 | ||
|
|
c05c0353fd | ||
|
|
e944e2ecae | ||
|
|
47b3c37bf0 | ||
|
|
190666dc8a | ||
|
|
8c6e7ce61a | ||
|
|
04ead526b9 | ||
|
|
135fb7e6e4 | ||
|
|
ce1e22ac95 | ||
|
|
a216f6bd46 | ||
|
|
c47e5372b5 | ||
|
|
daa673eff1 | ||
|
|
5099dd6aa3 | ||
|
|
f982e754e4 | ||
|
|
4a1ad7755a | ||
|
|
946589d1f1 | ||
|
|
7168ceab94 | ||
|
|
868d57b818 | ||
|
|
6057c57ea4 | ||
|
|
e8437fd435 | ||
|
|
9e317971b4 | ||
|
|
bdb55a02fa | ||
|
|
02e062af65 | ||
|
|
54c5faa12d | ||
|
|
9b0bd81661 | ||
|
|
52da68876d | ||
|
|
2780630a18 | ||
|
|
8f77cc65e2 | ||
|
|
7224363639 | ||
|
|
43851fc1c4 | ||
|
|
e33bf11012 | ||
|
|
0e21376538 | ||
|
|
7a463c1240 | ||
|
|
8e03da855f | ||
|
|
942881d1b7 | ||
|
|
2a50203fcd | ||
|
|
08ce8ffa54 | ||
|
|
90561682cf | ||
|
|
71027e74e0 | ||
|
|
be8bfbe370 | ||
|
|
f7e4447b66 | ||
|
|
df4e58bc39 | ||
|
|
441ea7d696 | ||
|
|
2b06ca5e1a | ||
|
|
434ef97952 | ||
|
|
e1504c4775 | ||
|
|
74f0057ec8 | ||
|
|
7cd74cb852 | ||
|
|
b2e74e4830 | ||
|
|
f54a3e8036 | ||
|
|
f3088c5646 | ||
|
|
db00adaf69 | ||
|
|
ea74add814 | ||
|
|
ba1b3c3c2d | ||
|
|
d5c48ddb94 | ||
|
|
9687318b22 | ||
|
|
f6fb36c58a | ||
|
|
c8a258f36b | ||
|
|
14e0b01b40 | ||
|
|
76de99a5a8 | ||
|
|
8613c6e1cd | ||
|
|
5bb6b29ca3 | ||
|
|
61558d518e | ||
|
|
3b724c8415 | ||
|
|
910565aa7c | ||
|
|
c7a2e060e3 | ||
|
|
740e787f54 | ||
|
|
bd87d50948 | ||
|
|
d3c0c422a5 | ||
|
|
72718cbab7 | ||
|
|
ea28951e0e | ||
|
|
39a78f6be3 | ||
|
|
fe8aaca320 | ||
|
|
f5cc58ea9d | ||
|
|
44992452cf | ||
|
|
f080871911 | ||
|
|
6789eb88e2 | ||
|
|
1a97be4a25 | ||
|
|
c9ef5c0715 | ||
|
|
0a9f8d4f6a | ||
|
|
cac2c153c2 | ||
|
|
f2d6ba8775 | ||
|
|
92abaad22c | ||
|
|
3df0194f6e | ||
|
|
f774841ee0 | ||
|
|
a9de91087a | ||
|
|
76a536ece0 | ||
|
|
719339e116 | ||
|
|
1c3910de5c | ||
|
|
39d3ef6fe8 | ||
|
|
064d0fa6f1 | ||
|
|
99a5843595 | ||
|
|
39e75e39e8 | ||
|
|
322a80e76e | ||
|
|
d4b048735d | ||
|
|
c23e04fd03 | ||
|
|
f67009f554 | ||
|
|
4013544efb | ||
|
|
31d9db7e74 | ||
|
|
384c56f834 | ||
|
|
3282c8200d | ||
|
|
ffbc9174f8 | ||
|
|
dceb00f669 | ||
|
|
920086ae88 | ||
|
|
a1311a2332 | ||
|
|
4c392426f6 | ||
|
|
4528173ff5 | ||
|
|
d3b63a9c45 | ||
|
|
c30a249cf6 | ||
|
|
20962d989f | ||
|
|
a1e4b19486 | ||
|
|
fd0262413e | ||
|
|
aa4d36cc57 | ||
|
|
8d119f06b3 | ||
|
|
708c5126b9 | ||
|
|
577de9f746 | ||
|
|
38a70f5b51 | ||
|
|
118306a9ff | ||
|
|
704717ff1d | ||
|
|
405c472d13 | ||
|
|
17c994db57 | ||
|
|
e227264bad | ||
|
|
8751ea79e7 | ||
|
|
3b3ffa7455 | ||
|
|
4322825ac7 | ||
|
|
d29fa7b382 | ||
|
|
08bb63fa92 | ||
|
|
5dde31f80c | ||
|
|
3bb869f725 | ||
|
|
11686b90f7 | ||
|
|
6df78fa67c | ||
|
|
53b1607c4d | ||
|
|
2341a27f63 | ||
|
|
0661caf9da | ||
|
|
5b5bd77c53 | ||
|
|
d36f6b39c7 | ||
|
|
b2a5b07f92 | ||
|
|
46889a3a32 | ||
|
|
958ebb72a6 | ||
|
|
febc5c53a6 | ||
|
|
6e921300e2 | ||
|
|
e167dec8a9 | ||
|
|
0019f07cc0 | ||
|
|
20fc1e6b0c | ||
|
|
908946a067 | ||
|
|
f1df7b1c1f | ||
|
|
65b9c69bd8 | ||
|
|
b91e47c3b6 | ||
|
|
0e3a11c53b | ||
|
|
53d8d2aaad | ||
|
|
84303cbf2e | ||
|
|
5e457da30b | ||
|
|
855718b179 | ||
|
|
d037c0b0fc | ||
|
|
90f5937413 | ||
|
|
cbbea37b0c | ||
|
|
4d74b23f20 | ||
|
|
79dd26a43a | ||
|
|
259e3076fa | ||
|
|
f2cb2422f0 | ||
|
|
4aea64261e | ||
|
|
cf0cd9acd9 | ||
|
|
b4b0bdc853 | ||
|
|
0b2837fcfa | ||
|
|
f20ad7df01 | ||
|
|
c9071a66ca | ||
|
|
b0ea960159 | ||
|
|
0e7f1d60d6 | ||
|
|
03c79502f9 | ||
|
|
d4d4e00f9d | ||
|
|
b52e5e795e | ||
|
|
68df13d3fe | ||
|
|
5f3d90e411 | ||
|
|
1b68e41db4 | ||
|
|
817ac82968 | ||
|
|
43fd8cb13c | ||
|
|
397fbe7ad3 | ||
|
|
3095e7a256 | ||
|
|
df9e893cbe | ||
|
|
6ff69c88df | ||
|
|
6e4d3c98da | ||
|
|
2404eba11f | ||
|
|
e5c57a679d | ||
|
|
33de55540b | ||
|
|
99e1605bba | ||
|
|
95c4e26b24 | ||
|
|
795953a341 | ||
|
|
75d5e386d3 | ||
|
|
00d2a8527f | ||
|
|
a1ade8fc42 | ||
|
|
7f9fec061a | ||
|
|
ec782d3296 | ||
|
|
2444864508 | ||
|
|
1ccc50b21b | ||
|
|
4552a474b7 | ||
|
|
85c6d8f16e | ||
|
|
14d391cc2e | ||
|
|
8fe87a0df5 | ||
|
|
ff01df3b16 | ||
|
|
c713dc0ca8 | ||
|
|
31ea5d74a7 | ||
|
|
31b01d42c0 | ||
|
|
482b853908 | ||
|
|
675411df85 | ||
|
|
f94d33fa6a | ||
|
|
7fe110dff1 | ||
|
|
591f711886 | ||
|
|
1dc7fc8ac7 | ||
|
|
ffa79d731c | ||
|
|
01dd0416ac | ||
|
|
0bd1676978 | ||
|
|
f3407959a6 | ||
|
|
30e635a934 | ||
|
|
f6706a55ec | ||
|
|
f0b79f88b2 | ||
|
|
262ba0197d | ||
|
|
31c2447fb8 | ||
|
|
54a4ebfe48 | ||
|
|
106c7a1706 | ||
|
|
e469b46ce5 | ||
|
|
0c79561977 | ||
|
|
9e82397de9 | ||
|
|
7c166e2194 | ||
|
|
0a2b09da40 | ||
|
|
c02d578468 | ||
|
|
e990d233e5 | ||
|
|
a7cdcfcc16 | ||
|
|
75224e1661 | ||
|
|
d236b34fd4 | ||
|
|
c8313409ff | ||
|
|
6cba8e0166 | ||
|
|
0067726bbe | ||
|
|
ce620cec0a | ||
|
|
e797ba4800 | ||
|
|
fe27ee2d79 | ||
|
|
95efeee7de | ||
|
|
da30536709 | ||
|
|
108ccffcd8 | ||
|
|
c3ce0c26e7 | ||
|
|
e0c984046c | ||
|
|
64d6337612 | ||
|
|
603684211f | ||
|
|
499b30d175 | ||
|
|
3f3efab221 | ||
|
|
ef9adc92c8 | ||
|
|
ad7c251f56 | ||
|
|
1c0d254ec6 | ||
|
|
f1ce8c0e8a | ||
|
|
1c558be524 | ||
|
|
7d653cb7bf | ||
|
|
b4cc38a1d9 | ||
|
|
8867317b6a | ||
|
|
4b6bfaffba | ||
|
|
fadae42715 | ||
|
|
77c9affc00 | ||
|
|
e5ba15949b | ||
|
|
a3e59697a1 | ||
|
|
0e2125cda3 | ||
|
|
95da414511 | ||
|
|
c0d5ace640 | ||
|
|
ae48407b20 | ||
|
|
e06bd68379 | ||
|
|
ae6665493a | ||
|
|
c6f37afeff | ||
|
|
59f656e3ca | ||
|
|
12658c4756 | ||
|
|
37c185462a | ||
|
|
53c8485a7a | ||
|
|
846021296f | ||
|
|
5c8651c7cd | ||
|
|
ad91f5af53 | ||
|
|
02a68e7541 | ||
|
|
6e5dbc5285 | ||
|
|
fda9415873 | ||
|
|
4054163447 | ||
|
|
e1f5ef59c8 | ||
|
|
44baeb6924 | ||
|
|
c00e8b1709 | ||
|
|
3fb7ce7100 | ||
|
|
86b15ad693 | ||
|
|
53c00ac631 | ||
|
|
99d0c2d846 | ||
|
|
55ede897b9 | ||
|
|
a71e7d7eb1 | ||
|
|
3c28a4f723 | ||
|
|
6b681df473 | ||
|
|
22c1ee7dc8 | ||
|
|
c24e16e8cd | ||
|
|
d452a5cdce | ||
|
|
198dd52700 | ||
|
|
2dfea0f213 | ||
|
|
5064b5c2b1 | ||
|
|
78b553e55e | ||
|
|
8ca3a511cc | ||
|
|
bf26a3f569 | ||
|
|
ddb8753548 | ||
|
|
e73525d0a2 | ||
|
|
f37d947dd5 | ||
|
|
4279f20daf | ||
|
|
2dd7c3b939 | ||
|
|
cf01480ec8 | ||
|
|
d8ed42ae8e | ||
|
|
297592242c | ||
|
|
1c0a8a49fc | ||
|
|
16298e49c7 | ||
|
|
abb0b95006 | ||
|
|
32f5ea7b63 |
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -42,11 +42,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@master
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Set up Python ${{ matrix.pyver }}
|
||||
uses: actions/setup-python@master
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.pyver }}
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
CFLAGS: -funsigned-char
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@master
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 10
|
||||
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
run: if grep -Inr '\s$' kitty kitty_tests kittens docs *.py *.asciidoc *.rst .gitattributes .gitignore; then echo Trailing whitespace found, aborting.; exit 1; fi
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@master
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
||||
@@ -49,14 +49,15 @@ particular desktop, but it should work for most major desktop environments.
|
||||
.. code-block:: sh
|
||||
|
||||
# Create a symbolic link to add kitty to PATH (assuming ~/.local/bin is in
|
||||
# your PATH)
|
||||
# your system-wide PATH)
|
||||
ln -s ~/.local/kitty.app/bin/kitty ~/.local/bin/
|
||||
# Place the kitty.desktop file somewhere it can be found by the OS
|
||||
cp ~/.local/kitty.app/share/applications/kitty.desktop ~/.local/share/applications/
|
||||
# If you want to open text files and images in kitty via your file manager also add the kitty-open.desktop file
|
||||
cp ~/.local/kitty.app/share/applications/kitty-open.desktop ~/.local/share/applications/
|
||||
# Update the path to the kitty icon in the kitty.desktop file(s)
|
||||
# Update the paths to the kitty and its icon in the kitty.desktop file(s)
|
||||
sed -i "s|Icon=kitty|Icon=/home/$USER/.local/kitty.app/share/icons/hicolor/256x256/apps/kitty.png|g" ~/.local/share/applications/kitty*.desktop
|
||||
sed -i "s|Exec=kitty|Exec=/home/$USER/.local/kitty.app/bin/kitty|g" ~/.local/share/applications/kitty*.desktop
|
||||
|
||||
.. note::
|
||||
If you use the venerable `stow <https://www.gnu.org/software/stow/>`_
|
||||
|
||||
@@ -125,6 +125,19 @@ make them available in the newly spawned shell.
|
||||
Then proceed with ``make`` or ``make app`` according to the platform specific instructions above.
|
||||
|
||||
|
||||
Debug builds
|
||||
--------------
|
||||
|
||||
A basic debug build can be done with::
|
||||
|
||||
make debug
|
||||
|
||||
This includes debug info in the binary for better traces. To build with address sanitizer, use::
|
||||
|
||||
make asan
|
||||
|
||||
Which will result in a debug binary that uses the address sanitizer as well.
|
||||
|
||||
.. _packagers:
|
||||
|
||||
Notes for Linux/macOS packagers
|
||||
@@ -174,29 +187,3 @@ servers into which they ssh, without needing to install all of |kitty|.
|
||||
|
||||
This applies to creating packages for |kitty| for macOS package managers such as
|
||||
Homebrew or MacPorts as well.
|
||||
|
||||
|
||||
Changing defaults for packages
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|kitty| has its defaults chosen to be suitable for a standalone distributable.
|
||||
If you are packaging it a few of these might need to be changed.
|
||||
|
||||
update-checking
|
||||
|kitty| has its own update check mechanism, if you would like to turn
|
||||
it off for your package, use::
|
||||
|
||||
./setup.py linux-package --update-check-interval=0
|
||||
|
||||
shell-integration
|
||||
|kitty| by default injects its :ref:`shell_integration` code into the user's
|
||||
shell using environment variables. For a package, it might make more sense
|
||||
to distribute the shell integration scripts into the system-wide shell
|
||||
vendor locations. The shell integration files are found in the
|
||||
:file:`shell-integration` directory. Copy them to the system wide shell
|
||||
vendor locations for each shell, and use::
|
||||
|
||||
./setup.py linux-package --shell-integration=enabled\ no-rc
|
||||
|
||||
This will prevent kitty from modifying the user's shell environment to load
|
||||
the integration scripts.
|
||||
|
||||
@@ -9,23 +9,14 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
|
||||
Recent major new features
|
||||
---------------------------
|
||||
|
||||
.. only:: dirhtml
|
||||
Truly convenient SSH
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A demo video
|
||||
~~~~~~~~~~~~~~~~
|
||||
The :doc:`ssh kitten <kittens/ssh>` is redesigned with powerful new features:
|
||||
|
||||
A new video showcasing some of kitty's many productivity enhancing features.
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<video controls width="640" height="360" poster="../_static/poster.png">
|
||||
<source src="https://download.calibre-ebook.com/videos/kitty.mp4" type="video/mp4">
|
||||
<source src="https://download.calibre-ebook.com/videos/kitty.webm" type="video/webm">
|
||||
</video>
|
||||
|
||||
.. rst-class:: caption caption-text
|
||||
|
||||
Watch kitty in action!
|
||||
* Automatic :ref:`shell_integration` on remote machines
|
||||
* Easily :ref:`clone local shell/editor config <real_world_ssh_kitten_config>` on remote machines
|
||||
* Automatic :opt:`re-use of existing connections <kitten-ssh.share_connections>` to avoid connection setup latency
|
||||
|
||||
|
||||
Shell integration
|
||||
@@ -38,43 +29,57 @@ scrollback by pressing :sc:`scroll_to_previous_prompt` and clicking with the
|
||||
mouse anywhere in the current command to move the cursor there. See
|
||||
:doc:`shell-integration` for details.
|
||||
|
||||
Logos for windows
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
kitty has the ability to display arbitrary ``PNG`` images at the corner
|
||||
of any kitty window to serve as a *logo* for quick window identification.
|
||||
Can be controlled via :opt:`window_logo_path` and also and changed via
|
||||
the :ref:`at_set-window-logo` remote control command.
|
||||
Useful to quickly identify special windows or just for some *bling*.
|
||||
|
||||
.. figure:: screenshots/window-logo.png
|
||||
:alt: A screenshot of the kitty shell window showing the kitty logo
|
||||
:align: center
|
||||
:width: 100%
|
||||
|
||||
A screenshot of the kitty shell window showing the kitty logo
|
||||
|
||||
|
||||
Visual keyboard based window select
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Select a kitty window visually using the keyboard, like the
|
||||
:doc:`kittens/hints`, but for kitty windows. Can also be used from shell
|
||||
scripts/third party integrations via the :ref:`at_select-window` remote control
|
||||
command.
|
||||
|
||||
.. figure:: screenshots/select-window.png
|
||||
:alt: A screenshot of the kitty select window function
|
||||
:align: center
|
||||
:width: 100%
|
||||
|
||||
Hints for selecting kitty windows visually
|
||||
|
||||
.. }}}
|
||||
|
||||
Detailed list of changes
|
||||
-------------------------------------
|
||||
|
||||
0.25.0 [2022-04-11]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- :doc:`kittens/ssh`: automatic shell integration when using SSH. Easily
|
||||
clone local shell and editor configuration on remote machines, and automatic
|
||||
re-use of existing connections to avoid connection setup latency.
|
||||
|
||||
- When pasting URLs at shell prompts automatically quote them. Also allow filtering pasted text and confirm pastes. See :opt:`paste_actions` for details. (:iss:`4873`)
|
||||
|
||||
- macOS: When using Apple's less as the pager for viewing scrollback strip out OSC codes as it cant parse them (:iss:`4788`)
|
||||
|
||||
- diff kitten: Fix incorrect rendering in rare circumstances when scrolling after changing the context size (:iss:`4831`)
|
||||
|
||||
- icat kitten: Fix a regression that broke :option:`kitty +kitten icat --print-window-size` (:pull:`4818`)
|
||||
|
||||
- Wayland: Fix :opt:`hide_window_decorations` causing docked windows to be resized on blur (:iss:`4797`)
|
||||
|
||||
- Bash integration: Prevent shell integration code from running twice if user enables both automatic and manual integration
|
||||
|
||||
- Bash integration: Handle existing PROMPT_COMMAND ending with a literal newline
|
||||
|
||||
- Fix continued lines not having their continued status reset on line feed (:iss:`4837`)
|
||||
|
||||
- macOS: Allow the New kitty Tab/Window Here services to open multiple selected folders. (:pull:`4848`)
|
||||
|
||||
- Wayland: Fix a regression that broke IME when changing windows/tabs (:iss:`4853`)
|
||||
|
||||
- macOS: Fix Unicode paths not decoded correctly when dropping files (:pull:`4879`)
|
||||
|
||||
- Avoid flicker when starting kittens such as the hints kitten (:iss:`4674`)
|
||||
|
||||
- A new action :ac:`scroll_prompt_to_top` to move the current prompt to the top (:pull:`4891`)
|
||||
|
||||
- :ac:`select_tab`: Use stable numbers when selecting the tab (:iss:`4792`)
|
||||
|
||||
- Only check for updates in the official binary builds. Distro packages or source builds will no longer check for updates, regardless of the
|
||||
value of :opt:`update_check_interval`.
|
||||
|
||||
- Fix :opt:`inactive_text_alpha` still being applied to the cursor hidden window after focus (:iss:`4928`)
|
||||
|
||||
- Fix resizing window that is extra tall/wide because of left-over cells not
|
||||
working reliably (:iss:`4913`)
|
||||
|
||||
- A new action :ac:`close_other_tabs_in_os_window` to close other tabs in the active OS window (:pull:`4944`)
|
||||
|
||||
|
||||
0.24.4 [2022-03-03]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -200,6 +200,13 @@ def commit_role(
|
||||
def write_cli_docs(all_kitten_names: Iterable[str]) -> None:
|
||||
from kitty.cli import option_spec_as_rst
|
||||
from kitty.launch import options_spec as launch_options_spec
|
||||
from kittens.ssh.copy import option_text
|
||||
from kittens.ssh.options.definition import copy_message
|
||||
with open('generated/ssh-copy.rst', 'w') as f:
|
||||
f.write(option_spec_as_rst(
|
||||
appname='copy', ospec=option_text, heading_char='^',
|
||||
usage='file-or-dir-to-copy ...', message=copy_message
|
||||
))
|
||||
with open('generated/launch.rst', 'w') as f:
|
||||
f.write(option_spec_as_rst(
|
||||
appname='launch', ospec=launch_options_spec, heading_char='_',
|
||||
|
||||
@@ -24,15 +24,21 @@ Comments can be added to the config file as lines starting with the ``#``
|
||||
character. This works only if the ``#`` character is the first character
|
||||
in the line.
|
||||
|
||||
.. _include:
|
||||
|
||||
You can include secondary config files via the :code:`include` directive. If
|
||||
you use a relative path for :code:`include`, it is resolved with respect to the
|
||||
location of the current config file. Note that environment variables are
|
||||
expanded, so :code:`${USER}.conf` becomes :file:`name.conf` if
|
||||
:code:`USER=name`. Also, you can use :code:`globinclude` to include files
|
||||
matching a shell glob pattern. For example::
|
||||
matching a shell glob pattern and :code:`envinclude` to include configuration
|
||||
from environment variables. For example::
|
||||
|
||||
include other.conf
|
||||
# Include *.conf files from all subdirs of kitty.d inside the kitty config dir
|
||||
globinclude kitty.d/**/*.conf
|
||||
# Include the *contents* of all env vars starting with KITTY_CONF_
|
||||
envinclude KITTY_CONF_*
|
||||
|
||||
|
||||
.. note:: Syntax highlighting for :file:`kitty.conf` in vim is available via
|
||||
|
||||
31
docs/faq.rst
31
docs/faq.rst
@@ -46,30 +46,21 @@ terminfo files to the server::
|
||||
|
||||
kitty +kitten ssh myserver
|
||||
|
||||
This ssh kitten takes all the same command line arguments
|
||||
as ssh, you can alias it to ssh in your shell's rc files to avoid having to
|
||||
type it each time::
|
||||
This :doc:`ssh kitten <kittens/ssh>` takes all the same command line arguments
|
||||
as ssh, you can alias it to something small in your shell's rc files to avoid
|
||||
having to type it each time::
|
||||
|
||||
alias ssh="kitty +kitten ssh"
|
||||
alias s="kitty +kitten ssh"
|
||||
|
||||
Remember to also setup :ref:`shell_integration` for completion and other
|
||||
niceties.
|
||||
|
||||
If for some reason that does not work (typically because the server is using a
|
||||
non POSIX compliant shell as ``/bin/sh``), you can try using it with ``python``
|
||||
instead::
|
||||
|
||||
kitty +kitten ssh use-python myserver
|
||||
|
||||
If that also fails, perhaps because python is not installed on the remote
|
||||
server, use the following one-liner instead (it
|
||||
If the ssh kitten fails, use the following one-liner instead (it
|
||||
is slower as it needs to ssh into the server twice, but will work with most
|
||||
servers)::
|
||||
|
||||
infocmp -a xterm-kitty | ssh myserver tic -x -o \~/.terminfo /dev/stdin
|
||||
|
||||
If you are behind a proxy (like Balabit) that prevents this, you must redirect the
|
||||
1st command to a file, copy that to the server and run ``tic`` manually. If you
|
||||
If you are behind a proxy (like Balabit) that prevents this, or ``tic`` comes
|
||||
with macOS that does not support reading from STDIN, you must redirect the 1st
|
||||
command to a file, copy that to the server and run ``tic`` manually. If you
|
||||
connect to a server, embedded or Android system that doesn't have ``tic``, copy over
|
||||
your local file terminfo to the other system as :file:`~/.terminfo/x/xterm-kitty`.
|
||||
|
||||
@@ -97,6 +88,12 @@ Make sure the TERM environment variable, is ``xterm-kitty``. And either the
|
||||
TERMINFO environment variable points to a directory containing :file:`x/xterm-kitty`
|
||||
or that file is under :file:`~/.terminfo/x/`.
|
||||
|
||||
For macOS, you may also need to put that file under :file:`~/.terminfo/78/`::
|
||||
|
||||
mkdir -p ~/.terminfo/{78,x}
|
||||
ln -snf ../x/xterm-kitty ~/.terminfo/78/xterm-kitty
|
||||
tic -x -o ~/.terminfo "$KITTY_INSTALLATION_DIR/terminfo/kitty.terminfo"
|
||||
|
||||
Note that ``sudo`` might remove TERMINFO. Then setting it at the shell prompt can
|
||||
be too late, because command line editing may not be reinitialized. In that case
|
||||
you can either ask ``sudo`` to set it or if that is not supported, insert an ``env``
|
||||
|
||||
@@ -57,6 +57,12 @@ Variables that influence kitty behavior
|
||||
Controls where kitty stores cache files. Defaults to :file:`~/.cache/kitty`
|
||||
or :file:`~/Library/Caches/kitty` on macOS.
|
||||
|
||||
.. envvar:: KITTY_RUNTIME_DIRECTORY
|
||||
|
||||
Controls where kitty stores runtime files like sockets. Defaults to
|
||||
the :code:`XDG_RUNTIME_DIR` environment variable if that is defined
|
||||
otherwise the run directory inside the kitty cache directory is used.
|
||||
|
||||
.. envvar:: VISUAL
|
||||
|
||||
The terminal editor (such as ``vi`` or ``nano``) kitty uses, when, for
|
||||
|
||||
@@ -47,6 +47,7 @@ Other terminals that have implemented the graphics protocol:
|
||||
|
||||
* `WezTerm <https://github.com/wez/wezterm/issues/986>`_
|
||||
* `Konsole <https://invent.kde.org/utilities/konsole/-/merge_requests/594>`_
|
||||
* `wayst <https://github.com/91861/wayst>`_
|
||||
|
||||
|
||||
Getting the window size
|
||||
@@ -326,14 +327,14 @@ use the *query action*, set ``a=q``. Then the terminal emulator will try to load
|
||||
the image and respond with either OK or an error, as above, but it will not
|
||||
replace an existing image with the same id, nor will it store the image.
|
||||
|
||||
As of September 2021, kitty and WezTerm are the only terminal emulators to support this
|
||||
graphics protocol. We intend that any terminal emulator that wishes to support
|
||||
it can do so. To check if a terminal emulator supports the graphics protocol the best way
|
||||
is to send the above *query action* followed by a request for the
|
||||
`primary device attributes <https://vt100.net/docs/vt510-rm/DA1.html>`_. If you
|
||||
get back an answer for the device attributes without getting back an answer for
|
||||
the *query action* the terminal emulator does not support the graphics
|
||||
protocol.
|
||||
As of April 2022, kitty and WezTerm are the only terminal emulators to
|
||||
support this graphics protocol completely, with Konsole and wayst having partial support.
|
||||
We intend that any terminal emulator that wishes to support it can do so. To
|
||||
check if a terminal emulator supports the graphics protocol the best way is to
|
||||
send the above *query action* followed by a request for the `primary device
|
||||
attributes <https://vt100.net/docs/vt510-rm/DA1.html>`_. If you get back an
|
||||
answer for the device attributes without getting back an answer for the *query
|
||||
action* the terminal emulator does not support the graphics protocol.
|
||||
|
||||
This means that terminal emulators that support the graphics protocol, **must**
|
||||
reply to *query actions* immediately without processing other input. Most
|
||||
|
||||
@@ -1,261 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from __future__ import (
|
||||
absolute_import, division, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import atexit
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
py3 = sys.version_info[0] > 2
|
||||
is64bit = platform.architecture()[0] == '64bit'
|
||||
is_macos = 'darwin' in sys.platform.lower()
|
||||
is_linux_arm = is_linux_arm64 = False
|
||||
if is_macos:
|
||||
mac_ver = tuple(map(int, platform.mac_ver()[0].split('.')))
|
||||
if mac_ver[:2] < (10, 12):
|
||||
raise SystemExit('Your version of macOS is too old, at least 10.12 is required')
|
||||
else:
|
||||
machine = (os.uname()[4] or '').lower()
|
||||
if machine.startswith('arm') or machine.startswith('aarch64'):
|
||||
is_linux_arm = True
|
||||
is_linux_arm64 = machine.startswith('arm64') or machine.startswith('aarch64')
|
||||
|
||||
try:
|
||||
__file__
|
||||
from_file = True
|
||||
except NameError:
|
||||
from_file = False
|
||||
|
||||
if py3:
|
||||
unicode = str
|
||||
raw_input = input
|
||||
import urllib.request as urllib
|
||||
|
||||
def encode_for_subprocess(x):
|
||||
return x
|
||||
else:
|
||||
from future_builtins import map
|
||||
import urllib2 as urllib
|
||||
|
||||
def encode_for_subprocess(x):
|
||||
if isinstance(x, unicode):
|
||||
x = x.encode('utf-8')
|
||||
return x
|
||||
|
||||
|
||||
def run(*args):
|
||||
if len(args) == 1:
|
||||
args = shlex.split(args[0])
|
||||
args = list(map(encode_for_subprocess, args))
|
||||
ret = subprocess.Popen(args).wait()
|
||||
if ret != 0:
|
||||
raise SystemExit(ret)
|
||||
|
||||
|
||||
class Reporter: # {{{
|
||||
|
||||
def __init__(self, fname):
|
||||
self.fname = fname
|
||||
self.last_percent = 0
|
||||
|
||||
def __call__(self, blocks, block_size, total_size):
|
||||
percent = (blocks*block_size)/float(total_size)
|
||||
report = '\rDownloaded {:.1%} '.format(percent)
|
||||
if percent - self.last_percent > 0.05:
|
||||
self.last_percent = percent
|
||||
print(report, end='')
|
||||
sys.stdout.flush()
|
||||
# }}}
|
||||
|
||||
|
||||
def get_release_data(relname='latest'):
|
||||
print('Checking for latest release on GitHub...')
|
||||
req = urllib.Request(
|
||||
'https://api.github.com/repos/kovidgoyal/kitty/releases/' + relname,
|
||||
headers={'Accept': 'application/vnd.github.v3+json'})
|
||||
try:
|
||||
res = urllib.urlopen(req).read().decode('utf-8')
|
||||
except Exception as err:
|
||||
raise SystemExit('Failed to contact {} with error: {}'.format(req.get_full_url(), err))
|
||||
data = json.loads(res)
|
||||
html_url = data['html_url'].replace('/tag/', '/download/').rstrip('/')
|
||||
for asset in data.get('assets', ()):
|
||||
name = asset['name']
|
||||
if is_macos:
|
||||
if name.endswith('.dmg'):
|
||||
return html_url + '/' + name, asset['size']
|
||||
else:
|
||||
if name.endswith('.txz'):
|
||||
if is64bit:
|
||||
q = '-arm64.txz' if is_linux_arm64 else '-x86_64.txz'
|
||||
if name.endswith(q):
|
||||
return html_url + '/' + name, asset['size']
|
||||
else:
|
||||
if name.endswith('-i686.txz'):
|
||||
return html_url + '/' + name, asset['size']
|
||||
raise SystemExit('Failed to find the installer package on github')
|
||||
|
||||
|
||||
def do_download(url, size, dest):
|
||||
print('Will download and install', os.path.basename(dest))
|
||||
reporter = Reporter(os.path.basename(dest))
|
||||
|
||||
# Get content length and check if range is supported
|
||||
rq = urllib.urlopen(url)
|
||||
headers = rq.info()
|
||||
sent_size = int(headers['content-length'])
|
||||
if sent_size != size:
|
||||
raise SystemExit('Failed to download from {} Content-Length ({}) != {}'.format(url, sent_size, size))
|
||||
with open(dest, 'wb') as f:
|
||||
while f.tell() < size:
|
||||
raw = rq.read(8192)
|
||||
if not raw:
|
||||
break
|
||||
f.write(raw)
|
||||
reporter(f.tell(), 1, size)
|
||||
rq.close()
|
||||
if os.path.getsize(dest) < size:
|
||||
raise SystemExit('Download failed, try again later')
|
||||
print('\rDownloaded {} bytes'.format(os.path.getsize(dest)))
|
||||
|
||||
|
||||
def clean_cache(cache, fname):
|
||||
for x in os.listdir(cache):
|
||||
if fname not in x:
|
||||
os.remove(os.path.join(cache, x))
|
||||
|
||||
|
||||
def download_installer(url, size):
|
||||
fname = url.rpartition('/')[-1]
|
||||
tdir = tempfile.gettempdir()
|
||||
cache = os.path.join(tdir, 'kitty-installer-cache')
|
||||
if not os.path.exists(cache):
|
||||
os.makedirs(cache)
|
||||
clean_cache(cache, fname)
|
||||
dest = os.path.join(cache, fname)
|
||||
if os.path.exists(dest) and os.path.getsize(dest) == size:
|
||||
print('Using previously downloaded', fname)
|
||||
return dest
|
||||
if os.path.exists(dest):
|
||||
os.remove(dest)
|
||||
do_download(url, size, dest)
|
||||
return dest
|
||||
|
||||
|
||||
def macos_install(dmg, dest='/Applications', launch=True):
|
||||
mp = tempfile.mkdtemp()
|
||||
atexit.register(shutil.rmtree, mp)
|
||||
run('hdiutil', 'attach', dmg, '-mountpoint', mp)
|
||||
try:
|
||||
os.chdir(mp)
|
||||
app = 'kitty.app'
|
||||
d = os.path.join(dest, app)
|
||||
if os.path.exists(d):
|
||||
shutil.rmtree(d)
|
||||
dest = os.path.join(dest, app)
|
||||
run('ditto', '-v', app, dest)
|
||||
print('Successfully installed kitty into', dest)
|
||||
if launch:
|
||||
run('open', dest)
|
||||
finally:
|
||||
os.chdir('/')
|
||||
run('hdiutil', 'detach', mp)
|
||||
|
||||
|
||||
def linux_install(installer, dest=os.path.expanduser('~/.local'), launch=True):
|
||||
dest = os.path.join(dest, 'kitty.app')
|
||||
if os.path.exists(dest):
|
||||
shutil.rmtree(dest)
|
||||
os.makedirs(dest)
|
||||
print('Extracting tarball...')
|
||||
run('tar', '-C', dest, '-xJof', installer)
|
||||
print('kitty successfully installed to', dest)
|
||||
kitty = os.path.join(dest, 'bin', 'kitty')
|
||||
print('Use', kitty, 'to run kitty')
|
||||
if launch:
|
||||
run(kitty, '--detach')
|
||||
|
||||
|
||||
def main(dest=None, launch=True, installer=None):
|
||||
if not dest:
|
||||
if is_macos:
|
||||
dest = '/Applications'
|
||||
else:
|
||||
dest = os.path.expanduser('~/.local')
|
||||
if is_linux_arm and not is_linux_arm64:
|
||||
raise SystemExit(
|
||||
'You are running on a 32-bit ARM system. The kitty binaries are only'
|
||||
' available for 64 bit ARM systems. You will have to build from'
|
||||
' source.')
|
||||
if not installer:
|
||||
url, size = get_release_data()
|
||||
installer = download_installer(url, size)
|
||||
else:
|
||||
if installer == 'nightly':
|
||||
url, size = get_release_data('tags/nightly')
|
||||
installer = download_installer(url, size)
|
||||
else:
|
||||
installer = os.path.abspath(installer)
|
||||
if not os.access(installer, os.R_OK):
|
||||
raise SystemExit('Could not read from: {}'.format(installer))
|
||||
if is_macos:
|
||||
macos_install(installer, dest=dest, launch=launch)
|
||||
else:
|
||||
linux_install(installer, dest=dest, launch=launch)
|
||||
|
||||
|
||||
def script_launch():
|
||||
# To test: python3 -c "import runpy; runpy.run_path('installer.py', run_name='script_launch')"
|
||||
def path(x):
|
||||
return os.path.expandvars(os.path.expanduser(x))
|
||||
|
||||
def to_bool(x):
|
||||
return x.lower() in {'y', 'yes', '1', 'true'}
|
||||
|
||||
type_map = {x: path for x in 'dest installer'.split()}
|
||||
type_map['launch'] = to_bool
|
||||
kwargs = {}
|
||||
|
||||
for arg in sys.argv[1:]:
|
||||
if arg:
|
||||
m = re.match('([a-z_]+)=(.+)', arg)
|
||||
if m is None:
|
||||
raise SystemExit('Unrecognized command line argument: ' + arg)
|
||||
k = m.group(1)
|
||||
if k not in type_map:
|
||||
raise SystemExit('Unrecognized command line argument: ' + arg)
|
||||
kwargs[k] = type_map[k](m.group(2))
|
||||
main(**kwargs)
|
||||
|
||||
|
||||
def update_installer_wrapper():
|
||||
# To run: python3 -c "import runpy; runpy.run_path('installer.py', run_name='update_wrapper')" installer.sh
|
||||
with open(__file__, 'rb') as f:
|
||||
src = f.read().decode('utf-8')
|
||||
wrapper = sys.argv[-1]
|
||||
with open(wrapper, 'r+b') as f:
|
||||
raw = f.read().decode('utf-8')
|
||||
nraw = re.sub(r'^# HEREDOC_START.+^# HEREDOC_END', lambda m: '# HEREDOC_START\n{}\n# HEREDOC_END'.format(src), raw, flags=re.MULTILINE | re.DOTALL)
|
||||
if 'update_intaller_wrapper()' not in nraw:
|
||||
raise SystemExit('regex substitute of HEREDOC failed')
|
||||
f.seek(0), f.truncate()
|
||||
f.write(nraw.encode('utf-8'))
|
||||
|
||||
|
||||
if __name__ == '__main__' and from_file:
|
||||
main()
|
||||
elif __name__ == 'update_wrapper':
|
||||
update_installer_wrapper()
|
||||
elif __name__ == 'script_launch':
|
||||
script_launch()
|
||||
@@ -1,292 +1,174 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# installer.sh
|
||||
# Copyright (C) 2018 Kovid Goyal <kovid at kovidgoyal.net>
|
||||
#
|
||||
# Distributed under terms of the GPLv3 license.
|
||||
#
|
||||
|
||||
python=$(command -v python3)
|
||||
if [ -z "$python" ]; then
|
||||
python=$(command -v python2)
|
||||
fi
|
||||
if [ -z "$python" ]; then
|
||||
python=$(command -v python2.7)
|
||||
fi
|
||||
if [ -z "$python" ]; then
|
||||
python=$(command -v python)
|
||||
fi
|
||||
if [ -z "$python" ]; then
|
||||
python=python
|
||||
fi
|
||||
{ \unalias command; \unset -f command; } >/dev/null 2>&1
|
||||
tdir=''
|
||||
cleanup() {
|
||||
[ -n "$tdir" ] && {
|
||||
command rm -rf "$tdir"
|
||||
tdir=''
|
||||
}
|
||||
}
|
||||
|
||||
echo Using python executable: $python
|
||||
die() {
|
||||
cleanup
|
||||
printf "\033[31m%s\033[m\n\r" "$*" > /dev/stderr;
|
||||
exit 1;
|
||||
}
|
||||
|
||||
$python -c "import sys; script_launch=lambda:sys.exit('Download of installer failed!'); exec(sys.stdin.read()); script_launch()" "$@" <<'INSTALLER_HEREDOC'
|
||||
# {{{
|
||||
# HEREDOC_START
|
||||
#!/usr/bin/env python3
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from __future__ import (
|
||||
absolute_import, division, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import atexit
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
py3 = sys.version_info[0] > 2
|
||||
is64bit = platform.architecture()[0] == '64bit'
|
||||
is_macos = 'darwin' in sys.platform.lower()
|
||||
is_linux_arm = is_linux_arm64 = False
|
||||
if is_macos:
|
||||
mac_ver = tuple(map(int, platform.mac_ver()[0].split('.')))
|
||||
if mac_ver[:2] < (10, 12):
|
||||
raise SystemExit('Your version of macOS is too old, at least 10.12 is required')
|
||||
else:
|
||||
machine = (os.uname()[4] or '').lower()
|
||||
if machine.startswith('arm') or machine.startswith('aarch64'):
|
||||
is_linux_arm = True
|
||||
is_linux_arm64 = machine.startswith('arm64') or machine.startswith('aarch64')
|
||||
|
||||
try:
|
||||
__file__
|
||||
from_file = True
|
||||
except NameError:
|
||||
from_file = False
|
||||
|
||||
if py3:
|
||||
unicode = str
|
||||
raw_input = input
|
||||
import urllib.request as urllib
|
||||
|
||||
def encode_for_subprocess(x):
|
||||
return x
|
||||
else:
|
||||
from future_builtins import map
|
||||
import urllib2 as urllib
|
||||
|
||||
def encode_for_subprocess(x):
|
||||
if isinstance(x, unicode):
|
||||
x = x.encode('utf-8')
|
||||
return x
|
||||
detect_network_tool() {
|
||||
if command -v curl 2> /dev/null > /dev/null; then
|
||||
fetch() {
|
||||
command curl -fL "$1"
|
||||
}
|
||||
fetch_quiet() {
|
||||
command curl -fsSL "$1"
|
||||
}
|
||||
elif command -v wget 2> /dev/null > /dev/null; then
|
||||
fetch() {
|
||||
command wget -O- "$1"
|
||||
}
|
||||
fetch_quiet() {
|
||||
command wget --quiet -O- "$1"
|
||||
}
|
||||
else
|
||||
die "Neither curl nor wget available, cannot download kitty"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
def run(*args):
|
||||
if len(args) == 1:
|
||||
args = shlex.split(args[0])
|
||||
args = list(map(encode_for_subprocess, args))
|
||||
ret = subprocess.Popen(args).wait()
|
||||
if ret != 0:
|
||||
raise SystemExit(ret)
|
||||
detect_os() {
|
||||
arch=""
|
||||
case "$(command uname)" in
|
||||
'Darwin') OS="macos";;
|
||||
'Linux')
|
||||
OS="linux"
|
||||
case "$(command uname -m)" in
|
||||
x86_64) arch="x86_64";;
|
||||
aarch64*) arch="arm64";;
|
||||
armv8*) arch="arm64";;
|
||||
i386) arch="i686";;
|
||||
i686) arch="i686";;
|
||||
*) die "Unknown CPU architecture $(command uname -m)";;
|
||||
esac
|
||||
;;
|
||||
*) die "kitty binaries are not available for $(command uname)"
|
||||
esac
|
||||
}
|
||||
|
||||
expand_tilde() {
|
||||
tilde_less="${1#\~/}"
|
||||
[ "$1" != "$tilde_less" ] && tilde_less="$HOME/$tilde_less"
|
||||
printf '%s' "$tilde_less"
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
dest='~/.local'
|
||||
[ "$OS" = "macos" ] && dest="/Applications"
|
||||
launch='y'
|
||||
installer=''
|
||||
while :; do
|
||||
case "$1" in
|
||||
dest=*) dest="${1#*=}";;
|
||||
launch=*) launch="${1#*=}";;
|
||||
installer=*) installer="${1#*=}";;
|
||||
"") break;;
|
||||
*) die "Unrecognized command line option: $1";;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
dest=$(expand_tilde "${dest}")
|
||||
[ "$launch" != "y" -a "$launch" != "n" ] && die "Unrecognized command line option: launch=$launch"
|
||||
dest="$dest/kitty.app"
|
||||
}
|
||||
|
||||
|
||||
class Reporter: # {{{
|
||||
get_file_url() {
|
||||
url="https://github.com/kovidgoyal/kitty/releases/download/$1/kitty-$2"
|
||||
if [ "$OS" = "macos" ]; then
|
||||
url="$url.dmg"
|
||||
else
|
||||
url="$url-$arch.txz"
|
||||
fi
|
||||
}
|
||||
|
||||
def __init__(self, fname):
|
||||
self.fname = fname
|
||||
self.last_percent = 0
|
||||
|
||||
def __call__(self, blocks, block_size, total_size):
|
||||
percent = (blocks*block_size)/float(total_size)
|
||||
report = '\rDownloaded {:.1%} '.format(percent)
|
||||
if percent - self.last_percent > 0.05:
|
||||
self.last_percent = percent
|
||||
print(report, end='')
|
||||
sys.stdout.flush()
|
||||
# }}}
|
||||
get_release_url() {
|
||||
release_version=$(fetch_quiet "https://sw.kovidgoyal.net/kitty/current-version.txt")
|
||||
[ $? -ne 0 -o -z "$release_version" ] && die "Could not get kitty latest release version"
|
||||
get_file_url "v$release_version" "$release_version"
|
||||
}
|
||||
|
||||
|
||||
def get_release_data(relname='latest'):
|
||||
print('Checking for latest release on GitHub...')
|
||||
req = urllib.Request(
|
||||
'https://api.github.com/repos/kovidgoyal/kitty/releases/' + relname,
|
||||
headers={'Accept': 'application/vnd.github.v3+json'})
|
||||
try:
|
||||
res = urllib.urlopen(req).read().decode('utf-8')
|
||||
except Exception as err:
|
||||
raise SystemExit('Failed to contact {} with error: {}'.format(req.get_full_url(), err))
|
||||
data = json.loads(res)
|
||||
html_url = data['html_url'].replace('/tag/', '/download/').rstrip('/')
|
||||
for asset in data.get('assets', ()):
|
||||
name = asset['name']
|
||||
if is_macos:
|
||||
if name.endswith('.dmg'):
|
||||
return html_url + '/' + name, asset['size']
|
||||
else:
|
||||
if name.endswith('.txz'):
|
||||
if is64bit:
|
||||
q = '-arm64.txz' if is_linux_arm64 else '-x86_64.txz'
|
||||
if name.endswith(q):
|
||||
return html_url + '/' + name, asset['size']
|
||||
else:
|
||||
if name.endswith('-i686.txz'):
|
||||
return html_url + '/' + name, asset['size']
|
||||
raise SystemExit('Failed to find the installer package on github')
|
||||
get_nightly_url() {
|
||||
get_file_url "nightly" "nightly"
|
||||
}
|
||||
|
||||
get_download_url() {
|
||||
installer_is_file="n"
|
||||
case "$installer" in
|
||||
"nightly") get_nightly_url ;;
|
||||
"") get_release_url ;;
|
||||
*) installer_is_file="y" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
def do_download(url, size, dest):
|
||||
print('Will download and install', os.path.basename(dest))
|
||||
reporter = Reporter(os.path.basename(dest))
|
||||
linux_install() {
|
||||
if [ "$installer_is_file" = "y" ]; then
|
||||
command tar -C "$dest" "-xJof" "$installer"
|
||||
else
|
||||
printf '%s\n\n' "Downloading from: $url"
|
||||
fetch "$url" | command tar -C "$dest" "-xJof" "-"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get content length and check if range is supported
|
||||
rq = urllib.urlopen(url)
|
||||
headers = rq.info()
|
||||
sent_size = int(headers['content-length'])
|
||||
if sent_size != size:
|
||||
raise SystemExit('Failed to download from {} Content-Length ({}) != {}'.format(url, sent_size, size))
|
||||
with open(dest, 'wb') as f:
|
||||
while f.tell() < size:
|
||||
raw = rq.read(8192)
|
||||
if not raw:
|
||||
break
|
||||
f.write(raw)
|
||||
reporter(f.tell(), 1, size)
|
||||
rq.close()
|
||||
if os.path.getsize(dest) < size:
|
||||
raise SystemExit('Download failed, try again later')
|
||||
print('\rDownloaded {} bytes'.format(os.path.getsize(dest)))
|
||||
macos_install() {
|
||||
tdir=$(command mktemp -d "/tmp/kitty-install-XXXXXXXXXXXX")
|
||||
[ "$installer_is_file" != "y" ] && {
|
||||
installer="$tdir/kitty.dmg"
|
||||
printf '%s\n\n' "Downloading from: $url"
|
||||
fetch "$url" > "$installer" || die "Failed to download: $url"
|
||||
}
|
||||
command mkdir "$tdir/mp"
|
||||
command hdiutil attach "$installer" "-mountpoint" "$tdir/mp" || die "Failed to mount kitty.dmg"
|
||||
command ditto -v "$tdir/mp/kitty.app" "$dest"
|
||||
rc="$?"
|
||||
command hdiutil detach "$tdir/mp"
|
||||
command rm -rf "$tdir"
|
||||
tdir=''
|
||||
[ "$rc" != "0" ] && die "Failed to copy kitty.app from mounted dmg"
|
||||
}
|
||||
|
||||
prepare_install_dest() {
|
||||
printf "%s\n" "Installing to $dest"
|
||||
command rm -rf "$dest"
|
||||
command mkdir -p "$dest" || die "Failed to create the directory: $dest"
|
||||
}
|
||||
|
||||
def clean_cache(cache, fname):
|
||||
for x in os.listdir(cache):
|
||||
if fname not in x:
|
||||
os.remove(os.path.join(cache, x))
|
||||
exec_kitty() {
|
||||
if [ "$OS" = "macos" ]; then
|
||||
exec "open" "$dest"
|
||||
else
|
||||
exec "$dest/bin/kitty" "--detach"
|
||||
fi
|
||||
die "Failed to launch kitty"
|
||||
}
|
||||
|
||||
main() {
|
||||
[ -n "$KITTY_WINDOW_ID" ] && die "You should not try to update kitty from within kitty itself. For best results, quit all kitty instances and run this script from another terminal"
|
||||
detect_os
|
||||
parse_args "$@"
|
||||
detect_network_tool
|
||||
get_download_url
|
||||
prepare_install_dest
|
||||
if [ "$OS" = "macos" ]; then
|
||||
macos_install
|
||||
else
|
||||
linux_install
|
||||
fi
|
||||
[ "$launch" = "y" ] && exec_kitty
|
||||
exit 0
|
||||
}
|
||||
|
||||
def download_installer(url, size):
|
||||
fname = url.rpartition('/')[-1]
|
||||
tdir = tempfile.gettempdir()
|
||||
cache = os.path.join(tdir, 'kitty-installer-cache')
|
||||
if not os.path.exists(cache):
|
||||
os.makedirs(cache)
|
||||
clean_cache(cache, fname)
|
||||
dest = os.path.join(cache, fname)
|
||||
if os.path.exists(dest) and os.path.getsize(dest) == size:
|
||||
print('Using previously downloaded', fname)
|
||||
return dest
|
||||
if os.path.exists(dest):
|
||||
os.remove(dest)
|
||||
do_download(url, size, dest)
|
||||
return dest
|
||||
|
||||
|
||||
def macos_install(dmg, dest='/Applications', launch=True):
|
||||
mp = tempfile.mkdtemp()
|
||||
atexit.register(shutil.rmtree, mp)
|
||||
run('hdiutil', 'attach', dmg, '-mountpoint', mp)
|
||||
try:
|
||||
os.chdir(mp)
|
||||
app = 'kitty.app'
|
||||
d = os.path.join(dest, app)
|
||||
if os.path.exists(d):
|
||||
shutil.rmtree(d)
|
||||
dest = os.path.join(dest, app)
|
||||
run('ditto', '-v', app, dest)
|
||||
print('Successfully installed kitty into', dest)
|
||||
if launch:
|
||||
run('open', dest)
|
||||
finally:
|
||||
os.chdir('/')
|
||||
run('hdiutil', 'detach', mp)
|
||||
|
||||
|
||||
def linux_install(installer, dest=os.path.expanduser('~/.local'), launch=True):
|
||||
dest = os.path.join(dest, 'kitty.app')
|
||||
if os.path.exists(dest):
|
||||
shutil.rmtree(dest)
|
||||
os.makedirs(dest)
|
||||
print('Extracting tarball...')
|
||||
run('tar', '-C', dest, '-xJof', installer)
|
||||
print('kitty successfully installed to', dest)
|
||||
kitty = os.path.join(dest, 'bin', 'kitty')
|
||||
print('Use', kitty, 'to run kitty')
|
||||
if launch:
|
||||
run(kitty, '--detach')
|
||||
|
||||
|
||||
def main(dest=None, launch=True, installer=None):
|
||||
if not dest:
|
||||
if is_macos:
|
||||
dest = '/Applications'
|
||||
else:
|
||||
dest = os.path.expanduser('~/.local')
|
||||
if is_linux_arm and not is_linux_arm64:
|
||||
raise SystemExit(
|
||||
'You are running on a 32-bit ARM system. The kitty binaries are only'
|
||||
' available for 64 bit ARM systems. You will have to build from'
|
||||
' source.')
|
||||
if not installer:
|
||||
url, size = get_release_data()
|
||||
installer = download_installer(url, size)
|
||||
else:
|
||||
if installer == 'nightly':
|
||||
url, size = get_release_data('tags/nightly')
|
||||
installer = download_installer(url, size)
|
||||
else:
|
||||
installer = os.path.abspath(installer)
|
||||
if not os.access(installer, os.R_OK):
|
||||
raise SystemExit('Could not read from: {}'.format(installer))
|
||||
if is_macos:
|
||||
macos_install(installer, dest=dest, launch=launch)
|
||||
else:
|
||||
linux_install(installer, dest=dest, launch=launch)
|
||||
|
||||
|
||||
def script_launch():
|
||||
# To test: python3 -c "import runpy; runpy.run_path('installer.py', run_name='script_launch')"
|
||||
def path(x):
|
||||
return os.path.expandvars(os.path.expanduser(x))
|
||||
|
||||
def to_bool(x):
|
||||
return x.lower() in {'y', 'yes', '1', 'true'}
|
||||
|
||||
type_map = {x: path for x in 'dest installer'.split()}
|
||||
type_map['launch'] = to_bool
|
||||
kwargs = {}
|
||||
|
||||
for arg in sys.argv[1:]:
|
||||
if arg:
|
||||
m = re.match('([a-z_]+)=(.+)', arg)
|
||||
if m is None:
|
||||
raise SystemExit('Unrecognized command line argument: ' + arg)
|
||||
k = m.group(1)
|
||||
if k not in type_map:
|
||||
raise SystemExit('Unrecognized command line argument: ' + arg)
|
||||
kwargs[k] = type_map[k](m.group(2))
|
||||
main(**kwargs)
|
||||
|
||||
|
||||
def update_installer_wrapper():
|
||||
# To run: python3 -c "import runpy; runpy.run_path('installer.py', run_name='update_wrapper')" installer.sh
|
||||
with open(__file__, 'rb') as f:
|
||||
src = f.read().decode('utf-8')
|
||||
wrapper = sys.argv[-1]
|
||||
with open(wrapper, 'r+b') as f:
|
||||
raw = f.read().decode('utf-8')
|
||||
nraw = re.sub(r'^# HEREDOC_START.+^# HEREDOC_END', lambda m: '# HEREDOC_START\n{}\n# HEREDOC_END'.format(src), raw, flags=re.MULTILINE | re.DOTALL)
|
||||
if 'update_intaller_wrapper()' not in nraw:
|
||||
raise SystemExit('regex substitute of HEREDOC failed')
|
||||
f.seek(0), f.truncate()
|
||||
f.write(nraw.encode('utf-8'))
|
||||
|
||||
|
||||
if __name__ == '__main__' and from_file:
|
||||
main()
|
||||
elif __name__ == 'update_wrapper':
|
||||
update_installer_wrapper()
|
||||
elif __name__ == 'script_launch':
|
||||
script_launch()
|
||||
|
||||
# HEREDOC_END
|
||||
# }}}
|
||||
INSTALLER_HEREDOC
|
||||
main "$@"
|
||||
|
||||
@@ -38,6 +38,7 @@ In addition to kitty, this protocol is also implemented in:
|
||||
* The `notcurses library
|
||||
<https://github.com/dankamongmen/notcurses/issues/2131>`__
|
||||
* The `kakoune text editor <https://github.com/mawww/kakoune/issues/4103>`__
|
||||
* The `dte text editor <https://gitlab.com/craigbarnes/dte/-/issues/138>`__
|
||||
|
||||
.. versionadded:: 0.20.0
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ your machine).
|
||||
# get the kitty window into which to paste answer
|
||||
w = boss.window_id_map.get(target_window_id)
|
||||
if w is not None:
|
||||
w.paste(answer)
|
||||
w.paste_text(answer)
|
||||
|
||||
|
||||
Now in :file:`kitty.conf` add the lines::
|
||||
|
||||
154
docs/kittens/ssh.rst
Normal file
154
docs/kittens/ssh.rst
Normal file
@@ -0,0 +1,154 @@
|
||||
Truly convenient SSH
|
||||
=========================================
|
||||
|
||||
* Automatic :ref:`shell_integration` on remote hosts
|
||||
|
||||
* Easily :ref:`clone local shell/editor config <real_world_ssh_kitten_config>` on remote hosts
|
||||
|
||||
* Automatic :opt:`re-use of existing connections <kitten-ssh.share_connections>` to avoid connection setup latency
|
||||
|
||||
* Make kitty itself available in the remote host :opt:`on demand <kitten-ssh.remote_kitty>`
|
||||
|
||||
* Easily :opt:`change terminal colors <kitten-ssh.color_scheme>` when connecting to remote hosts
|
||||
|
||||
.. versionadded:: 0.25.0
|
||||
Automatic shell integration, file transfer and reuse of connections
|
||||
|
||||
The ssh kitten allows you to login easily to remote hosts, and automatically
|
||||
setup the environment there to be as comfortable as your local shell. You
|
||||
can specify environment variables to set on the remote host and
|
||||
files to copy there, making your remote experience just like your
|
||||
local shell. Additionally, it automatically sets up :ref:`shell_integration` on
|
||||
the remote host and copies the kitty terminfo database there.
|
||||
|
||||
The ssh kitten is a thin wrapper around the traditional `ssh <https://man.openbsd.org/ssh>`__
|
||||
command line program and supports all the same options and arguments and configuration.
|
||||
In interactive usage scenarios it is a drop in replacement for ``ssh``. To try it
|
||||
out, simply run:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
kitty +kitten ssh some-hostname-to-connect-to
|
||||
|
||||
You should end up at a shell prompt on the remote host, with shell
|
||||
integration enabled. If you like it you can add an alias to it in your shell's
|
||||
rc files:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
alias s=kitty +kitten ssh
|
||||
|
||||
So now you can just type ``s hostname`` to connect.
|
||||
|
||||
If you define a mapping in :file:`kitty.conf` such as::
|
||||
|
||||
map f1 new_window_with_cwd
|
||||
|
||||
Then, pressing :kbd:`F1` will open a new window automatically logged
|
||||
into the same host using the ssh kitten, at the same directory.
|
||||
|
||||
The ssh kitten can be configured using the :file:`~/.config/kitty/ssh.conf`
|
||||
file where you can specify environment variables to set on the remote host
|
||||
and files to copy from the local to the remote host. Let's see a
|
||||
quick example:
|
||||
|
||||
.. code-block:: conf
|
||||
|
||||
# Copy the files and directories needed to setup some common tools
|
||||
copy .zshrc .vimrc .vim
|
||||
# Setup some environment variables
|
||||
env SOME_VAR=x
|
||||
# COPIED_VAR will have the same value on the remote host as it does locally
|
||||
env COPIED_VAR=_kitty_copy_env_var_
|
||||
|
||||
# Create some per hostname settings
|
||||
hostname someserver-*
|
||||
copy env-files
|
||||
env SOMETHING=else
|
||||
|
||||
hostname someuser@somehost
|
||||
copy --dest=foo/bar some-file
|
||||
copy --glob some/files.*
|
||||
|
||||
|
||||
See below for full details on the syntax and options of :file:`ssh.conf`.
|
||||
Additionally, you can pass config options on the command line:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
kitty +kitten ssh --kitten interpreter=python servername
|
||||
|
||||
The :code:`--kitten` argument can be specified multiple times, with directives
|
||||
from :file:`ssh.conf`. These are merged with :file:`ssh.conf` as if they were
|
||||
appended to the end of that file. They apply only to the host being SSHed to
|
||||
by this invocation, so any :opt:`hostname <kitten-ssh.hostname>` directives are ignored.
|
||||
|
||||
.. warning::
|
||||
|
||||
Due to limitations in the design of SSH, any typing you do before the
|
||||
shell prompt appears may be lost. So ideally don't start typing till you see
|
||||
the shell prompt. 😇
|
||||
|
||||
|
||||
.. _real_world_ssh_kitten_config:
|
||||
|
||||
A real world example
|
||||
----------------------
|
||||
|
||||
Suppose you often SSH into a production server, and you would like to setup
|
||||
your shell and editor there using your custom settings. However, other people
|
||||
could SSH in as well and you don't want to clobber their settings. Here is how
|
||||
this could be achieved using the ssh kitten with zsh and vim as the shell and
|
||||
editor, respectively:
|
||||
|
||||
.. code-block:: conf
|
||||
|
||||
# Have these settings apply to servers in my organization
|
||||
hostname myserver-*
|
||||
|
||||
# Setup zsh to read its files from my-conf/zsh
|
||||
env ZDOTDIR $HOME/my-conf/zsh
|
||||
copy --dest my-conf/zsh/.zshrc .zshrc
|
||||
copy --dest my-conf/zsh/.zshenv .zshenv
|
||||
# If you use other zsh init files add them in a similar manner
|
||||
|
||||
# Setup vim to read its config from my-conf/vim
|
||||
env VIMINIT $HOME/my-conf/vim/vimrc
|
||||
env VIMRUNTIME $HOME/my-conf/vim
|
||||
copy --dest my-conf/vim .vim
|
||||
copy --dest my-conf/vim/vimrc .vimrc
|
||||
|
||||
|
||||
How it works
|
||||
----------------
|
||||
|
||||
The ssh kitten works by having SSH transmit and execute a POSIX sh (or
|
||||
:opt:`optionally <kitten-ssh.interpreter>` Python) bootstrap script on the
|
||||
remote host using an :opt:`interpreter <kitten-ssh.interpreter>`. This script
|
||||
reads setup data over the tty device, which kitty sends as a base64 encoded
|
||||
compressed tarball. The script extracts it and places the :opt:`files <kitten-ssh.copy>`
|
||||
and sets the :opt:`environment variables <kitten-ssh.env>` before finally
|
||||
launching the :opt:`login shell <kitten-ssh.login_shell>` with :opt:`shell
|
||||
integration <kitten-ssh.shell_integration>` enabled. The data is requested by
|
||||
the kitten over the TTY with a random one time password. kitty reads the request
|
||||
and if the password matches a password pre-stored in shared memory on the
|
||||
localhost by the kitten, the transmission is allowed. If your OpenSSH version is
|
||||
>= 8.4 then the data is transmitted instantly without any roundtrip delay.
|
||||
|
||||
.. note::
|
||||
|
||||
When connecting to BSD hosts, it is possible the bootstrap script will
|
||||
fail or run slowly, because the default shells are crippled in various ways.
|
||||
Your best bet is to install Python on the remote, make sure the login shell
|
||||
is something POSIX sh compliant, and use :code:`python` as the :opt:`interpreter
|
||||
<kitten-ssh.interpreter>` in :file:`ssh.conf`.
|
||||
|
||||
.. include:: /generated/conf-kitten-ssh.rst
|
||||
|
||||
|
||||
.. _ssh_copy_command:
|
||||
|
||||
The copy command
|
||||
--------------------
|
||||
|
||||
.. include:: /generated/ssh-copy.rst
|
||||
@@ -15,6 +15,7 @@ Extend with kittens
|
||||
kittens/remote_file
|
||||
kittens/hyperlinked_grep
|
||||
kittens/transfer
|
||||
kittens/ssh
|
||||
kittens/custom
|
||||
kittens/*
|
||||
|
||||
@@ -62,6 +63,11 @@ Some prominent kittens:
|
||||
other :term:`kitty windows <window>`.
|
||||
|
||||
|
||||
:doc:`SSH <kittens/ssh>`
|
||||
SSH with automatic shell integration, connection re-use for low latency and easy cloning
|
||||
of local shell and editor configuration to the remote machine.
|
||||
|
||||
|
||||
:doc:`Panel <kittens/panel>`
|
||||
Draw a GPU accelerated dock panel on your desktop showing the output
|
||||
from an arbitrary terminal program.
|
||||
|
||||
@@ -67,6 +67,14 @@ no-title
|
||||
Note that for the ``fish`` shell kitty relies on fish's native title
|
||||
setting functionality instead.
|
||||
|
||||
no-cwd
|
||||
Turn off reporting the current working directory. This is used to allow
|
||||
:ac:`new_window_with_cwd` and similar to open windows logged
|
||||
into remote machines using the :doc:`ssh kitten <kittens/ssh>`
|
||||
automatically with the same working directory as the current window.
|
||||
Note that for the ``fish`` shell this will not disable its built-in current
|
||||
working directory reporting.
|
||||
|
||||
no-prompt-mark
|
||||
Turn off marking of prompts. This disables jumping to prompt, browsing
|
||||
output of last command and click to move cursor functionality.
|
||||
@@ -149,7 +157,7 @@ different shells.
|
||||
.. tab:: bash
|
||||
|
||||
For bash, kitty starts bash in POSIX mode, using the environment variable
|
||||
``ENV`` to load the shell integrtion script. This prevents bash from
|
||||
``ENV`` to load the shell integration script. This prevents bash from
|
||||
loading any startup files itself. The loading of the startup files is done
|
||||
by the integration script, after disabling POSIX mode. From the
|
||||
perspective of those scripts there should be no difference to running
|
||||
@@ -160,8 +168,7 @@ Then, when launching the shell, kitty sets the environment variable
|
||||
:envvar:`KITTY_SHELL_INTEGRATION` to the value of the :opt:`shell_integration`
|
||||
option. The shell integration code reads the environment variable, turns on the
|
||||
specified integration functionality and then unsets the variable so as to not
|
||||
pollute the system. This has the nice effect that the changes to the shell's rc
|
||||
files become no-ops when running the shell in anything other than kitty itself.
|
||||
pollute the system.
|
||||
|
||||
The actual shell integration code uses hooks provided by each shell to send
|
||||
special escape codes to kitty, to perform the various tasks. You can see the
|
||||
@@ -194,13 +201,27 @@ code used for each shell below:
|
||||
</details>
|
||||
|
||||
|
||||
Shell integration over SSH
|
||||
----------------------------
|
||||
|
||||
The easiest way to have shell integration work when SSHing into remote systems
|
||||
is to use the :doc:`ssh kitten <kittens/ssh>`. Simply run::
|
||||
|
||||
kitty +kitten ssh hostname
|
||||
|
||||
And, by magic, you will be logged into the remote system with fully functional
|
||||
shell integration. Alternately, you can :ref:`setup shell integration manually
|
||||
<manual_shell_integration>`, by copying the kitty shell integration scripts to
|
||||
the remote server and editing the shell rc files there, as described below.
|
||||
|
||||
|
||||
.. _manual_shell_integration:
|
||||
|
||||
Manual shell integration
|
||||
----------------------------
|
||||
|
||||
The automatic shell integration is designed to be minimally intrusive, as such
|
||||
it wont work for sub-shells, terminal multiplexers, containers, remote systems, etc.
|
||||
it wont work for sub-shells, terminal multiplexers, containers, etc.
|
||||
For such systems, you should setup manual shell integration by adding some code
|
||||
to your shells startup files to load the shell integration script.
|
||||
|
||||
@@ -248,11 +269,9 @@ The value of :envvar:`KITTY_SHELL_INTEGRATION` is the same as that for
|
||||
completely, in which case simply do not set the
|
||||
:envvar:`KITTY_SHELL_INTEGRATION` variable at all.
|
||||
|
||||
If you want this to work while SSHing into a remote system, then you will
|
||||
need to add some code to the snippets above to check if :envvar:`KITTY_INSTALLATION_DIR`
|
||||
is empty and if so to set it to some hard coded location with the shell
|
||||
integration scripts that need to be copied onto the remote system.
|
||||
|
||||
In a container, you will need to install the kitty shell integration scripts
|
||||
and make sure the :envvar:`KITTY_INSTALLATION_DIR` environment variable is set
|
||||
to point to the location of the scripts.
|
||||
|
||||
Integration with other shells
|
||||
-------------------------------
|
||||
|
||||
@@ -38,6 +38,8 @@ def main() -> None:
|
||||
|
||||
from kittens.diff.options.definition import definition as kd
|
||||
write_output('kittens.diff', kd)
|
||||
from kittens.ssh.options.definition import definition as sd
|
||||
write_output('kittens.ssh', sd)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -1377,7 +1377,7 @@ is_ascii_control_char(char x) {
|
||||
if ([obj isKindOfClass:[NSURL class]]) {
|
||||
NSURL *url = (NSURL*)obj;
|
||||
if ([uri_list length] > 0) [uri_list appendString:@("\n")];
|
||||
if (url.fileURL) [uri_list appendFormat:@("file://%s"), url.fileSystemRepresentation];
|
||||
if (url.fileURL) [uri_list appendString:url.filePathURL.absoluteString];
|
||||
else [uri_list appendString:url.absoluteString];
|
||||
} else if ([obj isKindOfClass:[NSString class]]) {
|
||||
const char *text = [obj UTF8String];
|
||||
|
||||
198
glfw/linux_desktop_settings.c
vendored
198
glfw/linux_desktop_settings.c
vendored
@@ -10,77 +10,118 @@
|
||||
#include <strings.h>
|
||||
#include <string.h>
|
||||
|
||||
static const char *DESKTOP_SERVICE = "org.freedesktop.portal.Desktop";
|
||||
static const char *DESKTOP_PATH = "/org/freedesktop/portal/desktop";
|
||||
static const char *DESKTOP_INTERFACE = "org.freedesktop.portal.Settings";
|
||||
static const char *GNOME_DESKTOP_NAMESPACE = "org.gnome.desktop.interface";
|
||||
#define DESKTOP_SERVICE "org.freedesktop.portal.Desktop"
|
||||
#define DESKTOP_PATH "/org/freedesktop/portal/desktop"
|
||||
#define DESKTOP_INTERFACE "org.freedesktop.portal.Settings"
|
||||
#define GNOME_DESKTOP_NAMESPACE "org.gnome.desktop.interface"
|
||||
#define FDO_DESKTOP_NAMESPACE "org.freedesktop.appearance"
|
||||
#define FDO_APPEARANCE_KEY "color-scheme"
|
||||
|
||||
|
||||
static char theme_name[64] = {0};
|
||||
static char theme_name[128] = {0};
|
||||
static int theme_size = -1;
|
||||
static bool gnome_cursor_theme_read = false, gnome_cursor_size_read = false;
|
||||
static uint32_t appearance = 0;
|
||||
static bool is_gnome = false;
|
||||
static bool cursor_theme_changed = false;
|
||||
|
||||
static bool
|
||||
parse_dbus_message_for_type(DBusMessage *const reply, const char *errmsg, const int type, void *value) {
|
||||
DBusMessageIter iter[3];
|
||||
dbus_message_iter_init(reply, &iter[0]);
|
||||
#define FAIL { _glfwInputError(GLFW_PLATFORM_ERROR, "%s", errmsg); return false; }
|
||||
if (dbus_message_iter_get_arg_type(&iter[0]) != DBUS_TYPE_VARIANT) FAIL;
|
||||
dbus_message_iter_recurse(&iter[0], &iter[1]);
|
||||
if (dbus_message_iter_get_arg_type(&iter[1]) != DBUS_TYPE_VARIANT) FAIL;
|
||||
dbus_message_iter_recurse(&iter[1], &iter[2]);
|
||||
if (dbus_message_iter_get_arg_type(&iter[2]) != type) FAIL;
|
||||
dbus_message_iter_get_basic(&iter[2], value);
|
||||
return true;
|
||||
#undef FAIL
|
||||
}
|
||||
|
||||
#define HANDLER(name) void name(DBusMessage *msg, const char* errmsg, void *data) { \
|
||||
#define HANDLER(name) static void name(DBusMessage *msg, const char* errmsg, void *data) { \
|
||||
(void)data; \
|
||||
if (errmsg) { \
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR, "%s: failed with error: %s", #name, errmsg); \
|
||||
return; \
|
||||
}
|
||||
|
||||
HANDLER(on_gnome_cursor_theme_read)
|
||||
const char *name;
|
||||
if (!parse_dbus_message_for_type(msg, "Failed to get cursor theme name from reply", DBUS_TYPE_STRING, &name)) return;
|
||||
if (name && name[0]) {
|
||||
gnome_cursor_theme_read = true;
|
||||
strncpy(theme_name, name, sizeof(theme_name) - 1);
|
||||
if (gnome_cursor_size_read) _glfwPlatformChangeCursorTheme();
|
||||
static void
|
||||
process_fdo_setting(const char *key, DBusMessageIter *value) {
|
||||
if (strcmp(key, FDO_APPEARANCE_KEY) == 0) {
|
||||
if (dbus_message_iter_get_arg_type(value) == DBUS_TYPE_UINT32) {
|
||||
dbus_message_iter_get_basic(value, &appearance);
|
||||
if (appearance > 2) appearance = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HANDLER(on_gnome_cursor_size_read)
|
||||
int32_t sz;
|
||||
if (!parse_dbus_message_for_type(msg, "Failed to get cursor theme size from reply", DBUS_TYPE_INT32, &sz)) return;
|
||||
gnome_cursor_size_read = true;
|
||||
theme_size = sz;
|
||||
if (gnome_cursor_theme_read) _glfwPlatformChangeCursorTheme();
|
||||
}
|
||||
#undef HANDLER
|
||||
|
||||
|
||||
static bool
|
||||
call_read(DBusConnection *session_bus, dbus_pending_callback callback, const char *namespace, const char *key) {
|
||||
return glfw_dbus_call_method_with_reply(
|
||||
session_bus, DESKTOP_SERVICE, DESKTOP_PATH, DESKTOP_INTERFACE, "Read", DBUS_TIMEOUT_USE_DEFAULT,
|
||||
callback, NULL, DBUS_TYPE_STRING, &namespace, DBUS_TYPE_STRING, &key, DBUS_TYPE_INVALID);
|
||||
}
|
||||
|
||||
static void
|
||||
get_from_gnome(void) {
|
||||
theme_size = 32;
|
||||
DBusConnection *session_bus = glfw_dbus_session_bus();
|
||||
if (session_bus) {
|
||||
const char *theme_key = "cursor-theme";
|
||||
call_read(session_bus, on_gnome_cursor_theme_read, GNOME_DESKTOP_NAMESPACE, theme_key);
|
||||
const char *size_key = "cursor-size";
|
||||
call_read(session_bus, on_gnome_cursor_size_read, GNOME_DESKTOP_NAMESPACE, size_key);
|
||||
process_gnome_setting(const char *key, DBusMessageIter *value) {
|
||||
if (strcmp(key, "cursor-size") == 0) {
|
||||
if (dbus_message_iter_get_arg_type(value) == DBUS_TYPE_INT32) {
|
||||
int32_t sz;
|
||||
dbus_message_iter_get_basic(value, &sz);
|
||||
if (sz > 0 && sz != theme_size) {
|
||||
theme_size = sz;
|
||||
cursor_theme_changed = true;
|
||||
}
|
||||
}
|
||||
} else if (strcmp(key, "cursor-theme") == 0) {
|
||||
if (dbus_message_iter_get_arg_type(value) == DBUS_TYPE_STRING) {
|
||||
const char *name;
|
||||
dbus_message_iter_get_basic(value, &name);
|
||||
if (name) {
|
||||
strncpy(theme_name, name, sizeof(theme_name) - 1);
|
||||
cursor_theme_changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
process_settings_dict(DBusMessageIter *array_iter, void(process_setting)(const char *, DBusMessageIter*)) {
|
||||
DBusMessageIter item_iter, value_iter;
|
||||
while (dbus_message_iter_get_arg_type(array_iter) == DBUS_TYPE_DICT_ENTRY) {
|
||||
dbus_message_iter_recurse(array_iter, &item_iter);
|
||||
if (dbus_message_iter_get_arg_type(&item_iter) == DBUS_TYPE_STRING) {
|
||||
const char *key;
|
||||
dbus_message_iter_get_basic(&item_iter, &key);
|
||||
if (dbus_message_iter_next(&item_iter) && dbus_message_iter_get_arg_type(&item_iter) == DBUS_TYPE_VARIANT) {
|
||||
dbus_message_iter_recurse(&item_iter, &value_iter);
|
||||
process_setting(key, &value_iter);
|
||||
}
|
||||
}
|
||||
if (!dbus_message_iter_next(array_iter)) break;
|
||||
}
|
||||
}
|
||||
|
||||
HANDLER(process_desktop_settings)
|
||||
cursor_theme_changed = false;
|
||||
DBusMessageIter root, array, item, settings;
|
||||
dbus_message_iter_init(msg, &root);
|
||||
#define die(...) { _glfwInputError(GLFW_PLATFORM_ERROR, __VA_ARGS__); return; }
|
||||
if (dbus_message_iter_get_arg_type(&root) != DBUS_TYPE_ARRAY) die("Reply to request for desktop settings is not an array");
|
||||
dbus_message_iter_recurse(&root, &array);
|
||||
while (dbus_message_iter_get_arg_type(&array) == DBUS_TYPE_DICT_ENTRY) {
|
||||
dbus_message_iter_recurse(&array, &item);
|
||||
if (dbus_message_iter_get_arg_type(&item) == DBUS_TYPE_STRING) {
|
||||
const char *namespace;
|
||||
dbus_message_iter_get_basic(&item, &namespace);
|
||||
if (dbus_message_iter_next(&item) && dbus_message_iter_get_arg_type(&item) == DBUS_TYPE_ARRAY) {
|
||||
dbus_message_iter_recurse(&item, &settings);
|
||||
if (strcmp(namespace, FDO_DESKTOP_NAMESPACE) == 0) {
|
||||
process_settings_dict(&settings, process_fdo_setting);
|
||||
} else if (is_gnome && strcmp(namespace, GNOME_DESKTOP_NAMESPACE) == 0) {
|
||||
process_settings_dict(&settings, process_gnome_setting);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!dbus_message_iter_next(&array)) break;
|
||||
}
|
||||
#undef die
|
||||
if (cursor_theme_changed) _glfwPlatformChangeCursorTheme();
|
||||
}
|
||||
|
||||
#undef HANDLER
|
||||
|
||||
static bool
|
||||
read_desktop_settings(DBusConnection *session_bus) {
|
||||
DBusMessage *msg = dbus_message_new_method_call(DESKTOP_SERVICE, DESKTOP_PATH, DESKTOP_INTERFACE, "ReadAll");
|
||||
if (!msg) return false;
|
||||
DBusMessageIter iter, array_iter;
|
||||
dbus_message_iter_init_append(msg, &iter);
|
||||
if (!dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "s", &array_iter)) { dbus_message_unref(msg); return false; }
|
||||
if (!dbus_message_iter_close_container(&iter, &array_iter)) { dbus_message_unref(msg); return false; }
|
||||
bool ok = call_method_with_msg(session_bus, msg, DBUS_TIMEOUT_USE_DEFAULT, process_desktop_settings, NULL);
|
||||
dbus_message_unref(msg);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void
|
||||
glfw_current_cursor_theme(const char **theme, int *size) {
|
||||
@@ -100,10 +141,55 @@ get_cursor_theme_from_env(void) {
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
on_color_scheme_change(DBusMessage *message) {
|
||||
DBusMessageIter iter[2];
|
||||
dbus_message_iter_init (message, &iter[0]);
|
||||
int current_type;
|
||||
while ((current_type = dbus_message_iter_get_arg_type (&iter[0])) != DBUS_TYPE_INVALID) {
|
||||
if (current_type == DBUS_TYPE_VARIANT) {
|
||||
dbus_message_iter_recurse(&iter[0], &iter[1]);
|
||||
if (dbus_message_iter_get_arg_type(&iter[1]) == DBUS_TYPE_UINT32) {
|
||||
uint32_t val = 0;
|
||||
dbus_message_iter_get_basic(&iter[1], &val);
|
||||
if (val > 2) val = 0;
|
||||
if (val != appearance) {
|
||||
appearance = val;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
dbus_message_iter_next(&iter[0]);
|
||||
}
|
||||
}
|
||||
|
||||
static DBusHandlerResult
|
||||
setting_changed(DBusConnection *conn UNUSED, DBusMessage *msg, void *user_data UNUSED) {
|
||||
/* printf("session_bus settings_changed invoked interface: %s member: %s\n", dbus_message_get_interface(msg), dbus_message_get_member(msg)); */
|
||||
if (dbus_message_is_signal(msg, DESKTOP_INTERFACE, "SettingChanged")) {
|
||||
const char *namespace = NULL, *key = NULL;
|
||||
if (glfw_dbus_get_args(msg, "Failed to get namespace and key from SettingChanged notification signal", DBUS_TYPE_STRING, &namespace, DBUS_TYPE_STRING, &key, DBUS_TYPE_INVALID)) {
|
||||
if (strcmp(namespace, FDO_DESKTOP_NAMESPACE) == 0) {
|
||||
if (strcmp(key, FDO_APPEARANCE_KEY) == 0) {
|
||||
on_color_scheme_change(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
glfw_initialize_desktop_settings(void) {
|
||||
get_cursor_theme_from_env();
|
||||
const char *desktop = getenv("XDG_CURRENT_DESKTOP");
|
||||
bool is_gnome = desktop && strncasecmp(desktop, "GNOME", sizeof("GNOME") - 1) == 0;
|
||||
if (is_gnome) get_from_gnome();
|
||||
is_gnome = desktop && strstr(desktop, "GNOME");
|
||||
DBusConnection *session_bus = glfw_dbus_session_bus();
|
||||
if (session_bus) {
|
||||
if (!read_desktop_settings(session_bus)) _glfwInputError(GLFW_PLATFORM_ERROR, "Failed to read desktop settings, make sure you have the desktop portal running.");
|
||||
dbus_bus_add_match(session_bus, "type='signal',interface='" DESKTOP_INTERFACE "',member='SettingChanged'", NULL);
|
||||
dbus_connection_add_filter(session_bus, setting_changed, NULL, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
19
glfw/wl_client_side_decorations.c
vendored
19
glfw/wl_client_side_decorations.c
vendored
@@ -263,11 +263,11 @@ static bool
|
||||
create_shm_buffers(_GLFWwindow* window) {
|
||||
const unsigned scale = window->wl.scale >= 1 ? window->wl.scale : 1;
|
||||
|
||||
const size_t vertical_width = decs.metrics.width, vertical_height = window->wl.current.height + decs.metrics.top;
|
||||
const size_t horizontal_height = decs.metrics.width, horizontal_width = window->wl.current.width + 2 * decs.metrics.width;
|
||||
const size_t vertical_width = decs.metrics.width, vertical_height = window->wl.height + decs.metrics.top;
|
||||
const size_t horizontal_height = decs.metrics.width, horizontal_width = window->wl.width + 2 * decs.metrics.width;
|
||||
|
||||
decs.mapping.size = 0;
|
||||
decs.mapping.size += init_buffer_pair(&decs.top.buffer, window->wl.current.width, decs.metrics.top, scale);
|
||||
decs.mapping.size += init_buffer_pair(&decs.top.buffer, window->wl.width, decs.metrics.top, scale);
|
||||
decs.mapping.size += init_buffer_pair(&decs.left.buffer, vertical_width, vertical_height, scale);
|
||||
decs.mapping.size += init_buffer_pair(&decs.bottom.buffer, horizontal_width, horizontal_height, scale);
|
||||
decs.mapping.size += init_buffer_pair(&decs.right.buffer, vertical_width, vertical_height, scale);
|
||||
@@ -347,12 +347,13 @@ ensure_csd_resources(_GLFWwindow *window) {
|
||||
const bool is_focused = window->id == _glfw.focusedWindowId;
|
||||
const bool focus_changed = is_focused != decs.for_window_state.focused;
|
||||
const bool size_changed = (
|
||||
decs.for_window_state.width != window->wl.current.width ||
|
||||
decs.for_window_state.height != window->wl.current.height ||
|
||||
decs.for_window_state.width != window->wl.width ||
|
||||
decs.for_window_state.height != window->wl.height ||
|
||||
decs.for_window_state.scale != window->wl.scale ||
|
||||
!decs.mapping.data
|
||||
);
|
||||
const bool needs_update = focus_changed || size_changed || !decs.left.surface;
|
||||
debug("CSD: old.size: %dx%d new.size: %dx%d needs_update: %d size_changed: %d\n", decs.for_window_state.width, decs.for_window_state.height, window->wl.width, window->wl.height, needs_update, size_changed);
|
||||
if (!needs_update) return false;
|
||||
if (size_changed) {
|
||||
free_csd_buffers(window);
|
||||
@@ -368,11 +369,11 @@ ensure_csd_resources(_GLFWwindow *window) {
|
||||
if (!decs.left.surface) create_csd_surfaces(window, &decs.left);
|
||||
position_csd_surface(&decs.left, x, y, scale);
|
||||
|
||||
x = -decs.metrics.width; y = window->wl.current.height;
|
||||
x = -decs.metrics.width; y = window->wl.height;
|
||||
if (!decs.bottom.surface) create_csd_surfaces(window, &decs.bottom);
|
||||
position_csd_surface(&decs.bottom, x, y, scale);
|
||||
|
||||
x = window->wl.current.width; y = -decs.metrics.top;
|
||||
x = window->wl.width; y = -decs.metrics.top;
|
||||
if (!decs.right.surface) create_csd_surfaces(window, &decs.right);
|
||||
position_csd_surface(&decs.right, x, y, scale);
|
||||
|
||||
@@ -382,8 +383,8 @@ ensure_csd_resources(_GLFWwindow *window) {
|
||||
damage_csd(bottom, is_focused ? decs.bottom.buffer.front : decs.bottom.buffer.back);
|
||||
damage_csd(right, is_focused ? decs.right.buffer.front : decs.right.buffer.back);
|
||||
|
||||
decs.for_window_state.width = window->wl.current.width;
|
||||
decs.for_window_state.height = window->wl.current.height;
|
||||
decs.for_window_state.width = window->wl.width;
|
||||
decs.for_window_state.height = window->wl.height;
|
||||
decs.for_window_state.scale = window->wl.scale;
|
||||
decs.for_window_state.focused = is_focused;
|
||||
return true;
|
||||
|
||||
7
glfw/wl_init.c
vendored
7
glfw/wl_init.c
vendored
@@ -54,6 +54,8 @@
|
||||
#endif
|
||||
|
||||
|
||||
#define debug(...) if (_glfw.hints.init.debugRendering) fprintf(stderr, __VA_ARGS__);
|
||||
|
||||
static int min(int n1, int n2)
|
||||
{
|
||||
return n1 < n2 ? n1 : n2;
|
||||
@@ -96,7 +98,6 @@ static void pointerHandleEnter(void* data UNUSED,
|
||||
if (!window)
|
||||
return;
|
||||
}
|
||||
|
||||
window->wl.decorations.focus = focus;
|
||||
_glfw.wl.serial = serial;
|
||||
_glfw.wl.pointerFocus = window;
|
||||
@@ -206,7 +207,7 @@ static void pointerHandleMotion(void* data UNUSED,
|
||||
case BOTTOM_DECORATION:
|
||||
if (x < window->wl.decorations.metrics.width)
|
||||
cursorShape = GLFW_SW_RESIZE_CURSOR;
|
||||
else if (x > window->wl.current.width + window->wl.decorations.metrics.width)
|
||||
else if (x > window->wl.width + window->wl.decorations.metrics.width)
|
||||
cursorShape = GLFW_SE_RESIZE_CURSOR;
|
||||
else
|
||||
cursorShape = GLFW_VRESIZE_CURSOR;
|
||||
@@ -274,7 +275,7 @@ static void pointerHandleButton(void* data UNUSED,
|
||||
case BOTTOM_DECORATION:
|
||||
if (x < window->wl.decorations.metrics.width)
|
||||
edges = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT;
|
||||
else if (x > window->wl.current.width + window->wl.decorations.metrics.width)
|
||||
else if (x > window->wl.width + window->wl.decorations.metrics.width)
|
||||
edges = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT;
|
||||
else
|
||||
edges = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM;
|
||||
|
||||
9
glfw/wl_platform.h
vendored
9
glfw/wl_platform.h
vendored
@@ -127,11 +127,16 @@ typedef enum WaylandWindowState {
|
||||
|
||||
static const WaylandWindowState TOPLEVEL_STATE_DOCKED = TOPLEVEL_STATE_MAXIMIZED | TOPLEVEL_STATE_FULLSCREEN | TOPLEVEL_STATE_TILED_TOP | TOPLEVEL_STATE_TILED_LEFT | TOPLEVEL_STATE_TILED_RIGHT | TOPLEVEL_STATE_TILED_BOTTOM;
|
||||
|
||||
enum WaylandWindowPendingState {
|
||||
PENDING_STATE_TOPLEVEL = 1,
|
||||
PENDING_STATE_DECORATION = 2
|
||||
};
|
||||
|
||||
// Wayland-specific per-window data
|
||||
//
|
||||
typedef struct _GLFWwindowWayland
|
||||
{
|
||||
int width, height;
|
||||
bool visible;
|
||||
bool hovered;
|
||||
bool transparent;
|
||||
@@ -215,11 +220,13 @@ typedef struct _GLFWwindowWayland
|
||||
struct {
|
||||
unsigned int x, y;
|
||||
} axis_discrete_count;
|
||||
bool surface_configured_once;
|
||||
|
||||
uint32_t pending_state;
|
||||
struct {
|
||||
int width, height;
|
||||
uint32_t toplevel_states;
|
||||
bool set;
|
||||
uint32_t decoration_mode;
|
||||
} current, pending;
|
||||
} _GLFWwindowWayland;
|
||||
|
||||
|
||||
20
glfw/wl_text_input.c
vendored
20
glfw/wl_text_input.c
vendored
@@ -47,7 +47,7 @@ static void
|
||||
send_text(const char *text, GLFWIMEState ime_state) {
|
||||
_GLFWwindow *w = _glfwFocusedWindow();
|
||||
if (w && w->callbacks.keyboard) {
|
||||
GLFWkeyevent fake_ev = {.action = GLFW_PRESS};
|
||||
GLFWkeyevent fake_ev = {.action = text ? GLFW_PRESS : GLFW_RELEASE};
|
||||
fake_ev.text = text;
|
||||
fake_ev.ime_state = ime_state;
|
||||
w->callbacks.keyboard((GLFWwindow*) w, &fake_ev);
|
||||
@@ -93,6 +93,9 @@ text_input_done(void *data UNUSED, struct zwp_text_input_v3 *txt_input UNUSED, u
|
||||
if (pending_pre_edit) {
|
||||
send_text(pending_pre_edit, GLFW_IME_PREEDIT_CHANGED);
|
||||
free(pending_pre_edit); pending_pre_edit = NULL;
|
||||
} else {
|
||||
// Clear pre-edit text
|
||||
send_text(NULL, GLFW_IME_PREEDIT_CHANGED);
|
||||
}
|
||||
if (pending_commit) {
|
||||
send_text(pending_commit, GLFW_IME_COMMIT_TEXT);
|
||||
@@ -139,7 +142,20 @@ _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
|
||||
switch(ev->type) {
|
||||
case GLFW_IME_UPDATE_FOCUS:
|
||||
debug("\ntext-input: updating IME focus state, focused: %d\n", ev->focused);
|
||||
if (ev->focused) zwp_text_input_v3_enable(text_input); else zwp_text_input_v3_disable(text_input);
|
||||
if (ev->focused) {
|
||||
zwp_text_input_v3_enable(text_input);
|
||||
zwp_text_input_v3_set_content_type(text_input, ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE, ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL);
|
||||
} else {
|
||||
if (pending_pre_edit) {
|
||||
// Clear pre-edit text
|
||||
send_text(NULL, GLFW_IME_PREEDIT_CHANGED);
|
||||
free(pending_pre_edit); pending_pre_edit = NULL;
|
||||
}
|
||||
if (pending_commit) {
|
||||
free(pending_commit); pending_commit = NULL;
|
||||
}
|
||||
zwp_text_input_v3_disable(text_input);
|
||||
}
|
||||
commit();
|
||||
break;
|
||||
case GLFW_IME_UPDATE_CURSOR_POSITION: {
|
||||
|
||||
103
glfw/wl_window.c
vendored
103
glfw/wl_window.c
vendored
@@ -206,7 +206,7 @@ static void setOpaqueRegion(_GLFWwindow* window)
|
||||
if (!region)
|
||||
return;
|
||||
|
||||
wl_region_add(region, 0, 0, window->wl.current.width, window->wl.current.height);
|
||||
wl_region_add(region, 0, 0, window->wl.width, window->wl.height);
|
||||
wl_surface_set_opaque_region(window->wl.surface, region);
|
||||
wl_surface_commit(window->wl.surface);
|
||||
wl_region_destroy(region);
|
||||
@@ -216,9 +216,9 @@ static void setOpaqueRegion(_GLFWwindow* window)
|
||||
static void
|
||||
resizeFramebuffer(_GLFWwindow* window) {
|
||||
int scale = window->wl.scale;
|
||||
int scaledWidth = window->wl.current.width * scale;
|
||||
int scaledHeight = window->wl.current.height * scale;
|
||||
debug("Resizing framebuffer to: %dx%d at scale: %d\n", window->wl.current.width, window->wl.current.height, scale);
|
||||
int scaledWidth = window->wl.width * scale;
|
||||
int scaledHeight = window->wl.height * scale;
|
||||
debug("Resizing framebuffer to: %dx%d at scale: %d\n", window->wl.width, window->wl.height, scale);
|
||||
wl_egl_window_resize(window->wl.native, scaledWidth, scaledHeight, 0, 0);
|
||||
if (!window->wl.transparent) setOpaqueRegion(window);
|
||||
_glfwInputFramebufferSize(window, scaledWidth, scaledHeight);
|
||||
@@ -234,24 +234,26 @@ clipboard_mime(void) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
static void
|
||||
static bool
|
||||
dispatchChangesAfterConfigure(_GLFWwindow *window, int32_t width, int32_t height) {
|
||||
bool size_changed = width != window->wl.current.width || height != window->wl.current.height;
|
||||
bool size_changed = width != window->wl.width || height != window->wl.height;
|
||||
bool scale_changed = checkScaleChange(window);
|
||||
|
||||
if (size_changed) {
|
||||
_glfwInputWindowSize(window, width, height);
|
||||
window->wl.current.width = width; window->wl.current.height = height;
|
||||
window->wl.width = width; window->wl.height = height;
|
||||
resizeFramebuffer(window);
|
||||
}
|
||||
|
||||
if (scale_changed) {
|
||||
if (!size_changed)
|
||||
resizeFramebuffer(window);
|
||||
debug("Scale changed to %d in dispatchChangesAfterConfigure\n", window->wl.scale);
|
||||
if (!size_changed) resizeFramebuffer(window);
|
||||
_glfwInputWindowContentScale(window, window->wl.scale, window->wl.scale);
|
||||
}
|
||||
|
||||
_glfwInputWindowDamage(window);
|
||||
|
||||
return size_changed || scale_changed;
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -269,24 +271,9 @@ xdgDecorationHandleConfigure(void* data,
|
||||
uint32_t mode)
|
||||
{
|
||||
_GLFWwindow* window = data;
|
||||
|
||||
bool has_server_side_decorations = (mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
|
||||
debug("XDG decoration configure event received: has_server_side_decorations: %d\n", has_server_side_decorations);
|
||||
if (has_server_side_decorations == window->wl.decorations.serverSide) return;
|
||||
window->wl.decorations.serverSide = has_server_side_decorations;
|
||||
int width = window->wl.current.width, height = window->wl.current.height;
|
||||
if (window->wl.decorations.serverSide) {
|
||||
free_csd_surfaces(window);
|
||||
height += window->wl.decorations.metrics.visible_titlebar_height;
|
||||
} else {
|
||||
ensure_csd_resources(window);
|
||||
}
|
||||
set_csd_window_geometry(window, &width, &height);
|
||||
dispatchChangesAfterConfigure(window, width, height);
|
||||
ensure_csd_resources(window);
|
||||
wl_surface_commit(window->wl.surface);
|
||||
debug("final window content size: %dx%d\n", window->wl.current.width, window->wl.current.height);
|
||||
inform_compositor_of_window_geometry(window, "configure-decorations");
|
||||
window->wl.pending.decoration_mode = mode;
|
||||
window->wl.pending_state |= PENDING_STATE_DECORATION;
|
||||
debug("XDG decoration configure event received: has_server_side_decorations: %d\n", (mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE));
|
||||
}
|
||||
|
||||
static const struct zxdg_toplevel_decoration_v1_listener xdgDecorationListener = {
|
||||
@@ -311,6 +298,7 @@ static void surfaceHandleEnter(void *data,
|
||||
window->wl.monitors[window->wl.monitorsCount++] = monitor;
|
||||
|
||||
if (checkScaleChange(window)) {
|
||||
debug("Scale changed to %d in surface enter event\n", window->wl.scale);
|
||||
resizeFramebuffer(window);
|
||||
_glfwInputWindowContentScale(window, window->wl.scale, window->wl.scale);
|
||||
ensure_csd_resources(window);
|
||||
@@ -336,6 +324,7 @@ static void surfaceHandleLeave(void *data,
|
||||
window->wl.monitors[--window->wl.monitorsCount] = NULL;
|
||||
|
||||
if (checkScaleChange(window)) {
|
||||
debug("Scale changed to %d in surface leave event\n", window->wl.scale);
|
||||
resizeFramebuffer(window);
|
||||
_glfwInputWindowContentScale(window, window->wl.scale, window->wl.scale);
|
||||
ensure_csd_resources(window);
|
||||
@@ -384,8 +373,8 @@ static bool createSurface(_GLFWwindow* window,
|
||||
if (!window->wl.native)
|
||||
return false;
|
||||
|
||||
window->wl.current.width = wndconfig->width;
|
||||
window->wl.current.height = wndconfig->height;
|
||||
window->wl.width = wndconfig->width;
|
||||
window->wl.height = wndconfig->height;
|
||||
window->wl.user_requested_content_size.width = wndconfig->width;
|
||||
window->wl.user_requested_content_size.height = wndconfig->height;
|
||||
window->wl.scale = 1;
|
||||
@@ -479,7 +468,7 @@ xdgToplevelHandleConfigure(void* data,
|
||||
window->wl.pending.toplevel_states = new_states;
|
||||
window->wl.pending.width = width;
|
||||
window->wl.pending.height = height;
|
||||
window->wl.pending.set = true;
|
||||
window->wl.pending_state |= PENDING_STATE_TOPLEVEL;
|
||||
}
|
||||
|
||||
static void xdgToplevelHandleClose(void* data,
|
||||
@@ -500,11 +489,18 @@ static void xdgSurfaceHandleConfigure(void* data,
|
||||
{
|
||||
_GLFWwindow* window = data;
|
||||
xdg_surface_ack_configure(surface, serial);
|
||||
if (window->wl.pending.set) {
|
||||
if (window->wl.pending_state & PENDING_STATE_TOPLEVEL) {
|
||||
uint32_t new_states = window->wl.pending.toplevel_states;
|
||||
int width = window->wl.pending.width;
|
||||
int height = window->wl.pending.height;
|
||||
window->wl.pending.set = false;
|
||||
if (!window->wl.surface_configured_once) {
|
||||
window->wl.surface_configured_once = true;
|
||||
if (!width && !height && !new_states && !window->wl.decorations.serverSide && getenv("XAUTHORITY") && strstr(getenv("XAUTHORITY"), "mutter")) {
|
||||
// https://github.com/kovidgoyal/kitty/issues/4802
|
||||
debug("Ignoring first empty surface configure event on mutter.\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (new_states != window->wl.current.toplevel_states ||
|
||||
width != window->wl.current.width ||
|
||||
@@ -512,16 +508,39 @@ static void xdgSurfaceHandleConfigure(void* data,
|
||||
|
||||
bool live_resize_done = !(new_states & TOPLEVEL_STATE_RESIZING) && (window->wl.current.toplevel_states & TOPLEVEL_STATE_RESIZING);
|
||||
window->wl.current.toplevel_states = new_states;
|
||||
set_csd_window_geometry(window, &width, &height);
|
||||
dispatchChangesAfterConfigure(window, width, height);
|
||||
debug("final window content size: %dx%d\n", window->wl.current.width, window->wl.current.height);
|
||||
window->wl.current.width = width;
|
||||
window->wl.current.height = height;
|
||||
_glfwInputWindowFocus(window, window->wl.current.toplevel_states & TOPLEVEL_STATE_ACTIVATED);
|
||||
ensure_csd_resources(window);
|
||||
inform_compositor_of_window_geometry(window, "configure");
|
||||
if (live_resize_done) _glfwInputLiveResize(window, false);
|
||||
}
|
||||
}
|
||||
wl_surface_commit(window->wl.surface);
|
||||
|
||||
if (window->wl.pending_state & PENDING_STATE_DECORATION) {
|
||||
uint32_t mode = window->wl.pending.decoration_mode;
|
||||
bool has_server_side_decorations = (mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
|
||||
window->wl.decorations.serverSide = has_server_side_decorations;
|
||||
window->wl.current.decoration_mode = mode;
|
||||
}
|
||||
|
||||
bool resized = false;
|
||||
if (window->wl.pending_state) {
|
||||
int width = window->wl.pending.width, height = window->wl.pending.height;
|
||||
set_csd_window_geometry(window, &width, &height);
|
||||
resized = dispatchChangesAfterConfigure(window, width, height);
|
||||
if (window->wl.decorations.serverSide) {
|
||||
free_csd_surfaces(window);
|
||||
} else {
|
||||
ensure_csd_resources(window);
|
||||
}
|
||||
debug("final window content size: %dx%d resized: %d\n", width, height, resized);
|
||||
}
|
||||
|
||||
inform_compositor_of_window_geometry(window, "configure");
|
||||
|
||||
// if a resize happened there will be a commit at the next render frame so
|
||||
// dont commit here, GNOME doesnt like it and its not really needed anyway
|
||||
if (!resized) wl_surface_commit(window->wl.surface);
|
||||
window->wl.pending_state = 0;
|
||||
}
|
||||
|
||||
static const struct xdg_surface_listener xdgSurfaceListener = {
|
||||
@@ -904,19 +923,19 @@ void _glfwPlatformSetWindowPos(_GLFWwindow* window UNUSED, int xpos UNUSED, int
|
||||
void _glfwPlatformGetWindowSize(_GLFWwindow* window, int* width, int* height)
|
||||
{
|
||||
if (width)
|
||||
*width = window->wl.current.width;
|
||||
*width = window->wl.width;
|
||||
if (height)
|
||||
*height = window->wl.current.height;
|
||||
*height = window->wl.height;
|
||||
}
|
||||
|
||||
void _glfwPlatformSetWindowSize(_GLFWwindow* window, int width, int height)
|
||||
{
|
||||
if (width != window->wl.current.width || height != window->wl.current.height) {
|
||||
if (width != window->wl.width || height != window->wl.height) {
|
||||
window->wl.user_requested_content_size.width = width;
|
||||
window->wl.user_requested_content_size.height = height;
|
||||
int32_t w = 0, h = 0;
|
||||
set_csd_window_geometry(window, &w, &h);
|
||||
window->wl.current.width = w; window->wl.current.height = h;
|
||||
window->wl.width = w; window->wl.height = h;
|
||||
resizeFramebuffer(window);
|
||||
ensure_csd_resources(window);
|
||||
wl_surface_commit(window->wl.surface);
|
||||
|
||||
@@ -74,7 +74,7 @@ class HistoryCompleter:
|
||||
def option_text() -> str:
|
||||
return '''\
|
||||
--type -t
|
||||
choices=line,yesno,choices
|
||||
choices=line,yesno,choices,password
|
||||
default=line
|
||||
Type of input. Defaults to asking for a line of text.
|
||||
|
||||
@@ -103,6 +103,11 @@ For example: y:Yes and n;red:No
|
||||
A default choice or text. If unspecified, it is "y" for :code:`yesno`, the first choice
|
||||
for :code:`choices` and empty for others. The default choice is selected when the user
|
||||
presses the Enter key.
|
||||
|
||||
|
||||
--prompt -p
|
||||
default="> "
|
||||
The prompt to use when inputting a line of text or a password.
|
||||
'''
|
||||
|
||||
|
||||
@@ -140,6 +145,55 @@ def extra_for(width: int, screen_width: int) -> int:
|
||||
return max(0, screen_width - width) // 2 + 1
|
||||
|
||||
|
||||
class Password(Handler):
|
||||
|
||||
def __init__(self, cli_opts: AskCLIOptions, prompt: str) -> None:
|
||||
self.cli_opts = cli_opts
|
||||
self.prompt = prompt
|
||||
from kittens.tui.line_edit import LineEdit
|
||||
self.line_edit = LineEdit(is_password=True)
|
||||
|
||||
def initialize(self) -> None:
|
||||
self.cmd.set_cursor_shape('beam')
|
||||
self.draw_screen()
|
||||
|
||||
@Handler.atomic_update
|
||||
def draw_screen(self) -> None:
|
||||
self.cmd.clear_screen()
|
||||
if self.cli_opts.message:
|
||||
for line in self.cli_opts.message.splitlines():
|
||||
self.print(line)
|
||||
self.print()
|
||||
self.line_edit.write(self.write, self.prompt)
|
||||
|
||||
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
|
||||
self.line_edit.on_text(text, in_bracketed_paste)
|
||||
self.draw_screen()
|
||||
|
||||
def on_key(self, key_event: KeyEventType) -> None:
|
||||
if self.line_edit.on_key(key_event):
|
||||
self.draw_screen()
|
||||
return
|
||||
if key_event.matches('enter'):
|
||||
self.quit_loop(0)
|
||||
if key_event.matches('esc'):
|
||||
self.quit_loop(1)
|
||||
|
||||
def on_resize(self, screen_size: ScreenSize) -> None:
|
||||
self.screen_size = screen_size
|
||||
self.draw_screen()
|
||||
|
||||
def on_interrupt(self) -> None:
|
||||
self.quit_loop(1)
|
||||
on_eot = on_interrupt
|
||||
|
||||
@property
|
||||
def response(self) -> str:
|
||||
if self._tui_loop.return_code == 0:
|
||||
return self.line_edit.current_input
|
||||
return ''
|
||||
|
||||
|
||||
class Choose(Handler):
|
||||
mouse_tracking = MouseTracking.buttons_only
|
||||
|
||||
@@ -361,6 +415,15 @@ def main(args: List[str]) -> Response:
|
||||
loop.loop(handler)
|
||||
return {'items': items, 'response': handler.response}
|
||||
|
||||
prompt = cli_opts.prompt
|
||||
if prompt[0] == prompt[-1] and prompt[0] in '\'"':
|
||||
prompt = prompt[1:-1]
|
||||
if cli_opts.type == 'password':
|
||||
loop = Loop()
|
||||
phandler = Password(cli_opts, prompt)
|
||||
loop.loop(phandler)
|
||||
return {'items': items, 'response': phandler.response}
|
||||
|
||||
import readline as rl
|
||||
readline = rl
|
||||
from kitty.shell import init_readline
|
||||
@@ -371,7 +434,6 @@ def main(args: List[str]) -> Response:
|
||||
if cli_opts.message:
|
||||
print(styled(cli_opts.message, bold=True))
|
||||
|
||||
prompt = '> '
|
||||
with suppress(KeyboardInterrupt, EOFError):
|
||||
if cli_opts.default:
|
||||
def prefill_text() -> None:
|
||||
|
||||
@@ -167,7 +167,9 @@ def highlight_collection(collection: Collection, aliases: Optional[Dict[str, str
|
||||
try:
|
||||
highlights = future.result()
|
||||
except Exception as e:
|
||||
return f'Running syntax highlighting for {path} generated an exception: {e}'
|
||||
import traceback
|
||||
tb = traceback.format_exc()
|
||||
return f'Running syntax highlighting for {path} generated an exception: {e} with traceback:\n{tb}'
|
||||
ans[path] = highlights
|
||||
return ans
|
||||
|
||||
|
||||
@@ -293,10 +293,11 @@ class DiffHandler(Handler):
|
||||
|
||||
def scroll_lines(self, amt: int = 1) -> None:
|
||||
new_pos = max(0, min(self.scroll_pos + amt, self.max_scroll_pos))
|
||||
amt = new_pos - self.scroll_pos
|
||||
if new_pos == self.scroll_pos:
|
||||
self.cmd.bell()
|
||||
return
|
||||
if abs(new_pos - self.scroll_pos) >= self.num_lines - 1:
|
||||
if abs(amt) >= self.num_lines - 1:
|
||||
self.scroll_pos = new_pos
|
||||
self.draw_screen()
|
||||
return
|
||||
|
||||
2
kittens/diff/options/parse.py
generated
2
kittens/diff/options/parse.py
generated
@@ -93,7 +93,7 @@ def create_result_dict() -> typing.Dict[str, typing.Any]:
|
||||
}
|
||||
|
||||
|
||||
actions = frozenset(('map',))
|
||||
actions: typing.FrozenSet[str] = frozenset(('map',))
|
||||
|
||||
|
||||
def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:
|
||||
|
||||
@@ -27,6 +27,7 @@ from kitty.utils import (
|
||||
from ..tui.handler import Handler, result_handler
|
||||
from ..tui.loop import Loop
|
||||
from ..tui.operations import faint, styled
|
||||
from ..tui.utils import report_error, report_unhandled_error
|
||||
|
||||
|
||||
@lru_cache()
|
||||
@@ -117,6 +118,9 @@ def render(text: str, current_input: str, all_marks: Sequence[Mark], ignore_mark
|
||||
|
||||
class Hints(Handler):
|
||||
|
||||
use_alternate_screen = False # disabled to avoid screen being blanked at exit causing flicker
|
||||
overlay_ready_report_needed = True
|
||||
|
||||
def __init__(self, text: str, all_marks: Sequence[Mark], index_map: Dict[int, Mark], args: HintsCLIOptions):
|
||||
self.text, self.index_map = text, index_map
|
||||
self.alphabet = args.alphabet or DEFAULT_HINT_ALPHABET
|
||||
@@ -500,7 +504,7 @@ def run(args: HintsCLIOptions, text: str, extra_cli_args: Sequence[str] = ()) ->
|
||||
all_marks = tuple(mark(pattern, post_processors, text, args))
|
||||
if not all_marks:
|
||||
none_of = {'url': 'URLs', 'hyperlink': 'hyperlinks'}.get(args.type, 'matches')
|
||||
input(_('No {} found, press Enter to quit.').format(none_of))
|
||||
report_error(_('No {} found.').format(none_of))
|
||||
return None
|
||||
|
||||
largest_index = all_marks[-1].index
|
||||
@@ -512,11 +516,7 @@ def run(args: HintsCLIOptions, text: str, extra_cli_args: Sequence[str] = ()) ->
|
||||
m.index = largest_index - m.index + offset
|
||||
index_map = {m.index: m for m in all_marks}
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
input('Press Enter to quit.')
|
||||
raise SystemExit(1)
|
||||
|
||||
report_unhandled_error()
|
||||
return run_loop(args, text, all_marks, index_map, extra_cli_args)
|
||||
|
||||
|
||||
@@ -677,9 +677,7 @@ def main(args: List[str]) -> Optional[Dict[str, Any]]:
|
||||
text = ''
|
||||
if sys.stdin.isatty():
|
||||
if '--help' not in args and '-h' not in args:
|
||||
print('You must pass the text to be hinted on STDIN', file=sys.stderr)
|
||||
input(_('Press Enter to quit'))
|
||||
return None
|
||||
report_unhandled_error('You must pass the text to be hinted on STDIN')
|
||||
else:
|
||||
text = sys.stdin.buffer.read().decode('utf-8')
|
||||
sys.stdin = open(os.ctermid())
|
||||
@@ -687,18 +685,14 @@ def main(args: List[str]) -> Optional[Dict[str, Any]]:
|
||||
opts, items = parse_hints_args(args[1:])
|
||||
except SystemExit as e:
|
||||
if e.code != 0:
|
||||
print(e.args[0], file=sys.stderr)
|
||||
input(_('Press Enter to quit'))
|
||||
report_unhandled_error(e.args[0])
|
||||
return None
|
||||
if items and not (opts.customize_processing or opts.type == 'linenum'):
|
||||
print('Extra command line arguments present: {}'.format(' '.join(items)), file=sys.stderr)
|
||||
input(_('Press Enter to quit'))
|
||||
report_unhandled_error('Extra command line arguments present: {}'.format(' '.join(items)))
|
||||
try:
|
||||
return run(opts, text, items)
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
input(_('Press Enter to quit'))
|
||||
report_unhandled_error()
|
||||
return None
|
||||
|
||||
|
||||
@@ -752,7 +746,7 @@ def linenum_handle_result(args: List[str], data: Dict[str, Any], target_window_i
|
||||
}[action])(*cmd)
|
||||
|
||||
|
||||
@result_handler(type_of_input='screen-ansi')
|
||||
@result_handler(type_of_input='screen-ansi', has_ready_notification=Hints.overlay_ready_report_needed)
|
||||
def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: BossType) -> None:
|
||||
if data['customize_processing']:
|
||||
m = load_custom_processor(data['customize_processing'])
|
||||
@@ -794,7 +788,7 @@ def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int,
|
||||
if program == '-':
|
||||
w = boss.window_id_map.get(target_window_id)
|
||||
if w is not None:
|
||||
w.paste(joined_text())
|
||||
w.paste_text(joined_text())
|
||||
elif program == '@':
|
||||
set_clipboard_string(joined_text())
|
||||
elif program == '*':
|
||||
|
||||
@@ -23,7 +23,10 @@ def main() -> None:
|
||||
if not sys.stdout.isatty() and '--pretty' not in sys.argv:
|
||||
os.execlp('rg', 'rg', *sys.argv[1:])
|
||||
cmdline = ['rg', '--pretty', '--with-filename'] + sys.argv[1:]
|
||||
p = subprocess.Popen(cmdline, stdout=subprocess.PIPE)
|
||||
try:
|
||||
p = subprocess.Popen(cmdline, stdout=subprocess.PIPE)
|
||||
except FileNotFoundError:
|
||||
raise SystemExit('Could not find the rg executable in your PATH. Is ripgrep installed?')
|
||||
assert p.stdout is not None
|
||||
write: Callable[[bytes], None] = cast(Callable[[bytes], None], sys.stdout.buffer.write)
|
||||
sgr_pat = re.compile(br'\x1b\[.*?m')
|
||||
|
||||
@@ -515,7 +515,12 @@ def main(args: List[str] = sys.argv) -> None:
|
||||
if cli_opts.print_window_size:
|
||||
screen_size_function.cache_clear()
|
||||
with open(os.ctermid()) as tty:
|
||||
ss = screen_size_function(tty)()
|
||||
try:
|
||||
fd = tty.fileno()
|
||||
except AttributeError:
|
||||
# use default value for fd if ctermid is not available
|
||||
fd = None
|
||||
ss = screen_size_function(fd)()
|
||||
print(f'{ss.width}x{ss.height}', end='')
|
||||
raise SystemExit(0)
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ def create_kitten_handler(kitten: str, orig_args: List[str]) -> Any:
|
||||
ans = partial(m['end'], [kitten] + orig_args)
|
||||
setattr(ans, 'type_of_input', getattr(m['end'], 'type_of_input', None))
|
||||
setattr(ans, 'no_ui', getattr(m['end'], 'no_ui', False))
|
||||
setattr(ans, 'has_ready_notification', getattr(m['end'], 'has_ready_notification', False))
|
||||
return ans
|
||||
|
||||
|
||||
@@ -85,32 +86,23 @@ def launch(args: List[str]) -> None:
|
||||
del args[:2]
|
||||
args = [kitten] + args
|
||||
os.environ['KITTY_CONFIG_DIRECTORY'] = config_dir
|
||||
from kittens.tui.operations import Mode, clear_screen, reset_mode
|
||||
set_debug(kitten)
|
||||
m = import_kitten_main_module(config_dir, kitten)
|
||||
try:
|
||||
result = m['start'](args)
|
||||
finally:
|
||||
sys.stdin = sys.__stdin__
|
||||
print(reset_mode(Mode.ALTERNATE_SCREEN) + clear_screen(), end='')
|
||||
if result is not None:
|
||||
import json
|
||||
data = json.dumps(result)
|
||||
print('OK:', len(data), data)
|
||||
import base64
|
||||
data = base64.b85encode(json.dumps(result).encode('utf-8'))
|
||||
sys.stdout.buffer.write(b'\x1bP@kitty-kitten-result|')
|
||||
sys.stdout.buffer.write(data)
|
||||
sys.stdout.buffer.write(b'\x1b\\')
|
||||
sys.stderr.flush()
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def deserialize(output: str) -> Any:
|
||||
import json
|
||||
if output.startswith('OK: '):
|
||||
try:
|
||||
prefix, sz, rest = output.split(' ', 2)
|
||||
return json.loads(rest[:int(sz)])
|
||||
except Exception:
|
||||
raise ValueError(f'Failed to parse kitten output: {output!r}')
|
||||
|
||||
|
||||
def run_kitten(kitten: str, run_name: str = '__main__') -> None:
|
||||
import runpy
|
||||
original_kitten_name = kitten
|
||||
|
||||
@@ -38,7 +38,7 @@ def main(args: List[str]) -> None:
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
input('Press Enter to close.')
|
||||
input('Press Enter to close')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
74
kittens/ssh/config.py
Normal file
74
kittens/ssh/config.py
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
import fnmatch
|
||||
import os
|
||||
from typing import Any, Dict, Iterable, Optional
|
||||
|
||||
from kitty.conf.utils import (
|
||||
load_config as _load_config, parse_config_base, resolve_config
|
||||
)
|
||||
from kitty.constants import config_dir
|
||||
|
||||
from .options.types import Options as SSHOptions, defaults
|
||||
|
||||
SYSTEM_CONF = '/etc/xdg/kitty/ssh.conf'
|
||||
defconf = os.path.join(config_dir, 'ssh.conf')
|
||||
|
||||
|
||||
def host_matches(mpat: str, hostname: str, username: str) -> bool:
|
||||
for pat in mpat.split():
|
||||
upat = '*'
|
||||
if '@' in pat:
|
||||
upat, pat = pat.split('@', 1)
|
||||
if fnmatch.fnmatchcase(hostname, pat) and fnmatch.fnmatchcase(username, upat):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def load_config(*paths: str, overrides: Optional[Iterable[str]] = None, hostname: str = '!', username: str = '') -> SSHOptions:
|
||||
from .options.parse import (
|
||||
create_result_dict, merge_result_dicts, parse_conf_item
|
||||
)
|
||||
from .options.utils import (
|
||||
first_seen_positions, get_per_hosts_dict, init_results_dict
|
||||
)
|
||||
|
||||
def merge_dicts(base: Dict[str, Any], vals: Dict[str, Any]) -> Dict[str, Any]:
|
||||
base_phd = get_per_hosts_dict(base)
|
||||
vals_phd = get_per_hosts_dict(vals)
|
||||
for hostname in base_phd:
|
||||
vals_phd[hostname] = merge_result_dicts(base_phd[hostname], vals_phd.get(hostname, {}))
|
||||
ans: Dict[str, Any] = vals_phd.pop(vals['hostname'])
|
||||
ans['per_host_dicts'] = vals_phd
|
||||
return ans
|
||||
|
||||
def parse_config(lines: Iterable[str]) -> Dict[str, Any]:
|
||||
ans: Dict[str, Any] = init_results_dict(create_result_dict())
|
||||
parse_config_base(lines, parse_conf_item, ans)
|
||||
return ans
|
||||
|
||||
overrides = tuple(overrides) if overrides is not None else ()
|
||||
first_seen_positions.clear()
|
||||
first_seen_positions['*'] = 0
|
||||
opts_dict, paths = _load_config(
|
||||
defaults, parse_config, merge_dicts, *paths, overrides=overrides, initialize_defaults=init_results_dict)
|
||||
phd = get_per_hosts_dict(opts_dict)
|
||||
final_dict: Dict[str, Any] = {}
|
||||
for hostname_pat in sorted(phd, key=first_seen_positions.__getitem__):
|
||||
if host_matches(hostname_pat, hostname, username):
|
||||
od = phd[hostname_pat]
|
||||
for k, v in od.items():
|
||||
if isinstance(v, dict):
|
||||
bv = final_dict.setdefault(k, {})
|
||||
bv.update(v)
|
||||
else:
|
||||
final_dict[k] = v
|
||||
first_seen_positions.clear()
|
||||
return SSHOptions(final_dict)
|
||||
|
||||
|
||||
def init_config(hostname: str, username: str, overrides: Optional[Iterable[str]] = None) -> SSHOptions:
|
||||
config = tuple(resolve_config(SYSTEM_CONF, defconf))
|
||||
return load_config(*config, overrides=overrides, hostname=hostname, username=username)
|
||||
104
kittens/ssh/copy.py
Normal file
104
kittens/ssh/copy.py
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
import glob
|
||||
import os
|
||||
import shlex
|
||||
import uuid
|
||||
from typing import (
|
||||
Dict, Iterable, Iterator, List, NamedTuple, Optional, Sequence, Tuple
|
||||
)
|
||||
|
||||
from kitty.cli import parse_args
|
||||
from kitty.cli_stub import CopyCLIOptions
|
||||
from kitty.types import run_once
|
||||
|
||||
from ..transfer.utils import expand_home, home_path
|
||||
|
||||
|
||||
@run_once
|
||||
def option_text() -> str:
|
||||
return '''
|
||||
--glob
|
||||
type=bool-set
|
||||
Interpret file arguments as glob patterns.
|
||||
|
||||
|
||||
--dest
|
||||
The destination on the remote host to copy to. Relative paths are resolved
|
||||
relative to HOME on the remote host. When this option is not specified, the
|
||||
local file path is used as the remote destination (with the HOME directory
|
||||
getting automatically replaced by the remote HOME). Note that environment
|
||||
variables and ~ are not expanded.
|
||||
|
||||
|
||||
--exclude
|
||||
type=list
|
||||
A glob pattern. Files with names matching this pattern are excluded from being
|
||||
transferred. Useful when adding directories. Can
|
||||
be specified multiple times, if any of the patterns match the file will be
|
||||
excluded.
|
||||
'''
|
||||
|
||||
|
||||
def parse_copy_args(args: Optional[Sequence[str]] = None) -> Tuple[CopyCLIOptions, List[str]]:
|
||||
args = list(args or ())
|
||||
try:
|
||||
opts, args = parse_args(result_class=CopyCLIOptions, args=args, ospec=option_text)
|
||||
except SystemExit as e:
|
||||
raise CopyCLIError from e
|
||||
return opts, args
|
||||
|
||||
|
||||
def resolve_file_spec(spec: str, is_glob: bool) -> Iterator[str]:
|
||||
ans = os.path.expandvars(expand_home(spec))
|
||||
if not os.path.isabs(ans):
|
||||
ans = expand_home(f'~/{ans}')
|
||||
if is_glob:
|
||||
files = glob.glob(ans)
|
||||
if not files:
|
||||
raise CopyCLIError(f'{spec} does not exist')
|
||||
else:
|
||||
if not os.path.exists(ans):
|
||||
raise CopyCLIError(f'{spec} does not exist')
|
||||
files = [ans]
|
||||
for x in files:
|
||||
yield os.path.normpath(x).replace(os.sep, '/')
|
||||
|
||||
|
||||
class CopyCLIError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def get_arcname(loc: str, dest: Optional[str], home: str) -> str:
|
||||
if dest:
|
||||
arcname = dest
|
||||
else:
|
||||
arcname = os.path.normpath(loc)
|
||||
if arcname.startswith(home):
|
||||
arcname = os.path.relpath(arcname, home)
|
||||
arcname = os.path.normpath(arcname).replace(os.sep, '/')
|
||||
prefix = 'root' if arcname.startswith('/') else 'home/'
|
||||
return prefix + arcname
|
||||
|
||||
|
||||
class CopyInstruction(NamedTuple):
|
||||
local_path: str
|
||||
arcname: str
|
||||
exclude_patterns: Tuple[str, ...]
|
||||
|
||||
|
||||
def parse_copy_instructions(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, CopyInstruction]]:
|
||||
opts, args = parse_copy_args(shlex.split(val))
|
||||
locations: List[str] = []
|
||||
for a in args:
|
||||
locations.extend(resolve_file_spec(a, opts.glob))
|
||||
if not locations:
|
||||
raise CopyCLIError('No files to copy specified')
|
||||
if len(locations) > 1 and opts.dest:
|
||||
raise CopyCLIError('Specifying a remote location with more than one file is not supported')
|
||||
home = home_path()
|
||||
for loc in locations:
|
||||
arcname = get_arcname(loc, opts.dest, home)
|
||||
yield str(uuid.uuid4()), CopyInstruction(loc, arcname, tuple(opts.exclude))
|
||||
@@ -1,139 +1,300 @@
|
||||
#!/usr/bin/env python3
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import atexit
|
||||
import fnmatch
|
||||
import glob
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import shlex
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
from typing import List, NoReturn, Optional, Set, Tuple
|
||||
from .completion import ssh_options, complete
|
||||
import tarfile
|
||||
import tempfile
|
||||
import termios
|
||||
import time
|
||||
import traceback
|
||||
from base64 import standard_b64decode, standard_b64encode
|
||||
from contextlib import contextmanager, suppress
|
||||
from getpass import getuser
|
||||
from select import select
|
||||
from typing import (
|
||||
Callable, Dict, Iterator, List, NoReturn, Optional, Sequence, Set, Tuple,
|
||||
Union
|
||||
)
|
||||
|
||||
from kitty.utils import SSHConnectionData
|
||||
from kittens.tui.operations import restore_colors, save_colors
|
||||
from kitty.constants import (
|
||||
cache_dir, runtime_dir, shell_integration_dir, ssh_control_master_template,
|
||||
str_version, terminfo_dir
|
||||
)
|
||||
from kitty.options.types import Options
|
||||
from kitty.shm import SharedMemory
|
||||
from kitty.types import run_once
|
||||
from kitty.utils import SSHConnectionData, set_echo as turn_off_echo, expandvars, resolve_abs_or_config_path
|
||||
|
||||
SHELL_SCRIPT = '''\
|
||||
#!/bin/sh
|
||||
# macOS ships with an ancient version of tic that cannot read from stdin, so we
|
||||
# create a temp file for it
|
||||
tmp=$(mktemp)
|
||||
cat >$tmp << 'TERMEOF'
|
||||
TERMINFO
|
||||
TERMEOF
|
||||
|
||||
tname=.terminfo
|
||||
if [ -e "/usr/share/misc/terminfo.cdb" ]; then
|
||||
# NetBSD requires this see https://github.com/kovidgoyal/kitty/issues/4622
|
||||
tname=".terminfo.cdb"
|
||||
fi
|
||||
tic_out=$(tic -x -o $HOME/$tname $tmp 2>&1)
|
||||
rc=$?
|
||||
rm $tmp
|
||||
if [ "$rc" != "0" ]; then echo "$tic_out"; exit 1; fi
|
||||
if [ -z "$USER" ]; then export USER=$(whoami); fi
|
||||
export TERMINFO="$HOME/$tname"
|
||||
login_shell=""
|
||||
python=""
|
||||
|
||||
login_shell_is_ok() {
|
||||
if [ -z "$login_shell" ] || [ ! -x "$login_shell" ]; then return 1; fi
|
||||
case "$login_shell" in
|
||||
*sh) return 0;
|
||||
esac
|
||||
return 1;
|
||||
}
|
||||
|
||||
detect_python() {
|
||||
python=$(command -v python3)
|
||||
if [ -z "$python" ]; then python=$(command -v python2); fi
|
||||
if [ -z "$python" ]; then python=python; fi
|
||||
}
|
||||
|
||||
using_getent() {
|
||||
cmd=$(command -v getent)
|
||||
if [ -z "$cmd" ]; then return; fi
|
||||
output=$($cmd passwd $USER 2>/dev/null)
|
||||
if [ $? = 0 ]; then login_shell=$(echo $output | cut -d: -f7); fi
|
||||
}
|
||||
|
||||
using_id() {
|
||||
cmd=$(command -v id)
|
||||
if [ -z "$cmd" ]; then return; fi
|
||||
output=$($cmd -P $USER 2>/dev/null)
|
||||
if [ $? = 0 ]; then login_shell=$(echo $output | cut -d: -f7); fi
|
||||
}
|
||||
|
||||
using_passwd() {
|
||||
cmd=$(command -v grep)
|
||||
if [ -z "$cmd" ]; then return; fi
|
||||
output=$($cmd "^$USER:" /etc/passwd 2>/dev/null)
|
||||
if [ $? = 0 ]; then login_shell=$(echo $output | cut -d: -f7); fi
|
||||
}
|
||||
|
||||
using_python() {
|
||||
detect_python
|
||||
if [ ! -x "$python" ]; then return; fi
|
||||
output=$($python -c "import pwd, os; print(pwd.getpwuid(os.geteuid()).pw_shell)")
|
||||
if [ $? = 0 ]; then login_shell=$output; fi
|
||||
}
|
||||
|
||||
execute_with_python() {
|
||||
detect_python
|
||||
exec $python -c "import os; os.execl('$login_shell', '-' '$shell_name')"
|
||||
}
|
||||
|
||||
die() { echo "$*" 1>&2 ; exit 1; }
|
||||
|
||||
using_getent
|
||||
if ! login_shell_is_ok; then using_id; fi
|
||||
if ! login_shell_is_ok; then using_python; fi
|
||||
if ! login_shell_is_ok; then using_passwd; fi
|
||||
if ! login_shell_is_ok; then die "Could not detect login shell"; fi
|
||||
from .completion import complete, ssh_options
|
||||
from .config import init_config
|
||||
from .copy import CopyInstruction
|
||||
from .options.types import Options as SSHOptions
|
||||
from .options.utils import DELETE_ENV_VAR
|
||||
|
||||
|
||||
# If a command was passed to SSH execute it here
|
||||
EXEC_CMD
|
||||
|
||||
# We need to pass the first argument to the executed program with a leading -
|
||||
# to make sure the shell executes as a login shell. Note that not all shells
|
||||
# support exec -a so we use the below to try to detect such shells
|
||||
shell_name=$(basename $login_shell)
|
||||
if [ -z "$PIPESTATUS" ]; then
|
||||
# the dash shell does not support exec -a and also does not define PIPESTATUS
|
||||
execute_with_python
|
||||
fi
|
||||
exec -a "-$shell_name" $login_shell
|
||||
'''
|
||||
@run_once
|
||||
def ssh_exe() -> str:
|
||||
return shutil.which('ssh') or 'ssh'
|
||||
|
||||
|
||||
PYTHON_SCRIPT = '''\
|
||||
#!/usr/bin/env python
|
||||
from __future__ import print_function
|
||||
from tempfile import NamedTemporaryFile
|
||||
import subprocess, os, sys, pwd, binascii, json
|
||||
def is_kitten_cmdline(q: List[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']
|
||||
|
||||
# macOS ships with an ancient version of tic that cannot read from stdin, so we
|
||||
# create a temp file for it
|
||||
with NamedTemporaryFile() as tmp:
|
||||
tname = '.terminfo'
|
||||
if os.path.exists('/usr/share/misc/terminfo.cdb'):
|
||||
tname += '.cdb'
|
||||
tmp.write(binascii.unhexlify('{terminfo}'))
|
||||
tmp.flush()
|
||||
p = subprocess.Popen(['tic', '-x', '-o', os.path.expanduser('~/' + tname), tmp.name], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
if p.wait() != 0:
|
||||
getattr(sys.stderr, 'buffer', sys.stderr).write(stdout + stderr)
|
||||
raise SystemExit('Failed to compile terminfo using tic')
|
||||
command_to_execute = json.loads(binascii.unhexlify('{command_to_execute}'))
|
||||
try:
|
||||
shell_path = pwd.getpwuid(os.geteuid()).pw_shell or '/bin/sh'
|
||||
except KeyError:
|
||||
shell_path = '/bin/sh'
|
||||
shell_name = '-' + os.path.basename(shell_path)
|
||||
if command_to_execute:
|
||||
os.execlp(shell_path, shell_path, '-c', command_to_execute)
|
||||
os.execlp(shell_path, shell_name)
|
||||
'''
|
||||
|
||||
def set_cwd_in_cmdline(cwd: str, argv: List[str]) -> None:
|
||||
for i, arg in enumerate(tuple(argv)):
|
||||
if arg.startswith('--kitten=cwd'):
|
||||
argv[i] = f'--kitten=cwd={cwd}'
|
||||
return
|
||||
elif i > 0 and argv[i-1] == '--kitten' and (arg.startswith('cwd=') or arg.startswith('cwd ')):
|
||||
argv[i] = cwd
|
||||
return
|
||||
idx = argv.index('ssh')
|
||||
argv.insert(idx + 1, f'--kitten=cwd={cwd}')
|
||||
|
||||
|
||||
# See https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html
|
||||
quote_pat = re.compile('([\\`"\n])')
|
||||
|
||||
|
||||
def quote_env_val(x: str) -> str:
|
||||
x = quote_pat.sub(r'\\\1', x)
|
||||
x = x.replace('$(', r'\$(') # prevent execution with $()
|
||||
return f'"{x}"'
|
||||
|
||||
|
||||
def serialize_env(env: Dict[str, str], base_env: Dict[str, str]) -> bytes:
|
||||
lines = []
|
||||
|
||||
def a(k: str, val: str) -> None:
|
||||
lines.append(f'export {shlex.quote(k)}={quote_env_val(val)}')
|
||||
|
||||
for k in sorted(env):
|
||||
v = env[k]
|
||||
if v == DELETE_ENV_VAR:
|
||||
lines.append(f'unset {shlex.quote(k)}')
|
||||
elif v == '_kitty_copy_env_var_':
|
||||
q = base_env.get(k)
|
||||
if q is not None:
|
||||
a(k, q)
|
||||
else:
|
||||
a(k, v)
|
||||
return '\n'.join(lines).encode('utf-8')
|
||||
|
||||
|
||||
@run_once
|
||||
def kitty_opts() -> Options:
|
||||
from kitty.cli import create_default_opts
|
||||
return create_default_opts()
|
||||
|
||||
|
||||
def make_tarfile(ssh_opts: SSHOptions, base_env: Dict[str, str], compression: str = 'gz') -> bytes:
|
||||
|
||||
def normalize_tarinfo(tarinfo: tarfile.TarInfo) -> tarfile.TarInfo:
|
||||
tarinfo.uname = tarinfo.gname = ''
|
||||
tarinfo.uid = tarinfo.gid = 0
|
||||
return tarinfo
|
||||
|
||||
def add_data_as_file(tf: tarfile.TarFile, arcname: str, data: Union[str, bytes]) -> tarfile.TarInfo:
|
||||
ans = tarfile.TarInfo(arcname)
|
||||
ans.mtime = 0
|
||||
ans.type = tarfile.REGTYPE
|
||||
if isinstance(data, str):
|
||||
data = data.encode('utf-8')
|
||||
ans.size = len(data)
|
||||
normalize_tarinfo(ans)
|
||||
tf.addfile(ans, io.BytesIO(data))
|
||||
return ans
|
||||
|
||||
def filter_from_globs(*pats: str) -> Callable[[tarfile.TarInfo], Optional[tarfile.TarInfo]]:
|
||||
def filter(tarinfo: tarfile.TarInfo) -> Optional[tarfile.TarInfo]:
|
||||
for junk_dir in ('.DS_Store', '__pycache__'):
|
||||
for pat in (f'*/{junk_dir}', f'*/{junk_dir}/*'):
|
||||
if fnmatch.fnmatch(tarinfo.name, pat):
|
||||
return None
|
||||
for pat in pats:
|
||||
if fnmatch.fnmatch(tarinfo.name, pat):
|
||||
return None
|
||||
return normalize_tarinfo(tarinfo)
|
||||
return filter
|
||||
|
||||
from kitty.shell_integration import get_effective_ksi_env_var
|
||||
if ssh_opts.shell_integration == 'inherited':
|
||||
ksi = get_effective_ksi_env_var(kitty_opts())
|
||||
else:
|
||||
from kitty.options.utils import shell_integration
|
||||
ksi = get_effective_ksi_env_var(Options({'shell_integration': shell_integration(ssh_opts.shell_integration)}))
|
||||
|
||||
env = {
|
||||
'TERM': os.environ.get('TERM') or kitty_opts().term,
|
||||
'COLORTERM': 'truecolor',
|
||||
}
|
||||
for q in ('KITTY_WINDOW_ID', 'WINDOWID'):
|
||||
val = os.environ.get(q)
|
||||
if val is not None:
|
||||
env[q] = val
|
||||
env.update(ssh_opts.env)
|
||||
env['KITTY_SHELL_INTEGRATION'] = ksi or DELETE_ENV_VAR
|
||||
env['KITTY_SSH_KITTEN_DATA_DIR'] = ssh_opts.remote_dir
|
||||
if ssh_opts.login_shell:
|
||||
env['KITTY_LOGIN_SHELL'] = ssh_opts.login_shell
|
||||
if ssh_opts.cwd:
|
||||
env['KITTY_LOGIN_CWD'] = ssh_opts.cwd
|
||||
if ssh_opts.remote_kitty != 'no':
|
||||
env['KITTY_REMOTE'] = ssh_opts.remote_kitty
|
||||
env_script = serialize_env(env, base_env)
|
||||
buf = io.BytesIO()
|
||||
with tarfile.open(mode=f'w:{compression}', fileobj=buf, encoding='utf-8') as tf:
|
||||
rd = ssh_opts.remote_dir.rstrip('/')
|
||||
for ci in ssh_opts.copy.values():
|
||||
tf.add(ci.local_path, arcname=ci.arcname, filter=filter_from_globs(*ci.exclude_patterns))
|
||||
add_data_as_file(tf, 'data.sh', env_script)
|
||||
if compression == 'gz':
|
||||
tf.add(f'{shell_integration_dir}/ssh/bootstrap-utils.sh', arcname='bootstrap-utils.sh', filter=normalize_tarinfo)
|
||||
if ksi:
|
||||
arcname = 'home/' + rd + '/shell-integration'
|
||||
tf.add(shell_integration_dir, arcname=arcname, filter=filter_from_globs(
|
||||
f'{arcname}/ssh/*', # bootstrap files are sent as command line args
|
||||
f'{arcname}/zsh/kitty.zsh', # present for legacy compat not needed by ssh kitten
|
||||
))
|
||||
if ssh_opts.remote_kitty != 'no':
|
||||
arcname = 'home/' + rd + '/kitty'
|
||||
add_data_as_file(tf, arcname + '/version', str_version.encode('ascii'))
|
||||
tf.add(shell_integration_dir + '/ssh/kitty', arcname=arcname + '/bin/kitty', filter=normalize_tarinfo)
|
||||
tf.add(f'{terminfo_dir}/kitty.terminfo', arcname='home/.terminfo/kitty.terminfo', filter=normalize_tarinfo)
|
||||
tf.add(glob.glob(f'{terminfo_dir}/*/xterm-kitty')[0], arcname='home/.terminfo/x/xterm-kitty', filter=normalize_tarinfo)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def get_ssh_data(msg: str, request_id: str) -> Iterator[bytes]:
|
||||
yield b'\nKITTY_DATA_START\n' # to discard leading data
|
||||
try:
|
||||
msg = standard_b64decode(msg).decode('utf-8')
|
||||
md = dict(x.split('=', 1) for x in msg.split(':'))
|
||||
pw = md['pw']
|
||||
pwfilename = md['pwfile']
|
||||
rq_id = md['id']
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
yield b'invalid ssh data request message\n'
|
||||
else:
|
||||
try:
|
||||
with SharedMemory(pwfilename, readonly=True) as shm:
|
||||
shm.unlink()
|
||||
if shm.stats.st_uid != os.geteuid() or shm.stats.st_gid != os.getegid():
|
||||
raise ValueError('Incorrect owner on pwfile')
|
||||
mode = stat.S_IMODE(shm.stats.st_mode)
|
||||
if mode != stat.S_IREAD:
|
||||
raise ValueError('Incorrect permissions on pwfile')
|
||||
env_data = json.loads(shm.read_data_with_size())
|
||||
if pw != env_data['pw']:
|
||||
raise ValueError('Incorrect password')
|
||||
if rq_id != request_id:
|
||||
raise ValueError('Incorrect request id')
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
yield f'{e}\n'.encode('utf-8')
|
||||
else:
|
||||
yield b'OK\n'
|
||||
ssh_opts = SSHOptions(env_data['opts'])
|
||||
ssh_opts.copy = {k: CopyInstruction(*v) for k, v in ssh_opts.copy.items()}
|
||||
encoded_data = memoryview(env_data['tarfile'].encode('ascii'))
|
||||
# macOS has a 255 byte limit on its input queue as per man stty.
|
||||
# Not clear if that applies to canonical mode input as well, but
|
||||
# better to be safe.
|
||||
line_sz = 254
|
||||
while encoded_data:
|
||||
yield encoded_data[:line_sz]
|
||||
yield b'\n'
|
||||
encoded_data = encoded_data[line_sz:]
|
||||
yield b'KITTY_DATA_END\n'
|
||||
|
||||
|
||||
def safe_remove(x: str) -> None:
|
||||
with suppress(OSError):
|
||||
os.remove(x)
|
||||
|
||||
|
||||
def prepare_script(ans: str, replacements: Dict[str, str], script_type: str) -> str:
|
||||
for k in ('EXEC_CMD', 'EXPORT_HOME_CMD'):
|
||||
replacements[k] = replacements.get(k, '')
|
||||
|
||||
def sub(m: 're.Match[str]') -> str:
|
||||
return replacements[m.group()]
|
||||
|
||||
return re.sub('|'.join(fr'\b{k}\b' for k in replacements), sub, ans)
|
||||
|
||||
|
||||
def prepare_exec_cmd(remote_args: Sequence[str], is_python: bool) -> str:
|
||||
# ssh simply concatenates multiple commands using a space see
|
||||
# line 1129 of ssh.c and on the remote side sshd.c runs the
|
||||
# concatenated command as shell -c cmd
|
||||
if is_python:
|
||||
return standard_b64encode(' '.join(remote_args).encode('utf-8')).decode('ascii')
|
||||
args = ' '.join(c.replace("'", """'"'"'""") for c in remote_args)
|
||||
return f"""unset KITTY_SHELL_INTEGRATION; exec "$login_shell" -c '{args}'"""
|
||||
|
||||
|
||||
def prepare_export_home_cmd(ssh_opts: SSHOptions, is_python: bool) -> str:
|
||||
home = ssh_opts.env.get('HOME')
|
||||
if home == '_kitty_copy_env_var_':
|
||||
home = os.environ.get('HOME')
|
||||
if home:
|
||||
if is_python:
|
||||
return standard_b64encode(home.encode('utf-8')).decode('ascii')
|
||||
else:
|
||||
return f'export HOME={quote_env_val(home)}; cd "$HOME"'
|
||||
return ''
|
||||
|
||||
|
||||
def bootstrap_script(
|
||||
ssh_opts: SSHOptions, script_type: str = 'sh', remote_args: Sequence[str] = (),
|
||||
test_script: str = '', request_id: Optional[str] = None, cli_hostname: str = '', cli_uname: str = '',
|
||||
request_data: bool = False, echo_on: bool = True
|
||||
) -> Tuple[str, Dict[str, str], SharedMemory]:
|
||||
if request_id is None:
|
||||
request_id = os.environ['KITTY_PID'] + '-' + os.environ['KITTY_WINDOW_ID']
|
||||
is_python = script_type == 'py'
|
||||
export_home_cmd = prepare_export_home_cmd(ssh_opts, is_python) if 'HOME' in ssh_opts.env else ''
|
||||
exec_cmd = prepare_exec_cmd(remote_args, is_python) if remote_args else ''
|
||||
with open(os.path.join(shell_integration_dir, 'ssh', f'bootstrap.{script_type}')) as f:
|
||||
ans = f.read()
|
||||
pw = secrets.token_hex()
|
||||
tfd = standard_b64encode(make_tarfile(ssh_opts, dict(os.environ), 'gz' if script_type == 'sh' else 'bz2')).decode('ascii')
|
||||
data = {'pw': pw, 'opts': ssh_opts._asdict(), 'hostname': cli_hostname, 'uname': cli_uname, 'tarfile': tfd}
|
||||
db = json.dumps(data)
|
||||
with SharedMemory(size=len(db) + SharedMemory.num_bytes_for_size, mode=stat.S_IREAD, prefix=f'kssh-{os.getpid()}-') as shm:
|
||||
shm.write_data_with_size(db)
|
||||
shm.flush()
|
||||
atexit.register(shm.unlink)
|
||||
sensitive_data = {'REQUEST_ID': request_id, 'DATA_PASSWORD': pw, 'PASSWORD_FILENAME': shm.name}
|
||||
replacements = {
|
||||
'EXPORT_HOME_CMD': export_home_cmd,
|
||||
'EXEC_CMD': exec_cmd, 'TEST_SCRIPT': test_script,
|
||||
'REQUEST_DATA': '1' if request_data else '0', 'ECHO_ON': '1' if echo_on else '0',
|
||||
}
|
||||
sd = replacements.copy()
|
||||
if request_data:
|
||||
sd.update(sensitive_data)
|
||||
replacements.update(sensitive_data)
|
||||
return prepare_script(ans, sd, script_type), replacements, shm
|
||||
|
||||
|
||||
def get_ssh_cli() -> Tuple[Set[str], Set[str]]:
|
||||
@@ -148,13 +309,22 @@ def get_ssh_cli() -> Tuple[Set[str], Set[str]]:
|
||||
return boolean_ssh_args, other_ssh_args
|
||||
|
||||
|
||||
def get_connection_data(args: List[str], cwd: str = '') -> Optional[SSHConnectionData]:
|
||||
def is_extra_arg(arg: str, extra_args: Tuple[str, ...]) -> str:
|
||||
for x in extra_args:
|
||||
if arg == x or arg.startswith(f'{x}='):
|
||||
return x
|
||||
return ''
|
||||
|
||||
|
||||
def get_connection_data(args: List[str], cwd: str = '', extra_args: Tuple[str, ...] = ()) -> Optional[SSHConnectionData]:
|
||||
boolean_ssh_args, other_ssh_args = get_ssh_cli()
|
||||
port: Optional[int] = None
|
||||
expecting_port = expecting_identity = False
|
||||
expecting_option_val = False
|
||||
expecting_hostname = False
|
||||
expecting_extra_val = ''
|
||||
host_name = identity_file = found_ssh = ''
|
||||
found_extra_args: List[Tuple[str, str]] = []
|
||||
|
||||
for i, arg in enumerate(args):
|
||||
if not found_ssh:
|
||||
@@ -182,6 +352,15 @@ def get_connection_data(args: List[str], cwd: str = '') -> Optional[SSHConnectio
|
||||
else:
|
||||
identity_file = arg[2:]
|
||||
continue
|
||||
if arg.startswith('--') and extra_args:
|
||||
matching_ex = is_extra_arg(arg, extra_args)
|
||||
if matching_ex:
|
||||
if '=' in arg:
|
||||
exval = arg.partition('=')[-1]
|
||||
found_extra_args.append((matching_ex, exval))
|
||||
continue
|
||||
expecting_extra_val = matching_ex
|
||||
|
||||
expecting_option_val = True
|
||||
continue
|
||||
|
||||
@@ -192,6 +371,9 @@ def get_connection_data(args: List[str], cwd: str = '') -> Optional[SSHConnectio
|
||||
expecting_port = False
|
||||
elif expecting_identity:
|
||||
identity_file = arg
|
||||
elif expecting_extra_val:
|
||||
found_extra_args.append((expecting_extra_val, arg))
|
||||
expecting_extra_val = ''
|
||||
expecting_option_val = False
|
||||
continue
|
||||
|
||||
@@ -199,13 +381,22 @@ def get_connection_data(args: List[str], cwd: str = '') -> Optional[SSHConnectio
|
||||
host_name = arg
|
||||
if not host_name:
|
||||
return None
|
||||
if host_name.startswith('ssh://'):
|
||||
from urllib.parse import urlparse
|
||||
purl = urlparse(host_name)
|
||||
if purl.hostname:
|
||||
host_name = purl.hostname
|
||||
if purl.username:
|
||||
host_name = f'{purl.username}@{host_name}'
|
||||
if port is None and purl.port:
|
||||
port = purl.port
|
||||
if identity_file:
|
||||
if not os.path.isabs(identity_file):
|
||||
identity_file = os.path.expanduser(identity_file)
|
||||
if not os.path.isabs(identity_file):
|
||||
identity_file = os.path.normpath(os.path.join(cwd or os.getcwd(), identity_file))
|
||||
|
||||
return SSHConnectionData(found_ssh, host_name, port, identity_file)
|
||||
return SSHConnectionData(found_ssh, host_name, port, identity_file, tuple(found_extra_args))
|
||||
|
||||
|
||||
class InvalidSSHArgs(ValueError):
|
||||
@@ -217,17 +408,19 @@ class InvalidSSHArgs(ValueError):
|
||||
def system_exit(self) -> None:
|
||||
if self.err_msg:
|
||||
print(self.err_msg, file=sys.stderr)
|
||||
os.execlp('ssh', 'ssh')
|
||||
os.execlp(ssh_exe(), 'ssh')
|
||||
|
||||
|
||||
def parse_ssh_args(args: List[str]) -> Tuple[List[str], List[str], bool]:
|
||||
def parse_ssh_args(args: List[str], extra_args: Tuple[str, ...] = ()) -> Tuple[List[str], List[str], bool, Tuple[str, ...]]:
|
||||
boolean_ssh_args, other_ssh_args = get_ssh_cli()
|
||||
passthrough_args = {f'-{x}' for x in 'Nnf'}
|
||||
passthrough_args = {f'-{x}' for x in 'NnfG'}
|
||||
ssh_args = []
|
||||
server_args: List[str] = []
|
||||
expecting_option_val = False
|
||||
passthrough = False
|
||||
stop_option_processing = False
|
||||
found_extra_args: List[str] = []
|
||||
expecting_extra_val = ''
|
||||
for argument in args:
|
||||
if len(server_args) > 1 or stop_option_processing:
|
||||
server_args.append(argument)
|
||||
@@ -236,6 +429,16 @@ def parse_ssh_args(args: List[str]) -> Tuple[List[str], List[str], bool]:
|
||||
if argument == '--':
|
||||
stop_option_processing = True
|
||||
continue
|
||||
if extra_args:
|
||||
matching_ex = is_extra_arg(argument, extra_args)
|
||||
if matching_ex:
|
||||
if '=' in argument:
|
||||
exval = argument.partition('=')[-1]
|
||||
found_extra_args.extend((matching_ex, exval))
|
||||
else:
|
||||
expecting_extra_val = matching_ex
|
||||
expecting_option_val = True
|
||||
continue
|
||||
# could be a multi-character option
|
||||
all_args = argument[1:]
|
||||
for i, arg in enumerate(all_args):
|
||||
@@ -256,75 +459,264 @@ def parse_ssh_args(args: List[str]) -> Tuple[List[str], List[str], bool]:
|
||||
raise InvalidSSHArgs(f'unknown option -- {arg[1:]}')
|
||||
continue
|
||||
if expecting_option_val:
|
||||
ssh_args.append(argument)
|
||||
if expecting_extra_val:
|
||||
found_extra_args.extend((expecting_extra_val, argument))
|
||||
expecting_extra_val = ''
|
||||
else:
|
||||
ssh_args.append(argument)
|
||||
expecting_option_val = False
|
||||
continue
|
||||
server_args.append(argument)
|
||||
if not server_args:
|
||||
raise InvalidSSHArgs()
|
||||
return ssh_args, server_args, passthrough
|
||||
return ssh_args, server_args, passthrough, tuple(found_extra_args)
|
||||
|
||||
|
||||
def quote(x: str) -> str:
|
||||
# we have to escape unbalanced quotes and other unparsable
|
||||
# args as they will break the shell script
|
||||
# But we do not want to quote things like * or 'echo hello'
|
||||
# See https://github.com/kovidgoyal/kitty/issues/1787
|
||||
try:
|
||||
shlex.split(x)
|
||||
except ValueError:
|
||||
x = shlex.quote(x)
|
||||
return x
|
||||
def wrap_bootstrap_script(sh_script: str, interpreter: str) -> List[str]:
|
||||
# sshd will execute the command we pass it by join all command line
|
||||
# arguments with a space and passing it as a single argument to the users
|
||||
# login shell with -c. If the user has a non POSIX login shell it might
|
||||
# have different escaping semantics and syntax, so the command it should
|
||||
# execute has to be as simple as possible, basically of the form
|
||||
# interpreter -c unwrap_script escaped_bootstrap_script
|
||||
# The unwrap_script is responsible for unescaping the bootstrap script and
|
||||
# executing it.
|
||||
q = os.path.basename(interpreter).lower()
|
||||
is_python = 'python' in q
|
||||
if is_python:
|
||||
es = standard_b64encode(sh_script.encode('utf-8')).decode('ascii')
|
||||
unwrap_script = '''"import base64, sys; eval(compile(base64.standard_b64decode(sys.argv[-1]), 'bootstrap.py', 'exec'))"'''
|
||||
else:
|
||||
# We cant rely on base64 being available on the remote system, so instead
|
||||
# we quote the bootstrap script by replacing ' and \ with \v and \f
|
||||
# also replacing \n and ! with \r and \b for tcsh
|
||||
# finally surrounding with '
|
||||
es = "'" + sh_script.replace("'", '\v').replace('\\', '\f').replace('\n', '\r').replace('!', '\b') + "'"
|
||||
unwrap_script = r"""'eval "$(echo "$0" | tr \\\v\\\f\\\r\\\b \\\047\\\134\\\n\\\041)"' """
|
||||
# exec is supported by all sh like shells, and fish and csh
|
||||
return ['exec', interpreter, '-c', unwrap_script, es]
|
||||
|
||||
|
||||
def get_posix_cmd(terminfo: str, remote_args: List[str]) -> List[str]:
|
||||
sh_script = SHELL_SCRIPT.replace('TERMINFO', terminfo, 1)
|
||||
command_to_execute = ''
|
||||
if remote_args:
|
||||
# ssh simply concatenates multiple commands using a space see
|
||||
# line 1129 of ssh.c and on the remote side sshd.c runs the
|
||||
# concatenated command as shell -c cmd
|
||||
args = [c.replace("'", """'"'"'""") for c in remote_args]
|
||||
command_to_execute = "exec $login_shell -c '{}'".format(' '.join(args))
|
||||
sh_script = sh_script.replace('EXEC_CMD', command_to_execute)
|
||||
return [f'sh -c {shlex.quote(sh_script)}']
|
||||
def get_remote_command(
|
||||
remote_args: List[str], ssh_opts: SSHOptions, cli_hostname: str = '', cli_uname: str = '',
|
||||
echo_on: bool = True, request_data: bool = False
|
||||
) -> Tuple[List[str], Dict[str, str], str]:
|
||||
interpreter = ssh_opts.interpreter
|
||||
q = os.path.basename(interpreter).lower()
|
||||
is_python = 'python' in q
|
||||
sh_script, replacements, shm = bootstrap_script(
|
||||
ssh_opts, script_type='py' if is_python else 'sh', remote_args=remote_args,
|
||||
cli_hostname=cli_hostname, cli_uname=cli_uname, echo_on=echo_on, request_data=request_data)
|
||||
return wrap_bootstrap_script(sh_script, interpreter), replacements, shm.name
|
||||
|
||||
|
||||
def get_python_cmd(terminfo: str, command_to_execute: List[str]) -> List[str]:
|
||||
import json
|
||||
script = PYTHON_SCRIPT.format(
|
||||
terminfo=terminfo.encode('utf-8').hex(),
|
||||
command_to_execute=json.dumps(' '.join(command_to_execute)).encode('utf-8').hex()
|
||||
)
|
||||
return [f'python -c "{script}"']
|
||||
def connection_sharing_args(opts: SSHOptions, 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
|
||||
# ~104 chars. macOS has no system runtime dir so we use a cache dir in
|
||||
# /Users/WHY_DOES_ANYONE_USE_MACOS/Library/Caches/APPLE_ARE_IDIOTIC
|
||||
if len(rd) > 35 and os.path.isdir('/tmp'):
|
||||
idiotic_design = f'/tmp/kssh-rdir-{os.getuid()}'
|
||||
try:
|
||||
os.symlink(rd, idiotic_design)
|
||||
except FileExistsError:
|
||||
try:
|
||||
dest = os.readlink(idiotic_design)
|
||||
except OSError as e:
|
||||
raise ValueError(f'The {idiotic_design} symlink could not be created as something with that name exists already') from e
|
||||
else:
|
||||
if dest != rd:
|
||||
with tempfile.TemporaryDirectory(dir='/tmp') as tdir:
|
||||
tlink = os.path.join(tdir, 'sigh')
|
||||
os.symlink(rd, tlink)
|
||||
os.rename(tlink, idiotic_design)
|
||||
rd = idiotic_design
|
||||
|
||||
cp = os.path.join(rd, ssh_control_master_template.format(kitty_pid=kitty_pid, ssh_placeholder='%C'))
|
||||
ans: List[str] = [
|
||||
'-o', 'ControlMaster=auto',
|
||||
'-o', f'ControlPath={cp}',
|
||||
'-o', 'ControlPersist=yes',
|
||||
'-o', 'ServerAliveInterval=60',
|
||||
'-o', 'ServerAliveCountMax=5',
|
||||
'-o', 'TCPKeepAlive=no',
|
||||
]
|
||||
return ans
|
||||
|
||||
|
||||
@contextmanager
|
||||
def restore_terminal_state() -> Iterator[bool]:
|
||||
with open(os.ctermid()) as f:
|
||||
val = termios.tcgetattr(f.fileno())
|
||||
try:
|
||||
yield bool(val[3] & termios.ECHO)
|
||||
finally:
|
||||
termios.tcsetattr(f.fileno(), termios.TCSAFLUSH, val)
|
||||
|
||||
|
||||
def dcs_to_kitty(payload: Union[bytes, str], type: str = 'ssh') -> bytes:
|
||||
if isinstance(payload, str):
|
||||
payload = payload.encode('utf-8')
|
||||
payload = standard_b64encode(payload)
|
||||
return b'\033P@kitty-' + type.encode('ascii') + b'|' + payload + b'\033\\'
|
||||
|
||||
|
||||
@run_once
|
||||
def ssh_version() -> Tuple[int, int]:
|
||||
o = subprocess.check_output([ssh_exe(), '-V'], stderr=subprocess.STDOUT).decode()
|
||||
m = re.match(r'OpenSSH_(\d+).(\d+)', o)
|
||||
if m is None:
|
||||
raise ValueError(f'Invalid version string for OpenSSH: {o}')
|
||||
return int(m.group(1)), int(m.group(2))
|
||||
|
||||
|
||||
@contextmanager
|
||||
def drain_potential_tty_garbage(p: 'subprocess.Popen[bytes]', data_request: str) -> Iterator[None]:
|
||||
ssh_started_at = time.monotonic()
|
||||
with open(os.open(os.ctermid(), os.O_CLOEXEC | os.O_RDWR | os.O_NOCTTY), 'wb') as tty:
|
||||
if data_request:
|
||||
turn_off_echo(tty.fileno())
|
||||
tty.write(dcs_to_kitty(data_request))
|
||||
tty.flush()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if p.returncode and time.monotonic() - ssh_started_at < 30:
|
||||
# discard queued input data on tty in case data transmission was
|
||||
# interrupted due to SSH failure, avoids spewing garbage to
|
||||
# screen
|
||||
data = b''
|
||||
give_up_at = time.monotonic() + 1
|
||||
tty_fd = tty.fileno()
|
||||
while time.monotonic() < give_up_at and b'KITTY_DATA_END' not in data:
|
||||
rd, wr, err = select([tty_fd], [], [tty_fd], max(0, give_up_at - time.monotonic()))
|
||||
if err or not rd:
|
||||
break
|
||||
q = os.read(tty_fd, io.DEFAULT_BUFFER_SIZE)
|
||||
if not q:
|
||||
break
|
||||
data += q
|
||||
|
||||
|
||||
def change_colors(color_scheme: str) -> bool:
|
||||
if not color_scheme:
|
||||
return False
|
||||
from kittens.themes.collection import load_themes, NoCacheFound, text_as_opts
|
||||
from kittens.themes.main import colors_as_escape_codes
|
||||
if color_scheme.endswith('.conf'):
|
||||
conf_file = resolve_abs_or_config_path(color_scheme)
|
||||
try:
|
||||
with open(conf_file) as f:
|
||||
opts = text_as_opts(f.read())
|
||||
except FileNotFoundError:
|
||||
raise SystemExit(f'Failed to find the color conf file: {expandvars(conf_file)}')
|
||||
else:
|
||||
try:
|
||||
themes = load_themes(-1)
|
||||
except NoCacheFound:
|
||||
themes = load_themes()
|
||||
cs = expandvars(color_scheme)
|
||||
try:
|
||||
theme = themes[cs]
|
||||
except KeyError:
|
||||
raise SystemExit(f'Failed to find the color theme: {cs}')
|
||||
opts = theme.kitty_opts
|
||||
raw = colors_as_escape_codes(opts)
|
||||
print(save_colors(), sep='', end=raw, flush=True)
|
||||
return True
|
||||
|
||||
|
||||
def run_ssh(ssh_args: List[str], server_args: List[str], found_extra_args: Tuple[str, ...]) -> NoReturn:
|
||||
cmd = [ssh_exe()] + ssh_args
|
||||
hostname, remote_args = server_args[0], server_args[1:]
|
||||
if not remote_args:
|
||||
cmd.append('-t')
|
||||
insertion_point = len(cmd)
|
||||
cmd.append('--')
|
||||
cmd.append(hostname)
|
||||
uname = getuser()
|
||||
if hostname.startswith('ssh://'):
|
||||
from urllib.parse import urlparse
|
||||
purl = urlparse(hostname)
|
||||
hostname_for_match = purl.hostname or hostname[6:].split('/', 1)[0]
|
||||
uname = purl.username or uname
|
||||
elif '@' in hostname and hostname[0] != '@':
|
||||
uname, hostname_for_match = hostname.split('@', 1)
|
||||
else:
|
||||
hostname_for_match = hostname
|
||||
hostname_for_match = hostname_for_match.split('@', 1)[-1].split(':', 1)[0]
|
||||
overrides = []
|
||||
pat = re.compile(r'^([a-zA-Z0-9_]+)[ \t]*=')
|
||||
for i, a in enumerate(found_extra_args):
|
||||
if i % 2 == 1:
|
||||
aq = pat.sub(r'\1 ', a.lstrip())
|
||||
if aq.split(maxsplit=1)[0] != 'hostname':
|
||||
overrides.append(aq)
|
||||
if overrides:
|
||||
overrides.insert(0, f'hostname {uname}@{hostname_for_match}')
|
||||
host_opts = init_config(hostname_for_match, uname, overrides)
|
||||
if host_opts.share_connections:
|
||||
cmd[insertion_point:insertion_point] = connection_sharing_args(host_opts, int(os.environ['KITTY_PID']))
|
||||
use_kitty_askpass = host_opts.askpass == 'native' or (host_opts.askpass == 'unless-set' and 'SSH_ASKPASS' not in os.environ)
|
||||
need_to_request_data = True
|
||||
if use_kitty_askpass:
|
||||
sentinel = os.path.join(cache_dir(), 'openssh-is-new-enough-for-askpass')
|
||||
sentinel_exists = os.path.exists(sentinel)
|
||||
if sentinel_exists or ssh_version() >= (8, 4):
|
||||
if not sentinel_exists:
|
||||
open(sentinel, 'w').close()
|
||||
# SSH_ASKPASS_REQUIRE was introduced in 8.4 release on 2020-09-27
|
||||
need_to_request_data = False
|
||||
os.environ['SSH_ASKPASS_REQUIRE'] = 'force'
|
||||
os.environ['SSH_ASKPASS'] = os.path.join(shell_integration_dir, 'ssh', 'askpass.py')
|
||||
if need_to_request_data and host_opts.share_connections:
|
||||
cp = subprocess.run(cmd[:1] + ['-O', 'check'] + cmd[1:], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
if cp.returncode == 0:
|
||||
# we will use the master connection so SSH does not need to use the tty
|
||||
need_to_request_data = False
|
||||
with restore_terminal_state() as echo_on:
|
||||
rcmd, replacements, shm_name = get_remote_command(
|
||||
remote_args, host_opts, hostname_for_match, uname, echo_on, request_data=need_to_request_data)
|
||||
cmd += rcmd
|
||||
colors_changed = change_colors(host_opts.color_scheme)
|
||||
try:
|
||||
p = subprocess.Popen(cmd)
|
||||
except FileNotFoundError:
|
||||
raise SystemExit('Could not find the ssh executable, is it in your PATH?')
|
||||
else:
|
||||
rq = '' if need_to_request_data else 'id={REQUEST_ID}:pwfile={PASSWORD_FILENAME}:pw={DATA_PASSWORD}'.format(**replacements)
|
||||
with drain_potential_tty_garbage(p, rq):
|
||||
try:
|
||||
raise SystemExit(p.wait())
|
||||
except KeyboardInterrupt:
|
||||
raise SystemExit(1)
|
||||
finally:
|
||||
if colors_changed:
|
||||
print(end=restore_colors(), flush=True)
|
||||
|
||||
|
||||
def main(args: List[str]) -> NoReturn:
|
||||
args = args[1:]
|
||||
use_posix = True
|
||||
if args and args[0] == 'use-python':
|
||||
args = args[1:]
|
||||
use_posix = False
|
||||
args = args[1:] # backwards compat from when we had a python implementation
|
||||
try:
|
||||
ssh_args, server_args, passthrough = parse_ssh_args(args)
|
||||
ssh_args, server_args, passthrough, found_extra_args = parse_ssh_args(args, extra_args=('--kitten',))
|
||||
except InvalidSSHArgs as e:
|
||||
e.system_exit()
|
||||
cmd = ['ssh'] + ssh_args
|
||||
if not os.environ.get('KITTY_WINDOW_ID') or not os.environ.get('KITTY_PID'):
|
||||
raise SystemExit('The SSH kitten is meant to run inside a kitty window')
|
||||
if passthrough:
|
||||
cmd += server_args
|
||||
else:
|
||||
hostname, remote_args = server_args[0], server_args[1:]
|
||||
if not remote_args:
|
||||
cmd.append('-t')
|
||||
cmd.append('--')
|
||||
cmd.append(hostname)
|
||||
terminfo = subprocess.check_output(['infocmp', '-a']).decode('utf-8')
|
||||
f = get_posix_cmd if use_posix else get_python_cmd
|
||||
cmd += f(terminfo, remote_args)
|
||||
os.execvp('ssh', cmd)
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
elif __name__ == '__completer__':
|
||||
setattr(sys, 'kitten_completer', complete)
|
||||
elif __name__ == '__conf__':
|
||||
from .options.definition import definition
|
||||
sys.options_definition = definition # type: ignore
|
||||
|
||||
0
kittens/ssh/options/__init__.py
Normal file
0
kittens/ssh/options/__init__.py
Normal file
150
kittens/ssh/options/definition.py
Normal file
150
kittens/ssh/options/definition.py
Normal file
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
# After editing this file run ./gen-config.py to apply the changes
|
||||
|
||||
from kitty.conf.types import Definition
|
||||
|
||||
|
||||
copy_message = '''\
|
||||
Copy files and directories from local to remote hosts. The specified files are
|
||||
assumed to be relative to the HOME directory and copied to the HOME on the
|
||||
remote. Directories are copied recursively. If absolute paths are used, they are
|
||||
copied as is.'''
|
||||
|
||||
definition = Definition(
|
||||
'kittens.ssh',
|
||||
)
|
||||
|
||||
agr = definition.add_group
|
||||
egr = definition.end_group
|
||||
opt = definition.add_option
|
||||
|
||||
agr('bootstrap', 'Host bootstrap configuration') # {{{
|
||||
|
||||
opt('hostname', '*', option_type='hostname', long_text='''
|
||||
The hostname that the following options apply to. A glob pattern to match multiple
|
||||
hosts can be used. Multiple hostnames can also be specified separated by spaces.
|
||||
The hostname can include an optional username in the form :code:`user@host`.
|
||||
When not specified options apply to all hosts, until the
|
||||
first hostname specification is found. Note that matching of hostname is done against
|
||||
the name you specify on the command line to connect to the remote host.
|
||||
If you wish to include the same basic configuration for many
|
||||
different hosts, you can do so with the :ref:`include <include>` directive.
|
||||
''')
|
||||
|
||||
opt('interpreter', 'sh', long_text='''
|
||||
The interpreter to use on the remote host. Must be either a POSIX complaint shell
|
||||
or a python executable. If the default sh is not available or broken, using
|
||||
an alternate interpreter can be useful.
|
||||
''')
|
||||
|
||||
opt('remote_dir', '.local/share/kitty-ssh-kitten', option_type='relative_dir', long_text='''
|
||||
The location on the remote host where the files needed for this kitten
|
||||
are installed. The location is relative to the HOME directory. Absolute paths or paths
|
||||
that resolve to a location outside the HOME are not allowed.
|
||||
''')
|
||||
|
||||
opt('+copy', '', option_type='copy', add_to_default=False, long_text=f'''
|
||||
{copy_message} For example::
|
||||
|
||||
copy .vimrc .zshrc .config/some-dir
|
||||
|
||||
If a file should be copied to some other destination on the remote host,
|
||||
use :code:`--dest`::
|
||||
|
||||
copy --dest some-other-name some-file
|
||||
|
||||
Glob patterns can be specified to copy multiple files, with :code:`--glob`::
|
||||
|
||||
copy --glob images/*.png
|
||||
|
||||
Files can be excluded when copying with :code:`--exclude`::
|
||||
|
||||
copy --glob --exclude *.jpg --exclude *.bmp images/*
|
||||
|
||||
Files whose remote name matches the exclude pattern will not be copied.
|
||||
For more details, see :ref:`ssh_copy_command`.
|
||||
''')
|
||||
egr() # }}}
|
||||
|
||||
agr('shell', 'Login shell environment') # {{{
|
||||
|
||||
opt('shell_integration', 'inherited', long_text='''
|
||||
Control the shell integration on the remote host. See :ref:`shell_integration`
|
||||
for details on how this setting works. The special value :code:`inherited` means
|
||||
use the setting from :file:`kitty.conf`. This setting is useful for overriding
|
||||
integration on a per-host basis.
|
||||
''')
|
||||
|
||||
opt('login_shell', '', long_text='''
|
||||
The login shell to execute on the remote host. By default, the remote user account's
|
||||
login shell is used.
|
||||
''')
|
||||
|
||||
opt('+env', '', option_type='env', add_to_default=False, long_text='''
|
||||
Specify environment variables to set on the remote host. Note that
|
||||
environment variables can refer to each other, so if you use::
|
||||
|
||||
env MYVAR1=a
|
||||
env MYVAR2=$MYVAR1/$HOME/b
|
||||
|
||||
The value of MYVAR2 will be :code:`a/<path to home directory>/b`. Using
|
||||
:code:`VAR=` will set it to the empty string and using just :code:`VAR`
|
||||
will delete the variable from the child process' environment. The definitions
|
||||
are processed alphabetically. The special value :code:`_kitty_copy_env_var_`
|
||||
will cause the value of the variable to be copied from the local environment.
|
||||
''')
|
||||
|
||||
opt('cwd', '', long_text='''
|
||||
The working directory on the remote host to change to. Env vars in this
|
||||
value are expanded. The default is empty so no changing is done, which
|
||||
usually means the home directory is used.
|
||||
''')
|
||||
|
||||
opt('color_scheme', '', long_text='''
|
||||
Specify a color scheme to use when connecting to the remote host. If the
|
||||
color_scheme ends with :code:`.conf` it is assumed to be the name of a config
|
||||
file to load from the kitty config directory, otherwise it is assumed to be the
|
||||
name of a color theme to load via the themes kitten. Note that only colors
|
||||
applying to the text/background are changed, other config settings in the .conf
|
||||
files/themes are ignored.
|
||||
''')
|
||||
|
||||
opt('remote_kitty', 'if-needed', choices=('if-needed', 'no', 'yes'), long_text='''
|
||||
Make kitty available on the remote server. Useful to run kittens such as the
|
||||
icat kitten to display images or the transfer file kitten to transfer files.
|
||||
Only works if the remote server has an architecture for which pre-compiled
|
||||
kitty binaries are available. Note that kitty is not actually copied to the
|
||||
remote server, instead a small bootstrap script is copied which will download
|
||||
and run kitty when kitty is first executed on the remote server. A value of
|
||||
:code:`if-needed` means kitty is installed only if not already present in the
|
||||
system-wide PATH. A value of :code:`yes` means that kitty is installed even if
|
||||
already present, and the installed kitty takes precedence. Finally, :code:`no`
|
||||
means no kitty is installed on the remote machine. The installed kitty
|
||||
can be updated by running: :code:`kitty +update-kitty` on the remote machine.
|
||||
''')
|
||||
egr() # }}}
|
||||
|
||||
agr('ssh', 'SSH configuration') # {{{
|
||||
|
||||
opt('share_connections', 'yes', option_type='to_bool', long_text='''
|
||||
Within a single kitty instance, all connections to a particular server can be
|
||||
shared. This reduces startup latency for subsequent connections and means that you have
|
||||
to enter the password only once. Under the hood, it uses SSH ControlMasters and
|
||||
these are automatically cleaned up by kitty when it quits.
|
||||
''')
|
||||
|
||||
opt('askpass', 'unless-set', choices=('unless-set', 'ssh', 'native'), long_text='''
|
||||
Control the program SSH uses to ask for passwords or confirmation of host keys
|
||||
etc. The default is to use kitty's native askpass, unless the SSH_ASKPASS
|
||||
environment variable is set. Set it to :code:`ssh` to not interfere with the
|
||||
normal ssh askpass mechanism at all, which typically means that ssh will prompt
|
||||
at the terminal. Set it to :code:`native` to always use kitty's native,
|
||||
built-in askpass implementation. Note that not using the kitty askpass implementation
|
||||
means that SSH might need to use the terminal before the connection is established
|
||||
so the kitten cannot use the terminal to send data without an extra roundtrip,
|
||||
adding to initial connection latency.
|
||||
''')
|
||||
egr() # }}}
|
||||
89
kittens/ssh/options/parse.py
Normal file
89
kittens/ssh/options/parse.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# generated by gen-config.py DO NOT edit
|
||||
|
||||
import typing
|
||||
from kittens.ssh.options.utils import copy, env, hostname, relative_dir
|
||||
from kitty.conf.utils import merge_dicts, to_bool
|
||||
|
||||
|
||||
class Parser:
|
||||
|
||||
def askpass(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
val = val.lower()
|
||||
if val not in self.choices_for_askpass:
|
||||
raise ValueError(f"The value {val} is not a valid choice for askpass")
|
||||
ans["askpass"] = val
|
||||
|
||||
choices_for_askpass = frozenset(('unless-set', 'ssh', 'native'))
|
||||
|
||||
def color_scheme(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['color_scheme'] = str(val)
|
||||
|
||||
def copy(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
for k, v in copy(val, ans["copy"]):
|
||||
ans["copy"][k] = v
|
||||
|
||||
def cwd(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['cwd'] = str(val)
|
||||
|
||||
def env(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
for k, v in env(val, ans["env"]):
|
||||
ans["env"][k] = v
|
||||
|
||||
def hostname(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
hostname(val, ans)
|
||||
|
||||
def interpreter(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['interpreter'] = str(val)
|
||||
|
||||
def login_shell(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['login_shell'] = str(val)
|
||||
|
||||
def remote_dir(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['remote_dir'] = relative_dir(val)
|
||||
|
||||
def remote_kitty(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
val = val.lower()
|
||||
if val not in self.choices_for_remote_kitty:
|
||||
raise ValueError(f"The value {val} is not a valid choice for remote_kitty")
|
||||
ans["remote_kitty"] = val
|
||||
|
||||
choices_for_remote_kitty = frozenset(('if-needed', 'no', 'yes'))
|
||||
|
||||
def share_connections(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['share_connections'] = to_bool(val)
|
||||
|
||||
def shell_integration(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['shell_integration'] = str(val)
|
||||
|
||||
|
||||
def create_result_dict() -> typing.Dict[str, typing.Any]:
|
||||
return {
|
||||
'copy': {},
|
||||
'env': {},
|
||||
}
|
||||
|
||||
|
||||
actions: typing.FrozenSet[str] = frozenset(())
|
||||
|
||||
|
||||
def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:
|
||||
ans = {}
|
||||
for k, v in defaults.items():
|
||||
if isinstance(v, dict):
|
||||
ans[k] = merge_dicts(v, vals.get(k, {}))
|
||||
elif k in actions:
|
||||
ans[k] = v + vals.get(k, [])
|
||||
else:
|
||||
ans[k] = vals.get(k, v)
|
||||
return ans
|
||||
|
||||
|
||||
parser = Parser()
|
||||
|
||||
|
||||
def parse_conf_item(key: str, val: str, ans: typing.Dict[str, typing.Any]) -> bool:
|
||||
func = getattr(parser, key, None)
|
||||
if func is not None:
|
||||
func(val, ans)
|
||||
return True
|
||||
return False
|
||||
92
kittens/ssh/options/types.py
Normal file
92
kittens/ssh/options/types.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# generated by gen-config.py DO NOT edit
|
||||
|
||||
import typing
|
||||
import kittens.ssh.copy
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
choices_for_askpass = typing.Literal['unless-set', 'ssh', 'native']
|
||||
choices_for_remote_kitty = typing.Literal['if-needed', 'no', 'yes']
|
||||
else:
|
||||
choices_for_askpass = str
|
||||
choices_for_remote_kitty = str
|
||||
|
||||
option_names = ( # {{{
|
||||
'askpass',
|
||||
'color_scheme',
|
||||
'copy',
|
||||
'cwd',
|
||||
'env',
|
||||
'hostname',
|
||||
'interpreter',
|
||||
'login_shell',
|
||||
'remote_dir',
|
||||
'remote_kitty',
|
||||
'share_connections',
|
||||
'shell_integration') # }}}
|
||||
|
||||
|
||||
class Options:
|
||||
askpass: choices_for_askpass = 'unless-set'
|
||||
color_scheme: str = ''
|
||||
cwd: str = ''
|
||||
hostname: str = '*'
|
||||
interpreter: str = 'sh'
|
||||
login_shell: str = ''
|
||||
remote_dir: str = '.local/share/kitty-ssh-kitten'
|
||||
remote_kitty: choices_for_remote_kitty = 'if-needed'
|
||||
share_connections: bool = True
|
||||
shell_integration: str = 'inherited'
|
||||
copy: typing.Dict[str, kittens.ssh.copy.CopyInstruction] = {}
|
||||
env: typing.Dict[str, str] = {}
|
||||
config_paths: typing.Tuple[str, ...] = ()
|
||||
config_overrides: typing.Tuple[str, ...] = ()
|
||||
|
||||
def __init__(self, options_dict: typing.Optional[typing.Dict[str, typing.Any]] = None) -> None:
|
||||
if options_dict is not None:
|
||||
null = object()
|
||||
for key in option_names:
|
||||
val = options_dict.get(key, null)
|
||||
if val is not null:
|
||||
setattr(self, key, val)
|
||||
|
||||
@property
|
||||
def _fields(self) -> typing.Tuple[str, ...]:
|
||||
return option_names
|
||||
|
||||
def __iter__(self) -> typing.Iterator[str]:
|
||||
return iter(self._fields)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._fields)
|
||||
|
||||
def _copy_of_val(self, name: str) -> typing.Any:
|
||||
ans = getattr(self, name)
|
||||
if isinstance(ans, dict):
|
||||
ans = ans.copy()
|
||||
elif isinstance(ans, list):
|
||||
ans = ans[:]
|
||||
return ans
|
||||
|
||||
def _asdict(self) -> typing.Dict[str, typing.Any]:
|
||||
return {k: self._copy_of_val(k) for k in self}
|
||||
|
||||
def _replace(self, **kw: typing.Any) -> "Options":
|
||||
ans = Options()
|
||||
for name in self:
|
||||
setattr(ans, name, self._copy_of_val(name))
|
||||
for name, val in kw.items():
|
||||
setattr(ans, name, val)
|
||||
return ans
|
||||
|
||||
def __getitem__(self, key: typing.Union[int, str]) -> typing.Any:
|
||||
k = option_names[key] if isinstance(key, int) else key
|
||||
try:
|
||||
return getattr(self, k)
|
||||
except AttributeError:
|
||||
pass
|
||||
raise KeyError(f"No option named: {k}")
|
||||
|
||||
|
||||
defaults = Options()
|
||||
defaults.copy = {}
|
||||
defaults.env = {}
|
||||
67
kittens/ssh/options/utils.py
Normal file
67
kittens/ssh/options/utils.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import posixpath
|
||||
from typing import Any, Dict, Iterable, Optional, Tuple
|
||||
|
||||
from ..copy import CopyInstruction, parse_copy_instructions
|
||||
|
||||
DELETE_ENV_VAR = '_delete_this_env_var_'
|
||||
|
||||
|
||||
def relative_dir(val: str) -> str:
|
||||
if posixpath.isabs(val):
|
||||
raise ValueError(f'Absolute paths not allowed. {val} is invalid.')
|
||||
base = '/ffjdg'
|
||||
q = posixpath.normpath(posixpath.join(base, val))
|
||||
if q == base or not q.startswith(base):
|
||||
raise ValueError(f'Paths that escape their parent dir are not allowed. {val} is not valid')
|
||||
return posixpath.normpath(val)
|
||||
|
||||
|
||||
def env(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, str]]:
|
||||
val = val.strip()
|
||||
if val:
|
||||
if '=' in val:
|
||||
key, v = val.split('=', 1)
|
||||
key, v = key.strip(), v.strip()
|
||||
if key:
|
||||
yield key, v
|
||||
else:
|
||||
yield val, DELETE_ENV_VAR
|
||||
|
||||
|
||||
def copy(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, CopyInstruction]]:
|
||||
yield from parse_copy_instructions(val, current_val)
|
||||
|
||||
|
||||
def init_results_dict(ans: Dict[str, Any]) -> Dict[str, Any]:
|
||||
ans['hostname'] = '*'
|
||||
ans['per_host_dicts'] = {}
|
||||
return ans
|
||||
|
||||
|
||||
def get_per_hosts_dict(results_dict: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
|
||||
ans: Dict[str, Dict[str, Any]] = results_dict.get('per_host_dicts', {}).copy()
|
||||
h = results_dict['hostname']
|
||||
hd = {k: v for k, v in results_dict.items() if k != 'per_host_dicts'}
|
||||
ans[h] = hd
|
||||
return ans
|
||||
|
||||
|
||||
first_seen_positions: Dict[str, int] = {}
|
||||
|
||||
|
||||
def hostname(val: str, dict_with_parse_results: Optional[Dict[str, Any]] = None) -> str:
|
||||
if dict_with_parse_results is not None:
|
||||
ch = dict_with_parse_results['hostname']
|
||||
if val != ch:
|
||||
from .parse import create_result_dict
|
||||
phd = get_per_hosts_dict(dict_with_parse_results)
|
||||
dict_with_parse_results.clear()
|
||||
dict_with_parse_results.update(phd.pop(val, create_result_dict()))
|
||||
dict_with_parse_results['per_host_dicts'] = phd
|
||||
dict_with_parse_results['hostname'] = val
|
||||
if val not in first_seen_positions:
|
||||
first_seen_positions[val] = len(first_seen_positions)
|
||||
return val
|
||||
@@ -483,6 +483,10 @@ def update_theme_file(path: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def text_as_opts(text: str) -> KittyOptions:
|
||||
return KittyOptions(options_dict=parse_config(text.splitlines()))
|
||||
|
||||
|
||||
class Theme:
|
||||
name: str = ''
|
||||
author: str = ''
|
||||
@@ -516,7 +520,7 @@ class Theme:
|
||||
@property
|
||||
def kitty_opts(self) -> KittyOptions:
|
||||
if self._opts is None:
|
||||
self._opts = KittyOptions(options_dict=parse_config(self.raw.splitlines()))
|
||||
self._opts = text_as_opts(self.raw)
|
||||
return self._opts
|
||||
|
||||
def save_in_dir(self, dirpath: str) -> None:
|
||||
|
||||
@@ -14,6 +14,7 @@ from typing import (
|
||||
from kitty.cli import create_default_opts, parse_args
|
||||
from kitty.cli_stub import ThemesCLIOptions
|
||||
from kitty.config import cached_values_for
|
||||
from kitty.options.types import Options as KittyOptions
|
||||
from kitty.constants import config_dir
|
||||
from kitty.fast_data_types import truncate_point_for_length, wcswidth
|
||||
from kitty.rgb import color_as_sharp, color_from_int
|
||||
@@ -23,7 +24,7 @@ from kitty.utils import ScreenSize
|
||||
from ..tui.handler import Handler
|
||||
from ..tui.line_edit import LineEdit
|
||||
from ..tui.loop import Loop
|
||||
from ..tui.operations import color_code, styled
|
||||
from ..tui.operations import color_code, set_default_colors, styled
|
||||
from .collection import MARK_AFTER, NoCacheFound, Theme, Themes, load_themes
|
||||
|
||||
separator = '║'
|
||||
@@ -132,6 +133,17 @@ class ThemesList:
|
||||
return self.themes[self.current_idx]
|
||||
|
||||
|
||||
def colors_as_escape_codes(o: KittyOptions) -> str:
|
||||
ans = set_default_colors(
|
||||
fg=o.foreground, bg=o.background, cursor=o.cursor, select_bg=o.selection_background, select_fg=o.selection_foreground
|
||||
)
|
||||
cmds = []
|
||||
for i in range(256):
|
||||
col = color_as_sharp(color_from_int(o.color_table[i]))
|
||||
cmds.append(f'{i};{col}')
|
||||
return ans + '\033]4;' + ';'.join(cmds) + '\033\\'
|
||||
|
||||
|
||||
class ThemesHandler(Handler):
|
||||
|
||||
def __init__(self, cached_values: Dict[str, Any], cli_opts: ThemesCLIOptions) -> None:
|
||||
@@ -195,15 +207,9 @@ class ThemesHandler(Handler):
|
||||
o = self.themes_list.current_theme.kitty_opts
|
||||
else:
|
||||
o = create_default_opts()
|
||||
self.cmd.set_default_colors(
|
||||
fg=o.foreground, bg=o.background, cursor=o.cursor, select_bg=o.selection_background, select_fg=o.selection_foreground
|
||||
)
|
||||
self.current_opts = o
|
||||
cmds = []
|
||||
for i in range(256):
|
||||
col = color_as_sharp(color_from_int(o.color_table[i]))
|
||||
cmds.append(f'{i};{col}')
|
||||
self.print(end='\033]4;' + ';'.join(cmds) + '\033\\')
|
||||
cmd = colors_as_escape_codes(o)
|
||||
self.write(cmd)
|
||||
return True
|
||||
|
||||
def redraw_after_category_change(self) -> None:
|
||||
|
||||
@@ -126,7 +126,7 @@ class PatchFile(StreamingJob):
|
||||
|
||||
def read_from_src(self, b: memoryview, pos: int) -> int:
|
||||
self.src_file.seek(pos)
|
||||
return self.src_file.readinto(b) # type: ignore
|
||||
return self.src_file.readinto(b)
|
||||
|
||||
def close(self) -> None:
|
||||
if not self.src_file.closed:
|
||||
|
||||
@@ -44,6 +44,7 @@ class Handler:
|
||||
use_alternate_screen = True
|
||||
mouse_tracking = MouseTracking.none
|
||||
terminal_io_ended = False
|
||||
overlay_ready_report_needed = False
|
||||
|
||||
def _initialize(
|
||||
self,
|
||||
@@ -206,18 +207,23 @@ class HandleResult:
|
||||
type_of_input: Optional[str] = None
|
||||
no_ui: bool = False
|
||||
|
||||
def __init__(self, impl: Callable[..., Any], type_of_input: Optional[str], no_ui: bool):
|
||||
def __init__(self, impl: Callable[..., Any], type_of_input: Optional[str], no_ui: bool, has_ready_notification: bool):
|
||||
self.impl = impl
|
||||
self.no_ui = no_ui
|
||||
self.type_of_input = type_of_input
|
||||
self.has_ready_notification = has_ready_notification
|
||||
|
||||
def __call__(self, args: Sequence[str], data: Any, target_window_id: int, boss: BossType) -> Any:
|
||||
return self.impl(args, data, target_window_id, boss)
|
||||
|
||||
|
||||
def result_handler(type_of_input: Optional[str] = None, no_ui: bool = False) -> Callable[[Callable[..., Any]], HandleResult]:
|
||||
def result_handler(
|
||||
type_of_input: Optional[str] = None,
|
||||
no_ui: bool = False,
|
||||
has_ready_notification: bool = Handler.overlay_ready_report_needed
|
||||
) -> Callable[[Callable[..., Any]], HandleResult]:
|
||||
|
||||
def wrapper(impl: Callable[..., Any]) -> HandleResult:
|
||||
return HandleResult(impl, type_of_input, no_ui)
|
||||
return HandleResult(impl, type_of_input, no_ui, has_ready_notification)
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -13,8 +13,9 @@ from .operations import (
|
||||
|
||||
class LineEdit:
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, is_password: bool = False) -> None:
|
||||
self.clear()
|
||||
self.is_password = is_password
|
||||
|
||||
def clear(self) -> None:
|
||||
self.current_input = ''
|
||||
@@ -31,7 +32,10 @@ class LineEdit:
|
||||
if self.pending_bell:
|
||||
write('\a')
|
||||
self.pending_bell = False
|
||||
text = prompt + self.current_input
|
||||
ci = self.current_input
|
||||
if self.is_password:
|
||||
ci = '*' * wcswidth(ci)
|
||||
text = prompt + ci
|
||||
cursor_pos = self.cursor_pos + wcswidth(prompt)
|
||||
if screen_cols:
|
||||
write(SAVE_CURSOR + text + RESTORE_CURSOR)
|
||||
|
||||
@@ -182,7 +182,7 @@ class UnhandledException(Handler):
|
||||
self.cmd.set_default_colors()
|
||||
self.write(self.tb.replace('\n', '\r\n'))
|
||||
self.write('\r\n')
|
||||
self.write('Press the Enter key to quit')
|
||||
self.write('Press Enter to quit')
|
||||
|
||||
def on_key(self, key_event: KeyEventType) -> None:
|
||||
if key_event.key == 'ENTER':
|
||||
@@ -236,6 +236,7 @@ class Loop:
|
||||
else:
|
||||
self.asyncio_loop = asyncio.get_event_loop()
|
||||
self.return_code = 0
|
||||
self.overlay_ready_reported = False
|
||||
self.optional_actions = optional_actions
|
||||
self.read_buf = ''
|
||||
self.decoder = codecs.getincrementaldecoder('utf-8')('ignore')
|
||||
@@ -435,6 +436,8 @@ class Loop:
|
||||
self.asyncio_loop.set_exception_handler(handle_exception)
|
||||
handler._initialize(self._get_screen_size(), term_manager, schedule_write, self, debug, image_manager)
|
||||
with handler:
|
||||
if handler.overlay_ready_report_needed:
|
||||
handler.cmd.overlay_ready()
|
||||
self.asyncio_loop.add_reader(
|
||||
tty_fd, self._read_ready, handler, tty_fd)
|
||||
self.asyncio_loop.add_writer(
|
||||
@@ -467,9 +470,12 @@ class Loop:
|
||||
|
||||
term_manager.extra_finalize = b''.join(self.write_buf).decode('utf-8')
|
||||
if tb is not None:
|
||||
report_overlay_ready = handler.overlay_ready_report_needed and not self.overlay_ready_reported
|
||||
self.return_code = 1
|
||||
if not handler.terminal_io_ended:
|
||||
self._report_error_loop(tb, term_manager)
|
||||
self._report_error_loop(tb, term_manager, report_overlay_ready)
|
||||
|
||||
def _report_error_loop(self, tb: str, term_manager: TermManager) -> None:
|
||||
self.loop_impl(UnhandledException(tb), term_manager)
|
||||
def _report_error_loop(self, tb: str, term_manager: TermManager, overlay_ready_report_needed: bool) -> None:
|
||||
handler = UnhandledException(tb)
|
||||
handler.overlay_ready_report_needed = overlay_ready_report_needed
|
||||
self.loop_impl(handler, term_manager)
|
||||
|
||||
@@ -413,6 +413,11 @@ def restore_colors() -> str:
|
||||
return '\x1b[#Q'
|
||||
|
||||
|
||||
@cmd
|
||||
def overlay_ready() -> str:
|
||||
return '\x1bP@kitty-overlay-ready|\x1b\\'
|
||||
|
||||
|
||||
@cmd
|
||||
def write_to_clipboard(data: Union[str, bytes], use_primary: bool = False) -> str:
|
||||
from base64 import standard_b64encode
|
||||
|
||||
@@ -48,3 +48,24 @@ def human_size(
|
||||
from math import log
|
||||
exponent = min(int(log(size, 1024)), len(unit_list) - 1)
|
||||
return format_number(size / 1024**exponent, max_num_of_decimals) + sep + unit_list[exponent]
|
||||
|
||||
|
||||
def report_error(msg: str = '', return_code: int = 1, print_exc: bool = False) -> None:
|
||||
' Report an error also sending the overlay ready message to ensure kitten is visible '
|
||||
from .operations import overlay_ready
|
||||
print(end=overlay_ready())
|
||||
if msg:
|
||||
print(msg, file=sys.stderr)
|
||||
if print_exc:
|
||||
cls, e, tb = sys.exc_info()
|
||||
if e and not isinstance(e, (SystemExit, KeyboardInterrupt)):
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
with suppress(KeyboardInterrupt, EOFError):
|
||||
input('Press Enter to quit')
|
||||
raise SystemExit(return_code)
|
||||
|
||||
|
||||
def report_unhandled_error(msg: str = '') -> None:
|
||||
' Report an unhandled exception with the overlay ready message '
|
||||
return report_error(msg, print_exc=True)
|
||||
|
||||
@@ -28,6 +28,7 @@ from ..tui.operations import (
|
||||
clear_screen, colored, cursor, faint, set_line_wrapping, set_window_title,
|
||||
sgr, styled
|
||||
)
|
||||
from ..tui.utils import report_unhandled_error
|
||||
|
||||
HEX, NAME, EMOTICONS, FAVORITES = 'HEX', 'NAME', 'EMOTICONS', 'FAVORITES'
|
||||
favorites_path = os.path.join(config_dir, 'unicode-input-favorites.conf')
|
||||
@@ -281,6 +282,8 @@ def is_index(w: str) -> bool:
|
||||
|
||||
class UnicodeInput(Handler):
|
||||
|
||||
overlay_ready_report_needed = True
|
||||
|
||||
def __init__(self, cached_values: Dict[str, Any], emoji_variation: str = 'none') -> None:
|
||||
self.cached_values = cached_values
|
||||
self.emoji_variation = ''
|
||||
@@ -566,8 +569,7 @@ def main(args: List[str]) -> Optional[str]:
|
||||
cli_opts, items = parse_unicode_input_args(args[1:])
|
||||
except SystemExit as e:
|
||||
if e.code != 0:
|
||||
print(e.args[0], file=sys.stderr)
|
||||
input(_('Press Enter to quit'))
|
||||
report_unhandled_error(e.args[0])
|
||||
return None
|
||||
|
||||
loop = Loop()
|
||||
@@ -585,11 +587,11 @@ def main(args: List[str]) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
@result_handler()
|
||||
@result_handler(has_ready_notification=UnicodeInput.overlay_ready_report_needed)
|
||||
def handle_result(args: List[str], current_char: str, target_window_id: int, boss: BossType) -> None:
|
||||
w = boss.window_id_map.get(target_window_id)
|
||||
if w is not None:
|
||||
w.paste(current_char)
|
||||
w.paste_text(current_char)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
60870
kittens/unicode_input/names.h
generated
60870
kittens/unicode_input/names.h
generated
File diff suppressed because one or more lines are too long
189
kitty/boss.py
189
kitty/boss.py
@@ -16,16 +16,14 @@ from typing import (
|
||||
)
|
||||
from weakref import WeakValueDictionary
|
||||
|
||||
from .child import (
|
||||
cached_process_data, cwd_of_process, default_env, set_default_env
|
||||
)
|
||||
from .child import cached_process_data, default_env, set_default_env
|
||||
from .cli import create_opts, parse_args
|
||||
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, config_dir, is_macos, is_wayland, kitty_exe, logo_png_file,
|
||||
supports_primary_selection, website_url
|
||||
appname, cache_dir, config_dir, 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,
|
||||
@@ -58,12 +56,13 @@ from .tabs import (
|
||||
from .types import _T, AsyncResponse, SingleKey, WindowSystemMouseEvent, ac
|
||||
from .typing import PopenType, TypedDict
|
||||
from .utils import (
|
||||
func_name, get_editor, get_new_os_window_size, get_primary_selection,
|
||||
is_path_in_temp_dir, log_error, open_url, parse_address_spec,
|
||||
parse_uri_list, platform_window_id, remove_socket_file, safe_print,
|
||||
set_primary_selection, single_instance, startup_notification_handler, which
|
||||
cleanup_ssh_control_masters, func_name, get_editor, get_new_os_window_size,
|
||||
get_primary_selection, is_path_in_temp_dir, less_version, log_error,
|
||||
macos_version, open_url, parse_address_spec, parse_uri_list,
|
||||
platform_window_id, remove_socket_file, safe_print, set_primary_selection,
|
||||
single_instance, startup_notification_handler, which
|
||||
)
|
||||
from .window import CommandOutput, MatchPatternType, Window
|
||||
from .window import CommandOutput, CwdRequest, MatchPatternType, Window
|
||||
|
||||
|
||||
class OSWindowDict(TypedDict):
|
||||
@@ -440,7 +439,7 @@ class Boss:
|
||||
return os_window_id
|
||||
return None
|
||||
|
||||
def _new_os_window(self, args: Union[SpecialWindowInstance, Iterable[str]], cwd_from: Optional[int] = None) -> int:
|
||||
def _new_os_window(self, args: Union[SpecialWindowInstance, Iterable[str]], cwd_from: Optional[CwdRequest] = None) -> int:
|
||||
if isinstance(args, SpecialWindowInstance):
|
||||
sw: Optional[SpecialWindowInstance] = args
|
||||
else:
|
||||
@@ -462,12 +461,14 @@ class Boss:
|
||||
@ac('win', 'New OS Window with the same working directory as the currently active window')
|
||||
def new_os_window_with_cwd(self, *args: str) -> None:
|
||||
w = self.active_window_for_cwd
|
||||
cwd_from = w.child.pid_for_cwd if w is not None else None
|
||||
self._new_os_window(args, cwd_from)
|
||||
self._new_os_window(args, CwdRequest(w))
|
||||
|
||||
def new_os_window_with_wd(self, wd: str) -> None:
|
||||
special_window = SpecialWindow(None, cwd=wd)
|
||||
self._new_os_window(special_window)
|
||||
def new_os_window_with_wd(self, wd: Union[str, List[str]], str_is_multiple_paths: bool = False) -> None:
|
||||
if isinstance(wd, str):
|
||||
wd = wd.split(os.pathsep) if str_is_multiple_paths else [wd]
|
||||
for path in wd:
|
||||
special_window = SpecialWindow(None, cwd=path)
|
||||
self._new_os_window(special_window)
|
||||
|
||||
def add_child(self, window: Window) -> None:
|
||||
assert window.child.pid is not None and window.child.child_fd is not None
|
||||
@@ -672,6 +673,15 @@ class Boss:
|
||||
if tab:
|
||||
self.confirm_tab_close(tab)
|
||||
|
||||
@ac('tab', 'Close all the tabs in the current OS window other than the currently active tab')
|
||||
def close_other_tabs_in_os_window(self) -> None:
|
||||
tm = self.active_tab_manager
|
||||
if tm is not None and len(tm.tabs) > 1:
|
||||
active_tab = self.active_tab
|
||||
for tab in tm:
|
||||
if tab is not active_tab:
|
||||
self.close_tab(tab)
|
||||
|
||||
def confirm(
|
||||
self, msg: str, # can contain newlines and ANSI formatting
|
||||
callback: Callable[..., None], # called with True or False and *args
|
||||
@@ -701,6 +711,18 @@ class Boss:
|
||||
cmd += ['-c', c]
|
||||
self._run_kitten('ask', cmd, window=window, custom_callback=callback_, default_data={'response': ''})
|
||||
|
||||
def get_line(
|
||||
self, msg: str, # can contain newlines and ANSI formatting
|
||||
callback: Callable[..., None], # called with the answer or empty string when aborted
|
||||
window: Optional[Window] = None, # the window associated with the confirmation
|
||||
prompt: str = '> ',
|
||||
is_password: bool = False
|
||||
) -> None:
|
||||
def callback_(res: Dict[str, Any], x: int, boss: Boss) -> None:
|
||||
callback(res.get('response') or '')
|
||||
cmd = ['--type', 'password' if is_password else 'line', '--message', msg, '--prompt', prompt]
|
||||
self._run_kitten('ask', cmd, window=window, custom_callback=callback_, default_data={'response': ''})
|
||||
|
||||
def confirm_tab_close(self, tab: Tab) -> None:
|
||||
x = get_options().confirm_os_window_close
|
||||
num = tab.number_of_windows_with_running_programs if x < 0 else len(tab)
|
||||
@@ -760,7 +782,7 @@ class Boss:
|
||||
else:
|
||||
self.startup_first_child(first_os_window_id)
|
||||
|
||||
if get_options().update_check_interval > 0 and not hasattr(self, 'update_check_started'):
|
||||
if get_options().update_check_interval > 0 and not hasattr(self, 'update_check_started') and getattr(sys, 'frozen', False):
|
||||
from .update_check import run_update_check
|
||||
run_update_check(get_options().update_check_interval * 60 * 60)
|
||||
self.update_check_started = True
|
||||
@@ -784,15 +806,15 @@ class Boss:
|
||||
See :sc:`reset_terminal <reset_terminal>` for details. For example::
|
||||
|
||||
# Reset the terminal
|
||||
map kitty_mod+f9 clear_terminal reset active
|
||||
map f1 clear_terminal reset active
|
||||
# Clear the terminal screen by erasing all contents
|
||||
map kitty_mod+f10 clear_terminal clear active
|
||||
map f1 clear_terminal clear active
|
||||
# Clear the terminal scrollback by erasing it
|
||||
map kitty_mod+f11 clear_terminal scrollback active
|
||||
map f1 clear_terminal scrollback active
|
||||
# Scroll the contents of the screen into the scrollback
|
||||
map kitty_mod+f12 clear_terminal scroll active
|
||||
map f1 clear_terminal scroll active
|
||||
# Clear everything up to the line with the cursor
|
||||
map kitty_mod+f9 clear_terminal to_cursor active
|
||||
map f1 clear_terminal to_cursor active
|
||||
''')
|
||||
def clear_terminal(self, action: str, only_active: bool) -> None:
|
||||
if only_active:
|
||||
@@ -802,19 +824,21 @@ class Boss:
|
||||
windows.append(w)
|
||||
else:
|
||||
windows = list(self.all_windows)
|
||||
reset = action == 'reset'
|
||||
how = 3 if action == 'scrollback' else 2
|
||||
for w in windows:
|
||||
if action in ('to_cursor', 'scroll'):
|
||||
w.screen.scroll_until_cursor_prompt()
|
||||
if action == 'to_cursor':
|
||||
w.screen.clear_scrollback()
|
||||
continue
|
||||
w.screen.cursor.x = w.screen.cursor.y = 0
|
||||
if reset:
|
||||
w.screen.reset()
|
||||
else:
|
||||
w.screen.erase_in_display(how, False)
|
||||
if action == 'reset':
|
||||
for w in windows:
|
||||
w.clear_screen(reset=True, scrollback=True)
|
||||
elif action == 'scrollback':
|
||||
for w in windows:
|
||||
w.clear_screen(scrollback=True)
|
||||
elif action == 'clear':
|
||||
for w in windows:
|
||||
w.clear_screen()
|
||||
elif action == 'scroll':
|
||||
for w in windows:
|
||||
w.scroll_prompt_to_top()
|
||||
elif action == 'to_cursor':
|
||||
for w in windows:
|
||||
w.scroll_prompt_to_top(clear_scrollback=True)
|
||||
|
||||
def increase_font_size(self) -> None: # legacy
|
||||
cfs = global_font_size()
|
||||
@@ -1063,10 +1087,11 @@ class Boss:
|
||||
self.mouse_handler(ev)
|
||||
|
||||
def select_window_in_tab_using_overlay(self, tab: Tab, msg: str, only_window_ids: Container[int] = ()) -> Optional[Window]:
|
||||
windows = tuple((w.id, w.title) for i, w in tab.windows.iter_windows_with_number(only_visible=False)
|
||||
if not only_window_ids or w.id in only_window_ids)
|
||||
windows = tuple((None, f'Current window: {w.title}' if w is self.active_window else w.title)
|
||||
if only_window_ids and w.id not in only_window_ids else (w.id, w.title)
|
||||
for i, w in tab.windows.iter_windows_with_number(only_visible=False))
|
||||
if len(windows) < 1:
|
||||
self.visual_window_select_action_trigger(windows[0][0] if windows else 0)
|
||||
self.visual_window_select_action_trigger(windows[0][0] if windows and windows[0][0] is not None else 0)
|
||||
if get_options().enable_audio_bell:
|
||||
ring_bell()
|
||||
return None
|
||||
@@ -1077,7 +1102,8 @@ class Boss:
|
||||
self.current_visual_select = None
|
||||
if cvs and q is cvs:
|
||||
q.trigger(ans if isinstance(ans, int) else 0)
|
||||
return self.choose_entry(msg, windows, chosen)
|
||||
|
||||
return self.choose_entry(msg, windows, chosen, hints_args=('--hints-offset=0', '--alphabet', get_options().visual_window_select_characters.lower()))
|
||||
|
||||
@ac('win', '''
|
||||
Resize the active window interactively
|
||||
@@ -1227,7 +1253,7 @@ class Boss:
|
||||
text = ' '.join(map(shlex.quote, urls))
|
||||
else:
|
||||
text = '\n'.join(urls)
|
||||
w.paste(text)
|
||||
w.paste_text(text)
|
||||
|
||||
@ac('win', 'Focus the nth OS window')
|
||||
def nth_os_window(self, num: int = 1) -> None:
|
||||
@@ -1313,6 +1339,7 @@ class Boss:
|
||||
s.close()
|
||||
|
||||
def display_scrollback(self, window: Window, data: Union[bytes, str], input_line_number: int = 0, title: str = '', report_cursor: bool = True) -> None:
|
||||
|
||||
def prepare_arg(x: str) -> str:
|
||||
x = x.replace('INPUT_LINE_NUMBER', str(input_line_number))
|
||||
x = x.replace('CURSOR_LINE', str(window.screen.cursor.y + 1) if report_cursor else '0')
|
||||
@@ -1328,6 +1355,15 @@ class Boss:
|
||||
tab = self.active_tab
|
||||
if tab is not None:
|
||||
bdata = data.encode('utf-8') if isinstance(data, str) else data
|
||||
if is_macos and cmd[0] == '/usr/bin/less' and macos_version()[:2] < (12, 3):
|
||||
# the system less before macOS 12.3 barfs up OSC codes, so sanitize them ourselves
|
||||
sentinel = os.path.join(cache_dir(), 'less-is-new-enough')
|
||||
if not os.path.exists(sentinel):
|
||||
if less_version(cmd[0]) >= 581:
|
||||
open(sentinel, 'w').close()
|
||||
else:
|
||||
bdata = re.sub(br'\x1b\].*?\x1b\\', b'', bdata)
|
||||
|
||||
tab.new_special_window(
|
||||
SpecialWindow(cmd, bdata, title or _('History'), overlay_for=window.id, cwd=window.cwd_of_child),
|
||||
copy_colors_from=self.active_window
|
||||
@@ -1341,15 +1377,6 @@ class Boss:
|
||||
cmd = [kitty_exe(), '+runpy', 'import os, sys, time; time.sleep(0.05); os.execvp(sys.argv[1], sys.argv[1:])'] + get_editor(get_options()) + [confpath]
|
||||
self.new_os_window(*cmd)
|
||||
|
||||
def get_output(self, source_window: Window, num_lines: Optional[int] = 1) -> str:
|
||||
output = ''
|
||||
s = source_window.screen
|
||||
if num_lines is None:
|
||||
num_lines = s.lines
|
||||
for i in range(min(num_lines, s.lines)):
|
||||
output += str(s.linebuf.line(i))
|
||||
return output
|
||||
|
||||
def _run_kitten(
|
||||
self,
|
||||
kitten: str,
|
||||
@@ -1413,7 +1440,8 @@ class Boss:
|
||||
'OVERLAID_WINDOW_COLS': str(w.screen.columns),
|
||||
},
|
||||
cwd=w.cwd_of_child,
|
||||
overlay_for=w.id
|
||||
overlay_for=w.id,
|
||||
overlay_behind=end_kitten.has_ready_notification,
|
||||
),
|
||||
copy_colors_from=w
|
||||
)
|
||||
@@ -1439,9 +1467,7 @@ class Boss:
|
||||
source_window: Window,
|
||||
default_data: Optional[Dict[str, Any]] = None
|
||||
) -> None:
|
||||
output = self.get_output(source_window, num_lines=None)
|
||||
from kittens.runner import deserialize
|
||||
data = deserialize(output)
|
||||
data, source_window.kitten_result = source_window.kitten_result, None
|
||||
if data is None:
|
||||
data = default_data
|
||||
if data is not None:
|
||||
@@ -1577,7 +1603,7 @@ class Boss:
|
||||
if text:
|
||||
w = self.active_window
|
||||
if w is not None:
|
||||
w.paste(text)
|
||||
w.paste_with_actions(text)
|
||||
|
||||
@ac('cp', 'Paste from the clipboard to the active window')
|
||||
def paste_from_clipboard(self) -> None:
|
||||
@@ -1702,7 +1728,7 @@ class Boss:
|
||||
self, cmd: List[str],
|
||||
window: Optional[Window] = None,
|
||||
stdin: Optional[str] = None,
|
||||
cwd_from: Optional[int] = None,
|
||||
cwd_from: Optional[CwdRequest] = None,
|
||||
as_overlay: bool = False
|
||||
) -> SpecialWindowInstance:
|
||||
w = window or self.active_window
|
||||
@@ -1724,7 +1750,7 @@ class Boss:
|
||||
cwd: Optional[str] = None,
|
||||
env: Optional[Dict[str, str]] = None,
|
||||
stdin: Optional[bytes] = None,
|
||||
cwd_from: Optional[int] = None
|
||||
cwd_from: Optional[CwdRequest] = None
|
||||
) -> None:
|
||||
import subprocess
|
||||
env = env or None
|
||||
@@ -1734,7 +1760,7 @@ class Boss:
|
||||
env = env_
|
||||
if cwd_from:
|
||||
with suppress(Exception):
|
||||
cwd = cwd_of_process(cwd_from)
|
||||
cwd = cwd_from.cwd_of_child
|
||||
|
||||
if stdin:
|
||||
r, w = safe_pipe(False)
|
||||
@@ -1752,7 +1778,7 @@ class Boss:
|
||||
def pipe(self, source: str, dest: str, exe: str, *args: str) -> Optional[Window]:
|
||||
cmd = [exe] + list(args)
|
||||
window = self.active_window
|
||||
cwd_from = window.child.pid_for_cwd if window else None
|
||||
cwd_from = CwdRequest(window) if window else None
|
||||
|
||||
def create_window() -> SpecialWindowInstance:
|
||||
return self.special_window_for_cmd(
|
||||
@@ -1780,7 +1806,7 @@ class Boss:
|
||||
self.run_background_process(cmd, cwd_from=cwd_from, stdin=stdin, env=env)
|
||||
return None
|
||||
|
||||
def args_to_special_window(self, args: Iterable[str], cwd_from: Optional[int] = None) -> SpecialWindowInstance:
|
||||
def args_to_special_window(self, args: Iterable[str], cwd_from: Optional[CwdRequest] = None) -> SpecialWindowInstance:
|
||||
args = list(args)
|
||||
stdin = None
|
||||
w = self.active_window
|
||||
@@ -1801,7 +1827,7 @@ class Boss:
|
||||
cmd.append(arg)
|
||||
return SpecialWindow(cmd, stdin, cwd_from=cwd_from)
|
||||
|
||||
def _new_tab(self, args: Union[SpecialWindowInstance, Iterable[str]], cwd_from: Optional[int] = None, as_neighbor: bool = False) -> Optional[Tab]:
|
||||
def _new_tab(self, args: Union[SpecialWindowInstance, Iterable[str]], cwd_from: Optional[CwdRequest] = None, as_neighbor: bool = False) -> Optional[Tab]:
|
||||
special_window = None
|
||||
if args:
|
||||
if isinstance(args, SpecialWindowInstance):
|
||||
@@ -1813,7 +1839,7 @@ class Boss:
|
||||
return tm.new_tab(special_window=special_window, cwd_from=cwd_from, as_neighbor=as_neighbor)
|
||||
return None
|
||||
|
||||
def _create_tab(self, args: List[str], cwd_from: Optional[int] = None) -> None:
|
||||
def _create_tab(self, args: List[str], cwd_from: Optional[CwdRequest] = None) -> None:
|
||||
as_neighbor = False
|
||||
if args and args[0].startswith('!'):
|
||||
as_neighbor = 'neighbor' in args[0][1:].split(',')
|
||||
@@ -1826,17 +1852,18 @@ class Boss:
|
||||
|
||||
@ac('tab', 'Create a new tab with working directory for the window in it set to the same as the active window')
|
||||
def new_tab_with_cwd(self, *args: str) -> None:
|
||||
w = self.active_window_for_cwd
|
||||
cwd_from = w.child.pid_for_cwd if w is not None else None
|
||||
self._create_tab(list(args), cwd_from=cwd_from)
|
||||
self._create_tab(list(args), cwd_from=CwdRequest(self.active_window_for_cwd))
|
||||
|
||||
def new_tab_with_wd(self, wd: str) -> None:
|
||||
def new_tab_with_wd(self, wd: Union[str, List[str]], str_is_multiple_paths: bool = False) -> None:
|
||||
if not self.os_window_map:
|
||||
self.add_os_window()
|
||||
special_window = SpecialWindow(None, cwd=wd)
|
||||
self._new_tab(special_window)
|
||||
if isinstance(wd, str):
|
||||
wd = wd.split(os.pathsep) if str_is_multiple_paths else [wd]
|
||||
for path in wd:
|
||||
special_window = SpecialWindow(None, cwd=path)
|
||||
self._new_tab(special_window)
|
||||
|
||||
def _new_window(self, args: List[str], cwd_from: Optional[int] = None) -> Optional[Window]:
|
||||
def _new_window(self, args: List[str], cwd_from: Optional[CwdRequest] = None) -> Optional[Window]:
|
||||
tab = self.active_tab
|
||||
if tab is None:
|
||||
return None
|
||||
@@ -1864,8 +1891,7 @@ class Boss:
|
||||
w = self.active_window_for_cwd
|
||||
if w is None:
|
||||
return self.new_window(*args)
|
||||
cwd_from = w.child.pid_for_cwd
|
||||
self._new_window(list(args), cwd_from=cwd_from)
|
||||
self._new_window(list(args), cwd_from=CwdRequest(w))
|
||||
|
||||
@ac('misc', '''
|
||||
Launch the specified program in a new window/tab/etc.
|
||||
@@ -2118,18 +2144,19 @@ class Boss:
|
||||
def choose_entry(
|
||||
self, title: str, entries: Iterable[Tuple[Union[_T, str, None], str]],
|
||||
callback: Callable[[Union[_T, str, None]], None],
|
||||
subtitle: str = ''
|
||||
subtitle: str = '',
|
||||
hints_args: Optional[Tuple[str, ...]] = None,
|
||||
) -> Optional[Window]:
|
||||
lines = [title, subtitle, '']
|
||||
idx_map: List[Union[_T, str]] = []
|
||||
lines = [title, subtitle, ' '] if subtitle else [title, ' ']
|
||||
idx_map: List[Union[_T, str, None]] = []
|
||||
ans: Union[str, _T, None] = None
|
||||
fmt = ': {1}'
|
||||
|
||||
for obj, text in entries:
|
||||
idx_map.append(obj)
|
||||
if obj is None:
|
||||
lines.append(text)
|
||||
else:
|
||||
idx_map.append(obj)
|
||||
lines.append(fmt.format(len(idx_map), text))
|
||||
|
||||
def done(data: Dict[str, Any], target_window_id: int, self: Boss) -> None:
|
||||
@@ -2142,7 +2169,8 @@ class Boss:
|
||||
q = self._run_kitten(
|
||||
'hints', args=(
|
||||
'--ascending', '--customize-processing=::import::kitty.choose_entry',
|
||||
r'--regex=(?m)^:\s+.+$', '--window-title', title,
|
||||
'--window-title', title,
|
||||
*(hints_args or ())
|
||||
), input_data='\r\n'.join(lines).encode('utf-8'), custom_callback=done, action_on_removal=done2
|
||||
)
|
||||
return q if isinstance(q, Window) else None
|
||||
@@ -2161,13 +2189,10 @@ class Boss:
|
||||
return f'{tab.name or tab.title} [{tab.num_window_groups} {w}]'
|
||||
|
||||
ct = self.active_tab
|
||||
st = ''
|
||||
if ct is not None:
|
||||
st = f'Current tab: {format_tab_title(ct)}'
|
||||
self.choose_entry(
|
||||
'Choose a tab to switch to',
|
||||
((t.id, format_tab_title(t)) for t in self.all_tabs if t is not ct),
|
||||
chosen, subtitle=st
|
||||
((None, f'Current tab: {format_tab_title(t)}') if t is ct else (t.id, format_tab_title(t)) for t in self.all_tabs),
|
||||
chosen
|
||||
)
|
||||
|
||||
@ac('win', '''
|
||||
@@ -2251,6 +2276,10 @@ class Boss:
|
||||
output = '\n'.join(f'{k}={v}' for k, v in os.environ.items())
|
||||
self.display_scrollback(w, output, title=_('Current kitty env vars'), report_cursor=False)
|
||||
|
||||
@ac('debug', 'Close all shared SSH connections')
|
||||
def close_shared_ssh_connections(self) -> None:
|
||||
cleanup_ssh_control_masters()
|
||||
|
||||
def launch_urls(self, *urls: str, no_replace_window: bool = False) -> None:
|
||||
from .launch import force_window_launch
|
||||
from .open_actions import actions_for_launch
|
||||
|
||||
@@ -319,10 +319,10 @@ schedule_write_to_child_python(unsigned long id, const char *prefix, PyObject *a
|
||||
static PyObject *
|
||||
needs_write(ChildMonitor UNUSED *self, PyObject *args) {
|
||||
#define needs_write_doc "needs_write(id, data) -> Queue data to be written to child."
|
||||
unsigned long id, sz;
|
||||
const char *data;
|
||||
if (!PyArg_ParseTuple(args, "ks#", &id, &data, &sz)) return NULL;
|
||||
if (schedule_write_to_child(id, 1, data, (size_t)sz)) { Py_RETURN_TRUE; }
|
||||
unsigned long id;
|
||||
Py_buffer buf;
|
||||
if (!PyArg_ParseTuple(args, "ky*", &id, &buf)) return NULL;
|
||||
if (schedule_write_to_child(id, 1, buf.buf, (size_t)buf.len)) { Py_RETURN_TRUE; }
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
@@ -637,6 +637,7 @@ prepare_to_render_os_window(OSWindow *os_window, monotonic_t now, unsigned int *
|
||||
if (is_active_window) {
|
||||
*active_window_id = w->id;
|
||||
if (collect_cursor_info(&WD.screen->cursor_render_info, w, now, os_window)) needs_render = true;
|
||||
WD.screen->cursor_render_info.is_focused = os_window->is_focused;
|
||||
set_os_window_title_from_window(w, os_window);
|
||||
*active_window_bg = window_bg;
|
||||
} else {
|
||||
@@ -1041,8 +1042,8 @@ process_cocoa_pending_actions(void) {
|
||||
if (cocoa_pending_actions[TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY]) { call_boss(toggle_macos_secure_keyboard_entry, NULL); }
|
||||
if (cocoa_pending_actions[TOGGLE_FULLSCREEN]) { call_boss(toggle_fullscreen, NULL); }
|
||||
if (cocoa_pending_actions_data.wd) {
|
||||
if (cocoa_pending_actions[NEW_OS_WINDOW_WITH_WD]) { call_boss(new_os_window_with_wd, "s", cocoa_pending_actions_data.wd); }
|
||||
if (cocoa_pending_actions[NEW_TAB_WITH_WD]) { call_boss(new_tab_with_wd, "s", cocoa_pending_actions_data.wd); }
|
||||
if (cocoa_pending_actions[NEW_OS_WINDOW_WITH_WD]) { call_boss(new_os_window_with_wd, "sO", cocoa_pending_actions_data.wd, Py_True); }
|
||||
if (cocoa_pending_actions[NEW_TAB_WITH_WD]) { call_boss(new_tab_with_wd, "sO", cocoa_pending_actions_data.wd, Py_True); }
|
||||
free(cocoa_pending_actions_data.wd);
|
||||
cocoa_pending_actions_data.wd = NULL;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import sys
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager, suppress
|
||||
from typing import (
|
||||
DefaultDict, Dict, Generator, List, Optional, Sequence, Tuple
|
||||
DefaultDict, Dict, Generator, List, Optional, Sequence, Tuple, TYPE_CHECKING
|
||||
)
|
||||
|
||||
import kitty.fast_data_types as fast_data_types
|
||||
@@ -20,6 +20,8 @@ try:
|
||||
from typing import TypedDict
|
||||
except ImportError:
|
||||
TypedDict = dict
|
||||
if TYPE_CHECKING:
|
||||
from .window import CwdRequest
|
||||
|
||||
|
||||
if is_macos:
|
||||
@@ -192,14 +194,14 @@ class Child:
|
||||
cwd: str,
|
||||
stdin: Optional[bytes] = None,
|
||||
env: Optional[Dict[str, str]] = None,
|
||||
cwd_from: Optional[int] = None,
|
||||
cwd_from: Optional['CwdRequest'] = None,
|
||||
allow_remote_control: bool = False
|
||||
):
|
||||
self.allow_remote_control = allow_remote_control
|
||||
self.argv = list(argv)
|
||||
if cwd_from is not None:
|
||||
if cwd_from:
|
||||
try:
|
||||
cwd = cwd_of_process(cwd_from)
|
||||
cwd = cwd_from.modify_argv_for_launch_with_cwd(self.argv) or cwd
|
||||
except Exception as err:
|
||||
log_error(f'Failed to read cwd of {cwd_from} with error: {err}')
|
||||
else:
|
||||
|
||||
@@ -10,7 +10,17 @@ from .typing import MarkType
|
||||
|
||||
|
||||
def mark(text: str, args: HintsCLIOptions, Mark: Type[MarkType], extra_cli_args: List[str], *a: Any) -> Generator[MarkType, None, None]:
|
||||
for idx, m in enumerate(re.finditer(args.regex, text)):
|
||||
idx = 0
|
||||
found_start_line = False
|
||||
for m in re.finditer(r'(?m)^.+$', text):
|
||||
start, end = m.span()
|
||||
mark_text = text[start:end].replace('\n', '').replace('\0', '')
|
||||
yield Mark(idx, start, end, mark_text, {'index': idx})
|
||||
line = text[start:end].replace('\0', '').replace('\n', '')
|
||||
if line == ' ':
|
||||
found_start_line = True
|
||||
continue
|
||||
if line.startswith(': '):
|
||||
yield Mark(idx, start, end, line, {'index': idx})
|
||||
idx += 1
|
||||
elif found_start_line:
|
||||
# skip this line incrementing the index
|
||||
idx += 1
|
||||
|
||||
@@ -43,7 +43,8 @@ directory is always used and the above searching does not happen.
|
||||
|
||||
If :file:`/etc/xdg/{appname}/{conf_name}.conf` exists it is merged before (i.e. with lower
|
||||
priority) than any user config files. It can be used to specify system-wide
|
||||
defaults for all users.
|
||||
defaults for all users. You can use either :code:`-` or :code:`/dev/stdin` to read the
|
||||
config from STDIN.
|
||||
'''.replace(
|
||||
'{macos_confpath}',
|
||||
(':file:`~/Library/Preferences/{appname}/{conf_name}.conf`,' if is_macos else ''), 1
|
||||
|
||||
@@ -13,7 +13,7 @@ LaunchCLIOptions = AskCLIOptions = ClipboardCLIOptions = DiffCLIOptions = CLIOpt
|
||||
HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions
|
||||
ErrorCLIOptions = UnicodeCLIOptions = RCOptions = RemoteFileCLIOptions = CLIOptions
|
||||
QueryTerminalCLIOptions = BroadcastCLIOptions = ShowKeyCLIOptions = CLIOptions
|
||||
ThemesCLIOptions = TransferCLIOptions = CLIOptions
|
||||
ThemesCLIOptions = TransferCLIOptions = CopyCLIOptions = CLIOptions
|
||||
|
||||
|
||||
def generate_stub() -> None:
|
||||
@@ -78,6 +78,9 @@ def generate_stub() -> None:
|
||||
from kittens.transfer.main import option_text as OPTIONS
|
||||
do(OPTIONS(), 'TransferCLIOptions')
|
||||
|
||||
from kittens.ssh.copy import option_text as OPTIONS
|
||||
do(OPTIONS(), 'CopyCLIOptions')
|
||||
|
||||
from kitty.rc.base import all_command_names, command_for_name
|
||||
for cmd_name in all_command_names():
|
||||
cmd = command_for_name(cmd_name)
|
||||
|
||||
@@ -499,27 +499,30 @@ cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
|
||||
|
||||
- (BOOL)openTab:(NSPasteboard*)pasteboard
|
||||
userData:(NSString *) UNUSED userData error:(NSError **) UNUSED error {
|
||||
return [self openFilesFromPasteboard:pasteboard type:NEW_TAB_WITH_WD];
|
||||
return [self openDirsFromPasteboard:pasteboard type:NEW_TAB_WITH_WD];
|
||||
}
|
||||
|
||||
- (BOOL)openOSWindow:(NSPasteboard*)pasteboard
|
||||
userData:(NSString *) UNUSED userData error:(NSError **) UNUSED error {
|
||||
return [self openFilesFromPasteboard:pasteboard type:NEW_OS_WINDOW_WITH_WD];
|
||||
return [self openDirsFromPasteboard:pasteboard type:NEW_OS_WINDOW_WITH_WD];
|
||||
}
|
||||
|
||||
- (BOOL)openFilesFromPasteboard:(NSPasteboard *)pasteboard type:(int)type {
|
||||
- (BOOL)openDirsFromPasteboard:(NSPasteboard *)pasteboard type:(int)type {
|
||||
NSDictionary *options = @{ NSPasteboardURLReadingFileURLsOnlyKey: @YES };
|
||||
NSArray *filePathArray = [pasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]] options:options];
|
||||
NSMutableArray<NSString*> *dirPathArray = [NSMutableArray arrayWithCapacity:[filePathArray count]];
|
||||
for (NSURL *url in filePathArray) {
|
||||
NSString *path = [url path];
|
||||
BOOL isDirectory = NO;
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDirectory]) {
|
||||
if (!isDirectory) {
|
||||
path = [path stringByDeletingLastPathComponent];
|
||||
}
|
||||
set_cocoa_pending_action(type, [path UTF8String]);
|
||||
if (!isDirectory) path = [path stringByDeletingLastPathComponent];
|
||||
if (![dirPathArray containsObject:path]) [dirPathArray addObject:path];
|
||||
}
|
||||
}
|
||||
if ([dirPathArray count] > 0) {
|
||||
// Colons are not valid in paths under macOS.
|
||||
set_cocoa_pending_action(type, [[dirPathArray componentsJoinedByString:@":"] UTF8String]);
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
@@ -285,7 +285,10 @@ def zsh_output_serializer(ans: Completions) -> str:
|
||||
has_descriptions = any(matches.values())
|
||||
if has_descriptions or matches.word_transforms:
|
||||
lines.append('compdescriptions=(')
|
||||
sz = max(map(wcswidth, matches.transformed_words()))
|
||||
try:
|
||||
sz = max(map(wcswidth, matches.transformed_words()))
|
||||
except ValueError:
|
||||
sz = 0
|
||||
limit = min(16, sz)
|
||||
for word, desc in matches.transformed_items():
|
||||
lines.extend(map(shlex.quote, fmt_desc(word, desc, limit)))
|
||||
@@ -695,21 +698,27 @@ def find_completions(words: Sequence[str], new_word: bool, entry_points: Iterabl
|
||||
ans.add_match_group('Kittens', (k for k in all_kitten_names() if k.startswith('' if len(words) == 2 else words[2])))
|
||||
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)
|
||||
return ans
|
||||
if words[0].startswith('+'):
|
||||
if len(words) == 1:
|
||||
if new_word:
|
||||
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)
|
||||
else:
|
||||
prefix = words[0]
|
||||
ans.add_match_group('Entry points', (c for c in namespaced_entry_points if c.startswith(prefix)))
|
||||
else:
|
||||
if len(words) == 2 and not new_word:
|
||||
ans.add_match_group('Kittens', (k for k in all_kitten_names() if k.startswith(words[1])))
|
||||
else:
|
||||
if words[0] == '+kitten':
|
||||
if words[0] == '+kitten':
|
||||
if len(words) == 2 and not new_word:
|
||||
ans.add_match_group('Kittens', (k for k in all_kitten_names() if k.startswith(words[1])))
|
||||
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)
|
||||
else:
|
||||
complete_cli(ans, words, new_word, options_for_completion(), complete_kitty_cli_arg)
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
|
||||
choices = {}
|
||||
imports: Set[Tuple[str, str]] = set()
|
||||
tc_imports: Set[Tuple[str, str]] = set()
|
||||
ki_imports: 're.Pattern[str]' = re.compile(r'\b((?:kittens|kitty).+?)[,\]]')
|
||||
|
||||
def option_type_as_str(x: Any) -> str:
|
||||
needs_import = False
|
||||
@@ -61,6 +62,11 @@ def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
|
||||
if isinstance(option, MultiOption):
|
||||
typ = typ[typ.index('[') + 1:-1]
|
||||
typ = typ.replace('Tuple', 'Dict', 1)
|
||||
kq = ki_imports.search(typ)
|
||||
if kq is not None:
|
||||
kqi = kq.group(1)
|
||||
kqim, kqii = kqi.rsplit('.', 1)
|
||||
imports.add((kqim, ''))
|
||||
return func, typ
|
||||
|
||||
is_mutiple_vars = {}
|
||||
@@ -306,7 +312,7 @@ def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
|
||||
|
||||
t('')
|
||||
t('')
|
||||
t(f'actions = frozenset({tuple(defn.actions)!r})')
|
||||
t(f'actions: typing.FrozenSet[str] = frozenset({tuple(defn.actions)!r})')
|
||||
t('')
|
||||
t('')
|
||||
t('def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:')
|
||||
@@ -343,14 +349,17 @@ def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
|
||||
for mod, name in imports:
|
||||
mmap.setdefault(mod, []).append(name)
|
||||
for mod in sorted(mmap):
|
||||
names = sorted(mmap[mod])
|
||||
lines = textwrap.wrap(', '.join(names), 100)
|
||||
if len(lines) == 1:
|
||||
s = lines[0]
|
||||
names = list(filter(None, sorted(mmap[mod])))
|
||||
if names:
|
||||
lines = textwrap.wrap(', '.join(names), 100)
|
||||
if len(lines) == 1:
|
||||
s = lines[0]
|
||||
else:
|
||||
s = '\n '.join(lines)
|
||||
s = f'(\n {s}\n)'
|
||||
a(f'from {mod} import {s}')
|
||||
else:
|
||||
s = '\n '.join(lines)
|
||||
s = f'(\n {s}\n)'
|
||||
a(f'from {mod} import {s}')
|
||||
s = ''
|
||||
if add_module_imports and mod not in seen_mods and mod != s:
|
||||
a(f'import {mod}')
|
||||
seen_mods.add(mod)
|
||||
|
||||
@@ -49,9 +49,11 @@ 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"),
|
||||
'github_discussions': 'https://github.com/kovidgoyal/kitty/discussions',
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from typing import (
|
||||
Any, Callable, Dict, Generator, Generic, Iterable, Iterator, List,
|
||||
@@ -155,6 +156,16 @@ class CurrentlyParsing:
|
||||
currently_parsing = CurrentlyParsing()
|
||||
|
||||
|
||||
class NamedLineIterator:
|
||||
|
||||
def __init__(self, name: str, lines: Iterator[str]):
|
||||
self.lines = lines
|
||||
self.name = name
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return self.lines
|
||||
|
||||
|
||||
def parse_line(
|
||||
line: str,
|
||||
parse_conf_item: ItemParser,
|
||||
@@ -170,11 +181,18 @@ def parse_line(
|
||||
log_error(f'Ignoring invalid config line: {line}')
|
||||
return
|
||||
key, val = m.groups()
|
||||
if key in ('include', 'globinclude'):
|
||||
if key in ('include', 'globinclude', 'envinclude'):
|
||||
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)))
|
||||
elif key == 'envinclude':
|
||||
from fnmatch import fnmatchcase
|
||||
for x in os.environ:
|
||||
if fnmatchcase(x, val):
|
||||
with currently_parsing.set_file(f'<env var: {x}>'):
|
||||
_parse(NamedLineIterator(base_path_for_includes, iter(os.environ[x].splitlines())), parse_conf_item, ans, accumulate_bad_lines)
|
||||
return
|
||||
else:
|
||||
if not os.path.isabs(val):
|
||||
val = os.path.join(base_path_for_includes, val)
|
||||
@@ -238,7 +256,7 @@ def merge_dicts(defaults: Dict[str, Any], newvals: Dict[str, Any]) -> Dict[str,
|
||||
return ans
|
||||
|
||||
|
||||
def resolve_config(SYSTEM_CONF: str, defconf: str, config_files_on_cmd_line: Sequence[str]) -> Generator[str, None, None]:
|
||||
def resolve_config(SYSTEM_CONF: str, defconf: str, config_files_on_cmd_line: Sequence[str] = ()) -> Generator[str, None, None]:
|
||||
if config_files_on_cmd_line:
|
||||
if 'NONE' not in config_files_on_cmd_line:
|
||||
yield SYSTEM_CONF
|
||||
@@ -253,19 +271,25 @@ def load_config(
|
||||
parse_config: Callable[[Iterable[str]], Dict[str, Any]],
|
||||
merge_configs: Callable[[Dict[str, Any], Dict[str, Any]], Dict[str, Any]],
|
||||
*paths: str,
|
||||
overrides: Optional[Iterable[str]] = None
|
||||
overrides: Optional[Iterable[str]] = None,
|
||||
initialize_defaults: Callable[[Dict[str, Any]], Dict[str, Any]] = lambda x: x,
|
||||
) -> Tuple[Dict[str, Any], Tuple[str, ...]]:
|
||||
ans = defaults._asdict()
|
||||
ans = initialize_defaults(defaults._asdict())
|
||||
found_paths = []
|
||||
for path in paths:
|
||||
if not path:
|
||||
continue
|
||||
try:
|
||||
with open(path, encoding='utf-8', errors='replace') as f:
|
||||
with currently_parsing.set_file(path):
|
||||
vals = parse_config(f)
|
||||
except (FileNotFoundError, PermissionError):
|
||||
continue
|
||||
if path == '-':
|
||||
path = '/dev/stdin'
|
||||
with currently_parsing.set_file(path):
|
||||
vals = parse_config(sys.stdin)
|
||||
else:
|
||||
try:
|
||||
with open(path, encoding='utf-8', errors='replace') as f:
|
||||
with currently_parsing.set_file(path):
|
||||
vals = parse_config(f)
|
||||
except (FileNotFoundError, PermissionError):
|
||||
continue
|
||||
found_paths.append(path)
|
||||
ans = merge_configs(ans, vals)
|
||||
if overrides is not None:
|
||||
|
||||
@@ -22,16 +22,37 @@ class Version(NamedTuple):
|
||||
|
||||
appname: str = 'kitty'
|
||||
kitty_face = '🐱'
|
||||
version: Version = Version(0, 24, 4)
|
||||
version: Version = Version(0, 25, 0)
|
||||
str_version: str = '.'.join(map(str, version))
|
||||
_plat = sys.platform.lower()
|
||||
is_macos: bool = 'darwin' in _plat
|
||||
is_running_from_develop: bool = False
|
||||
if getattr(sys, 'frozen', False):
|
||||
extensions_dir: str = getattr(sys, 'kitty_extensions_dir')
|
||||
kitty_base_dir = os.path.dirname(extensions_dir)
|
||||
if is_macos:
|
||||
kitty_base_dir = os.path.dirname(os.path.dirname(kitty_base_dir))
|
||||
kitty_base_dir = os.path.join(kitty_base_dir, 'kitty')
|
||||
|
||||
def get_frozen_base() -> str:
|
||||
global is_running_from_develop
|
||||
try:
|
||||
from bypy_importer import running_in_develop_mode # type: ignore
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
is_running_from_develop = running_in_develop_mode()
|
||||
|
||||
if is_running_from_develop:
|
||||
q = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
try:
|
||||
if os.path.isdir(q):
|
||||
return q
|
||||
except OSError:
|
||||
pass
|
||||
ans = os.path.dirname(extensions_dir)
|
||||
if is_macos:
|
||||
ans = os.path.dirname(os.path.dirname(ans))
|
||||
ans = os.path.join(ans, 'kitty')
|
||||
return ans
|
||||
kitty_base_dir = get_frozen_base()
|
||||
del get_frozen_base
|
||||
else:
|
||||
kitty_base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
extensions_dir = os.path.join(kitty_base_dir, 'kitty')
|
||||
@@ -116,6 +137,26 @@ def cache_dir() -> str:
|
||||
return candidate
|
||||
|
||||
|
||||
@run_once
|
||||
def runtime_dir() -> str:
|
||||
if 'KITTY_RUNTIME_DIRECTORY' in os.environ:
|
||||
candidate = os.path.abspath(os.environ['KITTY_RUNTIME_DIRECTORY'])
|
||||
elif is_macos:
|
||||
from .fast_data_types import user_cache_dir
|
||||
candidate = user_cache_dir()
|
||||
elif 'XDG_RUNTIME_DIR' in os.environ:
|
||||
candidate = os.path.abspath(os.environ['XDG_RUNTIME_DIR'])
|
||||
else:
|
||||
candidate = f'/run/user/{os.geteuid()}'
|
||||
if not os.path.isdir(candidate) or not os.access(candidate, os.X_OK | os.W_OK | os.R_OK):
|
||||
candidate = os.path.join(cache_dir(), 'run')
|
||||
os.makedirs(candidate, exist_ok=True)
|
||||
import stat
|
||||
if stat.S_IMODE(os.stat(candidate).st_mode) != 0o700:
|
||||
os.chmod(candidate, 0o700)
|
||||
return candidate
|
||||
|
||||
|
||||
def wakeup() -> None:
|
||||
from .fast_data_types import get_boss
|
||||
b = get_boss()
|
||||
@@ -133,6 +174,9 @@ except KeyError:
|
||||
with suppress(Exception):
|
||||
print('Failed to read login shell via getpwuid() for current user, falling back to /bin/sh', file=sys.stderr)
|
||||
shell_path = '/bin/sh'
|
||||
# Keep this short as it is limited to 103 bytes on macOS
|
||||
# https://github.com/ansible/ansible/issues/11536#issuecomment-153030743
|
||||
ssh_control_master_template = 'kssh-{kitty_pid}-{ssh_placeholder}'
|
||||
|
||||
|
||||
def glfw_path(module: str) -> str:
|
||||
|
||||
@@ -156,6 +156,24 @@ close_tty(PyObject *self UNUSED, PyObject *args) {
|
||||
|
||||
#undef TTY_ARGS
|
||||
|
||||
static PyObject*
|
||||
py_shm_open(PyObject UNUSED *self, PyObject *args) {
|
||||
char *name;
|
||||
int flags, mode = 0600;
|
||||
if (!PyArg_ParseTuple(args, "si|i", &name, &flags, &mode)) return NULL;
|
||||
long fd = safe_shm_open(name, flags, mode);
|
||||
if (fd < 0) return PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, PyTuple_GET_ITEM(args, 0));
|
||||
return PyLong_FromLong(fd);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
py_shm_unlink(PyObject UNUSED *self, PyObject *args) {
|
||||
char *name;
|
||||
if (!PyArg_ParseTuple(args, "s", &name)) return NULL;
|
||||
if (shm_unlink(name) != 0) return PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, PyTuple_GET_ITEM(args, 0));
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
wcwidth_wrap(PyObject UNUSED *self, PyObject *chr) {
|
||||
return PyLong_FromLong(wcwidth_std(PyLong_AsLong(chr)));
|
||||
@@ -184,6 +202,8 @@ static PyMethodDef module_methods[] = {
|
||||
{"parse_bytes_dump", (PyCFunction)parse_bytes_dump, METH_VARARGS, ""},
|
||||
{"redirect_std_streams", (PyCFunction)redirect_std_streams, METH_VARARGS, ""},
|
||||
{"locale_is_valid", (PyCFunction)locale_is_valid, METH_VARARGS, ""},
|
||||
{"shm_open", (PyCFunction)py_shm_open, METH_VARARGS, ""},
|
||||
{"shm_unlink", (PyCFunction)py_shm_unlink, METH_VARARGS, ""},
|
||||
#ifdef __APPLE__
|
||||
METHODB(user_cache_dir, METH_NOARGS),
|
||||
METHODB(process_group_map, METH_NOARGS),
|
||||
@@ -321,6 +341,14 @@ PyInit_fast_data_types(void) {
|
||||
PyModule_AddIntMacro(m, APC);
|
||||
PyModule_AddIntMacro(m, OSC);
|
||||
PyModule_AddIntMacro(m, FILE_TRANSFER_CODE);
|
||||
#ifdef __APPLE__
|
||||
// Apple says its SHM_NAME_MAX but SHM_NAME_MAX is not actually declared in typical CrApple style.
|
||||
// This value is based on experimentation and from qsharedmemory.cpp in Qt
|
||||
PyModule_AddIntConstant(m, "SHM_NAME_MAX", 30);
|
||||
#else
|
||||
// FreeBSD's man page says this is 1023. Linux says its PATH_MAX.
|
||||
PyModule_AddIntConstant(m, "SHM_NAME_MAX", MIN(1023, PATH_MAX));
|
||||
#endif
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ from kitty.marks import MarkerFunc
|
||||
from kitty.options.types import Options
|
||||
|
||||
# Constants {{{
|
||||
SHM_NAME_MAX: int
|
||||
MOUSE_SELECTION_LINE: int
|
||||
MOUSE_SELECTION_EXTEND: int
|
||||
MOUSE_SELECTION_NORMAL: int
|
||||
@@ -1030,6 +1031,7 @@ class Screen:
|
||||
cursor_key_mode: bool
|
||||
auto_repeat_enabled: bool
|
||||
render_unfocused_cursor: int
|
||||
last_reported_cwd: Optional[str]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -1359,5 +1361,17 @@ def set_os_window_title(os_window_id: int, title: str) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def update_ime_position_for_window(window_id: int, force: bool = False, lost_focus: bool = False) -> bool:
|
||||
def get_os_window_title(os_window_id: int) -> Optional[str]:
|
||||
pass
|
||||
|
||||
|
||||
def update_ime_position_for_window(window_id: int, force: bool = False, update_focus: int = 0) -> bool:
|
||||
pass
|
||||
|
||||
|
||||
def shm_open(name: str, flags: int, mode: int = 0o600) -> int:
|
||||
pass
|
||||
|
||||
|
||||
def shm_unlink(name: str) -> None:
|
||||
pass
|
||||
|
||||
@@ -210,6 +210,14 @@ init_ft_face(Face *self, PyObject *path, int hinting, int hintstyle, FONTS_DATA_
|
||||
return true;
|
||||
}
|
||||
|
||||
static void*
|
||||
set_load_error(const char *path, int error) {
|
||||
char buf[2048];
|
||||
snprintf(buf, sizeof(buf), "Failed to load face from path: %s with error:", path);
|
||||
set_freetype_error(buf, error);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject*
|
||||
face_from_descriptor(PyObject *descriptor, FONTS_DATA_HANDLE fg) {
|
||||
#define D(key, conv, missing_ok) { \
|
||||
@@ -230,7 +238,7 @@ face_from_descriptor(PyObject *descriptor, FONTS_DATA_HANDLE fg) {
|
||||
Face *self = (Face *)Face_Type.tp_alloc(&Face_Type, 0);
|
||||
if (self != NULL) {
|
||||
int error = FT_New_Face(library, path, index, &(self->face));
|
||||
if(error) { set_freetype_error("Failed to load face, with error:", error); Py_CLEAR(self); return NULL; }
|
||||
if(error) { Py_CLEAR(self); return set_load_error(path, error); }
|
||||
if (!init_ft_face(self, PyDict_GetItemString(descriptor, "path"), hinting, hint_style, fg)) { Py_CLEAR(self); return NULL; }
|
||||
}
|
||||
return (PyObject*)self;
|
||||
@@ -241,7 +249,7 @@ native_face_from_path(const char *path, int index) {
|
||||
int error;
|
||||
FT_Face ans;
|
||||
error = FT_New_Face(library, path, index, &ans);
|
||||
if (error) { set_freetype_error("Failed to load face, with error:", error); return NULL; }
|
||||
if (error) return set_load_error(path, error);
|
||||
return ans;
|
||||
}
|
||||
|
||||
@@ -251,7 +259,7 @@ face_from_path(const char *path, int index, FONTS_DATA_HANDLE fg) {
|
||||
if (ans == NULL) return NULL;
|
||||
int error;
|
||||
error = FT_New_Face(library, path, index, &ans->face);
|
||||
if (error) { set_freetype_error("Failed to load face, with error:", error); ans->face = NULL; return NULL; }
|
||||
if (error) { ans->face = NULL; return set_load_error(path, error); }
|
||||
if (!init_ft_face(ans, Py_None, true, 3, fg)) { Py_CLEAR(ans); return NULL; }
|
||||
return (PyObject*)ans;
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ get_load_flags(int hinting, int hintstyle, int base) {
|
||||
static bool
|
||||
load_font(FontConfigFace *info, Face *ans) {
|
||||
ans->freetype = native_face_from_path(info->path, info->index);
|
||||
if (!ans->freetype) return false;
|
||||
if (!ans->freetype || PyErr_Occurred()) return false;
|
||||
ans->hb = hb_ft_font_create(ans->freetype, NULL);
|
||||
if (!ans->hb) { PyErr_NoMemory(); return false; }
|
||||
ans->hinting = info->hinting; ans->hintstyle = info->hintstyle;
|
||||
@@ -268,6 +268,7 @@ find_fallback_font_for(RenderCtx *ctx, char_type codep, char_type next_codep) {
|
||||
ensure_space_for(&main_face, fallbacks, Face, main_face.count + 1, capacity, 8, true);
|
||||
Face *ans = main_face.fallbacks + main_face.count;
|
||||
bool ok = load_font(&q, ans);
|
||||
if (PyErr_Occurred()) PyErr_Print();
|
||||
free(q.path);
|
||||
if (!ok) return NULL;
|
||||
main_face.count++;
|
||||
@@ -556,10 +557,10 @@ create_freetype_render_context(const char *family, bool bold, bool italic) {
|
||||
RenderCtx *ctx = calloc(1, sizeof(RenderCtx));
|
||||
main_face_family.name = family ? strdup(family) : NULL;
|
||||
main_face_family.bold = bold; main_face_family.italic = italic;
|
||||
if (!information_for_font_family(main_face_family.name, main_face_family.bold, main_face_family.italic, &main_face_information)) return false;
|
||||
if (!load_font(&main_face_information, &main_face)) return false;
|
||||
if (!information_for_font_family(main_face_family.name, main_face_family.bold, main_face_family.italic, &main_face_information)) return NULL;
|
||||
if (!load_font(&main_face_information, &main_face)) return NULL;
|
||||
hb_buffer = hb_buffer_create();
|
||||
if (!hb_buffer) { PyErr_NoMemory(); return false; }
|
||||
if (!hb_buffer) { PyErr_NoMemory(); return NULL; }
|
||||
ctx->created = true;
|
||||
return (FreeTypeRenderCtx)ctx;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ known_extensions = {
|
||||
}
|
||||
|
||||
|
||||
text_mimes = ('application/javascript', 'application/x-sh', 'application/json')
|
||||
text_mimes = ('application/javascript', 'application/x-sh', 'application/x-shellscript', 'application/json')
|
||||
|
||||
|
||||
def is_rc_file(path: str) -> bool:
|
||||
|
||||
@@ -9,12 +9,14 @@ from .child import Child
|
||||
from .cli import parse_args
|
||||
from .cli_stub import LaunchCLIOptions
|
||||
from .constants import kitty_exe, shell_path
|
||||
from .fast_data_types import patch_color_profiles, set_clipboard_string
|
||||
from .fast_data_types import (
|
||||
get_os_window_title, patch_color_profiles, set_clipboard_string
|
||||
)
|
||||
from .options.utils import env as parse_env
|
||||
from .tabs import Tab, TabManager
|
||||
from .types import run_once
|
||||
from .utils import log_error, resolve_custom_file, set_primary_selection, which
|
||||
from .window import Watchers, Window
|
||||
from .window import CwdRequest, CwdRequestType, Watchers, Window
|
||||
|
||||
try:
|
||||
from typing import TypedDict
|
||||
@@ -32,12 +34,14 @@ def options_spec() -> str:
|
||||
return '''
|
||||
--window-title --title
|
||||
The title to set for the new window. By default, title is controlled by the
|
||||
child process.
|
||||
child process. The special value :code:`current` will copy the title from the currently
|
||||
active window.
|
||||
|
||||
|
||||
--tab-title
|
||||
The title for the new tab if launching in a new tab. By default, the title
|
||||
of the active window in the tab is used as the tab title.
|
||||
of the active window in the tab is used as the tab title. The special value
|
||||
:code:`current` will copy the title form the title of the currently active tab.
|
||||
|
||||
|
||||
--type
|
||||
@@ -62,6 +66,8 @@ to the newly opened window.
|
||||
--cwd
|
||||
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 by the shell (needs :ref:`shell_integration` to work).
|
||||
|
||||
|
||||
--env
|
||||
@@ -173,7 +179,8 @@ Set the WM_NAME property on X11 for the newly created OS Window when using
|
||||
|
||||
--os-window-title
|
||||
Set the title for the newly created OS window. This title will override any
|
||||
titles set by programs running in kitty.
|
||||
titles set by programs running in kitty. The special value :code:`current`
|
||||
will use the title of the current OS Window, if any.
|
||||
|
||||
|
||||
--logo
|
||||
@@ -290,7 +297,7 @@ def load_watch_modules(watchers: Iterable[str]) -> Optional[Watchers]:
|
||||
class LaunchKwds(TypedDict):
|
||||
|
||||
allow_remote_control: bool
|
||||
cwd_from: Optional[int]
|
||||
cwd_from: Optional[CwdRequest]
|
||||
cwd: Optional[str]
|
||||
location: Optional[str]
|
||||
override_title: Optional[str]
|
||||
@@ -342,6 +349,14 @@ def launch(
|
||||
active_child = active.child
|
||||
else:
|
||||
active_child = None
|
||||
if opts.window_title == 'current':
|
||||
opts.window_title = active.title if active else None
|
||||
if opts.tab_title == 'current':
|
||||
atab = boss.active_tab
|
||||
opts.tab_title = atab.title if atab else None
|
||||
if opts.os_window_title == 'current':
|
||||
tm = boss.active_tab_manager
|
||||
opts.os_window_title = get_os_window_title(tm.os_window_id) if tm else None
|
||||
env = get_env(opts, active_child)
|
||||
kw: LaunchKwds = {
|
||||
'allow_remote_control': opts.allow_remote_control,
|
||||
@@ -357,8 +372,11 @@ def launch(
|
||||
}
|
||||
if opts.cwd:
|
||||
if opts.cwd == 'current':
|
||||
if active_child:
|
||||
kw['cwd_from'] = active_child.pid_for_cwd
|
||||
if active:
|
||||
kw['cwd_from'] = CwdRequest(active)
|
||||
elif opts.cwd == 'last_reported':
|
||||
if active:
|
||||
kw['cwd_from'] = CwdRequest(active, CwdRequestType.last_reported)
|
||||
else:
|
||||
kw['cwd'] = opts.cwd
|
||||
if opts.location != 'default':
|
||||
|
||||
@@ -5,7 +5,7 @@ from functools import partial
|
||||
from itertools import repeat
|
||||
from typing import (
|
||||
Any, Dict, Generator, Iterable, Iterator, List, NamedTuple, Optional,
|
||||
Sequence, Tuple
|
||||
Sequence, Tuple, Union
|
||||
)
|
||||
|
||||
from kitty.borders import BorderColor
|
||||
@@ -78,9 +78,22 @@ def set_layout_options(opts: Options) -> None:
|
||||
lgd.align_top_left = opts.placement_strategy == 'top-left'
|
||||
|
||||
|
||||
def calculate_cells_map(bias: Optional[Sequence[float]], number_of_windows: int, number_of_cells: int) -> List[int]:
|
||||
def convert_bias_map(bias: Dict[int, float], number_of_windows: int, number_of_cells: int) -> Sequence[float]:
|
||||
cells_per_window, extra = divmod(number_of_cells, number_of_windows)
|
||||
cell_map = list(repeat(cells_per_window, number_of_windows))
|
||||
cell_map[-1] += extra
|
||||
base_bias = [x / number_of_cells for x in cell_map]
|
||||
return distribute_indexed_bias(base_bias, bias)
|
||||
|
||||
|
||||
def calculate_cells_map(
|
||||
bias: Union[None, Sequence[float], Dict[int, float]],
|
||||
number_of_windows: int, number_of_cells: int
|
||||
) -> List[int]:
|
||||
if isinstance(bias, dict):
|
||||
bias = convert_bias_map(bias, number_of_windows, number_of_cells)
|
||||
cells_per_window = number_of_cells // number_of_windows
|
||||
if bias is not None and 1 < number_of_windows == len(bias) and cells_per_window > 5:
|
||||
if bias is not None and number_of_windows > 1 and number_of_windows == len(bias) and cells_per_window > 5:
|
||||
cells_map = [int(b * number_of_cells) for b in bias]
|
||||
while min(cells_map) < 5:
|
||||
maxi, mini = map(cells_map.index, (max(cells_map), min(cells_map)))
|
||||
@@ -100,11 +113,12 @@ def layout_dimension(
|
||||
start_at: int, length: int, cell_length: int,
|
||||
decoration_pairs: DecorationPairs,
|
||||
left_align: bool = False,
|
||||
bias: Optional[Sequence[float]] = None
|
||||
bias: Union[None, Sequence[float], Dict[int, float]] = None
|
||||
) -> LayoutDimension:
|
||||
number_of_windows = len(decoration_pairs)
|
||||
number_of_cells = length // cell_length
|
||||
space_needed_for_decorations: int = sum(map(sum, decoration_pairs))
|
||||
dec_vals: Iterable[int] = map(sum, decoration_pairs)
|
||||
space_needed_for_decorations = sum(dec_vals)
|
||||
extra = length - number_of_cells * cell_length
|
||||
while extra < space_needed_for_decorations:
|
||||
number_of_cells -= 1
|
||||
@@ -196,10 +210,6 @@ def distribute_indexed_bias(base_bias: Sequence[float], index_bias_map: Dict[int
|
||||
return normalize_biases(ans)
|
||||
|
||||
|
||||
def variable_bias(num_windows: int, candidate: Dict[int, float]) -> Sequence[float]:
|
||||
return distribute_indexed_bias(list(repeat(1/(num_windows), num_windows)), candidate)
|
||||
|
||||
|
||||
class Layout:
|
||||
|
||||
name: Optional[str] = None
|
||||
@@ -220,8 +230,11 @@ class Layout:
|
||||
self.full_name = f'{self.name}:{layout_opts}' if layout_opts else self.name
|
||||
self.remove_all_biases()
|
||||
|
||||
def bias_increment_for_cell(self, is_horizontal: bool) -> float:
|
||||
def bias_increment_for_cell(self, all_windows: WindowList, window_id: int, is_horizontal: bool) -> float:
|
||||
self._set_dimensions()
|
||||
return self.calculate_bias_increment_for_a_single_cell(all_windows, window_id, is_horizontal)
|
||||
|
||||
def calculate_bias_increment_for_a_single_cell(self, all_windows: WindowList, window_id: int, is_horizontal: bool) -> float:
|
||||
if is_horizontal:
|
||||
return (lgd.cell_width + 1) / lgd.central.width
|
||||
return (lgd.cell_height + 1) / lgd.central.height
|
||||
@@ -270,9 +283,12 @@ class Layout:
|
||||
def move_window_to_group(self, all_windows: WindowList, group: int) -> bool:
|
||||
return all_windows.move_window_group(to_group=group)
|
||||
|
||||
def add_window(self, all_windows: WindowList, window: WindowType, location: Optional[str] = None, overlay_for: Optional[int] = None) -> None:
|
||||
def add_window(
|
||||
self, all_windows: WindowList, window: WindowType, location: Optional[str] = None,
|
||||
overlay_for: Optional[int] = None, put_overlay_behind: bool = False
|
||||
) -> None:
|
||||
if overlay_for is not None and overlay_for in all_windows:
|
||||
all_windows.add_window(window, group_of=overlay_for)
|
||||
all_windows.add_window(window, group_of=overlay_for, head_of_group=put_overlay_behind)
|
||||
return
|
||||
if location == 'neighbor':
|
||||
location = 'after'
|
||||
@@ -327,7 +343,7 @@ class Layout:
|
||||
def xlayout(
|
||||
self,
|
||||
groups: Iterator[WindowGroup],
|
||||
bias: Optional[Sequence[float]] = None,
|
||||
bias: Union[None, Sequence[float], Dict[int, float]] = None,
|
||||
start: Optional[int] = None,
|
||||
size: Optional[int] = None,
|
||||
offset: int = 0,
|
||||
@@ -346,7 +362,7 @@ class Layout:
|
||||
def ylayout(
|
||||
self,
|
||||
groups: Iterator[WindowGroup],
|
||||
bias: Optional[Sequence[float]] = None,
|
||||
bias: Union[None, Sequence[float], Dict[int, float]] = None,
|
||||
start: Optional[int] = None,
|
||||
size: Optional[int] = None,
|
||||
offset: int = 0,
|
||||
|
||||
@@ -15,7 +15,7 @@ from kitty.window_list import WindowGroup, WindowList
|
||||
|
||||
from .base import (
|
||||
BorderLine, Layout, LayoutData, LayoutDimension, ListOfWindows,
|
||||
NeighborsMap, layout_dimension, lgd, variable_bias
|
||||
NeighborsMap, layout_dimension, lgd
|
||||
)
|
||||
from .tall import neighbors_for_tall_window
|
||||
|
||||
@@ -61,7 +61,7 @@ class Grid(Layout):
|
||||
return layout_dimension(lgd.central.top, lgd.central.height, lgd.cell_height, decoration_pairs, bias=bias, left_align=lgd.align_top_left)
|
||||
|
||||
def variable_layout(self, layout_func: Callable[..., LayoutDimension], num_windows: int, biased_map: Dict[int, float]) -> LayoutDimension:
|
||||
return layout_func(num_windows, bias=variable_bias(num_windows, biased_map) if num_windows > 1 else None)
|
||||
return layout_func(num_windows, bias=biased_map if num_windows > 1 else None)
|
||||
|
||||
def apply_bias(self, idx: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool:
|
||||
num_windows = all_windows.num_groups
|
||||
|
||||
@@ -12,7 +12,7 @@ from kitty.window_list import WindowGroup, WindowList
|
||||
|
||||
from .base import (
|
||||
BorderLine, Layout, LayoutData, LayoutDimension, LayoutOpts, NeighborsMap,
|
||||
lgd, normalize_biases, safe_increment_bias, variable_bias
|
||||
lgd, normalize_biases, safe_increment_bias
|
||||
)
|
||||
from .vertical import borders
|
||||
|
||||
@@ -96,7 +96,7 @@ class Tall(Layout):
|
||||
|
||||
def variable_layout(self, all_windows: WindowList, biased_map: Dict[int, float]) -> LayoutDimension:
|
||||
num = all_windows.num_groups - self.num_full_size_windows
|
||||
bias = variable_bias(num, biased_map) if num > 1 else None
|
||||
bias = biased_map if num > 1 else None
|
||||
return self.perp_axis_layout(all_windows.iter_all_layoutable_groups(), bias=bias, offset=self.num_full_size_windows)
|
||||
|
||||
def apply_bias(self, idx: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool:
|
||||
|
||||
@@ -10,7 +10,7 @@ from kitty.window_list import WindowGroup, WindowList
|
||||
|
||||
from .base import (
|
||||
BorderLine, Layout, LayoutData, LayoutDimension, NeighborsMap,
|
||||
lgd, variable_bias
|
||||
lgd
|
||||
)
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ class Vertical(Layout):
|
||||
|
||||
def variable_layout(self, all_windows: WindowList, biased_map: Dict[int, float]) -> LayoutDimension:
|
||||
num_windows = all_windows.num_groups
|
||||
bias = variable_bias(num_windows, biased_map) if num_windows else None
|
||||
bias = biased_map if num_windows > 1 else None
|
||||
return self.main_axis_layout(all_windows.iter_all_layoutable_groups(), bias=bias)
|
||||
|
||||
def fixed_layout(self, wg: WindowGroup) -> LayoutDimension:
|
||||
|
||||
@@ -32,8 +32,8 @@ from .os_window_size import initial_window_size_func
|
||||
from .session import get_os_window_sizing_data
|
||||
from .types import SingleKey
|
||||
from .utils import (
|
||||
detach, expandvars, log_error, single_instance,
|
||||
startup_notification_handler, unix_socket_paths
|
||||
cleanup_ssh_control_masters, detach, expandvars, log_error,
|
||||
single_instance, startup_notification_handler, unix_socket_paths
|
||||
)
|
||||
from .window import load_shader_programs
|
||||
|
||||
@@ -338,11 +338,13 @@ def set_locale() -> None:
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
except Exception:
|
||||
log_error('Failed to set locale with LANG:', os.environ.get('LANG'))
|
||||
os.environ.pop('LANG', None)
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
except Exception:
|
||||
log_error('Failed to set locale with no LANG')
|
||||
old_lang = os.environ.pop('LANG', None)
|
||||
if old_lang is not None:
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
except Exception:
|
||||
log_error('Failed to set locale with no LANG')
|
||||
os.environ['LANG'] = old_lang
|
||||
|
||||
|
||||
def _main() -> None:
|
||||
@@ -408,6 +410,7 @@ def _main() -> None:
|
||||
run_app(opts, cli_opts, bad_lines)
|
||||
finally:
|
||||
glfw_terminate()
|
||||
cleanup_ssh_control_masters()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
@@ -67,11 +67,11 @@ static char mouse_event_buf[64];
|
||||
|
||||
static int
|
||||
encode_mouse_event_impl(const MousePosition *mpos, int mouse_tracking_protocol, int button, MouseAction action, int mods) {
|
||||
unsigned int cb = 0;
|
||||
unsigned int cb = encode_button(button);
|
||||
if (action == MOVE) {
|
||||
cb = 3;
|
||||
if (cb == UINT_MAX) cb = 3;
|
||||
cb += 32;
|
||||
} else {
|
||||
cb = encode_button(button);
|
||||
if (cb == UINT_MAX) return 0;
|
||||
}
|
||||
if (action == DRAG || action == MOVE) cb |= MOTION_INDICATOR;
|
||||
|
||||
@@ -447,6 +447,24 @@ system clipboard.
|
||||
'''
|
||||
)
|
||||
|
||||
opt(
|
||||
'paste_actions', 'quote-urls-at-prompt', option_type='paste_actions',
|
||||
long_text='''
|
||||
A comma separated list of actions to take when pasting text into the terminal.
|
||||
Possibilities are:
|
||||
|
||||
:code:`quote-urls-at-prompt`:
|
||||
If the text being pasted is a URL and the cursor is at a shell prompt,
|
||||
automatically quote the URL (needs :ref:`shell_integration`).
|
||||
:code:`confirm`:
|
||||
Confirm the paste if bracketed paste mode is not active or there is more
|
||||
a large amount of text being pasted.
|
||||
:code:`filter`:
|
||||
Run the filter_paste() function from the file :file:`paste-actions.py` in
|
||||
the kitty config directory on the pasted text. The text returned by the
|
||||
function will be actually pasted.
|
||||
''')
|
||||
|
||||
opt('strip_trailing_spaces', 'never',
|
||||
choices=('always', 'never', 'smart'),
|
||||
long_text='''
|
||||
@@ -962,7 +980,7 @@ be a series of unique numbers or alphabets, case insensitive, from the set
|
||||
'''
|
||||
)
|
||||
|
||||
opt('confirm_os_window_close', '0',
|
||||
opt('confirm_os_window_close', '-1',
|
||||
option_type='int',
|
||||
long_text='''
|
||||
Ask for confirmation when closing an OS window or a Tab with at least this
|
||||
@@ -2671,7 +2689,7 @@ opt('+exe_search_path', '',
|
||||
long_text='''
|
||||
Control where kitty looks to find programs to run. The default search order is:
|
||||
First search the system wide :code:`PATH`, then :file:`~/.local/bin` and :file:`~/bin`.
|
||||
If not still not found, the :code:`PATH` defined in the login shell after sourcing
|
||||
If still not found, the :code:`PATH` defined in the login shell after sourcing
|
||||
all its startup files is tried. Finally, if present, the :code:`PATH` in the :opt:`env`
|
||||
option is tried.
|
||||
|
||||
@@ -2692,8 +2710,9 @@ opt('update_check_interval', '24',
|
||||
long_text='''
|
||||
Periodically check if an update to kitty is available. If an update is found a
|
||||
system notification is displayed informing you of the available update. The
|
||||
default is to check every 24 hrs, set to zero to disable. Changing this option
|
||||
by reloading the config is not supported.
|
||||
default is to check every 24 hrs, set to zero to disable. Update checking is
|
||||
only done by the official binary builds. Distro packages or source builds do not
|
||||
do update checking. Changing this option by reloading the config is not supported.
|
||||
'''
|
||||
)
|
||||
|
||||
@@ -2802,12 +2821,17 @@ just hiding the titlebar with :opt:`hide_window_decorations`.
|
||||
opt('macos_option_as_alt', 'no',
|
||||
option_type='macos_option_as_alt', ctype='uint',
|
||||
long_text='''
|
||||
Use the option key as an alt key. With this set to :code:`no`, kitty will use
|
||||
the macOS native :kbd:`Option+Key` = unicode character behavior. This will break
|
||||
any :kbd:`Alt+key` keyboard shortcuts in your terminal programs, but you can use
|
||||
the macOS unicode input technique. You can use the values: :code:`left`,
|
||||
:code:`right`, or :code:`both` to use only the left, right or both Option keys
|
||||
as Alt, instead. Changing this setting by reloading the config is not supported.
|
||||
Use the :kbd:`Option` key as an :kbd:`Alt` key. With this set to :code:`no`,
|
||||
kitty will use the macOS native :kbd:`Option+Key` = unicode character behavior.
|
||||
This will break any :kbd:`Alt+Key` keyboard shortcuts in your terminal programs,
|
||||
but you can use the macOS unicode input technique. You can use the values:
|
||||
:code:`left`, :code:`right`, or :code:`both` to use only the left, right or both
|
||||
:kbd:`Option` keys as :kbd:`Alt`, instead. Note that kitty itself always treats
|
||||
:kbd:`Option` the same as :kbd:`Alt`. This means you cannot use this setting to
|
||||
configure different kitty shortcuts for :kbd:`Option+Key` vs. `:kbd:`Alt+Key`.
|
||||
Also, any kitty shortcuts using :kbd:`Option/Alt+Key` will take priority, so
|
||||
that any such key presses will not be passed to terminal programs running inside
|
||||
kitty. Changing this setting by reloading the config is not supported.
|
||||
'''
|
||||
)
|
||||
|
||||
@@ -2964,7 +2988,7 @@ the default shortcuts.
|
||||
opt('clear_all_shortcuts', 'no',
|
||||
option_type='clear_all_shortcuts',
|
||||
long_text='''
|
||||
You can have kitty remove all shortcut definition seen up to this point. Useful,
|
||||
You can have kitty remove all shortcut definitions seen up to this point. Useful,
|
||||
for instance, to remove the default shortcuts.
|
||||
'''
|
||||
)
|
||||
@@ -3632,13 +3656,15 @@ map('Reset the terminal',
|
||||
You can create shortcuts to clear/reset the terminal. For example::
|
||||
|
||||
# Reset the terminal
|
||||
map kitty_mod+f9 clear_terminal reset active
|
||||
map f1 clear_terminal reset active
|
||||
# Clear the terminal screen by erasing all contents
|
||||
map kitty_mod+f10 clear_terminal clear active
|
||||
map f1 clear_terminal clear active
|
||||
# Clear the terminal scrollback by erasing it
|
||||
map kitty_mod+f11 clear_terminal scrollback active
|
||||
map f1 clear_terminal scrollback active
|
||||
# Scroll the contents of the screen into the scrollback
|
||||
map kitty_mod+f12 clear_terminal scroll active
|
||||
map f1 clear_terminal scroll active
|
||||
# Clear everything up to the line with the cursor
|
||||
map f1 clear_terminal to_cursor active
|
||||
|
||||
If you want to operate on all windows instead of just the current one, use
|
||||
:italic:`all` instead of :italic:`active`.
|
||||
|
||||
11
kitty/options/parse.py
generated
11
kitty/options/parse.py
generated
@@ -12,9 +12,9 @@ from kitty.options.utils import (
|
||||
deprecated_hide_window_decorations_aliases, deprecated_macos_show_window_title_in_menubar_alias,
|
||||
deprecated_send_text, disable_ligatures, edge_width, env, font_features, hide_window_decorations,
|
||||
macos_option_as_alt, macos_titlebar_color, narrow_symbols, optional_edge_width, parse_map,
|
||||
parse_mouse_map, resize_draw_strategy, scrollback_lines, scrollback_pager_history_size,
|
||||
shell_integration, store_multiple, symbol_map, tab_activity_symbol, tab_bar_edge,
|
||||
tab_bar_margin_height, tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator,
|
||||
parse_mouse_map, paste_actions, resize_draw_strategy, scrollback_lines,
|
||||
scrollback_pager_history_size, shell_integration, store_multiple, symbol_map, tab_activity_symbol,
|
||||
tab_bar_edge, tab_bar_margin_height, tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator,
|
||||
tab_title_template, titlebar_color, to_cursor_shape, to_font_size, to_layout_names, to_modifiers,
|
||||
url_prefixes, url_style, visual_window_select_characters, window_border_width, window_size
|
||||
)
|
||||
@@ -1086,6 +1086,9 @@ class Parser:
|
||||
def open_url_with(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['open_url_with'] = to_cmdline(val)
|
||||
|
||||
def paste_actions(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['paste_actions'] = paste_actions(val)
|
||||
|
||||
def placement_strategy(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
val = val.lower()
|
||||
if val not in self.choices_for_placement_strategy:
|
||||
@@ -1346,7 +1349,7 @@ def create_result_dict() -> typing.Dict[str, typing.Any]:
|
||||
}
|
||||
|
||||
|
||||
actions = frozenset(('map', 'mouse_map'))
|
||||
actions: typing.FrozenSet[str] = frozenset(('map', 'mouse_map'))
|
||||
|
||||
|
||||
def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:
|
||||
|
||||
5
kitty/options/types.py
generated
5
kitty/options/types.py
generated
@@ -6,6 +6,7 @@ from kitty.constants import is_macos
|
||||
import kitty.constants
|
||||
from kitty.fast_data_types import Color
|
||||
import kitty.fast_data_types
|
||||
import kitty.fonts
|
||||
from kitty.options.utils import AliasMap, KeyDefinition, KeyMap, MouseMap, MouseMapping, SequenceMap, TabBarMarginHeight
|
||||
import kitty.options.utils
|
||||
from kitty.types import FloatEdges, SingleKey
|
||||
@@ -389,6 +390,7 @@ option_names = ( # {{{
|
||||
'mouse_map',
|
||||
'narrow_symbols',
|
||||
'open_url_with',
|
||||
'paste_actions',
|
||||
'placement_strategy',
|
||||
'pointer_shape_when_dragging',
|
||||
'pointer_shape_when_grabbed',
|
||||
@@ -480,7 +482,7 @@ class Options:
|
||||
clipboard_max_size: float = 64.0
|
||||
close_on_child_death: bool = False
|
||||
command_on_bell: typing.List[str] = ['none']
|
||||
confirm_os_window_close: int = 0
|
||||
confirm_os_window_close: int = -1
|
||||
copy_on_select: str = ''
|
||||
cursor: typing.Optional[kitty.fast_data_types.Color] = Color(204, 204, 204)
|
||||
cursor_beam_thickness: float = 1.5
|
||||
@@ -535,6 +537,7 @@ class Options:
|
||||
mark3_foreground: Color = Color(0, 0, 0)
|
||||
mouse_hide_wait: float = 0.0 if is_macos else 3.0
|
||||
open_url_with: typing.List[str] = ['default']
|
||||
paste_actions: typing.FrozenSet[str] = frozenset({'quote-urls-at-prompt'})
|
||||
placement_strategy: choices_for_placement_strategy = 'center'
|
||||
pointer_shape_when_dragging: choices_for_pointer_shape_when_dragging = 'beam'
|
||||
pointer_shape_when_grabbed: choices_for_pointer_shape_when_grabbed = 'arrow'
|
||||
|
||||
@@ -800,11 +800,20 @@ def store_multiple(val: str, current_val: Container[str]) -> Iterable[Tuple[str,
|
||||
|
||||
|
||||
def shell_integration(x: str) -> FrozenSet[str]:
|
||||
s = frozenset({'enabled', 'disabled', 'no-rc', 'no-cursor', 'no-title', 'no-prompt-mark', 'no-complete'})
|
||||
s = frozenset({'enabled', 'disabled', 'no-rc', 'no-cursor', 'no-title', 'no-prompt-mark', 'no-complete', 'no-cwd'})
|
||||
q = frozenset(x.lower().split())
|
||||
if not q.issubset(s):
|
||||
log_error(f'Invalid shell integration options: {q - s}, ignoring')
|
||||
return q & s
|
||||
return q & s or frozenset({'invalid'})
|
||||
return q
|
||||
|
||||
|
||||
def paste_actions(x: str) -> FrozenSet[str]:
|
||||
s = frozenset({'quote-urls-at-prompt', 'confirm', 'filter'})
|
||||
q = frozenset(x.lower().split(','))
|
||||
if not q.issubset(s):
|
||||
log_error(f'Invalid paste actions: {q - s}, ignoring')
|
||||
return q & s or frozenset({'invalid'})
|
||||
return q
|
||||
|
||||
|
||||
|
||||
@@ -411,6 +411,7 @@ dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
|
||||
START_DISPATCH
|
||||
DISPATCH_OSC_WITH_CODE(set_color_table_color);
|
||||
END_DISPATCH
|
||||
case 6:
|
||||
case 7:
|
||||
START_DISPATCH
|
||||
DISPATCH_OSC_WITH_CODE(process_cwd_notification);
|
||||
@@ -1016,10 +1017,9 @@ dispatch_csi(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
|
||||
// DCS mode {{{
|
||||
|
||||
static bool
|
||||
startswith(const uint32_t *string, size_t sz, const char *prefix) {
|
||||
size_t l = strlen(prefix);
|
||||
startswith(const uint32_t *string, ssize_t sz, const char *prefix, ssize_t l) {
|
||||
if (sz < l) return false;
|
||||
for (size_t i = 0; i < l; i++) {
|
||||
for (ssize_t i = 0; i < l; i++) {
|
||||
if (string[i] != (unsigned char)prefix[i]) return false;
|
||||
}
|
||||
return true;
|
||||
@@ -1062,29 +1062,36 @@ dispatch_dcs(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
|
||||
}
|
||||
break;
|
||||
case '@':
|
||||
#define CMD_PREFIX "kitty-cmd{"
|
||||
if (startswith(screen->parser_buf + 1, screen->parser_buf_pos - 2, CMD_PREFIX)) {
|
||||
PyObject *cmd = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, screen->parser_buf + 10, screen->parser_buf_pos - 10);
|
||||
if (cmd != NULL) {
|
||||
REPORT_OSC2(screen_handle_cmd, (char)screen->parser_buf[0], cmd);
|
||||
screen_handle_cmd(screen, cmd);
|
||||
Py_DECREF(cmd);
|
||||
} else PyErr_Clear();
|
||||
#undef CMD_PREFIX
|
||||
#define PRINT_PREFIX "kitty-print|"
|
||||
} else if (startswith(screen->parser_buf + 1, screen->parser_buf_pos - 1, PRINT_PREFIX)) {
|
||||
const size_t pp_size = sizeof(PRINT_PREFIX);
|
||||
PyObject *msg = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, screen->parser_buf + pp_size, screen->parser_buf_pos - pp_size);
|
||||
if (msg != NULL) {
|
||||
REPORT_OSC2(screen_handle_print, (char)screen->parser_buf[0], msg);
|
||||
screen_handle_print(screen, msg);
|
||||
Py_DECREF(msg);
|
||||
} else PyErr_Clear();
|
||||
} else {
|
||||
REPORT_ERROR("Unrecognized DCS @ code: 0x%x", screen->parser_buf[1]);
|
||||
if (startswith(screen->parser_buf + 1, screen->parser_buf_pos - 2, "kitty-", sizeof("kitty-") - 1)) {
|
||||
if (startswith(screen->parser_buf + 7, screen->parser_buf_pos - 2, "cmd{", sizeof("cmd{") - 1)) {
|
||||
PyObject *cmd = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, screen->parser_buf + 10, screen->parser_buf_pos - 10);
|
||||
if (cmd != NULL) {
|
||||
REPORT_OSC2(screen_handle_cmd, (char)screen->parser_buf[0], cmd);
|
||||
screen_handle_cmd(screen, cmd);
|
||||
Py_DECREF(cmd);
|
||||
} else PyErr_Clear();
|
||||
#define IF_SIMPLE_PREFIX(prefix, func) \
|
||||
if (startswith(screen->parser_buf + 7, screen->parser_buf_pos - 1, prefix, sizeof(prefix) - 1)) { \
|
||||
const size_t pp_size = sizeof("kitty") + sizeof(prefix); \
|
||||
PyObject *msg = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, screen->parser_buf + pp_size, screen->parser_buf_pos - pp_size); \
|
||||
if (msg != NULL) { \
|
||||
REPORT_OSC2(func, (char)screen->parser_buf[0], msg); \
|
||||
screen_handle_kitty_dcs(screen, #func, msg); \
|
||||
Py_DECREF(msg); \
|
||||
} else PyErr_Clear();
|
||||
|
||||
} else IF_SIMPLE_PREFIX("overlay-ready|", handle_overlay_ready)
|
||||
} else IF_SIMPLE_PREFIX("kitten-result|", handle_kitten_result)
|
||||
} else IF_SIMPLE_PREFIX("print|", handle_remote_print)
|
||||
} else IF_SIMPLE_PREFIX("echo|", handle_remote_echo)
|
||||
} else IF_SIMPLE_PREFIX("ssh|", handle_remote_ssh)
|
||||
} else IF_SIMPLE_PREFIX("ask|", handle_remote_askpass)
|
||||
#undef IF_SIMPLE_PREFIX
|
||||
} else {
|
||||
REPORT_ERROR("Unrecognized DCS @ code: 0x%x", screen->parser_buf[1]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
#undef PRINT_PREFIX
|
||||
default:
|
||||
REPORT_ERROR("Unrecognized DCS code: 0x%x", screen->parser_buf[0]);
|
||||
break;
|
||||
@@ -1177,7 +1184,7 @@ START_ALLOW_CASE_RANGE
|
||||
END_ALLOW_CASE_RANGE
|
||||
if (screen->parser_buf_pos > 0 && screen->parser_buf[screen->parser_buf_pos-1] == ESC) {
|
||||
if (ch == '\\') { screen->parser_buf_pos--; return true; }
|
||||
REPORT_ERROR("DCS sequence contained ESC without trailing \\ ignoring the sequence");
|
||||
REPORT_ERROR("DCS sequence contained ESC without trailing \\ at pos: %u ignoring the sequence", screen->parser_buf_pos);
|
||||
SET_STATE(ESC); return false;
|
||||
}
|
||||
if (screen->parser_buf_pos >= PARSER_BUF_SZ - 1) {
|
||||
|
||||
@@ -18,6 +18,7 @@ class CloseTab(RemoteCommand):
|
||||
'''
|
||||
match: Which tab to close
|
||||
self: Boolean indicating whether to close the tab of the window the command is run in
|
||||
target_group: The target group to close
|
||||
'''
|
||||
|
||||
short_desc = 'Close the specified tab(s)'
|
||||
@@ -25,13 +26,51 @@ class CloseTab(RemoteCommand):
|
||||
--self
|
||||
type=bool-set
|
||||
If specified close the tab of the window this command is run in, rather than the active tab.
|
||||
|
||||
|
||||
--target-group
|
||||
choices=unactive-in-os-window,unactive,others,others-in-os-window,none
|
||||
default=none
|
||||
Close the specified group of tabs. When specified, this option takes precedence over other
|
||||
options controlling which tabs to close.
|
||||
unactive is all tabs in the kitty instance except the currently active tab.
|
||||
unactive-in-os-window is the same as unactive except restricted to the OS Window with the currently active tab
|
||||
others is all tabs except the tab containing the window this command was run in
|
||||
others-in-os-window is the same as others except restricted to the OS window this command was run in
|
||||
'''
|
||||
argspec = ''
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
return {'match': opts.match, 'self': opts.self}
|
||||
return {'match': opts.match, 'self': opts.self, 'target_group': opts.target_group}
|
||||
|
||||
def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get: PayloadGetType) -> ResponseType:
|
||||
g = payload_get('target_group')
|
||||
if g == 'others' and window:
|
||||
avoid = boss.tab_for_window(window)
|
||||
for tab in boss.all_tabs:
|
||||
if tab is not avoid:
|
||||
boss.close_tab_no_confirm(tab)
|
||||
elif g == 'others-in-os-window' and window:
|
||||
avoid = boss.tab_for_window(window)
|
||||
if avoid:
|
||||
tm = boss.os_window_map[avoid.os_window_id]
|
||||
for tab in tm:
|
||||
if tab is not avoid:
|
||||
boss.close_tab_no_confirm(tab)
|
||||
elif g == 'unactive':
|
||||
avoid = boss.active_tab
|
||||
for tab in boss.all_tabs:
|
||||
if tab is not avoid:
|
||||
boss.close_tab_no_confirm(tab)
|
||||
elif g == 'unactive-in-os-window':
|
||||
avoid = boss.active_tab
|
||||
if avoid:
|
||||
tm = boss.os_window_map[avoid.os_window_id]
|
||||
for tab in tm:
|
||||
if tab is not avoid:
|
||||
boss.close_tab_no_confirm(tab)
|
||||
if g:
|
||||
return None
|
||||
for tab in self.tabs_for_match_payload(boss, window, payload_get):
|
||||
if tab:
|
||||
boss.close_tab_no_confirm(tab)
|
||||
|
||||
@@ -27,7 +27,9 @@ class DetachWindow(RemoteCommand):
|
||||
' or add it to the specified tab. Use the special value :code:`new` for :option:`kitty @ detach-window --target-tab`'
|
||||
' to move to a new tab. If no target tab is specified the window is moved to a new OS window.'
|
||||
)
|
||||
options_spec = MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--target-tab -t') + '''\n
|
||||
options_spec = MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--target-tab -t') + '''Use the special value :code:`new` to move to a new tab.
|
||||
|
||||
|
||||
--self
|
||||
type=bool-set
|
||||
If specified detach the window this command is run in, rather than the active window.
|
||||
|
||||
@@ -41,7 +41,7 @@ class ScrollWindow(RemoteCommand):
|
||||
pages = 'p' in amt
|
||||
unscroll = 'u' in amt
|
||||
mult = -1 if amt.endswith('-') and not unscroll else 1
|
||||
q = int(amt.rstrip('+-pu'))
|
||||
q = int(amt.rstrip('+-plu'))
|
||||
amount = q * mult, 'p' if pages else ('u' if unscroll else 'l')
|
||||
|
||||
return {'match': opts.match, 'amount': amount}
|
||||
|
||||
@@ -23,7 +23,7 @@ class SignalChild(RemoteCommand):
|
||||
desc = (
|
||||
'Send one or more signals to the foreground process in the specified window(s).'
|
||||
' If you use the :option:`kitty @ signal-child --match` option'
|
||||
' the title will be set for all matched windows. By default, only the active'
|
||||
' the signal will be sent for all matched windows. By default, only the active'
|
||||
' window is affected. If you do not specify any signals, :code:`SIGINT` is sent by default.'
|
||||
' You can also map this to a keystroke in kitty.conf, for example::\n\n'
|
||||
' map F1 signal_child SIGTERM'
|
||||
|
||||
@@ -154,6 +154,7 @@ void
|
||||
screen_reset(Screen *self) {
|
||||
if (self->linebuf == self->alt_linebuf) screen_toggle_screen_buffer(self, true, true);
|
||||
if (self->overlay_line.is_active) deactivate_overlay_line(self);
|
||||
Py_CLEAR(self->last_reported_cwd);
|
||||
self->render_unfocused_cursor = false;
|
||||
memset(self->main_key_encoding_flags, 0, sizeof(self->main_key_encoding_flags));
|
||||
memset(self->alt_key_encoding_flags, 0, sizeof(self->alt_key_encoding_flags));
|
||||
@@ -445,6 +446,7 @@ dealloc(Screen* self) {
|
||||
pthread_mutex_destroy(&self->write_buf_lock);
|
||||
Py_CLEAR(self->main_grman);
|
||||
Py_CLEAR(self->alt_grman);
|
||||
Py_CLEAR(self->last_reported_cwd);
|
||||
PyMem_RawFree(self->write_buf);
|
||||
Py_CLEAR(self->callbacks);
|
||||
Py_CLEAR(self->test_child);
|
||||
@@ -750,7 +752,7 @@ struct SaveOverlayLine {
|
||||
|
||||
static void
|
||||
save_overlay_line(struct SaveOverlayLine *sol) {
|
||||
if (sol->screen->overlay_line.is_active) {
|
||||
if (sol->screen->overlay_line.is_active && screen_is_cursor_visible(sol->screen)) {
|
||||
sol->overlay_text = get_overlay_text(sol->screen);
|
||||
deactivate_overlay_line(sol->screen);
|
||||
}
|
||||
@@ -762,7 +764,7 @@ restore_overlay_line(struct SaveOverlayLine *sol) {
|
||||
debug("Received input from child (%s) while overlay active. Overlay contents: %s\n", sol->func_name, PyUnicode_AsUTF8(sol->overlay_text));
|
||||
screen_draw_overlay_text(sol->screen, PyUnicode_AsUTF8(sol->overlay_text));
|
||||
Py_DECREF(sol->overlay_text);
|
||||
update_ime_position_for_window(sol->screen->window_id, false, false);
|
||||
update_ime_position_for_window(sol->screen->window_id, false, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1353,6 +1355,7 @@ screen_linefeed(Screen *self) {
|
||||
bool in_margins = cursor_within_margins(self);
|
||||
screen_index(self);
|
||||
if (self->modes.mLNM) screen_carriage_return(self);
|
||||
if (self->cursor->y < self->lines) linebuf_mark_line_as_not_continued(self->linebuf, self->cursor->y);
|
||||
screen_ensure_bounds(self, false, in_margins);
|
||||
}
|
||||
|
||||
@@ -2070,9 +2073,11 @@ set_color_table_color(Screen *self, unsigned int code, PyObject *color) {
|
||||
|
||||
void
|
||||
process_cwd_notification(Screen *self, unsigned int code, PyObject *cwd) {
|
||||
(void)self; (void)code; (void)cwd;
|
||||
// we ignore this as we dont need the stupid OSC 7 cwd reporting protocol,
|
||||
// since, being moderately intelligent, we can get CWD directly.
|
||||
if (code == 7) {
|
||||
Py_CLEAR(self->last_reported_cwd);
|
||||
self->last_reported_cwd = cwd;
|
||||
Py_INCREF(self->last_reported_cwd);
|
||||
} // we ignore OSC 6 document reporting as we dont have a use for it
|
||||
}
|
||||
|
||||
void
|
||||
@@ -2087,7 +2092,12 @@ screen_push_colors(Screen *self, unsigned int idx) {
|
||||
|
||||
void
|
||||
screen_pop_colors(Screen *self, unsigned int idx) {
|
||||
if (colorprofile_pop_colors(self->color_profile, idx)) self->color_profile->dirty = true;
|
||||
color_type bg_before = colorprofile_to_color(self->color_profile, self->color_profile->overridden.default_bg, self->color_profile->configured.default_bg).rgb;
|
||||
if (colorprofile_pop_colors(self->color_profile, idx)) {
|
||||
self->color_profile->dirty = true;
|
||||
color_type bg_after = colorprofile_to_color(self->color_profile, self->color_profile->overridden.default_bg, self->color_profile->configured.default_bg).rgb;
|
||||
CALLBACK("color_profile_popped", "O", bg_before == bg_after ? Py_False : Py_True);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -2099,9 +2109,8 @@ screen_report_color_stack(Screen *self) {
|
||||
write_escape_code_to_child(self, CSI, buf);
|
||||
}
|
||||
|
||||
void
|
||||
screen_handle_print(Screen *self, PyObject *msg) {
|
||||
CALLBACK("handle_remote_print", "O", msg);
|
||||
void screen_handle_kitty_dcs(Screen *self, const char *callback_name, PyObject *cmd) {
|
||||
CALLBACK(callback_name, "O", cmd);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -3982,6 +3991,7 @@ static PyGetSetDef getsetters[] = {
|
||||
static PyMemberDef members[] = {
|
||||
{"callbacks", T_OBJECT_EX, offsetof(Screen, callbacks), 0, "callbacks"},
|
||||
{"cursor", T_OBJECT_EX, offsetof(Screen, cursor), READONLY, "cursor"},
|
||||
{"last_reported_cwd", T_OBJECT, offsetof(Screen, last_reported_cwd), READONLY, "last_reported_cwd"},
|
||||
{"grman", T_OBJECT_EX, offsetof(Screen, grman), READONLY, "grman"},
|
||||
{"color_profile", T_OBJECT_EX, offsetof(Screen, color_profile), READONLY, "color_profile"},
|
||||
{"linebuf", T_OBJECT_EX, offsetof(Screen, linebuf), READONLY, "linebuf"},
|
||||
|
||||
@@ -149,6 +149,7 @@ typedef struct {
|
||||
index_type y;
|
||||
bool is_set;
|
||||
} last_visited_prompt;
|
||||
PyObject *last_reported_cwd;
|
||||
} Screen;
|
||||
|
||||
|
||||
@@ -207,7 +208,7 @@ void screen_handle_cmd(Screen *, PyObject *cmd);
|
||||
void screen_push_colors(Screen *, unsigned int);
|
||||
void screen_pop_colors(Screen *, unsigned int);
|
||||
void screen_report_color_stack(Screen *);
|
||||
void screen_handle_print(Screen *, PyObject *cmd);
|
||||
void screen_handle_kitty_dcs(Screen *, const char *callback_name, PyObject *cmd);
|
||||
void screen_designate_charset(Screen *, uint32_t which, uint32_t as);
|
||||
void screen_use_latin1(Screen *, bool);
|
||||
void set_title(Screen *self, PyObject*);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import shlex
|
||||
import sys
|
||||
from typing import Generator, List, Optional, Union
|
||||
from typing import Generator, List, Optional, Union, TYPE_CHECKING
|
||||
|
||||
from .cli_stub import CLIOptions
|
||||
from .options.utils import to_layout_names, window_size
|
||||
@@ -15,6 +15,10 @@ from .typing import SpecialWindowInstance
|
||||
from .utils import log_error, resolved_shell
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .window import CwdRequest
|
||||
|
||||
|
||||
def get_os_window_sizing_data(opts: Options, session: Optional['Session'] = None) -> WindowSizeData:
|
||||
if session is None or session.os_window_size is None:
|
||||
sizes = WindowSizes(WindowSize(*opts.initial_window_width), WindowSize(*opts.initial_window_height))
|
||||
@@ -146,7 +150,7 @@ def create_sessions(
|
||||
opts: Options,
|
||||
args: Optional[CLIOptions] = None,
|
||||
special_window: Optional['SpecialWindowInstance'] = None,
|
||||
cwd_from: Optional[int] = None,
|
||||
cwd_from: Optional['CwdRequest'] = None,
|
||||
respect_cwd: bool = False,
|
||||
default_session: Optional[str] = None
|
||||
) -> Generator[Session, None, None]:
|
||||
|
||||
@@ -8,8 +8,9 @@ from contextlib import suppress
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from .constants import shell_integration_dir
|
||||
from .options.types import Options
|
||||
from .options.types import Options, defaults
|
||||
from .utils import log_error, which
|
||||
from .fast_data_types import get_options
|
||||
|
||||
|
||||
def setup_fish_env(env: Dict[str, str], argv: List[str]) -> None:
|
||||
@@ -70,21 +71,66 @@ def setup_bash_env(env: Dict[str, str], argv: List[str]) -> None:
|
||||
inject = {'1'}
|
||||
posix_env = rcfile = ''
|
||||
remove_args = set()
|
||||
|
||||
expecting_multi_chars_opt = True
|
||||
expecting_option_arg = False
|
||||
interactive_opt = False
|
||||
expecting_file_arg = False
|
||||
file_arg_set = False
|
||||
|
||||
for i in range(1, len(argv)):
|
||||
arg = argv[i]
|
||||
if arg == '--posix':
|
||||
inject.add('posix')
|
||||
posix_env = env.get('ENV', '')
|
||||
remove_args.add(i)
|
||||
elif arg == '--norc':
|
||||
inject.add('no-rc')
|
||||
remove_args.add(i)
|
||||
elif arg == '--noprofile':
|
||||
inject.add('no-profile')
|
||||
remove_args.add(i)
|
||||
elif arg in ('--rcfile', '--init-file') and i + 1 < len(argv):
|
||||
rcfile = argv[i+1]
|
||||
remove_args |= {i, i+1}
|
||||
if expecting_file_arg:
|
||||
file_arg_set = True
|
||||
break
|
||||
if expecting_option_arg:
|
||||
expecting_option_arg = False
|
||||
continue
|
||||
if arg in ('-', '--'):
|
||||
if not expecting_file_arg:
|
||||
expecting_file_arg = True
|
||||
continue
|
||||
elif len(arg) > 1 and arg[1] != '-' and (arg[0] == '-' or arg.startswith('+O')):
|
||||
expecting_multi_chars_opt = False
|
||||
options = arg.lstrip('-+')
|
||||
# shopt option
|
||||
if 'O' in options:
|
||||
t = options.split('O', maxsplit=1)
|
||||
if not t[1]:
|
||||
expecting_option_arg = True
|
||||
options = t[0]
|
||||
# command string
|
||||
if 'c' in options:
|
||||
# non-interactive shell
|
||||
# also skip `bash -ic` interactive mode with command string
|
||||
return
|
||||
# read from stdin and follow with args
|
||||
if 's' in options:
|
||||
break
|
||||
# interactive option
|
||||
if 'i' in options:
|
||||
interactive_opt = True
|
||||
elif arg.startswith('--') and expecting_multi_chars_opt:
|
||||
if arg == '--posix':
|
||||
inject.add('posix')
|
||||
posix_env = env.get('ENV', '')
|
||||
remove_args.add(i)
|
||||
elif arg == '--norc':
|
||||
inject.add('no-rc')
|
||||
remove_args.add(i)
|
||||
elif arg == '--noprofile':
|
||||
inject.add('no-profile')
|
||||
remove_args.add(i)
|
||||
elif arg in ('--rcfile', '--init-file') and i + 1 < len(argv):
|
||||
expecting_option_arg = True
|
||||
rcfile = argv[i+1]
|
||||
remove_args |= {i, i+1}
|
||||
else:
|
||||
file_arg_set = True
|
||||
break
|
||||
if file_arg_set and not interactive_opt:
|
||||
# non-interactive shell
|
||||
return
|
||||
env['ENV'] = os.path.join(shell_integration_dir, 'bash', 'kitty.bash')
|
||||
env['KITTY_BASH_INJECT'] = ' '.join(inject)
|
||||
if posix_env:
|
||||
@@ -120,11 +166,22 @@ def shell_integration_allows_rc_modification(opts: Options) -> bool:
|
||||
return not (opts.shell_integration & {'disabled', 'no-rc'})
|
||||
|
||||
|
||||
def get_effective_ksi_env_var(opts: Optional[Options] = None) -> str:
|
||||
opts = opts or get_options()
|
||||
if 'disabled' in opts.shell_integration:
|
||||
return ''
|
||||
# Use the default when shell_integration is empty due to misconfiguration
|
||||
if 'invalid' in opts.shell_integration:
|
||||
return ' '.join(defaults.shell_integration)
|
||||
return ' '.join(opts.shell_integration)
|
||||
|
||||
|
||||
def modify_shell_environ(opts: Options, env: Dict[str, str], argv: List[str]) -> None:
|
||||
shell = get_supported_shell_name(argv[0])
|
||||
if shell is None or 'disabled' in opts.shell_integration:
|
||||
ksi = get_effective_ksi_env_var(opts)
|
||||
if shell is None or not ksi:
|
||||
return
|
||||
env['KITTY_SHELL_INTEGRATION'] = ' '.join(opts.shell_integration)
|
||||
env['KITTY_SHELL_INTEGRATION'] = ksi
|
||||
if not shell_integration_allows_rc_modification(opts):
|
||||
return
|
||||
f = ENV_MODIFIERS.get(shell)
|
||||
|
||||
177
kitty/shm.py
Normal file
177
kitty/shm.py
Normal file
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
# This is present in the python stdlib (version 3.7) in
|
||||
# multiprocessing.shared_memory. However, it is crippled in various ways, most
|
||||
# notably using extremely small filenames.
|
||||
|
||||
import errno
|
||||
import mmap
|
||||
import os
|
||||
import secrets
|
||||
import stat
|
||||
import struct
|
||||
from typing import Optional, Union
|
||||
|
||||
from kitty.fast_data_types import SHM_NAME_MAX, shm_open, shm_unlink
|
||||
|
||||
|
||||
def make_filename(prefix: str) -> str:
|
||||
"Create a random filename for the shared memory object."
|
||||
# number of random bytes to use for name. Use a largeish value
|
||||
# to make double unlink safe.
|
||||
if not prefix.startswith('/'):
|
||||
# FreeBSD requires name to start with /
|
||||
prefix = '/' + prefix
|
||||
plen = len(prefix.encode('utf-8'))
|
||||
safe_length = min(plen + 64, SHM_NAME_MAX)
|
||||
if safe_length - plen < 2:
|
||||
raise OSError(errno.ENAMETOOLONG, f'SHM filename prefix {prefix} is too long')
|
||||
nbytes = (safe_length - plen) // 2
|
||||
name = prefix + secrets.token_hex(nbytes)
|
||||
return name
|
||||
|
||||
|
||||
class SharedMemory:
|
||||
'''
|
||||
Create or access randomly named shared memory. To create call with empty name and specific size.
|
||||
To access call with name only.
|
||||
|
||||
WARNING: The actual size of the shared memory may be larger than the requested size.
|
||||
'''
|
||||
_fd: int = -1
|
||||
_name: str = ''
|
||||
_mmap: Optional[mmap.mmap] = None
|
||||
_size: int = 0
|
||||
size_fmt = '!I'
|
||||
num_bytes_for_size = struct.calcsize(size_fmt)
|
||||
|
||||
def __init__(
|
||||
self, name: str = '', size: int = 0, readonly: bool = False,
|
||||
mode: int = stat.S_IREAD | stat.S_IWRITE,
|
||||
prefix: str = 'kitty-', unlink_on_exit: bool = False
|
||||
):
|
||||
self.unlink_on_exit = unlink_on_exit
|
||||
if size < 0:
|
||||
raise TypeError("'size' must be a non-negative integer")
|
||||
if size and name:
|
||||
raise TypeError('Cannot specify both name and size')
|
||||
if not name:
|
||||
flags = os.O_CREAT | os.O_EXCL
|
||||
if not size:
|
||||
raise TypeError("'size' must be > 0")
|
||||
else:
|
||||
flags = 0
|
||||
flags |= os.O_RDONLY if readonly else os.O_RDWR
|
||||
|
||||
tries = 30
|
||||
while not name and tries > 0:
|
||||
tries -= 1
|
||||
q = make_filename(prefix)
|
||||
try:
|
||||
self._fd = shm_open(q, flags, mode)
|
||||
name = q
|
||||
except FileExistsError:
|
||||
continue
|
||||
if tries <= 0:
|
||||
raise OSError(f'Failed to create a uniquely named SHM file, try shortening the prefix from: {prefix}')
|
||||
if self._fd < 0:
|
||||
self._fd = shm_open(name, flags, mode)
|
||||
self._name = name
|
||||
try:
|
||||
if flags & os.O_CREAT and size:
|
||||
os.ftruncate(self._fd, size)
|
||||
self.stats = os.fstat(self._fd)
|
||||
size = self.stats.st_size
|
||||
self._mmap = mmap.mmap(self._fd, size, access=mmap.ACCESS_READ if readonly else mmap.ACCESS_WRITE)
|
||||
except OSError:
|
||||
self.unlink()
|
||||
raise
|
||||
|
||||
self._size = size
|
||||
|
||||
def read(self, sz: int = 0) -> bytes:
|
||||
if sz <= 0:
|
||||
sz = self.size
|
||||
return self.mmap.read(sz)
|
||||
|
||||
def write(self, data: bytes) -> None:
|
||||
self.mmap.write(data)
|
||||
|
||||
def tell(self) -> int:
|
||||
return self.mmap.tell()
|
||||
|
||||
def seek(self, pos: int, whence: int = os.SEEK_SET) -> None:
|
||||
self.mmap.seek(pos, whence)
|
||||
|
||||
def flush(self) -> None:
|
||||
self.mmap.flush()
|
||||
|
||||
def write_data_with_size(self, data: Union[str, bytes]) -> None:
|
||||
if isinstance(data, str):
|
||||
data = data.encode('utf-8')
|
||||
sz = struct.pack(self.size_fmt, len(data))
|
||||
self.write(sz)
|
||||
self.write(data)
|
||||
|
||||
def read_data_with_size(self) -> bytes:
|
||||
sz = struct.unpack(self.size_fmt, self.read(self.num_bytes_for_size))[0]
|
||||
return self.read(sz)
|
||||
|
||||
def __del__(self) -> None:
|
||||
try:
|
||||
self.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def __enter__(self) -> 'SharedMemory':
|
||||
return self
|
||||
|
||||
def __exit__(self, *a: object) -> None:
|
||||
self.close()
|
||||
if self.unlink_on_exit:
|
||||
self.unlink()
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return self._size
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def mmap(self) -> mmap.mmap:
|
||||
ans = self._mmap
|
||||
if ans is None:
|
||||
raise RuntimeError('Cannot access the mmap of a closed shared memory object')
|
||||
return ans
|
||||
|
||||
def fileno(self) -> int:
|
||||
return self._fd
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'{self.__class__.__name__}({self.name!r}, size={self.size})'
|
||||
|
||||
def close(self) -> None:
|
||||
"""Closes access to the shared memory from this instance but does
|
||||
not destroy the shared memory block."""
|
||||
if self._mmap is not None:
|
||||
self._mmap.close()
|
||||
self._mmap = None
|
||||
if self._fd >= 0:
|
||||
os.close(self._fd)
|
||||
self._fd = -1
|
||||
|
||||
def unlink(self) -> None:
|
||||
"""Requests that the underlying shared memory block be destroyed.
|
||||
|
||||
In order to ensure proper cleanup of resources, unlink should be
|
||||
called once (and only once) across all processes which have access
|
||||
to the shared memory block."""
|
||||
if self._name:
|
||||
try:
|
||||
shm_unlink(self._name)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
self._name = ''
|
||||
@@ -55,3 +55,11 @@ _global_instance = ShortUUID()
|
||||
uuid4 = _global_instance.uuid4
|
||||
uuid5 = _global_instance.uuid5
|
||||
decode = _global_instance.decode
|
||||
_escape_code_instance: Optional[ShortUUID] = None
|
||||
|
||||
|
||||
def uuid4_for_escape_code() -> str:
|
||||
global _escape_code_instance
|
||||
if _escape_code_instance is None:
|
||||
_escape_code_instance = ShortUUID(escape_code_safe_alphabet)
|
||||
return _escape_code_instance.uuid4()
|
||||
|
||||
@@ -602,7 +602,7 @@ send_pending_click_to_window_id(id_type timer_id UNUSED, void *data) {
|
||||
}
|
||||
|
||||
bool
|
||||
update_ime_position_for_window(id_type window_id, bool force, bool lost_focus) {
|
||||
update_ime_position_for_window(id_type window_id, bool force, int update_focus) {
|
||||
for (size_t o = 0; o < global_state.num_os_windows; o++) {
|
||||
OSWindow *osw = global_state.os_windows + o;
|
||||
for (size_t t = 0; t < osw->num_tabs; t++) {
|
||||
@@ -610,11 +610,14 @@ update_ime_position_for_window(id_type window_id, bool force, bool lost_focus) {
|
||||
for (size_t w = 0; w < qtab->num_windows; w++) {
|
||||
Window *window = qtab->windows + w;
|
||||
if (window->id == window_id) {
|
||||
if (window->render_data.screen && (force || osw->is_focused)) {
|
||||
// The screen may not be ready after the new window is created and focused, and still needs to enable IME.
|
||||
if ((window->render_data.screen && (force || osw->is_focused)) || update_focus > 0) {
|
||||
OSWindow *orig = global_state.callback_os_window;
|
||||
global_state.callback_os_window = osw;
|
||||
if (lost_focus) update_ime_focus(osw, false);
|
||||
else update_ime_position(window, window->render_data.screen);
|
||||
if (update_focus) update_ime_focus(osw, update_focus > 0);
|
||||
if (update_focus >= 0 && window->render_data.screen) {
|
||||
update_ime_position(window, window->render_data.screen);
|
||||
}
|
||||
global_state.callback_os_window = orig;
|
||||
return true;
|
||||
}
|
||||
@@ -653,9 +656,9 @@ update_ime_position_for_window(id_type window_id, bool force, bool lost_focus) {
|
||||
PYWRAP1(update_ime_position_for_window) {
|
||||
id_type window_id;
|
||||
int force = 0;
|
||||
int lost_focus = 0;
|
||||
PA("K|pp", &window_id, &force, &lost_focus);
|
||||
if (update_ime_position_for_window(window_id, force, lost_focus)) Py_RETURN_TRUE;
|
||||
int update_focus = 0;
|
||||
PA("K|pi", &window_id, &force, &update_focus);
|
||||
if (update_ime_position_for_window(window_id, force, update_focus)) Py_RETURN_TRUE;
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
@@ -924,12 +927,15 @@ PYWRAP1(sync_os_window_title) {
|
||||
|
||||
PYWRAP1(set_os_window_title) {
|
||||
id_type os_window_id;
|
||||
const char *title;
|
||||
PA("Ks", &os_window_id, &title);
|
||||
PyObject *title;
|
||||
PA("KU", &os_window_id, &title);
|
||||
WITH_OS_WINDOW(os_window_id)
|
||||
if (strlen(title)) {
|
||||
if (PyUnicode_GetLength(title)) {
|
||||
os_window->title_is_overriden = true;
|
||||
set_os_window_title(os_window, title);
|
||||
Py_XDECREF(os_window->window_title);
|
||||
os_window->window_title = title;
|
||||
Py_INCREF(title);
|
||||
set_os_window_title(os_window, PyUnicode_AsUTF8(title));
|
||||
} else {
|
||||
os_window->title_is_overriden = false;
|
||||
if (os_window->window_title) set_os_window_title(os_window, PyUnicode_AsUTF8(os_window->window_title));
|
||||
@@ -939,6 +945,15 @@ PYWRAP1(set_os_window_title) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PYWRAP1(get_os_window_title) {
|
||||
id_type os_window_id;
|
||||
PA("K", &os_window_id);
|
||||
WITH_OS_WINDOW(os_window_id)
|
||||
if (os_window->window_title) return Py_BuildValue("O", os_window->window_title);
|
||||
END_WITH_OS_WINDOW
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
|
||||
|
||||
PYWRAP1(pt_to_px) {
|
||||
@@ -1277,6 +1292,7 @@ static PyMethodDef module_methods[] = {
|
||||
MW(background_opacity_of, METH_O),
|
||||
MW(update_window_visibility, METH_VARARGS),
|
||||
MW(sync_os_window_title, METH_VARARGS),
|
||||
MW(get_os_window_title, METH_VARARGS),
|
||||
MW(set_os_window_title, METH_VARARGS),
|
||||
MW(global_font_size, METH_VARARGS),
|
||||
MW(set_background_image, METH_VARARGS),
|
||||
|
||||
@@ -342,6 +342,6 @@ uint8_t* draw_single_ascii_char(const char ch, size_t *result_width, size_t *res
|
||||
bool is_os_window_fullscreen(OSWindow *);
|
||||
void update_ime_focus(OSWindow* osw, bool focused);
|
||||
void update_ime_position(Window* w, Screen *screen);
|
||||
bool update_ime_position_for_window(id_type window_id, bool force, bool lost_focus);
|
||||
bool update_ime_position_for_window(id_type window_id, bool force, int update_focus);
|
||||
void set_ignore_os_keyboard_processing(bool enabled);
|
||||
void update_menu_bar_title(PyObject *title UNUSED);
|
||||
|
||||
@@ -30,7 +30,7 @@ from .tab_bar import TabBar, TabBarData
|
||||
from .types import ac
|
||||
from .typing import EdgeLiteral, SessionTab, SessionType, TypedDict
|
||||
from .utils import log_error, platform_window_id, resolved_shell
|
||||
from .window import Watchers, Window, WindowDict
|
||||
from .window import CwdRequest, Watchers, Window, WindowDict
|
||||
from .window_list import WindowList
|
||||
|
||||
|
||||
@@ -58,24 +58,26 @@ class SpecialWindowInstance(NamedTuple):
|
||||
cmd: Optional[List[str]]
|
||||
stdin: Optional[bytes]
|
||||
override_title: Optional[str]
|
||||
cwd_from: Optional[int]
|
||||
cwd_from: Optional[CwdRequest]
|
||||
cwd: Optional[str]
|
||||
overlay_for: Optional[int]
|
||||
env: Optional[Dict[str, str]]
|
||||
watchers: Optional[Watchers]
|
||||
overlay_behind: bool
|
||||
|
||||
|
||||
def SpecialWindow(
|
||||
cmd: Optional[List[str]],
|
||||
stdin: Optional[bytes] = None,
|
||||
override_title: Optional[str] = None,
|
||||
cwd_from: Optional[int] = None,
|
||||
cwd_from: Optional[CwdRequest] = None,
|
||||
cwd: Optional[str] = None,
|
||||
overlay_for: Optional[int] = None,
|
||||
env: Optional[Dict[str, str]] = None,
|
||||
watchers: Optional[Watchers] = None
|
||||
watchers: Optional[Watchers] = None,
|
||||
overlay_behind: bool = False
|
||||
) -> SpecialWindowInstance:
|
||||
return SpecialWindowInstance(cmd, stdin, override_title, cwd_from, cwd, overlay_for, env, watchers)
|
||||
return SpecialWindowInstance(cmd, stdin, override_title, cwd_from, cwd, overlay_for, env, watchers, overlay_behind)
|
||||
|
||||
|
||||
def add_active_id_to_history(items: Deque[int], item_id: int, maxlen: int = 64) -> None:
|
||||
@@ -98,7 +100,7 @@ class Tab: # {{{
|
||||
tab_manager: 'TabManager',
|
||||
session_tab: Optional['SessionTab'] = None,
|
||||
special_window: Optional[SpecialWindowInstance] = None,
|
||||
cwd_from: Optional[int] = None,
|
||||
cwd_from: Optional[CwdRequest] = None,
|
||||
no_initial_window: bool = False
|
||||
):
|
||||
self.tab_manager_ref = weakref.ref(tab_manager)
|
||||
@@ -304,7 +306,7 @@ class Tab: # {{{
|
||||
self.goto_layout(layout_name)
|
||||
|
||||
def resize_window_by(self, window_id: int, increment: float, is_horizontal: bool) -> Optional[str]:
|
||||
increment_as_percent = self.current_layout.bias_increment_for_cell(is_horizontal) * increment
|
||||
increment_as_percent = self.current_layout.bias_increment_for_cell(self.windows, window_id, is_horizontal) * increment
|
||||
if self.current_layout.modify_size_of_window(self.windows, window_id, increment_as_percent, is_horizontal):
|
||||
self.relayout()
|
||||
return None
|
||||
@@ -347,7 +349,7 @@ class Tab: # {{{
|
||||
use_shell: bool = False,
|
||||
cmd: Optional[List[str]] = None,
|
||||
stdin: Optional[bytes] = None,
|
||||
cwd_from: Optional[int] = None,
|
||||
cwd_from: Optional[CwdRequest] = None,
|
||||
cwd: Optional[str] = None,
|
||||
env: Optional[Dict[str, str]] = None,
|
||||
allow_remote_control: bool = False
|
||||
@@ -403,8 +405,8 @@ class Tab: # {{{
|
||||
ans.fork()
|
||||
return ans
|
||||
|
||||
def _add_window(self, window: Window, location: Optional[str] = None, overlay_for: Optional[int] = None) -> None:
|
||||
self.current_layout.add_window(self.windows, window, location, overlay_for)
|
||||
def _add_window(self, window: Window, location: Optional[str] = None, overlay_for: Optional[int] = None, overlay_behind: bool = False) -> None:
|
||||
self.current_layout.add_window(self.windows, window, location, overlay_for, put_overlay_behind=overlay_behind)
|
||||
self.mark_tab_bar_dirty()
|
||||
self.relayout()
|
||||
|
||||
@@ -414,7 +416,7 @@ class Tab: # {{{
|
||||
cmd: Optional[List[str]] = None,
|
||||
stdin: Optional[bytes] = None,
|
||||
override_title: Optional[str] = None,
|
||||
cwd_from: Optional[int] = None,
|
||||
cwd_from: Optional[CwdRequest] = None,
|
||||
cwd: Optional[str] = None,
|
||||
overlay_for: Optional[int] = None,
|
||||
env: Optional[Dict[str, str]] = None,
|
||||
@@ -422,7 +424,8 @@ class Tab: # {{{
|
||||
copy_colors_from: Optional[Window] = None,
|
||||
allow_remote_control: bool = False,
|
||||
marker: Optional[str] = None,
|
||||
watchers: Optional[Watchers] = None
|
||||
watchers: Optional[Watchers] = None,
|
||||
overlay_behind: bool = False
|
||||
) -> Window:
|
||||
child = self.launch_child(
|
||||
use_shell=use_shell, cmd=cmd, stdin=stdin, cwd_from=cwd_from, cwd=cwd, env=env, allow_remote_control=allow_remote_control)
|
||||
@@ -432,7 +435,7 @@ class Tab: # {{{
|
||||
)
|
||||
# Must add child before laying out so that resize_pty succeeds
|
||||
get_boss().add_child(window)
|
||||
self._add_window(window, location=location, overlay_for=overlay_for)
|
||||
self._add_window(window, location=location, overlay_for=overlay_for, overlay_behind=overlay_behind)
|
||||
if marker:
|
||||
try:
|
||||
window.set_marker(marker)
|
||||
@@ -453,7 +456,7 @@ class Tab: # {{{
|
||||
override_title=special_window.override_title,
|
||||
cwd_from=special_window.cwd_from, cwd=special_window.cwd, overlay_for=special_window.overlay_for,
|
||||
env=special_window.env, location=location, copy_colors_from=copy_colors_from,
|
||||
allow_remote_control=allow_remote_control, watchers=special_window.watchers
|
||||
allow_remote_control=allow_remote_control, watchers=special_window.watchers, overlay_behind=special_window.overlay_behind
|
||||
)
|
||||
|
||||
@ac('win', 'Close all windows in the tab other than the currently active window')
|
||||
@@ -464,6 +467,9 @@ class Tab: # {{{
|
||||
if window is not active_window:
|
||||
self.remove_window(window)
|
||||
|
||||
def move_window_to_top_of_group(self, window: Window) -> bool:
|
||||
return self.windows.move_window_to_top_of_group(window)
|
||||
|
||||
def remove_window(self, window: Window, destroy: bool = True) -> None:
|
||||
self.windows.remove_window(window)
|
||||
if destroy:
|
||||
@@ -942,7 +948,7 @@ class TabManager: # {{{
|
||||
def new_tab(
|
||||
self,
|
||||
special_window: Optional[SpecialWindowInstance] = None,
|
||||
cwd_from: Optional[int] = None,
|
||||
cwd_from: Optional[CwdRequest] = None,
|
||||
as_neighbor: bool = False,
|
||||
empty_tab: bool = False,
|
||||
location: str = 'last'
|
||||
@@ -955,6 +961,8 @@ class TabManager: # {{{
|
||||
location = 'after'
|
||||
if location == 'neighbor':
|
||||
location = 'after'
|
||||
if location == 'default':
|
||||
location = 'last'
|
||||
if len(self.tabs) > 1 and location != 'last':
|
||||
if location == 'first':
|
||||
desired_idx = 0
|
||||
|
||||
@@ -18,7 +18,8 @@ from typing import (
|
||||
)
|
||||
|
||||
from .constants import (
|
||||
appname, config_dir, is_macos, is_wayland, read_kitty_resource, shell_path,
|
||||
appname, config_dir, is_macos, is_wayland, read_kitty_resource,
|
||||
runtime_dir, shell_path, ssh_control_master_template,
|
||||
supports_primary_selection
|
||||
)
|
||||
from .fast_data_types import Color, open_tty
|
||||
@@ -178,7 +179,7 @@ class ScreenSizeGetter:
|
||||
return cast(ScreenSize, self.ans)
|
||||
|
||||
|
||||
@lru_cache(maxsize=64)
|
||||
@lru_cache(maxsize=64, typed=True)
|
||||
def screen_size_function(fd: Optional[int] = None) -> ScreenSizeGetter:
|
||||
return ScreenSizeGetter(fd)
|
||||
|
||||
@@ -454,6 +455,7 @@ def parse_address_spec(spec: str) -> Tuple[AddressFamily, Union[Tuple[str, int],
|
||||
def write_all(fd: int, data: Union[str, bytes], block_until_written: bool = True) -> None:
|
||||
if isinstance(data, str):
|
||||
data = data.encode('utf-8')
|
||||
data = memoryview(data)
|
||||
while data:
|
||||
try:
|
||||
n = os.write(fd, data)
|
||||
@@ -507,16 +509,25 @@ class TTYIO:
|
||||
break
|
||||
|
||||
|
||||
@contextmanager
|
||||
def no_echo(fd: int = -1) -> Generator[None, None, None]:
|
||||
def set_echo(fd: int = -1, on: bool = False) -> Tuple[int, List[Union[int, List[Union[bytes, int]]]]]:
|
||||
import termios
|
||||
if fd < 0:
|
||||
fd = sys.stdin.fileno()
|
||||
old = termios.tcgetattr(fd)
|
||||
new = termios.tcgetattr(fd)
|
||||
new[3] = new[3] & ~termios.ECHO
|
||||
if on:
|
||||
new[3] |= termios.ECHO
|
||||
else:
|
||||
new[3] &= ~termios.ECHO
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, new)
|
||||
return fd, old
|
||||
|
||||
|
||||
@contextmanager
|
||||
def no_echo(fd: int = -1) -> Generator[None, None, None]:
|
||||
import termios
|
||||
fd, old = set_echo(fd)
|
||||
try:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, new)
|
||||
yield
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
||||
@@ -819,6 +830,7 @@ class SSHConnectionData(NamedTuple):
|
||||
hostname: str
|
||||
port: Optional[int] = None
|
||||
identity_file: str = ''
|
||||
extra_args: Tuple[Tuple[str, str], ...] = ()
|
||||
|
||||
|
||||
def get_new_os_window_size(
|
||||
@@ -906,3 +918,46 @@ def hold_till_enter() -> None:
|
||||
break
|
||||
if q in b'\x03\x04':
|
||||
write_all(fd, msg)
|
||||
|
||||
|
||||
def cleanup_ssh_control_masters() -> None:
|
||||
import glob
|
||||
import subprocess
|
||||
try:
|
||||
files = frozenset(glob.glob(os.path.join(runtime_dir(), ssh_control_master_template.format(
|
||||
kitty_pid=os.getpid(), ssh_placeholder='*'))))
|
||||
except OSError:
|
||||
return
|
||||
workers = tuple(subprocess.Popen([
|
||||
'ssh', '-o', f'ControlPath={x}', '-O', 'exit', 'kitty-unused-host-name'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
for x in files)
|
||||
for w in workers:
|
||||
w.wait()
|
||||
for x in files:
|
||||
with suppress(OSError):
|
||||
os.remove(x)
|
||||
|
||||
|
||||
def path_from_osc7_url(url: str) -> str:
|
||||
if url.startswith('kitty-shell-cwd://'):
|
||||
return '/' + url.split('/', 3)[-1]
|
||||
if url.startswith('file://'):
|
||||
from urllib.parse import urlparse, unquote
|
||||
return unquote(urlparse(url).path)
|
||||
return ''
|
||||
|
||||
|
||||
@run_once
|
||||
def macos_version() -> Tuple[int, ...]:
|
||||
import platform
|
||||
return tuple(map(int, platform.mac_ver()[0].split('.')))
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def less_version(less_exe: str = 'less') -> int:
|
||||
import subprocess
|
||||
o = subprocess.check_output([less_exe, '-V'], stderr=subprocess.STDOUT).decode()
|
||||
m = re.match(r'less (\d+)', o)
|
||||
if m is None:
|
||||
raise ValueError(f'Invalid version string for less: {o}')
|
||||
return int(m.group(1))
|
||||
|
||||
257
kitty/window.py
257
kitty/window.py
@@ -7,7 +7,7 @@ import re
|
||||
import sys
|
||||
import weakref
|
||||
from collections import deque
|
||||
from enum import IntEnum
|
||||
from enum import Enum, IntEnum, auto
|
||||
from functools import partial
|
||||
from gettext import gettext as _
|
||||
from itertools import chain
|
||||
@@ -20,7 +20,7 @@ from typing import (
|
||||
from .child import ProcessDesc
|
||||
from .cli_stub import CLIOptions
|
||||
from .config import build_ansi_color_table
|
||||
from .constants import appname, is_macos, wakeup
|
||||
from .constants import appname, is_macos, wakeup, config_dir
|
||||
from .fast_data_types import (
|
||||
BGIMAGE_PROGRAM, BLIT_PROGRAM, CELL_BG_PROGRAM, CELL_FG_PROGRAM,
|
||||
CELL_PROGRAM, CELL_SPECIAL_PROGRAM, CURSOR_BEAM, CURSOR_BLOCK,
|
||||
@@ -42,12 +42,12 @@ from .notify import NotificationCommand, handle_notification_cmd
|
||||
from .options.types import Options
|
||||
from .rgb import to_color
|
||||
from .terminfo import get_capabilities
|
||||
from .types import MouseEvent, WindowGeometry, ac
|
||||
from .types import MouseEvent, WindowGeometry, ac, run_once
|
||||
from .typing import BossType, ChildType, EdgeLiteral, TabType, TypedDict
|
||||
from .utils import (
|
||||
get_primary_selection, kitty_ansi_sanitizer_pat, load_shaders, log_error,
|
||||
open_cmd, open_url, parse_color_set, resolve_custom_file, sanitize_title,
|
||||
set_primary_selection
|
||||
open_cmd, open_url, parse_color_set, path_from_osc7_url,
|
||||
resolve_custom_file, resolved_shell, sanitize_title, set_primary_selection
|
||||
)
|
||||
|
||||
MatchPatternType = Union[Pattern[str], Tuple[Pattern[str], Optional[Pattern[str]]]]
|
||||
@@ -57,6 +57,52 @@ if TYPE_CHECKING:
|
||||
from .file_transmission import FileTransmission
|
||||
|
||||
|
||||
class CwdRequestType(Enum):
|
||||
current: int = auto()
|
||||
last_reported: int = auto()
|
||||
|
||||
|
||||
class CwdRequest:
|
||||
|
||||
def __init__(self, window: Optional['Window'] = None, request_type: CwdRequestType = CwdRequestType.current) -> None:
|
||||
self.window_id = -1 if window is None else window.id
|
||||
self.request_type = request_type
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return self.window_id > -1
|
||||
|
||||
@property
|
||||
def window(self) -> Optional['Window']:
|
||||
return get_boss().window_id_map.get(self.window_id)
|
||||
|
||||
@property
|
||||
def cwd_of_child(self) -> str:
|
||||
window = self.window
|
||||
if not window:
|
||||
return ''
|
||||
reported_cwd = path_from_osc7_url(window.screen.last_reported_cwd) if window.screen.last_reported_cwd else ''
|
||||
if reported_cwd and not window.child_is_remote and (self.request_type is CwdRequestType.last_reported or window.at_prompt):
|
||||
return reported_cwd
|
||||
return window.cwd_of_child or ''
|
||||
|
||||
def modify_argv_for_launch_with_cwd(self, argv: List[str]) -> str:
|
||||
window = self.window
|
||||
if not window:
|
||||
return ''
|
||||
reported_cwd = path_from_osc7_url(window.screen.last_reported_cwd) if window.screen.last_reported_cwd else ''
|
||||
# First check if we are running ssh kitten, and trying to open the configured login shell
|
||||
if reported_cwd and argv[0] == resolved_shell(get_options())[0]:
|
||||
ssh_kitten_cmdline = window.ssh_kitten_cmdline()
|
||||
if ssh_kitten_cmdline:
|
||||
from kittens.ssh.main import set_cwd_in_cmdline
|
||||
argv[:] = ssh_kitten_cmdline
|
||||
set_cwd_in_cmdline(reported_cwd, argv)
|
||||
return ''
|
||||
if self.request_type is CwdRequestType.last_reported and reported_cwd and not window.child_is_remote:
|
||||
return reported_cwd
|
||||
return window.cwd_of_child or ''
|
||||
|
||||
|
||||
def process_title_from_child(title: str, is_base64: bool) -> str:
|
||||
if is_base64:
|
||||
from base64 import standard_b64decode
|
||||
@@ -293,6 +339,23 @@ def setup_colors(screen: Screen, opts: Options) -> None:
|
||||
)
|
||||
|
||||
|
||||
@run_once
|
||||
def load_paste_filter() -> Callable[[str], str]:
|
||||
import runpy
|
||||
import traceback
|
||||
try:
|
||||
m = runpy.run_path(os.path.join(config_dir, 'paste-actions.py'))
|
||||
func: Callable[[str], str] = m['filter_paste']
|
||||
except Exception as e:
|
||||
if not isinstance(e, FileNotFoundError):
|
||||
traceback.print_exc()
|
||||
log_error(f'Failed to load paste filter function with error: {e}')
|
||||
|
||||
def func(text: str) -> str:
|
||||
return text
|
||||
return func
|
||||
|
||||
|
||||
def text_sanitizer(as_ansi: bool, add_wrap_markers: bool) -> Callable[[str], str]:
|
||||
pat = kitty_ansi_sanitizer_pat()
|
||||
ansi, wrap_markers = not as_ansi, not add_wrap_markers
|
||||
@@ -321,8 +384,9 @@ def cmd_output(screen: Screen, which: CommandOutput = CommandOutput.last_run, as
|
||||
return ''.join(lines)
|
||||
|
||||
|
||||
def process_remote_print(msg: bytes) -> str:
|
||||
def process_remote_print(msg: str) -> str:
|
||||
from base64 import standard_b64decode
|
||||
|
||||
from .cli import green
|
||||
text = standard_b64decode(msg).decode('utf-8', 'replace')
|
||||
return text.replace('\x1b', green(r'\e')).replace('\a', green(r'\a')).replace('\0', green(r'\0'))
|
||||
@@ -408,6 +472,7 @@ class Window:
|
||||
self.id: int = add_window(tab.os_window_id, tab.id, self.title)
|
||||
self.margin = EdgeWidths()
|
||||
self.padding = EdgeWidths()
|
||||
self.kitten_result: Optional[Dict[str, Any]] = None
|
||||
if not self.id:
|
||||
raise Exception(f'No tab with id: {tab.id} in OS Window: {tab.os_window_id} was found, or the window counter wrapped')
|
||||
self.tab_id = tab.id
|
||||
@@ -499,7 +564,7 @@ class Window:
|
||||
return dict(
|
||||
id=self.id,
|
||||
is_focused=is_focused,
|
||||
title=self.override_title or self.title,
|
||||
title=self.title,
|
||||
pid=self.child.pid,
|
||||
cwd=self.child.current_cwd or self.child.cwd,
|
||||
cmdline=self.child.cmdline,
|
||||
@@ -733,7 +798,7 @@ class Window:
|
||||
self.screen.focus_changed(focused)
|
||||
if focused:
|
||||
self.last_focused_at = monotonic()
|
||||
update_ime_position_for_window(self.id)
|
||||
update_ime_position_for_window(self.id, False, 1)
|
||||
changed = self.needs_attention
|
||||
self.needs_attention = False
|
||||
if changed:
|
||||
@@ -742,7 +807,7 @@ class Window:
|
||||
tab.relayout_borders()
|
||||
elif self.os_window_id == current_os_window():
|
||||
# Cancel IME composition after loses focus
|
||||
update_ime_position_for_window(self.id, False, True)
|
||||
update_ime_position_for_window(self.id, False, -1)
|
||||
|
||||
def title_changed(self, new_title: Optional[str], is_base64: bool = False) -> None:
|
||||
self.child_title = process_title_from_child(new_title or self.default_title, is_base64)
|
||||
@@ -815,6 +880,10 @@ class Window:
|
||||
if default_bg_changed:
|
||||
get_boss().default_bg_changed_for(self.id)
|
||||
|
||||
def color_profile_popped(self, bg_changed: bool) -> None:
|
||||
if bg_changed:
|
||||
get_boss().default_bg_changed_for(self.id)
|
||||
|
||||
def report_color(self, code: str, r: int, g: int, b: int) -> None:
|
||||
r |= r << 8
|
||||
g |= g << 8
|
||||
@@ -875,7 +944,56 @@ class Window:
|
||||
def handle_remote_cmd(self, cmd: str) -> None:
|
||||
get_boss().handle_remote_cmd(cmd, self)
|
||||
|
||||
def handle_remote_print(self, msg: bytes) -> None:
|
||||
def handle_remote_echo(self, msg: str) -> None:
|
||||
from base64 import standard_b64decode
|
||||
data = standard_b64decode(msg)
|
||||
self.write_to_child(data)
|
||||
|
||||
def handle_remote_ssh(self, msg: str) -> None:
|
||||
from kittens.ssh.main import get_ssh_data
|
||||
for line in get_ssh_data(msg, f'{os.getpid()}-{self.id}'):
|
||||
self.write_to_child(line)
|
||||
|
||||
def handle_kitten_result(self, msg: str) -> None:
|
||||
import base64
|
||||
self.kitten_result = json.loads(base64.b85decode(msg))
|
||||
|
||||
def handle_overlay_ready(self, msg: str) -> None:
|
||||
boss = get_boss()
|
||||
tab = boss.tab_for_window(self)
|
||||
if tab is not None:
|
||||
tab.move_window_to_top_of_group(self)
|
||||
|
||||
def handle_remote_askpass(self, msg: str) -> None:
|
||||
from .shm import SharedMemory
|
||||
with SharedMemory(name=msg, readonly=True) as shm:
|
||||
shm.seek(1)
|
||||
data = json.loads(shm.read_data_with_size())
|
||||
|
||||
def callback(ans: Any) -> None:
|
||||
data = json.dumps(ans)
|
||||
with SharedMemory(name=msg) as shm:
|
||||
shm.seek(1)
|
||||
shm.write_data_with_size(data)
|
||||
shm.flush()
|
||||
shm.seek(0)
|
||||
shm.write(b'\x01')
|
||||
|
||||
message: str = data['message']
|
||||
if data['type'] == 'confirm':
|
||||
get_boss().confirm(
|
||||
message, callback, window=self, confirm_on_cancel=bool(data.get('confirm_on_cancel')),
|
||||
confirm_on_accept=bool(data.get('confirm_on_accept', True)))
|
||||
elif data['type'] == 'choose':
|
||||
get_boss().choose(
|
||||
message, callback, *data['choices'], window=self, default=data.get('default', ''))
|
||||
elif data['type'] == 'get_line':
|
||||
get_boss().get_line(
|
||||
message, callback, window=self, is_password=bool(data.get('is_password')), prompt=data.get('prompt', '> '))
|
||||
else:
|
||||
log_error(f'Ignoring ask request with unknown type: {data["type"]}')
|
||||
|
||||
def handle_remote_print(self, msg: str) -> None:
|
||||
text = process_remote_print(msg)
|
||||
print(text, end='', file=sys.stderr)
|
||||
sys.stderr.flush()
|
||||
@@ -1030,13 +1148,13 @@ class Window:
|
||||
def paste_selection(self) -> None:
|
||||
txt = get_boss().current_primary_selection()
|
||||
if txt:
|
||||
self.paste(txt)
|
||||
self.paste_with_actions(txt)
|
||||
|
||||
@ac('mouse', 'Paste the current primary selection or the clipboard if no selection is present')
|
||||
def paste_selection_or_clipboard(self) -> None:
|
||||
txt = get_boss().current_primary_selection_or_clipboard()
|
||||
if txt:
|
||||
self.paste(txt)
|
||||
self.paste_with_actions(txt)
|
||||
|
||||
@ac('mouse', '''
|
||||
Select clicked command output
|
||||
@@ -1077,7 +1195,7 @@ class Window:
|
||||
if hasattr(self, 'screen'):
|
||||
if self.is_active and self.os_window_id == current_os_window():
|
||||
# Cancel IME composition when window is destroyed
|
||||
update_ime_position_for_window(self.id, False, True)
|
||||
update_ime_position_for_window(self.id, False, -1)
|
||||
# Remove cycles so that screen is de-allocated immediately
|
||||
self.screen.reset_callbacks()
|
||||
del self.screen
|
||||
@@ -1099,6 +1217,22 @@ class Window:
|
||||
def cwd_of_child(self) -> Optional[str]:
|
||||
return self.child.foreground_cwd or self.child.current_cwd
|
||||
|
||||
@property
|
||||
def child_is_remote(self) -> bool:
|
||||
for p in self.child.foreground_processes:
|
||||
q = list(p['cmdline'] or ())
|
||||
if q and q[0].lower() == 'ssh':
|
||||
return True
|
||||
return False
|
||||
|
||||
def ssh_kitten_cmdline(self) -> List[str]:
|
||||
from kittens.ssh.main import is_kitten_cmdline
|
||||
for p in self.child.foreground_processes:
|
||||
q = list(p['cmdline'] or ())
|
||||
if is_kitten_cmdline(q):
|
||||
return q
|
||||
return []
|
||||
|
||||
def pipe_data(self, text: str, has_wrap_markers: bool = False) -> PipeData:
|
||||
text = text or ''
|
||||
if has_wrap_markers:
|
||||
@@ -1119,6 +1253,68 @@ class Window:
|
||||
path = resolve_custom_file(path) if path else ''
|
||||
set_window_logo(self.os_window_id, self.tab_id, self.id, path, position or '', alpha)
|
||||
|
||||
def paste_with_actions(self, text: str) -> None:
|
||||
if self.destroyed or not text:
|
||||
return
|
||||
opts = get_options()
|
||||
if 'filter' in opts.paste_actions:
|
||||
text = load_paste_filter()(text)
|
||||
if not text:
|
||||
return
|
||||
if 'quote-urls-at-prompt' in opts.paste_actions and self.at_prompt:
|
||||
prefixes = '|'.join(opts.url_prefixes)
|
||||
m = re.match(f'({prefixes}):(.+)', text)
|
||||
if m is not None:
|
||||
scheme, rest = m.group(1), m.group(2)
|
||||
if rest.startswith('//') or scheme in ('mailto', 'irc'):
|
||||
import shlex
|
||||
text = shlex.quote(text)
|
||||
btext = text.encode('utf-8')
|
||||
if 'confirm' in opts.paste_actions:
|
||||
msg = ''
|
||||
limit = 16 * 1024
|
||||
if not self.screen.in_bracketed_paste_mode:
|
||||
msg = _('Pasting text into shells that do not support bracketed paste can be dangerous.')
|
||||
elif len(btext) > limit:
|
||||
msg = _('Pasting very large amounts of text ({} bytes) can be slow.').format(len(btext))
|
||||
if msg:
|
||||
get_boss().confirm(msg + _(' Are you sure?'), partial(self.handle_paste_confirmation, btext), window=self)
|
||||
return
|
||||
self.paste_text(btext)
|
||||
|
||||
def handle_paste_confirmation(self, btext: bytes, confirmed: bool) -> None:
|
||||
if confirmed:
|
||||
self.paste_text(btext)
|
||||
|
||||
def paste_bytes(self, text: Union[str, bytes]) -> None:
|
||||
# paste raw bytes without any processing
|
||||
if isinstance(text, str):
|
||||
text = text.encode('utf-8')
|
||||
self.screen.paste_bytes(text)
|
||||
|
||||
def paste_text(self, text: Union[str, bytes]) -> None:
|
||||
if text and not self.destroyed:
|
||||
if isinstance(text, str):
|
||||
text = text.encode('utf-8')
|
||||
if self.screen.in_bracketed_paste_mode:
|
||||
while True:
|
||||
new_text = text.replace(b'\033[201~', b'').replace(b'\x9b201~', b'')
|
||||
if len(text) == len(new_text):
|
||||
break
|
||||
text = new_text
|
||||
else:
|
||||
# Workaround for broken editors like nano that cannot handle
|
||||
# newlines in pasted text see https://github.com/kovidgoyal/kitty/issues/994
|
||||
text = text.replace(b'\r\n', b'\n').replace(b'\n', b'\r')
|
||||
self.screen.paste(text)
|
||||
|
||||
def clear_screen(self, reset: bool = False, scrollback: bool = False) -> None:
|
||||
self.screen.cursor.x = self.screen.cursor.y = 0
|
||||
if reset:
|
||||
self.screen.reset()
|
||||
else:
|
||||
self.screen.erase_in_display(3 if scrollback else 2, False)
|
||||
|
||||
# actions {{{
|
||||
|
||||
@ac('cp', 'Show scrollback in a pager like less')
|
||||
@@ -1158,28 +1354,9 @@ class Window:
|
||||
def show_last_visited_command_output(self) -> None:
|
||||
self.show_cmd_output(CommandOutput.last_visited, 'Last visited command output')
|
||||
|
||||
def paste_bytes(self, text: Union[str, bytes]) -> None:
|
||||
# paste raw bytes without any processing
|
||||
if isinstance(text, str):
|
||||
text = text.encode('utf-8')
|
||||
self.screen.paste_bytes(text)
|
||||
|
||||
@ac('cp', 'Paste the specified text into the current window')
|
||||
def paste(self, text: Union[str, bytes]) -> None:
|
||||
if text and not self.destroyed:
|
||||
if isinstance(text, str):
|
||||
text = text.encode('utf-8')
|
||||
if self.screen.in_bracketed_paste_mode:
|
||||
while True:
|
||||
new_text = text.replace(b'\033[201~', b'').replace(b'\x9b201~', b'')
|
||||
if len(text) == len(new_text):
|
||||
break
|
||||
text = new_text
|
||||
else:
|
||||
# Workaround for broken editors like nano that cannot handle
|
||||
# newlines in pasted text see https://github.com/kovidgoyal/kitty/issues/994
|
||||
text = text.replace(b'\r\n', b'\n').replace(b'\n', b'\r')
|
||||
self.screen.paste(text)
|
||||
def paste(self, text: str) -> None:
|
||||
self.paste_with_actions(text)
|
||||
|
||||
@ac('cp', 'Copy the selected text from the active window to the clipboard')
|
||||
def copy_to_clipboard(self) -> None:
|
||||
@@ -1207,6 +1384,7 @@ class Window:
|
||||
if text:
|
||||
set_clipboard_string(text)
|
||||
else:
|
||||
self.scroll_end()
|
||||
self.write_to_child(self.encoded_key(KeyEvent(key=ord('c'), mods=GLFW_MOD_CONTROL)))
|
||||
|
||||
@ac('cp', 'Copy the selected text from the active window to the clipboard and clear selection, if no selection, send SIGINT (aka :kbd:`ctrl+c`)')
|
||||
@@ -1274,7 +1452,16 @@ class Window:
|
||||
if self.screen.is_main_linebuf():
|
||||
self.screen.scroll_to_prompt(num_of_prompts)
|
||||
|
||||
@ac('sc', 'Scroll prompt to the bottom of the screen, filling in extra lines form the scrollback buffer')
|
||||
@ac('sc', 'Scroll prompt to the top of the screen, filling screen with empty lines')
|
||||
def scroll_prompt_to_top(self, clear_scrollback: bool = False) -> None:
|
||||
if self.screen.is_main_linebuf():
|
||||
self.screen.scroll_until_cursor_prompt()
|
||||
if clear_scrollback:
|
||||
self.screen.clear_scrollback()
|
||||
elif self.screen.scrolled_by > 0:
|
||||
self.screen.scroll(SCROLL_FULL, False)
|
||||
|
||||
@ac('sc', 'Scroll prompt to the bottom of the screen, filling in extra lines from the scrollback buffer')
|
||||
def scroll_prompt_to_bottom(self) -> None:
|
||||
self.screen.scroll_prompt_to_bottom()
|
||||
|
||||
|
||||
@@ -61,8 +61,22 @@ class WindowGroup:
|
||||
def active_window_id(self) -> int:
|
||||
return self.windows[-1].id if self.windows else 0
|
||||
|
||||
def add_window(self, window: WindowType) -> None:
|
||||
def add_window(self, window: WindowType, head_of_group: bool = False) -> None:
|
||||
if head_of_group:
|
||||
self.windows.insert(0, window)
|
||||
else:
|
||||
self.windows.append(window)
|
||||
|
||||
def move_window_to_top_of_group(self, window: WindowType) -> bool:
|
||||
try:
|
||||
idx = self.windows.index(window)
|
||||
except ValueError:
|
||||
return False
|
||||
if idx == len(self.windows) - 1:
|
||||
return False
|
||||
del self.windows[idx]
|
||||
self.windows.append(window)
|
||||
return True
|
||||
|
||||
def remove_window(self, window: WindowType) -> None:
|
||||
with suppress(ValueError):
|
||||
@@ -252,6 +266,19 @@ class WindowList:
|
||||
return i
|
||||
return None
|
||||
|
||||
def move_window_to_top_of_group(self, window: WindowType) -> bool:
|
||||
g = self.group_for_window(window)
|
||||
if g is None:
|
||||
return False
|
||||
before = self.active_window
|
||||
if not g.move_window_to_top_of_group(window):
|
||||
return False
|
||||
after = self.active_window
|
||||
changed = before is not after
|
||||
if changed:
|
||||
self.notify_on_active_window_change(before, after)
|
||||
return changed
|
||||
|
||||
def windows_in_group_of(self, x: WindowOrId) -> Iterator[WindowType]:
|
||||
g = self.group_for_window(x)
|
||||
if g is not None:
|
||||
@@ -292,7 +319,8 @@ class WindowList:
|
||||
group_of: Optional[WindowOrId] = None,
|
||||
next_to: Optional[WindowOrId] = None,
|
||||
before: bool = False,
|
||||
make_active: bool = True
|
||||
make_active: bool = True,
|
||||
head_of_group: bool = False,
|
||||
) -> WindowGroup:
|
||||
self.all_windows.append(window)
|
||||
self.id_map[window.id] = window
|
||||
@@ -318,7 +346,7 @@ class WindowList:
|
||||
self.groups.append(target_group)
|
||||
|
||||
old_active_window = self.active_window
|
||||
target_group.add_window(window)
|
||||
target_group.add_window(window, head_of_group=head_of_group)
|
||||
if make_active:
|
||||
for i, g in enumerate(self.groups):
|
||||
if g is target_group:
|
||||
|
||||
@@ -26,8 +26,9 @@ from kitty.window import process_remote_print, process_title_from_child
|
||||
|
||||
class Callbacks:
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, pty=None) -> None:
|
||||
self.clear()
|
||||
self.pty = pty
|
||||
|
||||
def write(self, data) -> None:
|
||||
self.wtcbuf += data
|
||||
@@ -93,6 +94,18 @@ class Callbacks:
|
||||
text = process_remote_print(msg)
|
||||
print(text, file=sys.__stderr__)
|
||||
|
||||
def handle_remote_ssh(self, msg):
|
||||
from kittens.ssh.main import get_ssh_data
|
||||
if self.pty:
|
||||
for line in get_ssh_data(msg, "testing"):
|
||||
self.pty.write_to_child(line)
|
||||
|
||||
def handle_remote_echo(self, msg):
|
||||
from base64 import standard_b64decode
|
||||
if self.pty:
|
||||
data = standard_b64decode(msg)
|
||||
self.pty.write_to_child(data)
|
||||
|
||||
|
||||
def filled_line_buf(ynum=5, xnum=5, cursor=Cursor()):
|
||||
ans = LineBuf(ynum, xnum)
|
||||
@@ -177,10 +190,19 @@ class PTY:
|
||||
self.cell_width = cell_width
|
||||
self.cell_height = cell_height
|
||||
self.set_window_size(rows=rows, columns=columns)
|
||||
self.callbacks = Callbacks()
|
||||
self.callbacks = Callbacks(self)
|
||||
self.screen = Screen(self.callbacks, rows, columns, scrollback, cell_width, cell_height, 0, self.callbacks)
|
||||
self.received_bytes = b''
|
||||
|
||||
def turn_off_echo(self):
|
||||
s = termios.tcgetattr(self.master_fd)
|
||||
s[3] &= ~termios.ECHO
|
||||
termios.tcsetattr(self.master_fd, termios.TCSANOW, s)
|
||||
|
||||
def is_echo_on(self):
|
||||
s = termios.tcgetattr(self.master_fd)
|
||||
return True if s[3] & termios.ECHO else False
|
||||
|
||||
def __del__(self):
|
||||
if not self.is_child:
|
||||
fd = self.master_fd
|
||||
@@ -222,9 +244,9 @@ class PTY:
|
||||
return bytes_read
|
||||
|
||||
def wait_till(self, q, timeout=10):
|
||||
st = time.monotonic()
|
||||
while not q() and time.monotonic() - st < timeout:
|
||||
self.process_input_from_child(timeout=timeout - (time.monotonic() - st))
|
||||
end_time = time.monotonic() + timeout
|
||||
while not q() and time.monotonic() <= end_time:
|
||||
self.process_input_from_child(timeout=max(0, end_time - time.monotonic()))
|
||||
if not q():
|
||||
raise TimeoutError(f'The condition was not met. Screen contents: \n {repr(self.screen_contents())}')
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ class TestBuild(BaseTest):
|
||||
self.assertTrue(os.path.isdir(terminfo_dir), f'Terminfo dir: {terminfo_dir}')
|
||||
self.assertTrue(os.path.exists(logo_png_file), f'Logo file: {logo_png_file}')
|
||||
self.assertTrue(os.path.exists(zsh), f'Shell integration: {zsh}')
|
||||
self.assertTrue(os.access(os.path.join(shell_integration_dir, 'ssh', 'askpass.py'), os.X_OK))
|
||||
|
||||
def test_ca_certificates(self):
|
||||
import ssl
|
||||
|
||||
@@ -491,7 +491,7 @@ class TestKeys(BaseTest):
|
||||
|
||||
self.ae(enc(), '<0;1;1M')
|
||||
self.ae(enc(action=defines.RELEASE), '<0;1;1m')
|
||||
self.ae(enc(action=defines.MOVE), '<35;1;1M')
|
||||
self.ae(enc(action=defines.MOVE, button=0), '<35;1;1M')
|
||||
self.ae(enc(action=defines.DRAG), '<32;1;1M')
|
||||
|
||||
self.ae(enc(R), '<2;1;1M')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user