Compare commits
289 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90ed5959de | ||
|
|
d999cc4143 | ||
|
|
6c1d8d5c63 | ||
|
|
a316242a4b | ||
|
|
a4ae090c37 | ||
|
|
abdcc64053 | ||
|
|
ac407d42de | ||
|
|
19c4a3f0a8 | ||
|
|
3afd96d421 | ||
|
|
f421666a27 | ||
|
|
af6baa33be | ||
|
|
1096cbe236 | ||
|
|
998be9b90c | ||
|
|
854cb8f27e | ||
|
|
e359094cff | ||
|
|
db57230987 | ||
|
|
4de3cecbbe | ||
|
|
c37a8bd3b1 | ||
|
|
e103b280fd | ||
|
|
2235ea67e2 | ||
|
|
1f6a4f7bd4 | ||
|
|
8c7ffc90f3 | ||
|
|
29d85833f1 | ||
|
|
74c56d69ac | ||
|
|
cf520646a9 | ||
|
|
da5213346a | ||
|
|
bdcb98eafc | ||
|
|
ef637cd7d3 | ||
|
|
dbbabd73c5 | ||
|
|
436ef0074a | ||
|
|
0904dec810 | ||
|
|
0aa07ead7e | ||
|
|
d87cac87ff | ||
|
|
7e8c96896f | ||
|
|
2619db0e58 | ||
|
|
ee3632b85d | ||
|
|
1c88c13fc2 | ||
|
|
7f476eb924 | ||
|
|
2d4f7e3446 | ||
|
|
165c1240a9 | ||
|
|
ef5c66ee17 | ||
|
|
33abd31d1a | ||
|
|
f91463a494 | ||
|
|
30146007d3 | ||
|
|
d53f8f24c4 | ||
|
|
f9621b1e11 | ||
|
|
e2f16ff624 | ||
|
|
121778e5c6 | ||
|
|
ef32488890 | ||
|
|
9d349d618a | ||
|
|
9f0a4f43b8 | ||
|
|
a8826f0d02 | ||
|
|
6689d312a3 | ||
|
|
2d7cb25b20 | ||
|
|
da10eaca00 | ||
|
|
6d3995d4ac | ||
|
|
e3adf8c6bf | ||
|
|
b8e522484d | ||
|
|
ef4240e196 | ||
|
|
8fa4a48ae5 | ||
|
|
4f3da2bc00 | ||
|
|
7b8c2c846f | ||
|
|
b212fd5bcd | ||
|
|
0a297f4656 | ||
|
|
fa397a1d24 | ||
|
|
0610daaec7 | ||
|
|
8278e2b88d | ||
|
|
cb4a9d89cf | ||
|
|
7800c598f6 | ||
|
|
fc651b72ab | ||
|
|
a9a7912372 | ||
|
|
43f435c334 | ||
|
|
693fc0f0c7 | ||
|
|
12e011c481 | ||
|
|
668783ba1c | ||
|
|
c18ebef702 | ||
|
|
03f9f29ce4 | ||
|
|
f62acab715 | ||
|
|
28ab9bfa2a | ||
|
|
9c05481f14 | ||
|
|
8ca92e0536 | ||
|
|
c0f6201ac3 | ||
|
|
27f3a5e16a | ||
|
|
de10dfe91b | ||
|
|
444a690a86 | ||
|
|
3d7b3f7d72 | ||
|
|
741ef7f115 | ||
|
|
e0c84c7176 | ||
|
|
393425e3d1 | ||
|
|
fc9645832d | ||
|
|
c47f41cfc0 | ||
|
|
08221489fd | ||
|
|
3c6766ff06 | ||
|
|
e28aae620a | ||
|
|
c240e7deaf | ||
|
|
31a5965b01 | ||
|
|
7a2a849a97 | ||
|
|
2c96e49566 | ||
|
|
e7846f916a | ||
|
|
d4f4d56f94 | ||
|
|
0108037076 | ||
|
|
88091b4ab3 | ||
|
|
4487462b0d | ||
|
|
dac9b07f16 | ||
|
|
b59212696a | ||
|
|
7fe1376e34 | ||
|
|
e25b90c1b6 | ||
|
|
cb32a0b8fc | ||
|
|
05617f7dca | ||
|
|
2d2f4b9ba9 | ||
|
|
65c7ecbc30 | ||
|
|
da5e37620e | ||
|
|
261057396c | ||
|
|
a43f610555 | ||
|
|
45ae52e5d0 | ||
|
|
a565443d4a | ||
|
|
081d6a3f16 | ||
|
|
c9cc832875 | ||
|
|
595698d8e9 | ||
|
|
b28d94ac97 | ||
|
|
b9684879e7 | ||
|
|
63f974531b | ||
|
|
a559210923 | ||
|
|
8d36fb9edc | ||
|
|
f652b23169 | ||
|
|
d50a2ea288 | ||
|
|
80fc3a1faa | ||
|
|
9a04405ad2 | ||
|
|
fdc9835587 | ||
|
|
702bb2cd06 | ||
|
|
58e8609c1a | ||
|
|
69c5c49094 | ||
|
|
ff8de7607a | ||
|
|
cfd0872cea | ||
|
|
7007f2e7fc | ||
|
|
6d0721341b | ||
|
|
7a156d5ef3 | ||
|
|
185c3320a4 | ||
|
|
1c9cf32735 | ||
|
|
6bfb6da0ad | ||
|
|
56a5738018 | ||
|
|
919667129f | ||
|
|
e9f49a3292 | ||
|
|
711f8b024e | ||
|
|
53716c084b | ||
|
|
2b495bcf9d | ||
|
|
d10812c6b0 | ||
|
|
80202d2679 | ||
|
|
5d120a2f36 | ||
|
|
cc11326baa | ||
|
|
f63dbc0ebd | ||
|
|
88630731dc | ||
|
|
3e2a8d80d7 | ||
|
|
55626e5a41 | ||
|
|
7ff07f7620 | ||
|
|
d2f522277c | ||
|
|
0248edbdb9 | ||
|
|
d24248e93b | ||
|
|
dcd7890a0c | ||
|
|
f219b10c30 | ||
|
|
cc039afc1e | ||
|
|
d2288d8f83 | ||
|
|
4b4f904aac | ||
|
|
e06b774174 | ||
|
|
8d4772f804 | ||
|
|
3be00bd712 | ||
|
|
a0e4449fb2 | ||
|
|
ebfc19def5 | ||
|
|
a4abe26d32 | ||
|
|
5f8cb22d02 | ||
|
|
cafc3973ec | ||
|
|
436a57a61e | ||
|
|
da5111a267 | ||
|
|
ee5cf90684 | ||
|
|
108974f4f7 | ||
|
|
79fa9f1c95 | ||
|
|
40bf12af7a | ||
|
|
263d121f3e | ||
|
|
9222d002f7 | ||
|
|
9a6aab034d | ||
|
|
b931b06941 | ||
|
|
90dc9b6fe6 | ||
|
|
275ede6f0a | ||
|
|
9fd4f8e5c2 | ||
|
|
01b4654461 | ||
|
|
d40e6a9ece | ||
|
|
b2317e0f12 | ||
|
|
444d9bd341 | ||
|
|
8fbe96744b | ||
|
|
1c48ec7196 | ||
|
|
c4b3bbd057 | ||
|
|
502ed94f31 | ||
|
|
18ce091bfa | ||
|
|
60f675758f | ||
|
|
9bfa4553a8 | ||
|
|
358f30ca7a | ||
|
|
e32785831b | ||
|
|
728eceb620 | ||
|
|
57f591d1ce | ||
|
|
e4397a1c73 | ||
|
|
1170cf474f | ||
|
|
ce8b0cf748 | ||
|
|
696f371aa4 | ||
|
|
8ebd514251 | ||
|
|
d3bc6001d8 | ||
|
|
f5415ca824 | ||
|
|
0dbe27438d | ||
|
|
7448789951 | ||
|
|
dc6138d286 | ||
|
|
7457f637a2 | ||
|
|
c9da734c0e | ||
|
|
141c814d72 | ||
|
|
bdb98cf210 | ||
|
|
7cc3d5907f | ||
|
|
3c709a28f7 | ||
|
|
e9ea7b13b6 | ||
|
|
ac16880eec | ||
|
|
c54bdd921a | ||
|
|
fc17528337 | ||
|
|
9b2db6ec53 | ||
|
|
0fcfaa2a98 | ||
|
|
ba97a728f2 | ||
|
|
a3b046fdcd | ||
|
|
ddfda3efde | ||
|
|
a2269cb66e | ||
|
|
1358f00969 | ||
|
|
eedc849c45 | ||
|
|
89679d07ae | ||
|
|
01d866f482 | ||
|
|
73b0312dcb | ||
|
|
f9d9fe6db4 | ||
|
|
e31ca68875 | ||
|
|
8ae273ee3a | ||
|
|
73a197fcde | ||
|
|
648bff02b5 | ||
|
|
7740bc138b | ||
|
|
f5337096d5 | ||
|
|
dddff91fad | ||
|
|
f047678711 | ||
|
|
aa8b23395f | ||
|
|
7c36c19de0 | ||
|
|
49183ced3b | ||
|
|
007e9697c4 | ||
|
|
9989edbe42 | ||
|
|
9742e2ec48 | ||
|
|
b47711e23a | ||
|
|
3dfdb3ac89 | ||
|
|
4d3e2a07d1 | ||
|
|
a42eb3e643 | ||
|
|
39c77a9486 | ||
|
|
e22546c56a | ||
|
|
ced61096df | ||
|
|
c0ea127810 | ||
|
|
29b1e3fa46 | ||
|
|
b2faa0d9f7 | ||
|
|
ddb9b67de3 | ||
|
|
ffbc533565 | ||
|
|
206e490491 | ||
|
|
135066f38c | ||
|
|
51d591e177 | ||
|
|
80a62c8d71 | ||
|
|
90b54c5f7f | ||
|
|
99c81e6858 | ||
|
|
8fcb6278d7 | ||
|
|
89e0abd41d | ||
|
|
a1a0c9ab80 | ||
|
|
9ab2a38d5c | ||
|
|
b5676a53ee | ||
|
|
94898a8758 | ||
|
|
97a6a11066 | ||
|
|
9fe22a5c27 | ||
|
|
f9b35c71d7 | ||
|
|
6b47f6f769 | ||
|
|
37d8483728 | ||
|
|
4776a9e785 | ||
|
|
50bc5b0302 | ||
|
|
7d496f20a1 | ||
|
|
42fbd0a1af | ||
|
|
8960bfebc0 | ||
|
|
b1209c1e7a | ||
|
|
365583efd7 | ||
|
|
926c2d71ee | ||
|
|
ce57c747cc | ||
|
|
3e9129655a | ||
|
|
12f41f30b3 | ||
|
|
1e7edd0218 | ||
|
|
2fd8ef389e | ||
|
|
53589c3954 | ||
|
|
34a0218f35 |
12
.github/workflows/ci.py
vendored
12
.github/workflows/ci.py
vendored
@@ -30,13 +30,17 @@ def install_deps():
|
||||
print('Installing kitty dependencies...')
|
||||
sys.stdout.flush()
|
||||
if is_macos:
|
||||
items = (x.strip() for x in open('Brewfile').readlines() if not x.startswith('#'))
|
||||
run('brew', 'install', *items)
|
||||
items = (x.split()[1].strip('"') for x in open('Brewfile').readlines() if x.strip().startswith('brew '))
|
||||
run('brew', 'install', 'fish', *items)
|
||||
else:
|
||||
run('sudo apt-get update')
|
||||
run('sudo apt-get install -y libgl1-mesa-dev libxi-dev libxrandr-dev libxinerama-dev ca-certificates'
|
||||
' libxcursor-dev libxcb-xkb-dev libdbus-1-dev libxkbcommon-dev libharfbuzz-dev libx11-xcb-dev'
|
||||
' libpng-dev liblcms2-dev libfontconfig-dev libxkbcommon-x11-dev libcanberra-dev librsync-dev uuid-dev')
|
||||
' libxcursor-dev libxcb-xkb-dev libdbus-1-dev libxkbcommon-dev libharfbuzz-dev libx11-xcb-dev zsh'
|
||||
' libpng-dev liblcms2-dev libfontconfig-dev libxkbcommon-x11-dev libcanberra-dev librsync-dev uuid-dev'
|
||||
' zsh bash dash')
|
||||
# for some reason these directories are world writable which causes zsh
|
||||
# compinit to break
|
||||
run('sudo chmod -R og-w /usr/share/zsh')
|
||||
if is_bundle:
|
||||
install_bundle()
|
||||
else:
|
||||
|
||||
1
.github/workflows/codeql-analysis.yml
vendored
1
.github/workflows/codeql-analysis.yml
vendored
@@ -27,6 +27,7 @@ jobs:
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: python, c
|
||||
setup-python-dependencies: false
|
||||
|
||||
- name: Build kitty
|
||||
run: python3 .github/workflows/ci.py build
|
||||
|
||||
14
Brewfile
14
Brewfile
@@ -1,7 +1,7 @@
|
||||
pkg-config
|
||||
zlib
|
||||
librsync
|
||||
python
|
||||
imagemagick
|
||||
harfbuzz
|
||||
sphinx-doc
|
||||
brew "pkg-config"
|
||||
brew "zlib"
|
||||
brew "librsync"
|
||||
brew "python"
|
||||
brew "imagemagick"
|
||||
brew "harfbuzz"
|
||||
brew "sphinx-doc"
|
||||
|
||||
@@ -11,4 +11,4 @@ https://www.reddit.com/r/KittyTerminal[Reddit community]
|
||||
|
||||
Packaging status in various repositories:
|
||||
|
||||
image:https://repology.org/badge/vertical-allrepos/kitty.svg[https://repology.org/project/kitty/versions]
|
||||
image:https://repology.org/badge/vertical-allrepos/kitty.svg["Packaging status", link="https://repology.org/project/kitty/versions"]
|
||||
|
||||
18
__main__.py
18
__main__.py
@@ -31,7 +31,15 @@ def runpy(args: List[str]) -> None:
|
||||
|
||||
def hold(args: List[str]) -> None:
|
||||
import subprocess
|
||||
ret = subprocess.Popen(args[1:]).wait()
|
||||
ret = 1
|
||||
try:
|
||||
ret = subprocess.Popen(args[1:]).wait()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except FileNotFoundError:
|
||||
print(f'Could not find {args[1]!r} to execute', file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(e, file=sys.stderr)
|
||||
from kitty.utils import hold_till_enter
|
||||
hold_till_enter()
|
||||
raise SystemExit(ret)
|
||||
@@ -42,6 +50,13 @@ def complete(args: List[str]) -> None:
|
||||
complete_main(args[1:], entry_points, namespaced_entry_points)
|
||||
|
||||
|
||||
def open_urls(args: List[str]) -> None:
|
||||
setattr(sys, 'cmdline_args_for_open', True)
|
||||
sys.argv = ['kitty'] + args[1:]
|
||||
from kitty.main import main as kitty_main
|
||||
kitty_main()
|
||||
|
||||
|
||||
def launch(args: List[str]) -> None:
|
||||
import runpy
|
||||
sys.argv = args[1:]
|
||||
@@ -129,6 +144,7 @@ namespaced_entry_points['hold'] = hold
|
||||
namespaced_entry_points['complete'] = complete
|
||||
namespaced_entry_points['runpy'] = runpy
|
||||
namespaced_entry_points['launch'] = launch
|
||||
namespaced_entry_points['open'] = open_urls
|
||||
namespaced_entry_points['kitten'] = run_kitten
|
||||
namespaced_entry_points['edit-config'] = edit_config_file
|
||||
namespaced_entry_points['shebang'] = shebang
|
||||
|
||||
@@ -35,7 +35,8 @@ def binary_includes():
|
||||
'expat', 'sqlite3', 'ffi', 'z', 'lzma', 'png16', 'lcms2', 'crypt',
|
||||
'iconv', 'pcre', 'graphite2', 'glib-2.0', 'freetype', 'rsync',
|
||||
'harfbuzz', 'xkbcommon', 'xkbcommon-x11',
|
||||
'ncursesw', 'readline', 'brotlicommon', 'brotlienc', 'brotlidec'
|
||||
'ncursesw', 'readline', 'brotlicommon', 'brotlienc', 'brotlidec',
|
||||
'wayland-client', 'wayland-cursor',
|
||||
))) + (
|
||||
get_dll_path('bz2', 2), get_dll_path('ssl', 2), get_dll_path('crypto', 2),
|
||||
get_dll_path(f'python{py_ver}', 2),
|
||||
|
||||
@@ -185,7 +185,16 @@ class Freeze(object):
|
||||
def add_ca_certs(self):
|
||||
print('\nDownloading CA certs...')
|
||||
from urllib.request import urlopen
|
||||
cdata = urlopen(kitty_constants['cacerts_url']).read()
|
||||
cdata = None
|
||||
for i in range(5):
|
||||
try:
|
||||
cdata = urlopen(kitty_constants['cacerts_url']).read()
|
||||
break
|
||||
except Exception as e:
|
||||
print(f'Downloading CA certs failed with error: {e}, retrying...')
|
||||
|
||||
if cdata is None:
|
||||
raise SystemExit('Downloading C certs failed, giving up')
|
||||
dest = join(self.contents_dir, 'Resources', 'cacert.pem')
|
||||
with open(dest, 'wb') as f:
|
||||
f.write(cdata)
|
||||
|
||||
@@ -166,8 +166,8 @@
|
||||
{
|
||||
"name": "pygments",
|
||||
"unix": {
|
||||
"filename": "Pygments-2.7.4.tar.gz",
|
||||
"hash": "sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337",
|
||||
"filename": "Pygments-2.11.2.tar.gz",
|
||||
"hash": "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a",
|
||||
"urls": ["pypi"]
|
||||
}
|
||||
},
|
||||
@@ -273,8 +273,8 @@
|
||||
"name": "wayland",
|
||||
"os": "linux",
|
||||
"unix": {
|
||||
"filename": "wayland-1.19.0.tar.xz",
|
||||
"hash": "sha256:baccd902300d354581cd5ad3cc49daa4921d55fb416a5883e218750fef166d15",
|
||||
"filename": "wayland-1.20.0.tar.xz",
|
||||
"hash": "sha256:b8a034154c7059772e0fdbd27dbfcda6c732df29cae56a82274f6ec5d7cd8725",
|
||||
"urls": ["https://wayland.freedesktop.org/releases/{filename}"]
|
||||
}
|
||||
},
|
||||
@@ -283,8 +283,8 @@
|
||||
"name": "wayland-protocols",
|
||||
"os": "linux",
|
||||
"unix": {
|
||||
"filename": "wayland-protocols-1.20.tar.xz",
|
||||
"hash": "sha256:9782b7a1a863d82d7c92478497d13c758f52e7da4f197aa16443f73de77e4de7",
|
||||
"filename": "wayland-protocols-1.25.tar.xz",
|
||||
"hash": "sha256:f1ff0f7199d0a0da337217dd8c99979967808dc37731a1e759e822b75b571460",
|
||||
"urls": ["https://wayland.freedesktop.org/releases/{filename}"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,8 +53,10 @@ particular desktop, but it should work for most major desktop environments.
|
||||
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/
|
||||
# Update the path to the kitty icon in the kitty.desktop file
|
||||
sed -i "s|Icon=kitty|Icon=/home/$USER/.local/kitty.app/share/icons/hicolor/256x256/apps/kitty.png|g" ~/.local/share/applications/kitty.desktop
|
||||
# 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)
|
||||
sed -i "s|Icon=kitty|Icon=/home/$USER/.local/kitty.app/share/icons/hicolor/256x256/apps/kitty.png|g" ~/.local/share/applications/kitty*.desktop
|
||||
|
||||
.. note::
|
||||
If you use the venerable `stow <https://www.gnu.org/software/stow/>`_
|
||||
|
||||
@@ -173,7 +173,7 @@ servers into which they ssh, without needing to install all of |kitty|.
|
||||
``python -m pip -r docs/requirements.txt``.
|
||||
|
||||
This applies to creating packages for |kitty| for macOS package managers such as
|
||||
brew or MacPorts as well.
|
||||
Homebrew or MacPorts as well.
|
||||
|
||||
|
||||
Changing defaults for packages
|
||||
@@ -190,13 +190,11 @@ update-checking
|
||||
|
||||
shell-integration
|
||||
|kitty| by default injects its :ref:`shell_integration` code into the user's
|
||||
shell using environment variables or (for bash only) modifying
|
||||
the user's :file:`~/.bashrc` file.
|
||||
For a package, it might make more sense to distribute the shell
|
||||
integration scripts into the system-wide shell vendor locations. The
|
||||
shell integration files are found in the :file:`shell-integration`
|
||||
directory. Copy them to the system wide shell vendor locations for each
|
||||
shell, and use::
|
||||
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
|
||||
|
||||
|
||||
@@ -75,6 +75,110 @@ command.
|
||||
Detailed list of changes
|
||||
-------------------------------------
|
||||
|
||||
0.24.4 [2022-03-03]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Shell integration: Fix the default Bash :code:`$HISTFILE` changing to :file:`~/.sh_history` instead of :file:`~/.bash_history` (:iss:`4765`)
|
||||
|
||||
- Linux binaries: Fix binaries not working on systems with older Wayland client libraries (:iss:`4760`)
|
||||
|
||||
- Fix a regression in the previous release that broke kittens launched with :code:`STDIN` not connected to a terminal (:iss:`4763`)
|
||||
|
||||
- Wayland: Fix surface configure events not being acknowledged before commit
|
||||
the resized buffer (:pull:`4768`)
|
||||
|
||||
|
||||
0.24.3 [2022-02-28]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Bash integration: No longer modify :file:`~/.bashrc` to load :ref:`shell integration <shell_integration>`.
|
||||
It is recommended to remove the lines used to load the shell integration from :file:`~/.bashrc` as they are no-ops.
|
||||
|
||||
- macOS: Allow kitty to handle various URL types. Can be configured via
|
||||
:ref:`launch_actions` (:pull:`4618`)
|
||||
|
||||
- macOS: Add a new service ``Open with kitty`` to open file types that are not
|
||||
recognized by the system (:pull:`4641`)
|
||||
|
||||
- Splits layout: A new value for :option:`launch --location` to auto-select the split axis when splitting existing windows.
|
||||
Wide windows are split side-by-side and tall windows are split one-above-the-other
|
||||
|
||||
- hints kitten: Fix a regression that broke recognition of path:linenumber:colnumber (:iss:`4675`)
|
||||
|
||||
- Fix a regression in the previous release that broke :opt:`active_tab_foreground` (:iss:`4620`)
|
||||
|
||||
- Fix :ac:`show_last_command_output` not working when the output is stored
|
||||
partially in the scrollback pager history buffer (:iss:`4435`)
|
||||
|
||||
- When dropping URLs/files onto kitty at a shell prompt insert them appropriately quoted and space
|
||||
separated (:iss:`4734`)
|
||||
|
||||
- Improve CWD detection when there are multiple foreground processes in the TTY process group
|
||||
|
||||
- A new option :opt:`narrow_symbols` to turn off opportunistic wide rendering of private use codepoints
|
||||
|
||||
- ssh kitten: Fix location of generated terminfo files on NetBSD (:iss:`4622`)
|
||||
|
||||
- A new action to clear the screen up to the line containing the cursor, see
|
||||
:ac:`clear_terminal`
|
||||
|
||||
- A new action :ac:`copy_ansi_to_clipboard` to copy the current selection with ANSI formatting codes
|
||||
(:iss:`4665`)
|
||||
|
||||
- Linux: Do not rescale fallback fonts to match the main font cell height, instead just
|
||||
set the font size and let FreeType take care of it. This matches
|
||||
rendering on macOS (:iss:`4707`)
|
||||
|
||||
- macOS: Fix a regression in the previous release that broke switching input
|
||||
sources by keyboard (:iss:`4621`)
|
||||
|
||||
- macOS: Add the default shortcut :kbd:`cmd+k` to clear the terminal screen and
|
||||
scrollback up to the cursor (:iss:`4625`)
|
||||
|
||||
- Fix a regression in the previous release that broke strikethrough (:disc:`4632`)
|
||||
|
||||
- A new action :ac:`scroll_prompt_to_bottom` to move the current prompt
|
||||
to the bottom, filling in the window from the scrollback (:pull:`4634`)
|
||||
|
||||
- Add two special arguments ``@first-line-on-screen`` and ``@last-line-on-screen``
|
||||
for the :doc:`launch <launch>` command to be used for pager positioning.
|
||||
(:iss:`4462`)
|
||||
|
||||
- Linux: Fix rendering of emoji when using scalable fonts such as Segoe UI Emoji
|
||||
|
||||
- Shell integration: bash: Dont fail if an existing PROMPT_COMMAND ends with a semi-colon (:iss:`4645`)
|
||||
|
||||
- Shell integration: bash: Fix rendering of multiline prompts with more than two lines (:iss:`4681`)
|
||||
|
||||
- Shell integration: fish: Check fish version 3.3.0+ and exit on outdated versions (:pull:`4745`)
|
||||
|
||||
- Shell integration: fish: Fix pipestatus being overwritten (:pull:`4756`)
|
||||
|
||||
- Linux: Fix fontconfig alias not being used if the aliased font is dual spaced instead of monospaced (:iss:`4649`)
|
||||
|
||||
- macOS: Add an option :opt:`macos_menubar_title_max_length` to control the max length of the window title displayed in the global menubar (:iss:`2132`)
|
||||
|
||||
- Fix :opt:`touch_scroll_multiplier` also taking effect in terminal programs such as vim that handle mouse events themselves (:iss:`4680`)
|
||||
|
||||
- Fix symbol/PUA glyphs loaded via :opt:`symbol_map` instead of as fallbacks not using following spaces to render larger versions (:iss:`4670`)
|
||||
|
||||
- macOS: Fix regression in previous release that caused Apple's global shortcuts to not work if they had never been configured on a particular machine (:iss:`4657`)
|
||||
|
||||
- Fix a fast *click, move mouse, click* sequence causing the first click event to be discarded (:iss:`4603`)
|
||||
|
||||
- Wayland: Fix wheel mice with line based scrolling being incorrectly handled as high precision devices (:iss:`4694`)
|
||||
|
||||
- Wayland: Fix touchpads and high resolution wheels not scrolling at the same speed on monitors with different scales (:iss:`4703`)
|
||||
|
||||
- Add an option :opt:`wheel_scroll_min_lines` to set the minimum number of lines for mouse wheel scrolling when using a mouse with a wheel that generates very small offsets when slow scrolling (:pull:`4710`)
|
||||
|
||||
- macOS: Make the shortcut to toggle full screen configurable (:pull:`4714`)
|
||||
|
||||
- macOS: Fix the mouse cursor being set to arrow after switching desktops or toggling full screen (:pull:`4716`)
|
||||
|
||||
- Fix copying of selection after selection has been scrolled off history buffer raising an error (:iss:`4713`)
|
||||
|
||||
|
||||
0.24.2 [2022-02-03]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -420,7 +524,7 @@ Detailed list of changes
|
||||
0.22.0 [2021-07-26]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Add a new :ref:`action-toggle_layout` action to easily zoom/unzoom a window
|
||||
- Add a new :ac:`toggle_layout` action to easily zoom/unzoom a window
|
||||
|
||||
- When right clicking to extend a selection, move the nearest selection
|
||||
boundary rather than the end of the selection. To restore previous behavior
|
||||
|
||||
19
docs/conf.py
19
docs/conf.py
@@ -399,6 +399,12 @@ def parse_shortcut_node(env: Any, sig: str, signode: Any) -> str:
|
||||
return sig
|
||||
|
||||
|
||||
def parse_action_node(env: Any, sig: str, signode: Any) -> str:
|
||||
"""Transform an action description into RST nodes."""
|
||||
signode += addnodes.desc_name(sig, sig)
|
||||
return sig
|
||||
|
||||
|
||||
def process_opt_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
|
||||
conf_name, opt = target.partition('.')[::2]
|
||||
if not opt:
|
||||
@@ -407,6 +413,10 @@ def process_opt_link(env: Any, refnode: Any, has_explicit_title: bool, title: st
|
||||
return title, opt_aliases.get(full_name, full_name)
|
||||
|
||||
|
||||
def process_action_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
|
||||
return title, target
|
||||
|
||||
|
||||
def process_shortcut_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
|
||||
conf_name, slug = target.partition('.')[::2]
|
||||
if not slug:
|
||||
@@ -444,6 +454,15 @@ def write_conf_docs(app: Any, all_kitten_names: Iterable[str]) -> None:
|
||||
sc_role.process_link = process_shortcut_link
|
||||
shortcut_slugs.clear()
|
||||
|
||||
app.add_object_type(
|
||||
'action', 'ac',
|
||||
indextemplate="pair: %s; Action",
|
||||
parse_node=parse_action_node,
|
||||
)
|
||||
ac_role = app.registry.domain_roles['std']['ac']
|
||||
ac_role.warn_dangling = True
|
||||
ac_role.process_link = process_action_link
|
||||
|
||||
def generate_default_config(definition: Definition, name: str) -> None:
|
||||
with open(f'generated/conf-{name}.rst', 'w', encoding='utf-8') as f:
|
||||
print('.. highlight:: conf\n', file=f)
|
||||
|
||||
32
docs/faq.rst
32
docs/faq.rst
@@ -6,21 +6,22 @@ Frequently Asked Questions
|
||||
Some special symbols are rendered small/truncated in kitty?
|
||||
-----------------------------------------------------------
|
||||
|
||||
The number of cells a unicode character takes up are controlled by the unicode
|
||||
standard. All characters are rendered in a single cell unless the unicode
|
||||
The number of cells a Unicode character takes up are controlled by the Unicode
|
||||
standard. All characters are rendered in a single cell unless the Unicode
|
||||
standard says they should be rendered in two cells. When a symbol does not fit,
|
||||
it will either be rescaled to be smaller or truncated (depending on how much
|
||||
extra space it needs). This is often different from other terminals which just
|
||||
let the character overflow into neighboring cells, which is fine if the
|
||||
neighboring cell is empty, but looks terrible if it is not.
|
||||
|
||||
Some programs, like powerline, vim with fancy gutter symbols/status-bar, etc.
|
||||
misuse unicode characters from the private use area to represent symbols. Often
|
||||
these symbols are square and should be rendered in two cells. However, since
|
||||
private use area symbols all have their width set to one in the unicode
|
||||
Some programs, like Powerline, vim with fancy gutter symbols/status-bar, etc.
|
||||
use Unicode characters from the private use area to represent symbols. Often
|
||||
these symbols are wide and should be rendered in two cells. However, since
|
||||
private use area symbols all have their width set to one in the Unicode
|
||||
standard, |kitty| renders them either smaller or truncated. The exception is if
|
||||
these characters are followed by a space or empty cell in which case kitty
|
||||
makes use of the extra cell to render them in two cells.
|
||||
makes use of the extra cell to render them in two cells. This behavior can be
|
||||
turned off for specific symbols using :opt:`narrow_symbols`.
|
||||
|
||||
|
||||
Using a color theme with a background color does not work well in vim?
|
||||
@@ -202,13 +203,14 @@ bitmapped fonts.
|
||||
.. note::
|
||||
If you are trying to use a font patched with NERD font symbols, dont do that
|
||||
as patching destroys fonts. There is no need, simply install the standalone
|
||||
NERD font, kitty should pick up symbols from it automatically, and you can
|
||||
tell it to do so explicitly in case it doesnt with the :opt:`symbol_map`
|
||||
directive::
|
||||
NERD font (the file :file:`NerdFontsSymbolsOnly.zip` from the `NERD font
|
||||
releases page <https://github.com/ryanoasis/nerd-fonts/releases>`__). kitty
|
||||
should pick up symbols from it automatically, and you can tell it to do so
|
||||
explicitly in case it doesnt with the :opt:`symbol_map` directive::
|
||||
|
||||
symbol_map U+23FB-U+23FE,U+2B58,U+E200-U+E2A9,U+E0A0-U+E0A3,U+E0B0-U+E0BF,U+E0C0-U+E0C8,U+E0CC-U+E0CF,U+E0D0-U+E0D2,U+E0D4,U+E700-U+E7C5,U+F000-U+F2E0,U+2665,U+26A1,U+F400-U+F4A8,U+F67C,U+E000-U+E00A,U+F300-U+F313,U+E5FA-U+E62B Symbols Nerd Font
|
||||
symbol_map U+23FB-U+23FE,U+2665,U+26A1,U+2B58,U+E000-U+E00A,U+E0A0-U+E0A3,U+E0B0-U+E0C8,U+E0CA,U+E0CC-U+E0D2,U+E0D4,U+E200-U+E2A9,U+E300-U+E3E3,U+E5FA-U+E62F,U+E700-U+E7C5,U+F000-U+F2E0,U+F300-U+F31C,U+F400-U+F4A9,U+F500-U+F8FF Symbols Nerd Font
|
||||
|
||||
If your font is not listed in ``kitty list-fonts`` it means that it is not
|
||||
If your font is not listed in ``kitty +list-fonts`` it means that it is not
|
||||
monospace or is a bitmapped font. On Linux you can list all monospace fonts with::
|
||||
|
||||
fc-list : family spacing outline scalable | grep -e spacing=100 -e spacing=90 | grep -e outline=True | grep -e scalable=True
|
||||
@@ -236,7 +238,7 @@ command to rebuild your fontconfig cache::
|
||||
|
||||
fc-cache -r
|
||||
|
||||
Then, the font will be available in ``kitty list-fonts``.
|
||||
Then, the font will be available in ``kitty +list-fonts``.
|
||||
|
||||
|
||||
How can I assign a single global shortcut to bring up the kitty terminal?
|
||||
@@ -270,6 +272,10 @@ homepage:
|
||||
:target: https://github.com/hristost/kitty-alternative-icon
|
||||
:width: 256
|
||||
|
||||
.. image:: https://github.com/igrmk/whiskers/raw/main/whiskers.svg
|
||||
:target: https://github.com/igrmk/whiskers
|
||||
:width: 256
|
||||
|
||||
On macOS you can change the icon by following the steps:
|
||||
|
||||
#. Find :file:`kitty.app` in the Applications folder, select it and press :kbd:`⌘+i`
|
||||
|
||||
@@ -46,6 +46,7 @@ Some programs and libraries that use the kitty graphics protocol:
|
||||
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>`_
|
||||
|
||||
|
||||
Getting the window size
|
||||
|
||||
@@ -134,7 +134,7 @@ terminal program, you can tell the kittens system to run the
|
||||
|
||||
For example, here is a kitten that "zooms/unzooms" the current terminal window
|
||||
by switching to the stack layout or back to the previous layout. This is
|
||||
equivalent to the builtin :ref:`action-toggle_layout` action.
|
||||
equivalent to the builtin :ac:`toggle_layout` action.
|
||||
|
||||
Create a file in the kitty config folder, :file:`~/.config/kitty/zoom_toggle.py`
|
||||
|
||||
|
||||
@@ -95,6 +95,12 @@ the command line:
|
||||
``@cursor-y``
|
||||
replaced by the current cursor y position with 1 being the topmost cell
|
||||
|
||||
``@first-line-on-screen``
|
||||
replaced by the first line on screen. Can be used for pager positioning.
|
||||
|
||||
``@last-line-on-screen``
|
||||
replaced by the last line on screen. Can be used for pager positioning.
|
||||
|
||||
|
||||
For example::
|
||||
|
||||
|
||||
@@ -138,6 +138,11 @@ define a few extra key bindings in :file:`kitty.conf`::
|
||||
# the two windows are placed side by side
|
||||
map F6 launch --location=vsplit
|
||||
|
||||
# Create a new window splitting the space used by the existing one so that
|
||||
# the two windows are placed side by side if the existing window is wide or
|
||||
# one above the other if the existing window is tall.
|
||||
map F4 launch --location=split
|
||||
|
||||
# Rotate the current split, chaging its split axis from vertical to
|
||||
# horizontal or vice versa
|
||||
map F7 layout_action rotate
|
||||
@@ -248,6 +253,8 @@ action, for example::
|
||||
map ctrl+right resize_window wider
|
||||
map ctrl+up resize_window taller
|
||||
map ctrl+down resize_window shorter 3
|
||||
# reset all windows in the tab to default sizes
|
||||
map ctrl+home resize_window reset
|
||||
|
||||
The ``resize_window`` action has a second, optional argument to control
|
||||
the resizing increment (a positive integer that defaults to 1).
|
||||
|
||||
@@ -102,14 +102,38 @@ lines. The various available criteria are:
|
||||
``image-??.png``
|
||||
|
||||
|
||||
.. _launch_actions:
|
||||
|
||||
Scripting the opening of files with kitty on macOS
|
||||
-------------------------------------------------------
|
||||
|
||||
On macOS you can use :guilabel:`Open With` in Finder or drag and drop files
|
||||
onto the kitty dock icon to open them with kitty. The default action
|
||||
is to open text files in your editor and images using the icat kitten.
|
||||
Shell scripts are run in a shell. You can customize these actions by creating
|
||||
a :file:`launch-actions.conf` file in the kitty config directory, just like
|
||||
On macOS you can use :guilabel:`Open With` in Finder or drag and drop files and
|
||||
URLs onto the kitty dock icon to open them with kitty. The default actions are:
|
||||
|
||||
* Open text files in your editor and images using the icat kitten.
|
||||
* Run shell scripts in a shell
|
||||
* Open SSH urls using the ssh command
|
||||
|
||||
These actions can also be executed from the command line by running::
|
||||
|
||||
open -a kitty.app file_or_url ... (on macOS only)
|
||||
or
|
||||
kitty +open file_or_url ...
|
||||
|
||||
Since macOS lacks an official interface to set default URL scheme handlers,
|
||||
kitty has a command you can use for it. The first argument for is the URL
|
||||
scheme, and the second optional argument is the bundle id of the app, which
|
||||
defaults to kitty, if not specified. For example:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
# Set kitty as the handler for ssh:// URLs
|
||||
kitty +runpy 'from kitty.fast_data_types import cocoa_set_url_handler; import sys; cocoa_set_url_handler(*sys.argv[1:]); print("OK")' ssh
|
||||
# Set someapp as the handler for xyz:// URLs
|
||||
kitty +runpy 'from kitty.fast_data_types import cocoa_set_url_handler; import sys; cocoa_set_url_handler(*sys.argv[1:]); print("OK")' xyz someapp.bundle.identifier
|
||||
|
||||
You can customize these actions by creating a :file:`launch-actions.conf` file
|
||||
in the kitty config directory, just like
|
||||
the :file:`open-actions.conf` file above. For example:
|
||||
|
||||
.. code:: conf
|
||||
@@ -138,3 +162,7 @@ the :file:`open-actions.conf` file above. For example:
|
||||
protocol file
|
||||
mime image/*
|
||||
action launch --type=os-window kitty +kitten icat --hold $FILE_PATH
|
||||
|
||||
# Open ssh URLs with ssh command
|
||||
protocol ssh
|
||||
action launch --type=os-window ssh $URL
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
Press mentions of kitty
|
||||
========================
|
||||
|
||||
`Console #88 <https://console.substack.com/p/console-88>`__
|
||||
`Python Bytes 272 <https://youtu.be/8HKliSbA-gQ?t=815>`__ (Feb 2022)
|
||||
A podcast demoing some of kitty's coolness
|
||||
|
||||
`Console #88 <https://console.substack.com/p/console-88>`__ (Jan 2022)
|
||||
An interview with Kovid about kitty
|
||||
|
||||
|
||||
Video reviews
|
||||
--------------
|
||||
|
||||
`Review (Jan 2021) <https://www.youtube.com/watch?v=TTzP2zYJn2k>`__
|
||||
A kitty review by distrotube
|
||||
|
||||
`Review (Dec 2020) <https://www.youtube.com/watch?v=KUMkLhFeBrI>`__
|
||||
A kitty review/intro by TechHut
|
||||
|
||||
@@ -48,11 +48,16 @@ off or it can be disabled entirely as well. The :opt:`shell_integration` option
|
||||
takes a space separated list of keywords:
|
||||
|
||||
disabled
|
||||
Turn off all shell integration
|
||||
Turn off all shell integration. The shell's launch environment is not
|
||||
modified and :envvar:`KITTY_SHELL_INTEGRATION` is not set. Useful for
|
||||
:ref:`manual integration <manual_shell_integration>`.
|
||||
|
||||
no-rc
|
||||
Do not modify the shell's launch environment to enable integration. Useful if you prefer
|
||||
to :ref:`manually enable integration <manual_shell_integration>`.
|
||||
to load the kitty shell integration code yourself, either as part of
|
||||
:ref:`manually integration <manual_shell_integration>` or because you have
|
||||
some other software that sets up shell integration.
|
||||
This will still set the :envvar:`KITTY_SHELL_INTEGRATION` environment variable when kitty runs the shell.
|
||||
|
||||
no-cursor
|
||||
Turn off changing of the text cursor to a bar when editing text
|
||||
@@ -88,7 +93,7 @@ to be copied.
|
||||
|
||||
The feature to jump to previous prompts (
|
||||
:sc:`scroll_to_previous_prompt` and :sc:`scroll_to_next_prompt`) and mouse
|
||||
actions (:ref:`action-mouse_select_command_output` and :ref:`action-mouse_show_command_output`) can
|
||||
actions (:ac:`mouse_select_command_output` and :ac:`mouse_show_command_output`) can
|
||||
be integrated with browsing command output as well. For example, define the
|
||||
following mapping in :file:`kitty.conf`:
|
||||
|
||||
@@ -137,14 +142,18 @@ different shells.
|
||||
|
||||
For fish, to make it automatically load the integration code provided by
|
||||
kitty, the integration script directory path is prepended to the
|
||||
:code:`XDG_DATA_DIRS` environment variable. This is only applied to the fish
|
||||
``XDG_DATA_DIRS`` environment variable. This is only applied to the fish
|
||||
process and will be cleaned up by the integration script after startup. No files
|
||||
are added or modified.
|
||||
|
||||
.. tab:: bash
|
||||
|
||||
For bash, kitty adds a couple of lines to the bottom of :file:`~/.bashrc`
|
||||
(in an atomic manner) to load the shell integration code.
|
||||
For bash, kitty starts bash in POSIX mode, using the environment variable
|
||||
``ENV`` to load the shell integrtion script. This prevents bash from
|
||||
loading any startup files itself. The loading of the startup files is done
|
||||
by the integration script, after disabling POSIX mode. From the
|
||||
perspective of those scripts there should be no difference to running
|
||||
vanilla bash.
|
||||
|
||||
|
||||
Then, when launching the shell, kitty sets the environment variable
|
||||
|
||||
@@ -358,32 +358,49 @@ static GLFWapplicationwillfinishlaunchingfun finish_launching_callback = NULL;
|
||||
finish_launching_callback();
|
||||
}
|
||||
|
||||
- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename {
|
||||
(void)theApplication;
|
||||
if (!filename || !_glfw.ns.file_open_callback) return NO;
|
||||
const char *path = NULL;
|
||||
- (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename {
|
||||
(void)sender;
|
||||
if (!filename || !_glfw.ns.url_open_callback) return NO;
|
||||
const char *url = NULL;
|
||||
@try {
|
||||
path = [[NSFileManager defaultManager] fileSystemRepresentationWithPath: filename];
|
||||
url = [[[NSURL fileURLWithPath:filename] absoluteString] UTF8String];
|
||||
} @catch(NSException *exc) {
|
||||
NSLog(@"Converting openFile filename: %@ failed with error: %@", filename, exc.reason);
|
||||
return NO;
|
||||
}
|
||||
if (!path) return NO;
|
||||
return _glfw.ns.file_open_callback(path);
|
||||
if (!url) return NO;
|
||||
return _glfw.ns.url_open_callback(url);
|
||||
}
|
||||
|
||||
- (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames {
|
||||
(void)sender;
|
||||
if (!_glfw.ns.file_open_callback || !filenames) return;
|
||||
if (!_glfw.ns.url_open_callback || !filenames) return;
|
||||
for (id x in filenames) {
|
||||
NSString *filename = x;
|
||||
const char *path = NULL;
|
||||
const char *url = NULL;
|
||||
@try {
|
||||
path = [[NSFileManager defaultManager] fileSystemRepresentationWithPath: filename];
|
||||
url = [[[NSURL fileURLWithPath:filename] absoluteString] UTF8String];
|
||||
} @catch(NSException *exc) {
|
||||
NSLog(@"Converting openFiles filename: %@ failed with error: %@", filename, exc.reason);
|
||||
}
|
||||
if (path) _glfw.ns.file_open_callback(path);
|
||||
if (url) _glfw.ns.url_open_callback(url);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove openFile and openFiles when the minimum supported macOS version is 10.13
|
||||
- (void)application:(NSApplication *)sender openURLs:(NSArray<NSURL *> *)urls
|
||||
{
|
||||
(void)sender;
|
||||
if (!_glfw.ns.url_open_callback || !urls) return;
|
||||
for (id x in urls) {
|
||||
NSURL *ns_url = x;
|
||||
const char *url = NULL;
|
||||
@try {
|
||||
url = [[ns_url absoluteString] UTF8String];
|
||||
} @catch(NSException *exc) {
|
||||
NSLog(@"Converting openURLs url: %@ failed with error: %@", ns_url, exc.reason);
|
||||
}
|
||||
if (url) _glfw.ns.url_open_callback(url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,7 +408,6 @@ static GLFWapplicationwillfinishlaunchingfun finish_launching_callback = NULL;
|
||||
{
|
||||
(void)notification;
|
||||
[NSApp stop:nil];
|
||||
if (_glfw.ns.file_open_callback) _glfw.ns.file_open_callback(":cocoa::application launched::");
|
||||
|
||||
CGDisplayRegisterReconfigurationCallback(display_reconfigured, NULL);
|
||||
_glfwCocoaPostEmptyEvent();
|
||||
@@ -563,7 +579,10 @@ is_shiftable_shortcut(int scv) {
|
||||
|
||||
static void
|
||||
build_global_shortcuts_lookup(void) {
|
||||
// dump these in a terminal with: defaults read com.apple.symbolichotkeys
|
||||
NSMutableDictionary<NSString*, NSNumber*> *temp = [NSMutableDictionary dictionaryWithCapacity:128]; // will be autoreleased
|
||||
NSMutableSet<NSNumber*> *temp_configured = [NSMutableSet setWithCapacity:128]; // will be autoreleased
|
||||
NSMutableSet<NSNumber*> *temp_missing_value = [NSMutableSet setWithCapacity:128]; // will be autoreleased
|
||||
NSDictionary *apple_settings = [[NSUserDefaults standardUserDefaults] persistentDomainForName:@"com.apple.symbolichotkeys"];
|
||||
if (apple_settings) {
|
||||
NSDictionary<NSString*, id> *symbolic_hotkeys = [apple_settings objectForKey:@"AppleSymbolicHotKeys"];
|
||||
@@ -574,9 +593,14 @@ build_global_shortcuts_lookup(void) {
|
||||
NSInteger sc = [key integerValue];
|
||||
NSDictionary *sc_value = obj;
|
||||
id enabled = [sc_value objectForKey:@"enabled"];
|
||||
if (!enabled || ![enabled isKindOfClass:[NSNumber class]] || ![(NSNumber*)enabled boolValue]) continue;
|
||||
if (!enabled || ![enabled isKindOfClass:[NSNumber class]]) continue;
|
||||
[temp_configured addObject:@(sc)];
|
||||
if (![enabled boolValue]) continue;
|
||||
id v = [sc_value objectForKey:@"value"];
|
||||
if (!v || ![v isKindOfClass:[NSDictionary class]]) continue;
|
||||
if (!v || ![v isKindOfClass:[NSDictionary class]]) {
|
||||
if ([enabled boolValue]) [temp_missing_value addObject:@(sc)];
|
||||
continue;
|
||||
}
|
||||
NSDictionary *value = v;
|
||||
id t = [value objectForKey:@"type"];
|
||||
if (!t || ![t isKindOfClass:[NSString class]] || ![t isEqualToString:@"standard"]) continue;
|
||||
@@ -601,6 +625,39 @@ build_global_shortcuts_lookup(void) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add global shortcut definitions when the default enabled shortcut is not defined,
|
||||
// or when the default enabled shortcut is not disabled and is missing a value.
|
||||
// Here are the shortcuts that are enabled by default in the standard ANSI (US) layout.
|
||||
// macOS provides separate configurations for some languages or keyboards.
|
||||
// In general, the rules here will not take effect.
|
||||
static char buf[64];
|
||||
#define S(i, t, m, k) if ([temp_configured member:@(i)] == nil || [temp_missing_value member:@(i)] != nil) { \
|
||||
snprintf(buf, sizeof(buf) - 1, #t":%lx:%ld", (unsigned long)m, (long)k); \
|
||||
temp[@(buf)] = @(i); \
|
||||
}
|
||||
|
||||
// launchpad & dock
|
||||
S(kSHKTurnDockHidingOnOrOff, c, (NSEventModifierFlagOption | NSEventModifierFlagCommand), 'd'); // Opt, Cmd, D
|
||||
// mission control
|
||||
S(kSHKMissionControl, v, NSEventModifierFlagControl, 126); // Ctrl, Arrow Up
|
||||
S(kSHKApplicationWindows, v, NSEventModifierFlagControl, 125); // Ctrl, Arrow Down
|
||||
// keyboard
|
||||
S(kSHKMoveFocusToTheMenuBar, v, NSEventModifierFlagControl, 120); // Ctrl, F2
|
||||
S(kSHKMoveFocusToTheDock, v, NSEventModifierFlagControl, 99); // Ctrl, F3
|
||||
S(kSHKMoveFocusToActiveOrNextWindow, v, NSEventModifierFlagControl, 118); // Ctrl, F4
|
||||
S(kSHKMoveFocusToActiveOrNextWindow, v, (NSEventModifierFlagShift | NSEventModifierFlagControl), 118); // Shift, Ctrl, F4
|
||||
S(kSHKMoveFocusToNextWindow, c, NSEventModifierFlagCommand, 96); // Cmd, `
|
||||
S(kSHKMoveFocusToNextWindow, c, (NSEventModifierFlagShift | NSEventModifierFlagCommand), 96); // Shift, Cmd, `
|
||||
S(kSHKMoveFocusToStatusMenus, v, NSEventModifierFlagControl, 100); // Ctrl, F8
|
||||
// input sources
|
||||
S(kSHKSelectThePreviousInputSource, c, NSEventModifierFlagControl, 32); // Ctrl, Space bar
|
||||
S(kSHKSelectNextSourceInInputMenu, c, (NSEventModifierFlagControl | NSEventModifierFlagOption), 32); // Ctrl, Opt, Space bar
|
||||
// spotlight
|
||||
S(kSHKShowSpotlightSearch, c, NSEventModifierFlagCommand, 32); // Cmd, Space bar
|
||||
S(kSHKShowFinderSearchWindow, c, (NSEventModifierFlagOption | NSEventModifierFlagCommand), 32); // Opt, Cmd, Space bar
|
||||
|
||||
#undef S
|
||||
global_shortcuts = [[NSDictionary dictionaryWithDictionary:temp] retain];
|
||||
/* NSLog(@"global_shortcuts: %@", global_shortcuts); */
|
||||
}
|
||||
@@ -610,11 +667,10 @@ is_active_apple_global_shortcut(NSEvent *event) {
|
||||
if (global_shortcuts == nil) build_global_shortcuts_lookup();
|
||||
NSEventModifierFlags modifierFlags = USEFUL_MODS([event modifierFlags]);
|
||||
static char lookup_key[64];
|
||||
|
||||
#define LOOKUP(t, k) \
|
||||
snprintf(lookup_key, sizeof(lookup_key) - 1, #t":%lx:%ld", (unsigned long)modifierFlags, (long)k); \
|
||||
NSNumber *sc = global_shortcuts[@(lookup_key)]; \
|
||||
if (sc != nil) return [sc intValue];
|
||||
if (sc != nil) return [sc intValue]; \
|
||||
|
||||
if ([event.charactersIgnoringModifiers length] == 1) {
|
||||
if (modifierFlags & NSEventModifierFlagShift) {
|
||||
@@ -752,9 +808,11 @@ int _glfwPlatformInit(void)
|
||||
[NSApp setDelegate:_glfw.ns.delegate];
|
||||
static struct {
|
||||
unsigned short virtual_key_code;
|
||||
NSEventModifierFlags input_source_switch_modifiers;
|
||||
NSTimeInterval timestamp;
|
||||
} last_keydown_shortcut_event;
|
||||
last_keydown_shortcut_event.virtual_key_code = 0xffff;
|
||||
last_keydown_shortcut_event.input_source_switch_modifiers = 0;
|
||||
|
||||
NSEvent* (^keydown_block)(NSEvent*) = ^ NSEvent* (NSEvent* event)
|
||||
{
|
||||
@@ -765,6 +823,7 @@ int _glfwPlatformInit(void)
|
||||
if ([[NSApp mainMenu] performKeyEquivalent:event]) {
|
||||
debug_key("keyDown triggerred global menu bar action ignoring\n");
|
||||
last_keydown_shortcut_event.virtual_key_code = [event keyCode];
|
||||
last_keydown_shortcut_event.input_source_switch_modifiers = 0;
|
||||
last_keydown_shortcut_event.timestamp = [event timestamp];
|
||||
return nil;
|
||||
}
|
||||
@@ -773,6 +832,8 @@ int _glfwPlatformInit(void)
|
||||
if (is_useful_apple_global_shortcut(global_shortcut)) {
|
||||
debug_key("keyDown triggerred global macOS shortcut ignoring\n");
|
||||
last_keydown_shortcut_event.virtual_key_code = [event keyCode];
|
||||
// record the modifier keys if switching to the next input source
|
||||
last_keydown_shortcut_event.input_source_switch_modifiers = (global_shortcut == kSHKSelectNextSourceInInputMenu) ? USEFUL_MODS([event modifierFlags]) : 0;
|
||||
last_keydown_shortcut_event.timestamp = [event timestamp];
|
||||
return event;
|
||||
}
|
||||
@@ -805,6 +866,12 @@ int _glfwPlatformInit(void)
|
||||
debug_key("-------------- flags changed -----------------\n");
|
||||
debug_key("%s\n", [[event description] UTF8String]);
|
||||
last_keydown_shortcut_event.virtual_key_code = 0xffff;
|
||||
// switching to the next input source is only confirmed when all modifier keys are released
|
||||
if (last_keydown_shortcut_event.input_source_switch_modifiers) {
|
||||
if (!([event modifierFlags] & last_keydown_shortcut_event.input_source_switch_modifiers))
|
||||
last_keydown_shortcut_event.input_source_switch_modifiers = 0;
|
||||
return event;
|
||||
}
|
||||
NSWindow *kw = [NSApp keyWindow];
|
||||
if (kw && kw.contentView) [kw.contentView flagsChanged:event];
|
||||
else debug_key("flagsChanged ignored as no keyWindow present\n");
|
||||
|
||||
9
glfw/cocoa_platform.h
vendored
9
glfw/cocoa_platform.h
vendored
@@ -68,7 +68,7 @@ typedef void* CVDisplayLinkRef;
|
||||
|
||||
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int, unsigned long);
|
||||
typedef bool (* GLFWapplicationshouldhandlereopenfun)(int);
|
||||
typedef bool (* GLFWhandlefileopen)(const char*);
|
||||
typedef bool (* GLFWhandleurlopen)(const char*);
|
||||
typedef void (* GLFWapplicationwillfinishlaunchingfun)(void);
|
||||
typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
|
||||
typedef void (* GLFWcocoarenderframefun)(GLFWwindow*);
|
||||
@@ -131,6 +131,7 @@ typedef struct _GLFWwindowNS
|
||||
bool maximized;
|
||||
bool retina;
|
||||
bool in_traditional_fullscreen;
|
||||
bool in_fullscreen_transition;
|
||||
bool titlebar_hidden;
|
||||
unsigned long pre_full_screen_style_mask;
|
||||
|
||||
@@ -153,6 +154,8 @@ typedef struct _GLFWwindowNS
|
||||
// Whether a render frame has been requested for this window
|
||||
bool renderFrameRequested;
|
||||
GLFWcocoarenderframefun renderFrameCallback;
|
||||
// update cursor after switching desktops with Mission Control
|
||||
bool delayed_cursor_update_requested;
|
||||
} _GLFWwindowNS;
|
||||
|
||||
typedef struct _GLFWDisplayLinkNS
|
||||
@@ -199,8 +202,8 @@ typedef struct _GLFWlibraryNS
|
||||
_GLFWDisplayLinkNS entries[256];
|
||||
size_t count;
|
||||
} displayLinks;
|
||||
// the callback to handle file open events
|
||||
GLFWhandlefileopen file_open_callback;
|
||||
// the callback to handle url open events
|
||||
GLFWhandleurlopen url_open_callback;
|
||||
|
||||
} _GLFWlibraryNS;
|
||||
|
||||
|
||||
@@ -582,6 +582,7 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
|
||||
}
|
||||
|
||||
- (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow;
|
||||
- (void)request_delayed_cursor_update:(id)sender;
|
||||
|
||||
@end
|
||||
|
||||
@@ -692,6 +693,9 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
|
||||
_glfwPlatformGetCursorPos(window, &x, &y);
|
||||
_glfwInputCursorPos(window, x, y);
|
||||
}
|
||||
// macOS will send a delayed event to update the cursor to arrow after switching desktops.
|
||||
// So we need to delay and update the cursor once after that.
|
||||
[self performSelector:@selector(request_delayed_cursor_update:) withObject:nil afterDelay:0.3];
|
||||
}
|
||||
|
||||
- (void)windowDidResignKey:(NSNotification *)notification
|
||||
@@ -722,6 +726,38 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
|
||||
}
|
||||
}
|
||||
|
||||
- (void)request_delayed_cursor_update:(id)sender
|
||||
{
|
||||
(void)sender;
|
||||
if (window) window->ns.delayed_cursor_update_requested = true;
|
||||
}
|
||||
|
||||
- (void)windowWillEnterFullScreen:(NSNotification *)notification
|
||||
{
|
||||
(void)notification;
|
||||
if (window) window->ns.in_fullscreen_transition = true;
|
||||
}
|
||||
|
||||
- (void)windowDidEnterFullScreen:(NSNotification *)notification
|
||||
{
|
||||
(void)notification;
|
||||
if (window) window->ns.in_fullscreen_transition = false;
|
||||
[self performSelector:@selector(request_delayed_cursor_update:) withObject:nil afterDelay:0.3];
|
||||
}
|
||||
|
||||
- (void)windowWillExitFullScreen:(NSNotification *)notification
|
||||
{
|
||||
(void)notification;
|
||||
if (window) window->ns.in_fullscreen_transition = true;
|
||||
}
|
||||
|
||||
- (void)windowDidExitFullScreen:(NSNotification *)notification
|
||||
{
|
||||
(void)notification;
|
||||
if (window) window->ns.in_fullscreen_transition = false;
|
||||
[self performSelector:@selector(request_delayed_cursor_update:) withObject:nil afterDelay:0.3];
|
||||
}
|
||||
|
||||
@end // }}}
|
||||
|
||||
// Text input context class for the GLFW content view {{{
|
||||
@@ -904,6 +940,11 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
|
||||
|
||||
window->ns.cursorWarpDeltaX = 0;
|
||||
window->ns.cursorWarpDeltaY = 0;
|
||||
|
||||
if (window->ns.delayed_cursor_update_requested) {
|
||||
window->ns.delayed_cursor_update_requested = false;
|
||||
if (cursorInContentArea(window)) updateCursorImage(window);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)rightMouseDown:(NSEvent *)event
|
||||
@@ -1003,6 +1044,8 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
|
||||
|
||||
- (void)updateTrackingAreas
|
||||
{
|
||||
if (window && [window->ns.object areCursorRectsEnabled])
|
||||
[window->ns.object disableCursorRects];
|
||||
if (trackingArea != nil)
|
||||
{
|
||||
[self removeTrackingArea:trackingArea];
|
||||
@@ -1325,14 +1368,17 @@ is_ascii_control_char(char x) {
|
||||
options:options];
|
||||
if (!objs) return NO;
|
||||
const NSUInteger count = [objs count];
|
||||
NSMutableString *uri_list = [NSMutableString stringWithCapacity:4096]; // auto-released
|
||||
if (count)
|
||||
{
|
||||
for (NSUInteger i = 0; i < count; i++)
|
||||
{
|
||||
id obj = objs[i];
|
||||
if ([obj isKindOfClass:[NSURL class]]) {
|
||||
const char *path = [obj fileSystemRepresentation];
|
||||
_glfwInputDrop(window, "text/plain;charset=utf-8", path, strlen(path));
|
||||
NSURL *url = (NSURL*)obj;
|
||||
if ([uri_list length] > 0) [uri_list appendString:@("\n")];
|
||||
if (url.fileURL) [uri_list appendFormat:@("file://%s"), url.fileSystemRepresentation];
|
||||
else [uri_list appendString:url.absoluteString];
|
||||
} else if ([obj isKindOfClass:[NSString class]]) {
|
||||
const char *text = [obj UTF8String];
|
||||
_glfwInputDrop(window, "text/plain;charset=utf-8", text, strlen(text));
|
||||
@@ -1342,6 +1388,7 @@ is_ascii_control_char(char x) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if ([uri_list length] > 0) _glfwInputDrop(window, "text/uri-list", uri_list.UTF8String, strlen(uri_list.UTF8String));
|
||||
|
||||
return YES;
|
||||
}
|
||||
@@ -1554,8 +1601,11 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
|
||||
|
||||
- (void)toggleFullScreen:(nullable id)sender
|
||||
{
|
||||
if (glfw_window && glfw_window->ns.toggleFullscreenCallback && glfw_window->ns.toggleFullscreenCallback((GLFWwindow*)glfw_window) == 1)
|
||||
return;
|
||||
if (glfw_window) {
|
||||
if (glfw_window->ns.in_fullscreen_transition) return;
|
||||
if (glfw_window->ns.toggleFullscreenCallback && glfw_window->ns.toggleFullscreenCallback((GLFWwindow*)glfw_window) == 1) return;
|
||||
glfw_window->ns.in_fullscreen_transition = true;
|
||||
}
|
||||
// When resizeIncrements is set, Cocoa cannot restore the original window size after returning from fullscreen.
|
||||
const NSSize original = [self resizeIncrements];
|
||||
[self setResizeIncrements:NSMakeSize(1.0, 1.0)];
|
||||
@@ -2642,10 +2692,10 @@ GLFWAPI GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow *hand
|
||||
return previous;
|
||||
}
|
||||
|
||||
GLFWAPI GLFWhandlefileopen glfwSetCocoaFileOpenCallback(GLFWhandlefileopen callback) {
|
||||
GLFWAPI GLFWhandleurlopen glfwSetCocoaURLOpenCallback(GLFWhandleurlopen callback) {
|
||||
_GLFW_REQUIRE_INIT_OR_RETURN(nil);
|
||||
GLFWhandlefileopen prev = _glfw.ns.file_open_callback;
|
||||
_glfw.ns.file_open_callback = callback;
|
||||
GLFWhandleurlopen prev = _glfw.ns.url_open_callback;
|
||||
_glfw.ns.url_open_callback = callback;
|
||||
return prev;
|
||||
}
|
||||
|
||||
|
||||
2
glfw/dbus_glfw.c
vendored
2
glfw/dbus_glfw.c
vendored
@@ -263,7 +263,7 @@ call_method_with_msg(DBusConnection *conn, DBusMessage *msg, int timeout, dbus_p
|
||||
|
||||
static bool
|
||||
call_method(DBusConnection *conn, const char *node, const char *path, const char *interface, const char *method, int timeout, dbus_pending_callback callback, void *user_data, va_list ap) {
|
||||
if (!conn) return false;
|
||||
if (!conn || !path) return false;
|
||||
DBusMessage *msg = dbus_message_new_method_call(node, path, interface, method);
|
||||
if (!msg) return false;
|
||||
bool retval = false;
|
||||
|
||||
@@ -233,7 +233,7 @@ def generate_wrappers(glfw_header: str) -> None:
|
||||
void* glfwGetNSGLContext(GLFWwindow *window)
|
||||
uint32_t glfwGetCocoaMonitor(GLFWmonitor* monitor)
|
||||
GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow* window, GLFWcocoatextinputfilterfun callback)
|
||||
GLFWhandlefileopen glfwSetCocoaFileOpenCallback(GLFWhandlefileopen callback)
|
||||
GLFWhandleurlopen glfwSetCocoaURLOpenCallback(GLFWhandleurlopen callback)
|
||||
GLFWcocoatogglefullscreenfun glfwSetCocoaToggleFullscreenIntercept(GLFWwindow *window, GLFWcocoatogglefullscreenfun callback)
|
||||
GLFWapplicationshouldhandlereopenfun glfwSetApplicationShouldHandleReopen(GLFWapplicationshouldhandlereopenfun callback)
|
||||
GLFWapplicationwillfinishlaunchingfun glfwSetApplicationWillFinishLaunching(GLFWapplicationwillfinishlaunchingfun callback)
|
||||
@@ -277,7 +277,7 @@ const char *action_text, int32_t timeout, GLFWDBusnotificationcreatedfun callbac
|
||||
|
||||
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int,unsigned long);
|
||||
typedef bool (* GLFWapplicationshouldhandlereopenfun)(int);
|
||||
typedef bool (* GLFWhandlefileopen)(const char*);
|
||||
typedef bool (* GLFWhandleurlopen)(const char*);
|
||||
typedef void (* GLFWapplicationwillfinishlaunchingfun)(void);
|
||||
typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
|
||||
typedef void (* GLFWcocoarenderframefun)(GLFWwindow*);
|
||||
@@ -292,6 +292,8 @@ const char* load_glfw(const char* path);
|
||||
f.write(header)
|
||||
|
||||
code = '''
|
||||
// generated by glfw.py DO NOT edit
|
||||
|
||||
#define GFW_EXTERN
|
||||
#include "data-types.h"
|
||||
#include "glfw-wrapper.h"
|
||||
|
||||
20
glfw/wl_client_side_decorations.c
vendored
20
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.height + decs.metrics.top;
|
||||
const size_t horizontal_height = decs.metrics.width, horizontal_width = window->wl.width + 2 * decs.metrics.width;
|
||||
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;
|
||||
|
||||
decs.mapping.size = 0;
|
||||
decs.mapping.size += init_buffer_pair(&decs.top.buffer, window->wl.width, decs.metrics.top, scale);
|
||||
decs.mapping.size += init_buffer_pair(&decs.top.buffer, window->wl.current.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,8 +347,8 @@ 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.width ||
|
||||
decs.for_window_state.height != window->wl.height ||
|
||||
decs.for_window_state.width != window->wl.current.width ||
|
||||
decs.for_window_state.height != window->wl.current.height ||
|
||||
decs.for_window_state.scale != window->wl.scale ||
|
||||
!decs.mapping.data
|
||||
);
|
||||
@@ -368,11 +368,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.height;
|
||||
x = -decs.metrics.width; y = window->wl.current.height;
|
||||
if (!decs.bottom.surface) create_csd_surfaces(window, &decs.bottom);
|
||||
position_csd_surface(&decs.bottom, x, y, scale);
|
||||
|
||||
x = window->wl.width; y = -decs.metrics.top;
|
||||
x = window->wl.current.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 +382,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.width;
|
||||
decs.for_window_state.height = window->wl.height;
|
||||
decs.for_window_state.width = window->wl.current.width;
|
||||
decs.for_window_state.height = window->wl.current.height;
|
||||
decs.for_window_state.scale = window->wl.scale;
|
||||
decs.for_window_state.focused = is_focused;
|
||||
return true;
|
||||
@@ -409,7 +409,7 @@ change_csd_title(_GLFWwindow *window) {
|
||||
|
||||
void
|
||||
set_csd_window_geometry(_GLFWwindow *window, int32_t *width, int32_t *height) {
|
||||
bool has_csd = window->decorated && !window->wl.decorations.serverSide && window->wl.decorations.left.surface && !(window->wl.toplevel_states & TOPLEVEL_STATE_FULLSCREEN);
|
||||
bool has_csd = window->decorated && !window->wl.decorations.serverSide && window->wl.decorations.left.surface && !(window->wl.current.toplevel_states & TOPLEVEL_STATE_FULLSCREEN);
|
||||
bool size_specified_by_compositor = *width > 0 && *height > 0;
|
||||
if (!size_specified_by_compositor) {
|
||||
*width = window->wl.user_requested_content_size.width;
|
||||
|
||||
79
glfw/wl_init.c
vendored
79
glfw/wl_init.c
vendored
@@ -206,7 +206,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.width + window->wl.decorations.metrics.width)
|
||||
else if (x > window->wl.current.width + window->wl.decorations.metrics.width)
|
||||
cursorShape = GLFW_SE_RESIZE_CURSOR;
|
||||
else
|
||||
cursorShape = GLFW_VRESIZE_CURSOR;
|
||||
@@ -243,7 +243,7 @@ static void pointerHandleButton(void* data UNUSED,
|
||||
window->wl.decorations.last_click_on_top_decoration_at = monotonic();
|
||||
if (window->wl.decorations.last_click_on_top_decoration_at - last_click_at <= _glfwPlatformGetDoubleClickInterval(window)) {
|
||||
window->wl.decorations.last_click_on_top_decoration_at = 0;
|
||||
if (window->wl.toplevel_states & TOPLEVEL_STATE_MAXIMIZED)
|
||||
if (window->wl.current.toplevel_states & TOPLEVEL_STATE_MAXIMIZED)
|
||||
xdg_toplevel_unset_maximized(window->wl.xdg.toplevel);
|
||||
else
|
||||
xdg_toplevel_set_maximized(window->wl.xdg.toplevel);
|
||||
@@ -274,7 +274,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.width + window->wl.decorations.metrics.width)
|
||||
else if (x > window->wl.current.width + window->wl.decorations.metrics.width)
|
||||
edges = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT;
|
||||
else
|
||||
edges = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM;
|
||||
@@ -331,20 +331,83 @@ static void pointerHandleAxis(void* data UNUSED,
|
||||
assert(axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL ||
|
||||
axis == WL_POINTER_AXIS_VERTICAL_SCROLL);
|
||||
|
||||
if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL)
|
||||
x = -wl_fixed_to_double(value);
|
||||
else if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL)
|
||||
y = -wl_fixed_to_double(value);
|
||||
if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) {
|
||||
if (window->wl.axis_discrete_count.x) {
|
||||
window->wl.axis_discrete_count.x--;
|
||||
return;
|
||||
}
|
||||
x = -wl_fixed_to_double(value) * (window->wl.scale);
|
||||
}
|
||||
else if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) {
|
||||
if (window->wl.axis_discrete_count.y) {
|
||||
window->wl.axis_discrete_count.y--;
|
||||
return;
|
||||
}
|
||||
y = -wl_fixed_to_double(value) * (window->wl.scale);
|
||||
}
|
||||
|
||||
_glfwInputScroll(window, x, y, 1, _glfw.wl.xkb.states.modifiers);
|
||||
}
|
||||
|
||||
static void pointerHandleFrame(void* data UNUSED,
|
||||
struct wl_pointer* pointer UNUSED)
|
||||
{
|
||||
_GLFWwindow* window = _glfw.wl.pointerFocus;
|
||||
if (window) {
|
||||
window->wl.axis_discrete_count.x = 0;
|
||||
window->wl.axis_discrete_count.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void pointerHandleAxisSource(void* data UNUSED,
|
||||
struct wl_pointer* pointer UNUSED,
|
||||
uint32_t source UNUSED)
|
||||
{
|
||||
}
|
||||
|
||||
static void pointerHandleAxisStop(void *data UNUSED,
|
||||
struct wl_pointer *wl_pointer UNUSED,
|
||||
uint32_t time UNUSED,
|
||||
uint32_t axis UNUSED)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
static void pointerHandleAxisDiscrete(void *data UNUSED,
|
||||
struct wl_pointer *wl_pointer UNUSED,
|
||||
uint32_t axis,
|
||||
int32_t discrete)
|
||||
{
|
||||
_GLFWwindow* window = _glfw.wl.pointerFocus;
|
||||
double x = 0.0, y = 0.0;
|
||||
if (!window)
|
||||
return;
|
||||
|
||||
assert(axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL ||
|
||||
axis == WL_POINTER_AXIS_VERTICAL_SCROLL);
|
||||
|
||||
if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) {
|
||||
x = -discrete;
|
||||
window->wl.axis_discrete_count.x++;
|
||||
}
|
||||
else if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) {
|
||||
y = -discrete;
|
||||
window->wl.axis_discrete_count.y++;
|
||||
}
|
||||
|
||||
_glfwInputScroll(window, x, y, 0, _glfw.wl.xkb.states.modifiers);
|
||||
}
|
||||
|
||||
static const struct wl_pointer_listener pointerListener = {
|
||||
pointerHandleEnter,
|
||||
pointerHandleLeave,
|
||||
pointerHandleMotion,
|
||||
pointerHandleButton,
|
||||
pointerHandleAxis,
|
||||
pointerHandleFrame,
|
||||
pointerHandleAxisSource,
|
||||
pointerHandleAxisStop,
|
||||
pointerHandleAxisDiscrete,
|
||||
};
|
||||
|
||||
static void keyboardHandleKeymap(void* data UNUSED,
|
||||
@@ -571,7 +634,7 @@ static void registryHandleGlobal(void* data UNUSED,
|
||||
{
|
||||
if (!_glfw.wl.seat)
|
||||
{
|
||||
_glfw.wl.seatVersion = min(4, version);
|
||||
_glfw.wl.seatVersion = min(5, version);
|
||||
_glfw.wl.seat =
|
||||
wl_registry_bind(registry, name, &wl_seat_interface,
|
||||
_glfw.wl.seatVersion);
|
||||
|
||||
12
glfw/wl_platform.h
vendored
12
glfw/wl_platform.h
vendored
@@ -132,7 +132,6 @@ static const WaylandWindowState TOPLEVEL_STATE_DOCKED = TOPLEVEL_STATE_MAXIMIZED
|
||||
//
|
||||
typedef struct _GLFWwindowWayland
|
||||
{
|
||||
int width, height;
|
||||
bool visible;
|
||||
bool hovered;
|
||||
bool transparent;
|
||||
@@ -210,9 +209,18 @@ typedef struct _GLFWwindowWayland
|
||||
int32_t width, height;
|
||||
} user_requested_content_size;
|
||||
|
||||
uint32_t toplevel_states;
|
||||
bool maximize_on_first_show;
|
||||
// counters for ignoring axis events following axis_discrete events in the
|
||||
// same frame along the same axis
|
||||
struct {
|
||||
unsigned int x, y;
|
||||
} axis_discrete_count;
|
||||
|
||||
struct {
|
||||
int width, height;
|
||||
uint32_t toplevel_states;
|
||||
bool set;
|
||||
} current, pending;
|
||||
} _GLFWwindowWayland;
|
||||
|
||||
typedef enum _GLFWWaylandOfferType
|
||||
|
||||
76
glfw/wl_window.c
vendored
76
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.width, window->wl.height);
|
||||
wl_region_add(region, 0, 0, window->wl.current.width, window->wl.current.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.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);
|
||||
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);
|
||||
wl_egl_window_resize(window->wl.native, scaledWidth, scaledHeight, 0, 0);
|
||||
if (!window->wl.transparent) setOpaqueRegion(window);
|
||||
_glfwInputFramebufferSize(window, scaledWidth, scaledHeight);
|
||||
@@ -236,12 +236,12 @@ clipboard_mime(void) {
|
||||
|
||||
static void
|
||||
dispatchChangesAfterConfigure(_GLFWwindow *window, int32_t width, int32_t height) {
|
||||
bool size_changed = width != window->wl.width || height != window->wl.height;
|
||||
bool size_changed = width != window->wl.current.width || height != window->wl.current.height;
|
||||
bool scale_changed = checkScaleChange(window);
|
||||
|
||||
if (size_changed) {
|
||||
_glfwInputWindowSize(window, width, height);
|
||||
window->wl.width = width; window->wl.height = height;
|
||||
window->wl.current.width = width; window->wl.current.height = height;
|
||||
resizeFramebuffer(window);
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ xdgDecorationHandleConfigure(void* data,
|
||||
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.width, height = window->wl.height;
|
||||
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;
|
||||
@@ -285,7 +285,7 @@ xdgDecorationHandleConfigure(void* data,
|
||||
dispatchChangesAfterConfigure(window, width, height);
|
||||
ensure_csd_resources(window);
|
||||
wl_surface_commit(window->wl.surface);
|
||||
debug("final window content size: %dx%d\n", window->wl.width, window->wl.height);
|
||||
debug("final window content size: %dx%d\n", window->wl.current.width, window->wl.current.height);
|
||||
inform_compositor_of_window_geometry(window, "configure-decorations");
|
||||
}
|
||||
|
||||
@@ -384,8 +384,8 @@ static bool createSurface(_GLFWwindow* window,
|
||||
if (!window->wl.native)
|
||||
return false;
|
||||
|
||||
window->wl.width = wndconfig->width;
|
||||
window->wl.height = wndconfig->height;
|
||||
window->wl.current.width = wndconfig->width;
|
||||
window->wl.current.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;
|
||||
@@ -416,7 +416,7 @@ static void setFullscreen(_GLFWwindow* window, _GLFWmonitor* monitor, bool on)
|
||||
|
||||
bool
|
||||
_glfwPlatformIsFullscreen(_GLFWwindow *window, unsigned int flags UNUSED) {
|
||||
return window->wl.toplevel_states & TOPLEVEL_STATE_FULLSCREEN;
|
||||
return window->wl.current.toplevel_states & TOPLEVEL_STATE_FULLSCREEN;
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -458,7 +458,7 @@ xdgToplevelHandleConfigure(void* data,
|
||||
if (new_states & TOPLEVEL_STATE_RESIZING) {
|
||||
if (width) window->wl.user_requested_content_size.width = width;
|
||||
if (height) window->wl.user_requested_content_size.height = height;
|
||||
if (!(window->wl.toplevel_states & TOPLEVEL_STATE_RESIZING)) _glfwInputLiveResize(window, true);
|
||||
if (!(window->wl.current.toplevel_states & TOPLEVEL_STATE_RESIZING)) _glfwInputLiveResize(window, true);
|
||||
}
|
||||
if (width != 0 && height != 0)
|
||||
{
|
||||
@@ -475,16 +475,11 @@ xdgToplevelHandleConfigure(void* data,
|
||||
}
|
||||
}
|
||||
}
|
||||
bool live_resize_done = !(new_states & TOPLEVEL_STATE_RESIZING) && (window->wl.toplevel_states & TOPLEVEL_STATE_RESIZING);
|
||||
window->wl.toplevel_states = new_states;
|
||||
set_csd_window_geometry(window, &width, &height);
|
||||
dispatchChangesAfterConfigure(window, width, height);
|
||||
debug("final window content size: %dx%d\n", window->wl.width, window->wl.height);
|
||||
_glfwInputWindowFocus(window, window->wl.toplevel_states & TOPLEVEL_STATE_ACTIVATED);
|
||||
ensure_csd_resources(window);
|
||||
wl_surface_commit(window->wl.surface);
|
||||
inform_compositor_of_window_geometry(window, "configure");
|
||||
if (live_resize_done) _glfwInputLiveResize(window, false);
|
||||
|
||||
window->wl.pending.toplevel_states = new_states;
|
||||
window->wl.pending.width = width;
|
||||
window->wl.pending.height = height;
|
||||
window->wl.pending.set = true;
|
||||
}
|
||||
|
||||
static void xdgToplevelHandleClose(void* data,
|
||||
@@ -499,11 +494,34 @@ static const struct xdg_toplevel_listener xdgToplevelListener = {
|
||||
xdgToplevelHandleClose
|
||||
};
|
||||
|
||||
static void xdgSurfaceHandleConfigure(void* data UNUSED,
|
||||
static void xdgSurfaceHandleConfigure(void* data,
|
||||
struct xdg_surface* surface,
|
||||
uint32_t serial)
|
||||
{
|
||||
_GLFWwindow* window = data;
|
||||
xdg_surface_ack_configure(surface, serial);
|
||||
if (window->wl.pending.set) {
|
||||
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 (new_states != window->wl.current.toplevel_states ||
|
||||
width != window->wl.current.width ||
|
||||
height != window->wl.current.height) {
|
||||
|
||||
bool live_resize_done = !(new_states & TOPLEVEL_STATE_RESIZING) && (window->wl.current.toplevel_states & TOPLEVEL_STATE_RESIZING);
|
||||
window->wl.current.toplevel_states = new_states;
|
||||
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);
|
||||
_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);
|
||||
}
|
||||
|
||||
static const struct xdg_surface_listener xdgSurfaceListener = {
|
||||
@@ -886,19 +904,19 @@ void _glfwPlatformSetWindowPos(_GLFWwindow* window UNUSED, int xpos UNUSED, int
|
||||
void _glfwPlatformGetWindowSize(_GLFWwindow* window, int* width, int* height)
|
||||
{
|
||||
if (width)
|
||||
*width = window->wl.width;
|
||||
*width = window->wl.current.width;
|
||||
if (height)
|
||||
*height = window->wl.height;
|
||||
*height = window->wl.current.height;
|
||||
}
|
||||
|
||||
void _glfwPlatformSetWindowSize(_GLFWwindow* window, int width, int height)
|
||||
{
|
||||
if (width != window->wl.width || height != window->wl.height) {
|
||||
if (width != window->wl.current.width || height != window->wl.current.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.width = w; window->wl.height = h;
|
||||
window->wl.current.width = w; window->wl.current.height = h;
|
||||
resizeFramebuffer(window);
|
||||
ensure_csd_resources(window);
|
||||
wl_surface_commit(window->wl.surface);
|
||||
@@ -991,7 +1009,7 @@ void _glfwPlatformRestoreWindow(_GLFWwindow* window)
|
||||
{
|
||||
if (window->monitor)
|
||||
xdg_toplevel_unset_fullscreen(window->wl.xdg.toplevel);
|
||||
if (window->wl.toplevel_states & TOPLEVEL_STATE_MAXIMIZED)
|
||||
if (window->wl.current.toplevel_states & TOPLEVEL_STATE_MAXIMIZED)
|
||||
xdg_toplevel_unset_maximized(window->wl.xdg.toplevel);
|
||||
// There is no way to unset minimized, or even to know if we are
|
||||
// minimized, so there is nothing to do in this case.
|
||||
@@ -1092,7 +1110,7 @@ int _glfwPlatformWindowVisible(_GLFWwindow* window)
|
||||
|
||||
int _glfwPlatformWindowMaximized(_GLFWwindow* window)
|
||||
{
|
||||
return window->wl.toplevel_states & TOPLEVEL_STATE_MAXIMIZED;
|
||||
return window->wl.current.toplevel_states & TOPLEVEL_STATE_MAXIMIZED;
|
||||
}
|
||||
|
||||
int _glfwPlatformWindowHovered(_GLFWwindow* window)
|
||||
|
||||
2
glfw/xkb_glfw.c
vendored
2
glfw/xkb_glfw.c
vendored
@@ -33,7 +33,7 @@
|
||||
#include <X11/XKBlib.h>
|
||||
#endif
|
||||
|
||||
#define debug(...) if (_glfw.hints.init.debugKeyboard) printf(__VA_ARGS__);
|
||||
#define debug(...) if (_glfw.hints.init.debugKeyboard) fprintf(stderr, __VA_ARGS__);
|
||||
|
||||
#ifdef XKB_HAS_NO_UTF32
|
||||
#include "xkb-compat-shim.h"
|
||||
|
||||
@@ -20,7 +20,8 @@ from kitty.fast_data_types import get_options, set_clipboard_string
|
||||
from kitty.key_encoding import KeyEvent
|
||||
from kitty.typing import BossType, KittyCommonOpts
|
||||
from kitty.utils import (
|
||||
ScreenSize, resolve_custom_file, screen_size_function, set_primary_selection
|
||||
ScreenSize, kitty_ansi_sanitizer_pat, resolve_custom_file,
|
||||
screen_size_function, set_primary_selection
|
||||
)
|
||||
|
||||
from ..tui.handler import Handler, result_handler
|
||||
@@ -230,7 +231,7 @@ def regex_finditer(pat: 'Pattern[str]', minimum_match_length: int, text: str) ->
|
||||
yield s, e, m.groupdict()
|
||||
|
||||
|
||||
closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>', '*': '*', '"': '"', "'": "'"}
|
||||
closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>', '*': '*', '"': '"', "'": "'", "“": "”", "‘": "’"}
|
||||
opening_brackets = ''.join(closing_bracket_map)
|
||||
PostprocessorFunc = Callable[[str, int, int], Tuple[int, int]]
|
||||
postprocessor_map: Dict[str, PostprocessorFunc] = {}
|
||||
@@ -288,11 +289,12 @@ def quotes(text: str, s: int, e: int) -> Tuple[int, int]:
|
||||
# Remove matching quotes
|
||||
if s < e <= len(text):
|
||||
before = text[s]
|
||||
if before in '\'"':
|
||||
if text[e-1] == before:
|
||||
if before in '\'"“‘':
|
||||
q = closing_bracket_map[before]
|
||||
if text[e-1] == q:
|
||||
s += 1
|
||||
e -= 1
|
||||
elif text[e:e+1] == before:
|
||||
elif text[e:e+1] == q:
|
||||
s += 1
|
||||
return s, e
|
||||
|
||||
@@ -430,11 +432,7 @@ def load_custom_processor(customize_processing: str) -> Any:
|
||||
return runpy.run_path(custom_path, run_name='__main__')
|
||||
|
||||
|
||||
def remove_sgr(text: str) -> str:
|
||||
return re.sub(r'\x1b\[.*?m', '', text)
|
||||
|
||||
|
||||
def process_hyperlinks(text: str) -> Tuple[str, Tuple[Mark, ...]]:
|
||||
def process_escape_codes(text: str) -> Tuple[str, Tuple[Mark, ...]]:
|
||||
hyperlinks: List[Mark] = []
|
||||
removed_size = idx = 0
|
||||
active_hyperlink_url: Optional[str] = None
|
||||
@@ -457,6 +455,9 @@ def process_hyperlinks(text: str) -> Tuple[str, Tuple[Mark, ...]]:
|
||||
def process_hyperlink(m: 're.Match[str]') -> str:
|
||||
nonlocal removed_size, active_hyperlink_url, active_hyperlink_id, active_hyperlink_start_offset
|
||||
raw = m.group()
|
||||
if not raw.startswith('\x1b]8'):
|
||||
removed_size += len(raw)
|
||||
return ''
|
||||
start = m.start() - removed_size
|
||||
removed_size += len(raw)
|
||||
if active_hyperlink_url is not None:
|
||||
@@ -474,7 +475,7 @@ def process_hyperlinks(text: str) -> Tuple[str, Tuple[Mark, ...]]:
|
||||
|
||||
return ''
|
||||
|
||||
text = re.sub(r'\x1b\]8.+?\x1b\\', process_hyperlink, text)
|
||||
text = kitty_ansi_sanitizer_pat().sub(process_hyperlink, text)
|
||||
if active_hyperlink_url is not None:
|
||||
add_hyperlink(len(text))
|
||||
return text, tuple(hyperlinks)
|
||||
@@ -482,8 +483,8 @@ def process_hyperlinks(text: str) -> Tuple[str, Tuple[Mark, ...]]:
|
||||
|
||||
def run(args: HintsCLIOptions, text: str, extra_cli_args: Sequence[str] = ()) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
text = parse_input(remove_sgr(text))
|
||||
text, hyperlinks = process_hyperlinks(text)
|
||||
text = parse_input(text)
|
||||
text, hyperlinks = process_escape_codes(text)
|
||||
pattern, post_processors = functions_for(args)
|
||||
if args.type == 'linenum':
|
||||
args.customize_processing = '::linenum::'
|
||||
@@ -701,14 +702,26 @@ def main(args: List[str]) -> Optional[Dict[str, Any]]:
|
||||
return None
|
||||
|
||||
|
||||
def linenum_handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: BossType, extra_cli_args: Sequence[str], *a: Any) -> None:
|
||||
def linenum_process_result(data: Dict[str, Any]) -> Tuple[str, int]:
|
||||
pat = re.compile(r':(\d+)$')
|
||||
for m, g in zip(data['match'], data['groupdicts']):
|
||||
if m:
|
||||
path, line = g['path'], g['line']
|
||||
path = os.path.expanduser(path.split(':')[-1])
|
||||
line = int(line)
|
||||
break
|
||||
else:
|
||||
# look for trailers on path of the for :number
|
||||
while True:
|
||||
m = pat.search(path)
|
||||
if m is None:
|
||||
break
|
||||
line = m.group(1)
|
||||
path = path[:-len(m.group())]
|
||||
|
||||
return os.path.expanduser(path), int(line)
|
||||
return '', -1
|
||||
|
||||
|
||||
def linenum_handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: BossType, extra_cli_args: Sequence[str], *a: Any) -> None:
|
||||
path, line = linenum_process_result(data)
|
||||
if not path:
|
||||
return
|
||||
|
||||
cmd = [x.format(path=path, line=line) for x in extra_cli_args or ('vim', '+{line}', '{path}')]
|
||||
|
||||
@@ -102,7 +102,7 @@ detecting image display support.
|
||||
--print-window-size
|
||||
type=bool-set
|
||||
Print out the window size as :italic:`widthxheight` (in pixels) and quit. This is a
|
||||
convenience method to query the window size if using kitty icat from a
|
||||
convenience method to query the window size if using :code:`kitty +kitten icat` from a
|
||||
scripting language that cannot make termios calls.
|
||||
|
||||
|
||||
|
||||
@@ -122,10 +122,17 @@ class ControlMaster:
|
||||
cmd.extend(['-i', conn_data.identity_file])
|
||||
self.batch_cmd_prefix = cmd + ['-o', 'BatchMode=yes']
|
||||
|
||||
def check_call(self, cmd: List[str]) -> None:
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL)
|
||||
stdout = p.communicate()[0]
|
||||
if p.wait() != 0:
|
||||
out = stdout.decode('utf-8', 'replace')
|
||||
raise Exception(f'The ssh command: {shlex.join(cmd)} failed with exit code {p.returncode} and output: {out}')
|
||||
|
||||
def __enter__(self) -> 'ControlMaster':
|
||||
subprocess.check_call(
|
||||
self.check_call(
|
||||
self.cmd_prefix + ['-o', 'ControlMaster=auto', '-fN', self.conn_data.hostname])
|
||||
subprocess.check_call(
|
||||
self.check_call(
|
||||
self.batch_cmd_prefix + ['-O', 'check', self.conn_data.hostname])
|
||||
if not self.dest:
|
||||
self.tdir = tempfile.mkdtemp()
|
||||
|
||||
@@ -9,10 +9,10 @@ from contextlib import contextmanager
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Any, Dict, FrozenSet, Generator, List, cast
|
||||
|
||||
from kitty.constants import list_kitty_resources
|
||||
from kitty.types import run_once
|
||||
from kitty.utils import resolve_abs_or_config_path
|
||||
|
||||
|
||||
aliases = {'url_hints': 'hints'}
|
||||
if TYPE_CHECKING:
|
||||
from kitty.conf.types import Definition
|
||||
@@ -85,7 +85,7 @@ def launch(args: List[str]) -> None:
|
||||
del args[:2]
|
||||
args = [kitten] + args
|
||||
os.environ['KITTY_CONFIG_DIRECTORY'] = config_dir
|
||||
from kittens.tui.operations import clear_screen, reset_mode, Mode
|
||||
from kittens.tui.operations import Mode, clear_screen, reset_mode
|
||||
set_debug(kitten)
|
||||
m = import_kitten_main_module(config_dir, kitten)
|
||||
try:
|
||||
@@ -116,11 +116,9 @@ def run_kitten(kitten: str, run_name: str = '__main__') -> None:
|
||||
original_kitten_name = kitten
|
||||
kitten = resolved_kitten(kitten)
|
||||
set_debug(kitten)
|
||||
try:
|
||||
if kitten in all_kitten_names():
|
||||
runpy.run_module(f'kittens.{kitten}.main', run_name=run_name)
|
||||
return
|
||||
except ImportError:
|
||||
pass
|
||||
# Look for a custom kitten
|
||||
if not kitten.endswith('.py'):
|
||||
kitten += '.py'
|
||||
@@ -137,7 +135,6 @@ def run_kitten(kitten: str, run_name: str = '__main__') -> None:
|
||||
|
||||
@run_once
|
||||
def all_kitten_names() -> FrozenSet[str]:
|
||||
from kitty.constants import list_kitty_resources
|
||||
ans = []
|
||||
for name in list_kitty_resources('kittens'):
|
||||
if '__' not in name and '.' not in name and name != 'tui':
|
||||
|
||||
@@ -23,16 +23,17 @@ def real_main(args: List[str]) -> None:
|
||||
cli_opts, items = parse_args(args[1:], OPTIONS, '', msg, 'hints', result_class=ErrorCLIOptions)
|
||||
error_message = sys.stdin.buffer.read().decode('utf-8')
|
||||
sys.stdin = open(os.ctermid())
|
||||
print(styled(cli_opts.title, fg_intense=True, fg='red', bold=True))
|
||||
print()
|
||||
if cli_opts.title:
|
||||
print(styled(cli_opts.title, fg_intense=True, fg='red', bold=True))
|
||||
print()
|
||||
print(error_message)
|
||||
print()
|
||||
input('Press Enter to close.')
|
||||
input('\x1b[1;32mPress Enter to close\x1b[m')
|
||||
|
||||
|
||||
def main(args: List[str]) -> None:
|
||||
try:
|
||||
with suppress(KeyboardInterrupt):
|
||||
with suppress(KeyboardInterrupt, EOFError):
|
||||
real_main(args)
|
||||
except Exception:
|
||||
import traceback
|
||||
|
||||
@@ -20,12 +20,17 @@ cat >$tmp << 'TERMEOF'
|
||||
TERMINFO
|
||||
TERMEOF
|
||||
|
||||
tic_out=$(tic -x -o $HOME/.terminfo $tmp 2>&1)
|
||||
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/.terminfo"
|
||||
export TERMINFO="$HOME/$tname"
|
||||
login_shell=""
|
||||
python=""
|
||||
|
||||
@@ -109,8 +114,12 @@ import subprocess, os, sys, pwd, binascii, json
|
||||
# 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}'))
|
||||
p = subprocess.Popen(['tic', '-x', '-o', os.path.expanduser('~/.terminfo'), tmp.name], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
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)
|
||||
|
||||
@@ -36,7 +36,7 @@ def patch_conf(raw: str, theme_name: str) -> str:
|
||||
nraw = raw + addition
|
||||
# comment out all existing color definitions
|
||||
color_conf_items = ( # {{{
|
||||
# generated by gen_config.py do not EDIT
|
||||
# generated by gen-config.py DO NOT edit
|
||||
# ALL_COLORS_START
|
||||
'active_border_color',
|
||||
'active_tab_background',
|
||||
|
||||
@@ -110,14 +110,16 @@ class TermManager:
|
||||
|
||||
class MouseButton(IntFlag):
|
||||
NONE, LEFT, MIDDLE, RIGHT, FOURTH, FIFTH = 0, 1, 2, 4, 8, 16
|
||||
WHEEL_UP, WHEEL_DOWN = -1, -2
|
||||
|
||||
|
||||
bmap = {0: MouseButton.LEFT, 1: MouseButton.MIDDLE, 2: MouseButton.RIGHT}
|
||||
MOTION_INDICATOR = 1 << 5
|
||||
EXTRA_BUTTON_INDICATOR = 1 << 6
|
||||
SHIFT_INDICATOR = 1 << 2
|
||||
ALT_INDICATOR = 1 << 3
|
||||
CTRL_INDICATOR = 1 << 4
|
||||
MOTION_INDICATOR = 1 << 5
|
||||
SCROLL_BUTTON_INDICATOR = 1 << 6
|
||||
EXTRA_BUTTON_INDICATOR = 1 << 7
|
||||
|
||||
|
||||
class EventType(Enum):
|
||||
@@ -149,7 +151,9 @@ def decode_sgr_mouse(text: str, screen_size: ScreenSize) -> MouseEvent:
|
||||
buttons: MouseButton = MouseButton.NONE
|
||||
cb3 = cb & 3
|
||||
if cb3 != 3:
|
||||
if cb & EXTRA_BUTTON_INDICATOR:
|
||||
if cb & SCROLL_BUTTON_INDICATOR:
|
||||
buttons = MouseButton.WHEEL_DOWN if cb3 & 1 else MouseButton.WHEEL_UP
|
||||
elif cb & EXTRA_BUTTON_INDICATOR:
|
||||
buttons |= MouseButton.FIFTH if cb3 & 1 else MouseButton.FOURTH
|
||||
else:
|
||||
buttons |= bmap[cb3]
|
||||
|
||||
@@ -99,10 +99,7 @@ def as_rst() -> str:
|
||||
|
||||
for action in allg[group]:
|
||||
a('')
|
||||
a(f'.. _action-{action.name}:')
|
||||
a('')
|
||||
a(action.name)
|
||||
a('+' * len(action.name))
|
||||
a(f'.. action:: {action.name}')
|
||||
a('')
|
||||
a(action.short_help)
|
||||
a('')
|
||||
|
||||
@@ -5,12 +5,14 @@ import atexit
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
from functools import partial
|
||||
from gettext import gettext as _
|
||||
from time import monotonic
|
||||
from typing import (
|
||||
Any, Callable, Container, Dict, Iterable, Iterator, List, Optional, Tuple,
|
||||
Union
|
||||
Any, Callable, Container, Dict, Iterable, Iterator, List, Optional,
|
||||
Sequence, Tuple, Union
|
||||
)
|
||||
from weakref import WeakValueDictionary
|
||||
|
||||
@@ -229,7 +231,6 @@ class Boss:
|
||||
global_shortcuts: Dict[str, SingleKey]
|
||||
):
|
||||
set_layout_options(opts)
|
||||
self.cocoa_application_launched = False
|
||||
self.clipboard_buffers: Dict[str, str] = {}
|
||||
self.update_check_process: Optional['PopenType[bytes]'] = None
|
||||
self.window_id_map: WeakValueDictionary[int, Window] = WeakValueDictionary()
|
||||
@@ -273,9 +274,9 @@ class Boss:
|
||||
for sc in self.global_shortcuts.values():
|
||||
self.keymap.pop(sc, None)
|
||||
|
||||
def startup_first_child(self, os_window_id: Optional[int]) -> None:
|
||||
startup_sessions = create_sessions(get_options(), self.args, default_session=get_options().startup_session)
|
||||
for startup_session in startup_sessions:
|
||||
def startup_first_child(self, os_window_id: Optional[int], startup_sessions: Sequence[Optional[Session]] = ()) -> None:
|
||||
si = startup_sessions or create_sessions(get_options(), self.args, default_session=get_options().startup_session)
|
||||
for startup_session in si:
|
||||
self.add_os_window(startup_session, os_window_id=os_window_id)
|
||||
os_window_id = None
|
||||
if self.args.start_as != 'normal':
|
||||
@@ -546,6 +547,10 @@ class Boss:
|
||||
from .cli_stub import CLIOptions
|
||||
startup_id = data.get('startup_id')
|
||||
args, rest = parse_args(data['args'][1:], result_class=CLIOptions)
|
||||
cmdline_args_for_open = data.get('cmdline_args_for_open')
|
||||
if cmdline_args_for_open:
|
||||
self.launch_urls(*cmdline_args_for_open, no_replace_window=True)
|
||||
return None
|
||||
args.args = rest
|
||||
opts = create_opts(args)
|
||||
if args.session == '-':
|
||||
@@ -746,7 +751,14 @@ class Boss:
|
||||
if not getattr(self, 'io_thread_started', False):
|
||||
self.child_monitor.start()
|
||||
self.io_thread_started = True
|
||||
self.startup_first_child(first_os_window_id)
|
||||
urls: List[str] = getattr(sys, 'cmdline_args_for_open', [])
|
||||
sess = create_sessions(get_options(), self.args, special_window=SpecialWindow([kitty_exe(), '+runpy', 'input()']))
|
||||
if urls:
|
||||
delattr(sys, 'cmdline_args_for_open')
|
||||
self.startup_first_child(first_os_window_id, startup_sessions=tuple(sess))
|
||||
self.launch_urls(*urls)
|
||||
else:
|
||||
self.startup_first_child(first_os_window_id)
|
||||
|
||||
if get_options().update_check_interval > 0 and not hasattr(self, 'update_check_started'):
|
||||
from .update_check import run_update_check
|
||||
@@ -779,7 +791,8 @@ class Boss:
|
||||
map kitty_mod+f11 clear_terminal scrollback active
|
||||
# Scroll the contents of the screen into the scrollback
|
||||
map kitty_mod+f12 clear_terminal scroll active
|
||||
|
||||
# Clear everything up to the line with the cursor
|
||||
map kitty_mod+f9 clear_terminal to_cursor active
|
||||
''')
|
||||
def clear_terminal(self, action: str, only_active: bool) -> None:
|
||||
if only_active:
|
||||
@@ -792,8 +805,10 @@ class Boss:
|
||||
reset = action == 'reset'
|
||||
how = 3 if action == 'scrollback' else 2
|
||||
for w in windows:
|
||||
if action == 'scroll':
|
||||
w.screen.scroll_until_cursor()
|
||||
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:
|
||||
@@ -1206,7 +1221,12 @@ class Boss:
|
||||
if w is not None:
|
||||
text = data.decode('utf-8', 'replace')
|
||||
if mime == 'text/uri-list':
|
||||
text = '\n'.join(parse_uri_list(text))
|
||||
urls = parse_uri_list(text)
|
||||
if w.at_prompt:
|
||||
import shlex
|
||||
text = ' '.join(map(shlex.quote, urls))
|
||||
else:
|
||||
text = '\n'.join(urls)
|
||||
w.paste(text)
|
||||
|
||||
@ac('win', 'Focus the nth OS window')
|
||||
@@ -1811,6 +1831,8 @@ class Boss:
|
||||
self._create_tab(list(args), cwd_from=cwd_from)
|
||||
|
||||
def new_tab_with_wd(self, wd: str) -> None:
|
||||
if not self.os_window_map:
|
||||
self.add_os_window()
|
||||
special_window = SpecialWindow(None, cwd=wd)
|
||||
self._new_tab(special_window)
|
||||
|
||||
@@ -2136,7 +2158,7 @@ class Boss:
|
||||
|
||||
def format_tab_title(tab: Tab) -> str:
|
||||
w = 'windows' if tab.num_window_groups > 1 else 'window'
|
||||
return f'{tab.title} [{tab.num_window_groups} {w}]'
|
||||
return f'{tab.name or tab.title} [{tab.num_window_groups} {w}]'
|
||||
|
||||
ct = self.active_tab
|
||||
st = ''
|
||||
@@ -2208,8 +2230,6 @@ class Boss:
|
||||
|
||||
# Can be called with kitty -o "map f1 send_test_notification"
|
||||
def send_test_notification(self) -> None:
|
||||
from time import monotonic
|
||||
|
||||
from .notify import notify
|
||||
now = monotonic()
|
||||
ident = f'test-notify-{now}'
|
||||
@@ -2231,30 +2251,44 @@ 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)
|
||||
|
||||
def open_file(self, path: str) -> None:
|
||||
if path == ":cocoa::application launched::":
|
||||
self.cocoa_application_launched = True
|
||||
return
|
||||
from .open_actions import actions_for_launch
|
||||
def launch_urls(self, *urls: str, no_replace_window: bool = False) -> None:
|
||||
from .launch import force_window_launch
|
||||
actions = list(actions_for_launch(path))
|
||||
from .open_actions import actions_for_launch
|
||||
actions: List[KeyAction] = []
|
||||
failures = []
|
||||
for url in urls:
|
||||
uactions = tuple(actions_for_launch(url))
|
||||
if uactions:
|
||||
actions.extend(uactions)
|
||||
else:
|
||||
failures.append(url)
|
||||
tab = self.active_tab
|
||||
if tab is not None:
|
||||
w = tab.active_window
|
||||
else:
|
||||
w = None
|
||||
needs_window_replaced = not self.cocoa_application_launched or not self.os_window_map and w is not None and w.id == 1
|
||||
needs_window_replaced = False
|
||||
if not no_replace_window and not get_options().startup_session:
|
||||
if w is not None and w.id == 1 and monotonic() - w.started_at < 2 and len(tuple(self.all_windows)) == 1:
|
||||
# first window, soon after startup replace it
|
||||
needs_window_replaced = True
|
||||
|
||||
def clear_initial_window() -> None:
|
||||
if needs_window_replaced and tab is not None and w is not None:
|
||||
tab.remove_window(w)
|
||||
|
||||
if not actions:
|
||||
with force_window_launch(needs_window_replaced):
|
||||
self.launch(kitty_exe(), '+runpy', f'print("The file:", {path!r}, "is of unknown type, cannot open it.");'
|
||||
'from kitty.utils import hold_till_enter; hold_till_enter(); raise SystemExit(1)')
|
||||
if failures:
|
||||
from kittens.tui.operations import styled
|
||||
spec = '\n '.join(styled(u, fg='red') for u in failures)
|
||||
bdata = f"Unknown URL type, cannot open:\n\n {spec}".encode('utf-8')
|
||||
special_window = SpecialWindow([kitty_exe(), '+kitten', 'show_error', '--title', 'Open URL Error'], bdata, 'Open URL Error')
|
||||
if needs_window_replaced and tab is not None:
|
||||
tab.new_special_window(special_window)
|
||||
else:
|
||||
self._new_os_window(special_window)
|
||||
clear_initial_window()
|
||||
else:
|
||||
needs_window_replaced = False
|
||||
if actions:
|
||||
with force_window_launch(needs_window_replaced):
|
||||
self.dispatch_action(actions.pop(0))
|
||||
clear_initial_window()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#version GLSL_VERSION
|
||||
#define WHICH_PROGRAM
|
||||
#define {WHICH_PROGRAM}
|
||||
#define NOT_TRANSPARENT
|
||||
|
||||
#if defined(SIMPLE) || defined(BACKGROUND) || defined(SPECIAL)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#version GLSL_VERSION
|
||||
#define WHICH_PROGRAM
|
||||
#define {WHICH_PROGRAM}
|
||||
#define NOT_TRANSPARENT
|
||||
#define DECORATION_SHIFT {DECORATION_SHIFT}
|
||||
#define REVERSE_SHIFT {REVERSE_SHIFT}
|
||||
@@ -79,8 +79,8 @@ const uint COLOR_MASK = uint(0x4000);
|
||||
const uint ZERO = uint(0);
|
||||
const uint ONE = uint(1);
|
||||
const uint TWO = uint(2);
|
||||
const uint FOUR = uint(4);
|
||||
const uint SEVEN = uint(7);
|
||||
const uint STRIKE_SPRITE_INDEX = uint({STRIKE_SPRITE_INDEX});
|
||||
const uint DECORATION_MASK = uint({DECORATION_MASK});
|
||||
|
||||
vec3 color_to_vec(uint c) {
|
||||
uint r, g, b;
|
||||
@@ -193,8 +193,8 @@ void main() {
|
||||
foreground = choose_color(float(is_selected & ONE), selection_color, foreground);
|
||||
decoration_fg = choose_color(float(is_selected & ONE), selection_color, decoration_fg);
|
||||
// Underline and strike through (rendered via sprites)
|
||||
underline_pos = choose_color(in_url, to_sprite_pos(pos, url_style, ZERO, ZERO), to_sprite_pos(pos, (text_attrs >> DECORATION_SHIFT) & SEVEN, ZERO, ZERO));
|
||||
strike_pos = to_sprite_pos(pos, ((text_attrs >> STRIKE_SHIFT) & ONE) * FOUR, ZERO, ZERO);
|
||||
underline_pos = choose_color(in_url, to_sprite_pos(pos, url_style, ZERO, ZERO), to_sprite_pos(pos, (text_attrs >> DECORATION_SHIFT) & DECORATION_MASK, ZERO, ZERO));
|
||||
strike_pos = to_sprite_pos(pos, ((text_attrs >> STRIKE_SHIFT) & ONE) * STRIKE_SPRITE_INDEX, ZERO, ZERO);
|
||||
|
||||
// Cursor
|
||||
cursor_color_vec = vec4(color_to_vec(cursor_bg), 1.0);
|
||||
|
||||
@@ -553,8 +553,6 @@ pyset_iutf8(ChildMonitor *self, PyObject *args) {
|
||||
#undef INCREF_CHILD
|
||||
#undef DECREF_CHILD
|
||||
|
||||
extern void cocoa_update_menu_bar_title(PyObject*);
|
||||
|
||||
static bool
|
||||
cursor_needs_render(Window *w) {
|
||||
return w->cursor_visible_at_last_render != w->render_data.screen->cursor_render_info.is_visible || w->last_cursor_x != w->render_data.screen->cursor_render_info.x || w->last_cursor_y != w->render_data.screen->cursor_render_info.y || w->last_cursor_shape != w->render_data.screen->cursor_render_info.shape;
|
||||
@@ -592,7 +590,7 @@ change_menubar_title(PyObject *title UNUSED) {
|
||||
static PyObject *current_title = NULL;
|
||||
if (title != current_title) {
|
||||
current_title = title;
|
||||
if (title && OPT(macos_show_window_title_in) & MENUBAR) cocoa_update_menu_bar_title(title);
|
||||
if (title && OPT(macos_show_window_title_in) & MENUBAR) update_menu_bar_title(title);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -1001,21 +999,21 @@ static bool cocoa_pending_actions[NUM_COCOA_PENDING_ACTIONS] = {0};
|
||||
static bool has_cocoa_pending_actions = false;
|
||||
typedef struct {
|
||||
char* wd;
|
||||
char **open_files;
|
||||
size_t open_files_count;
|
||||
size_t open_files_capacity;
|
||||
char **open_urls;
|
||||
size_t open_urls_count;
|
||||
size_t open_urls_capacity;
|
||||
} CocoaPendingActionsData;
|
||||
static CocoaPendingActionsData cocoa_pending_actions_data = {0};
|
||||
|
||||
void
|
||||
set_cocoa_pending_action(CocoaPendingAction action, const char *wd) {
|
||||
if (wd) {
|
||||
if (action == OPEN_FILE) {
|
||||
ensure_space_for(&cocoa_pending_actions_data, open_files, char*, cocoa_pending_actions_data.open_files_count + 8, open_files_capacity, 8, true);
|
||||
cocoa_pending_actions_data.open_files[cocoa_pending_actions_data.open_files_count++] = strdup(wd);
|
||||
set_cocoa_pending_action(CocoaPendingAction action, const char *data) {
|
||||
if (data) {
|
||||
if (action == LAUNCH_URLS) {
|
||||
ensure_space_for(&cocoa_pending_actions_data, open_urls, char*, cocoa_pending_actions_data.open_urls_count + 8, open_urls_capacity, 8, true);
|
||||
cocoa_pending_actions_data.open_urls[cocoa_pending_actions_data.open_urls_count++] = strdup(data);
|
||||
} else {
|
||||
if (cocoa_pending_actions_data.wd) free(cocoa_pending_actions_data.wd);
|
||||
cocoa_pending_actions_data.wd = strdup(wd);
|
||||
cocoa_pending_actions_data.wd = strdup(data);
|
||||
}
|
||||
}
|
||||
cocoa_pending_actions[action] = true;
|
||||
@@ -1038,23 +1036,25 @@ process_cocoa_pending_actions(void) {
|
||||
if (cocoa_pending_actions[NEW_WINDOW]) { call_boss(new_window, NULL); }
|
||||
if (cocoa_pending_actions[CLOSE_WINDOW]) { call_boss(close_window, NULL); }
|
||||
if (cocoa_pending_actions[RESET_TERMINAL]) { call_boss(clear_terminal, "sO", "reset", Py_True ); }
|
||||
if (cocoa_pending_actions[CLEAR_TERMINAL_AND_SCROLLBACK]) { call_boss(clear_terminal, "sO", "to_cursor", Py_True ); }
|
||||
if (cocoa_pending_actions[RELOAD_CONFIG]) { call_boss(load_config_file, NULL); }
|
||||
if (cocoa_pending_actions[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); }
|
||||
free(cocoa_pending_actions_data.wd);
|
||||
cocoa_pending_actions_data.wd = NULL;
|
||||
}
|
||||
if (cocoa_pending_actions_data.open_files_count) {
|
||||
for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.open_files_count; cpa++) {
|
||||
if (cocoa_pending_actions_data.open_files[cpa]) {
|
||||
call_boss(open_file, "s", cocoa_pending_actions_data.open_files[cpa]);
|
||||
free(cocoa_pending_actions_data.open_files[cpa]);
|
||||
cocoa_pending_actions_data.open_files[cpa] = NULL;
|
||||
if (cocoa_pending_actions_data.open_urls_count) {
|
||||
for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.open_urls_count; cpa++) {
|
||||
if (cocoa_pending_actions_data.open_urls[cpa]) {
|
||||
call_boss(launch_urls, "s", cocoa_pending_actions_data.open_urls[cpa]);
|
||||
free(cocoa_pending_actions_data.open_urls[cpa]);
|
||||
cocoa_pending_actions_data.open_urls[cpa] = NULL;
|
||||
}
|
||||
}
|
||||
cocoa_pending_actions_data.open_files_count = 0;
|
||||
cocoa_pending_actions_data.open_urls_count = 0;
|
||||
}
|
||||
memset(cocoa_pending_actions, 0, sizeof(cocoa_pending_actions));
|
||||
has_cocoa_pending_actions = false;
|
||||
@@ -1114,11 +1114,11 @@ main_loop(ChildMonitor *self, PyObject *a UNUSED) {
|
||||
run_main_loop(process_global_state, self);
|
||||
#ifdef __APPLE__
|
||||
if (cocoa_pending_actions_data.wd) { free(cocoa_pending_actions_data.wd); cocoa_pending_actions_data.wd = NULL; }
|
||||
if (cocoa_pending_actions_data.open_files) {
|
||||
for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.open_files_count; cpa++) {
|
||||
if (cocoa_pending_actions_data.open_files[cpa]) free(cocoa_pending_actions_data.open_files[cpa]);
|
||||
if (cocoa_pending_actions_data.open_urls) {
|
||||
for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.open_urls_count; cpa++) {
|
||||
if (cocoa_pending_actions_data.open_urls[cpa]) free(cocoa_pending_actions_data.open_urls[cpa]);
|
||||
}
|
||||
free(cocoa_pending_actions_data.open_files); cocoa_pending_actions_data.open_files = NULL;
|
||||
free(cocoa_pending_actions_data.open_urls); cocoa_pending_actions_data.open_urls = NULL;
|
||||
}
|
||||
#endif
|
||||
if (PyErr_Occurred()) return NULL;
|
||||
|
||||
@@ -196,7 +196,7 @@ class Child:
|
||||
allow_remote_control: bool = False
|
||||
):
|
||||
self.allow_remote_control = allow_remote_control
|
||||
self.argv = argv
|
||||
self.argv = list(argv)
|
||||
if cwd_from is not None:
|
||||
try:
|
||||
cwd = cwd_of_process(cwd_from)
|
||||
@@ -233,7 +233,7 @@ class Child:
|
||||
opts = fast_data_types.get_options()
|
||||
if 'disabled' not in opts.shell_integration:
|
||||
from .shell_integration import modify_shell_environ
|
||||
modify_shell_environ(self.argv[0], opts, env)
|
||||
modify_shell_environ(opts, env, self.argv)
|
||||
env = {k: v for k, v in env.items() if v is not DELETE_ENV_VAR}
|
||||
return env
|
||||
|
||||
@@ -344,8 +344,18 @@ class Child:
|
||||
assert self.child_fd is not None
|
||||
pgrp = os.tcgetpgrp(self.child_fd)
|
||||
foreground_processes = processes_in_group(pgrp) if pgrp >= 0 else []
|
||||
if len(foreground_processes) == 1:
|
||||
return foreground_processes[0]
|
||||
if foreground_processes:
|
||||
# there is no easy way that I know of to know which process is the
|
||||
# foreground process in this group from the users perspective,
|
||||
# so we assume the one with the highest PID is as that is most
|
||||
# likely to be the newest process. This situation can happen
|
||||
# for example with a shell script such as:
|
||||
# #!/bin/bash
|
||||
# cd /tmp
|
||||
# vim
|
||||
# With this script , the foreground process group will contain
|
||||
# both the bash instance running the script and vim.
|
||||
return max(foreground_processes)
|
||||
return self.pid
|
||||
|
||||
@property
|
||||
|
||||
@@ -231,8 +231,10 @@ PENDING(previous_tab, PREVIOUS_TAB)
|
||||
PENDING(new_window, NEW_WINDOW)
|
||||
PENDING(close_window, CLOSE_WINDOW)
|
||||
PENDING(reset_terminal, RESET_TERMINAL)
|
||||
PENDING(clear_terminal_and_scrollback, CLEAR_TERMINAL_AND_SCROLLBACK)
|
||||
PENDING(reload_config, RELOAD_CONFIG)
|
||||
PENDING(toggle_macos_secure_keyboard_entry, TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY)
|
||||
PENDING(toggle_fullscreen, TOGGLE_FULLSCREEN)
|
||||
|
||||
- (void)open_kitty_website_url:(id)sender {
|
||||
(void)sender;
|
||||
@@ -242,6 +244,8 @@ PENDING(toggle_macos_secure_keyboard_entry, TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY)
|
||||
- (BOOL)validateMenuItem:(NSMenuItem *)item {
|
||||
if (item.action == @selector(toggle_macos_secure_keyboard_entry:)) {
|
||||
item.state = [SecureKeyboardEntryController sharedInstance].isDesired ? NSControlStateValueOn : NSControlStateValueOff;
|
||||
} else if (item.action == @selector(toggle_fullscreen:)) {
|
||||
item.title = ([NSApp currentSystemPresentationOptions] & NSApplicationPresentationFullScreen) ? @"Exit Full Screen" : @"Enter Full Screen";
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
@@ -270,8 +274,8 @@ typedef struct {
|
||||
} GlobalShortcut;
|
||||
typedef struct {
|
||||
GlobalShortcut new_os_window, close_os_window, close_tab, edit_config_file, reload_config;
|
||||
GlobalShortcut previous_tab, next_tab, new_tab, new_window, close_window, reset_terminal;
|
||||
GlobalShortcut toggle_macos_secure_keyboard_entry;
|
||||
GlobalShortcut previous_tab, next_tab, new_tab, new_window, close_window, reset_terminal, clear_terminal_and_scrollback;
|
||||
GlobalShortcut toggle_macos_secure_keyboard_entry, toggle_fullscreen;
|
||||
} GlobalShortcuts;
|
||||
static GlobalShortcuts global_shortcuts;
|
||||
|
||||
@@ -285,8 +289,8 @@ cocoa_set_global_shortcut(PyObject *self UNUSED, PyObject *args) {
|
||||
#define Q(x) if (strcmp(name, #x) == 0) gs = &global_shortcuts.x
|
||||
Q(new_os_window); else Q(close_os_window); else Q(close_tab); else Q(edit_config_file);
|
||||
else Q(new_tab); else Q(next_tab); else Q(previous_tab);
|
||||
else Q(new_window); else Q(close_window); else Q(reset_terminal); else Q(reload_config);
|
||||
else Q(toggle_macos_secure_keyboard_entry);
|
||||
else Q(new_window); else Q(close_window); else Q(reset_terminal); else Q(clear_terminal_and_scrollback); else Q(reload_config);
|
||||
else Q(toggle_macos_secure_keyboard_entry); else Q(toggle_fullscreen);
|
||||
#undef Q
|
||||
if (gs == NULL) { PyErr_SetString(PyExc_KeyError, "Unknown shortcut name"); return NULL; }
|
||||
int cocoa_mods;
|
||||
@@ -519,6 +523,19 @@ cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)openFileURLs:(NSPasteboard*)pasteboard
|
||||
userData:(NSString *) UNUSED userData error:(NSError **) UNUSED error {
|
||||
NSDictionary *options = @{ NSPasteboardURLReadingFileURLsOnlyKey: @YES };
|
||||
NSArray *urlArray = [pasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]] options:options];
|
||||
for (NSURL *url in urlArray) {
|
||||
NSString *path = [url path];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
|
||||
set_cocoa_pending_action(LAUNCH_URLS, [[[NSURL fileURLWithPath:path] absoluteString] UTF8String]);
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
// global menu {{{
|
||||
@@ -593,6 +610,7 @@ cocoa_create_global_menu(void) {
|
||||
MENU_ITEM(shellMenu, @"Close Window", close_window);
|
||||
[shellMenu addItem:[NSMenuItem separatorItem]];
|
||||
MENU_ITEM(shellMenu, @"Reset", reset_terminal);
|
||||
MENU_ITEM(shellMenu, @"Clear to Cursor Line", clear_terminal_and_scrollback);
|
||||
[shellMenu release];
|
||||
|
||||
NSMenuItem* windowMenuItem =
|
||||
@@ -621,10 +639,7 @@ cocoa_create_global_menu(void) {
|
||||
keyEquivalent:@""] setTarget:global_menu_target];
|
||||
|
||||
[windowMenu addItem:[NSMenuItem separatorItem]];
|
||||
[[windowMenu addItemWithTitle:@"Enter Full Screen"
|
||||
action:@selector(toggleFullScreen:)
|
||||
keyEquivalent:@"f"]
|
||||
setKeyEquivalentModifierMask:NSEventModifierFlagControl | NSEventModifierFlagCommand];
|
||||
MENU_ITEM(windowMenu, @"Enter Full Screen", toggle_fullscreen);
|
||||
[NSApp setWindowsMenu:windowMenu];
|
||||
[windowMenu release];
|
||||
|
||||
@@ -656,7 +671,16 @@ cocoa_create_global_menu(void) {
|
||||
|
||||
void
|
||||
cocoa_update_menu_bar_title(PyObject *pytitle) {
|
||||
NSString *title = @(PyUnicode_AsUTF8(pytitle));
|
||||
NSString *title = nil;
|
||||
if (OPT(macos_menubar_title_max_length) > 0 && PyUnicode_GetLength(pytitle) > OPT(macos_menubar_title_max_length)) {
|
||||
static char fmt[64];
|
||||
snprintf(fmt, sizeof(fmt), "%%%ld.%ldU%%s", OPT(macos_menubar_title_max_length), OPT(macos_menubar_title_max_length));
|
||||
DECREF_AFTER_FUNCTION PyObject *st = PyUnicode_FromFormat(fmt, pytitle, "…");
|
||||
if (st) title = @(PyUnicode_AsUTF8(st));
|
||||
} else {
|
||||
title = @(PyUnicode_AsUTF8(pytitle));
|
||||
}
|
||||
if (!title) return;
|
||||
NSMenu *bar = [NSApp mainMenu];
|
||||
if (title_menu != NULL) {
|
||||
[bar removeItem:title_menu];
|
||||
@@ -814,6 +838,33 @@ cocoa_set_titlebar_color(void *w, color_type titlebar_color)
|
||||
} // autoreleasepool
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
cocoa_set_url_handler(PyObject UNUSED *self, PyObject *args) {
|
||||
@autoreleasepool {
|
||||
|
||||
const char *url_scheme = NULL, *bundle_id = NULL;
|
||||
if (!PyArg_ParseTuple(args, "s|z", &url_scheme, &bundle_id)) return NULL;
|
||||
if (!url_scheme || url_scheme[0] == '\0') {
|
||||
PyErr_SetString(PyExc_TypeError, "Empty url scheme");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
NSString *scheme = [NSString stringWithUTF8String:url_scheme];
|
||||
NSString *identifier = @"";
|
||||
if (!bundle_id) {
|
||||
identifier = [[NSBundle mainBundle] bundleIdentifier];
|
||||
if (!identifier || identifier.length == 0) identifier = @"net.kovidgoyal.kitty";
|
||||
} else if (bundle_id[0] != '\0') {
|
||||
identifier = [NSString stringWithUTF8String:bundle_id];
|
||||
}
|
||||
// This API has been marked as deprecated. It will need to be replaced when a new approach is available.
|
||||
OSStatus err = LSSetDefaultHandlerForURLScheme((CFStringRef)scheme, (CFStringRef)identifier);
|
||||
if (err == noErr) Py_RETURN_NONE;
|
||||
PyErr_Format(PyExc_OSError, "Failed to set default handler with error code: %d", err);
|
||||
return NULL;
|
||||
} // autoreleasepool
|
||||
}
|
||||
|
||||
static NSSound *beep_sound = nil;
|
||||
|
||||
static void
|
||||
@@ -863,6 +914,7 @@ static PyMethodDef module_methods[] = {
|
||||
{"cocoa_set_global_shortcut", (PyCFunction)cocoa_set_global_shortcut, METH_VARARGS, ""},
|
||||
{"cocoa_send_notification", (PyCFunction)cocoa_send_notification, METH_VARARGS, ""},
|
||||
{"cocoa_set_notification_activated_callback", (PyCFunction)set_notification_activated_callback, METH_O, ""},
|
||||
{"cocoa_set_url_handler", (PyCFunction)cocoa_set_url_handler, METH_VARARGS, ""},
|
||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
|
||||
@@ -120,6 +120,8 @@ def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
|
||||
tc_imports.add((func.__module__, func.__name__))
|
||||
|
||||
defval = repr(func(option.defval_as_string))
|
||||
if len(defval) > 100:
|
||||
defval += ' # noqa'
|
||||
if option.macos_defval is not unset:
|
||||
md = repr(func(option.macos_defval))
|
||||
defval = f'{md} if is_macos else {defval}'
|
||||
|
||||
@@ -53,6 +53,7 @@ def ref_map() -> Dict[str, str]:
|
||||
'sessions': f'{website_url("overview")}#startup-sessions',
|
||||
'functional': f'{website_url("keyboard-protocol")}#functional-key-definitions',
|
||||
'shell_integration': website_url("shell-integration"),
|
||||
'github_discussions': 'https://github.com/kovidgoyal/kitty/discussions',
|
||||
}
|
||||
for actions in get_all_actions().values():
|
||||
for ac in actions:
|
||||
@@ -66,6 +67,14 @@ def remove_markup(text: str) -> str:
|
||||
if m.group(1) == 'ref':
|
||||
q = m.group(2).split('<')[-1].rstrip('>')
|
||||
return ref_map()[q]
|
||||
if m.group(1) == 'ac':
|
||||
q = m.group(2).split('<')[-1].rstrip('>')
|
||||
return ref_map()[f'action-{q}']
|
||||
if m.group(1) == 'disc':
|
||||
parts = m.group(2).split('<')
|
||||
t = parts[0].strip()
|
||||
q = parts[-1].rstrip('>')
|
||||
return f'{t} {ref_map()["github_discussions"]}/{q}'
|
||||
return str(m.group(2))
|
||||
|
||||
return re.sub(r':([a-zA-Z0-9]+):`(.+?)`', sub, text, flags=re.DOTALL)
|
||||
|
||||
@@ -22,7 +22,7 @@ class Version(NamedTuple):
|
||||
|
||||
appname: str = 'kitty'
|
||||
kitty_face = '🐱'
|
||||
version: Version = Version(0, 24, 2)
|
||||
version: Version = Version(0, 24, 4)
|
||||
str_version: str = '.'.join(map(str, version))
|
||||
_plat = sys.platform.lower()
|
||||
is_macos: bool = 'darwin' in _plat
|
||||
|
||||
@@ -302,6 +302,8 @@ PyInit_fast_data_types(void) {
|
||||
s(STRIKETHROUGH, strike); s(DIM, dim); s(DECORATION, decoration);
|
||||
#undef s
|
||||
PyModule_AddIntConstant(m, "MARK_MASK", MARK_MASK);
|
||||
PyModule_AddIntConstant(m, "DECORATION_MASK", DECORATION_MASK);
|
||||
PyModule_AddIntConstant(m, "NUM_UNDERLINE_STYLES", NUM_UNDERLINE_STYLES);
|
||||
PyModule_AddStringMacro(m, ERROR_PREFIX);
|
||||
#ifdef KITTY_VCS_REV
|
||||
PyModule_AddStringMacro(m, KITTY_VCS_REV);
|
||||
|
||||
@@ -154,6 +154,8 @@ typedef union CellAttrs {
|
||||
} CellAttrs;
|
||||
#define MARK_MASK (3u)
|
||||
#define WIDTH_MASK (3u)
|
||||
#define DECORATION_MASK (7u)
|
||||
#define NUM_UNDERLINE_STYLES (5u)
|
||||
#define SGR_MASK (~(((CellAttrs){.width=WIDTH_MASK, .mark=MARK_MASK}).val))
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -215,6 +215,19 @@ def debug_config(opts: KittyOpts) -> str:
|
||||
p(' ', '\n '.join(opts.config_overrides))
|
||||
compare_opts(opts, p)
|
||||
p()
|
||||
p(green('Environment variable names seen by the kitty process:'))
|
||||
p('\t' + '\n\t'.join(sorted(os.environ)))
|
||||
p(green('Important environment variables seen by the kitty process:'))
|
||||
|
||||
def penv(k: str) -> None:
|
||||
v = os.environ.get(k)
|
||||
if v is not None:
|
||||
p('\t' + k.ljust(35), styled(v, dim=True))
|
||||
|
||||
for k in (
|
||||
'PATH LANG KITTY_CONFIG_DIRECTORY KITTY_CACHE_DIRECTORY VISUAL EDITOR SHELL'
|
||||
' GLFW_IM_MODULE KITTY_WAYLAND_DETECT_MODIFIERS DISPLAY WAYLAND_DISPLAY USER XCURSOR_SIZE'
|
||||
).split():
|
||||
penv(k)
|
||||
for k in os.environ:
|
||||
if k.startswith('LC_') or k.startswith('XDG_'):
|
||||
penv(k)
|
||||
return out.getvalue()
|
||||
|
||||
@@ -154,6 +154,7 @@ play_canberra_sound(const char *which_sound, const char *event_id, bool is_path,
|
||||
which_type, which_sound,
|
||||
"event.description", event_id,
|
||||
"media.role", media_role,
|
||||
"canberra.cache-control", "permanent",
|
||||
NULL
|
||||
);
|
||||
}
|
||||
|
||||
@@ -256,6 +256,8 @@ GRAPHICS_PREMULT_PROGRAM: int
|
||||
GRAPHICS_PROGRAM: int
|
||||
MARK: int
|
||||
MARK_MASK: int
|
||||
DECORATION_MASK: int
|
||||
NUM_UNDERLINE_STYLES: int
|
||||
OSC: int
|
||||
FILE_TRANSFER_CODE: int
|
||||
REVERSE: int
|
||||
@@ -736,6 +738,10 @@ def cocoa_get_lang() -> Optional[str]:
|
||||
pass
|
||||
|
||||
|
||||
def cocoa_set_url_handler(url_scheme: str, bundle_id: Optional[str]) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def locale_is_valid(name: str) -> bool:
|
||||
pass
|
||||
|
||||
@@ -961,7 +967,8 @@ def set_font_data(
|
||||
descriptor_for_idx: Callable[[int], Tuple[FontObject, bool, bool]],
|
||||
bold: int, italic: int, bold_italic: int, num_symbol_fonts: int,
|
||||
symbol_maps: Tuple[Tuple[int, int, int], ...], font_sz_in_pts: float,
|
||||
font_feature_settings: Dict[str, Tuple[FontFeature, ...]]
|
||||
font_feature_settings: Dict[str, Tuple[FontFeature, ...]],
|
||||
narrow_symbols: Tuple[Tuple[int, int, int], ...],
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@@ -980,7 +987,7 @@ class HistoryBuf:
|
||||
def as_text(self, callback: Callable[[str], None], as_ansi: bool, insert_wrap_markers: bool) -> None:
|
||||
pass
|
||||
|
||||
def pagerhist_as_text(self) -> str:
|
||||
def pagerhist_as_text(self, upto_output_start: bool = False) -> str:
|
||||
pass
|
||||
|
||||
def pagerhist_as_bytes(self) -> bytes:
|
||||
@@ -1049,6 +1056,9 @@ class Screen:
|
||||
def line(self, num: int) -> Line:
|
||||
pass
|
||||
|
||||
def visual_line(self, num: int) -> Line:
|
||||
pass
|
||||
|
||||
def draw(self, text: str) -> None:
|
||||
pass
|
||||
|
||||
@@ -1076,7 +1086,7 @@ class Screen:
|
||||
def has_selection(self) -> bool:
|
||||
pass
|
||||
|
||||
def text_for_selection(self) -> Tuple[str, ...]:
|
||||
def text_for_selection(self, ansi: bool, strip_trailing_spaces: bool) -> Tuple[str, ...]:
|
||||
pass
|
||||
|
||||
def is_rectangle_select(self) -> bool:
|
||||
@@ -1103,6 +1113,9 @@ class Screen:
|
||||
def reverse_scroll(self, amt: int, fill_from_scrollback: bool = False) -> bool:
|
||||
pass
|
||||
|
||||
def scroll_prompt_to_bottom(self) -> None:
|
||||
pass
|
||||
|
||||
def clear_selection(self) -> None:
|
||||
pass
|
||||
|
||||
@@ -1124,10 +1137,10 @@ class Screen:
|
||||
as_text_non_visual = as_text
|
||||
as_text_alternate = as_text
|
||||
|
||||
def cmd_output(self, which: int, callback: Callable[[str], None], as_ansi: bool, insert_wrap_markers: bool) -> None:
|
||||
def cmd_output(self, which: int, callback: Callable[[str], None], as_ansi: bool, insert_wrap_markers: bool) -> bool:
|
||||
pass
|
||||
|
||||
def scroll_until_cursor(self) -> None:
|
||||
def scroll_until_cursor_prompt(self) -> None:
|
||||
pass
|
||||
|
||||
def reset(self) -> None:
|
||||
@@ -1136,6 +1149,9 @@ class Screen:
|
||||
def erase_in_display(self, how: int = 0, private: bool = False) -> None:
|
||||
pass
|
||||
|
||||
def clear_scrollback(self) -> None:
|
||||
pass
|
||||
|
||||
def focus_changed(self, focused: bool) -> bool:
|
||||
pass
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#include "charsets.h"
|
||||
#include "glyph-cache.h"
|
||||
|
||||
#define MISSING_GLYPH 4
|
||||
#define MISSING_GLYPH (NUM_UNDERLINE_STYLES + 2)
|
||||
#define MAX_NUM_EXTRA_GLYPHS_PUA 4u
|
||||
|
||||
typedef void (*send_sprite_to_gpu_func)(FONTS_DATA_HANDLE fg, unsigned int, unsigned int, unsigned int, pixel*);
|
||||
@@ -50,8 +50,8 @@ typedef struct {
|
||||
size_t font_idx;
|
||||
} SymbolMap;
|
||||
|
||||
static SymbolMap *symbol_maps = NULL;
|
||||
static size_t num_symbol_maps = 0;
|
||||
static SymbolMap *symbol_maps = NULL, *narrow_symbols = NULL;
|
||||
static size_t num_symbol_maps = 0, num_narrow_symbols = 0;
|
||||
|
||||
typedef enum { SPACER_STRATEGY_UNKNOWN, SPACERS_BEFORE, SPACERS_AFTER, SPACERS_IOSEVKA } SpacerStrategy;
|
||||
|
||||
@@ -492,8 +492,8 @@ in_symbol_maps(FontGroup *fg, char_type ch) {
|
||||
// - BOX_FONT
|
||||
// - an index in the fonts list
|
||||
static ssize_t
|
||||
font_for_cell(FontGroup *fg, CPUCell *cpu_cell, GPUCell *gpu_cell, bool *is_fallback_font, bool *is_emoji_presentation) {
|
||||
*is_fallback_font = false;
|
||||
font_for_cell(FontGroup *fg, CPUCell *cpu_cell, GPUCell *gpu_cell, bool *is_main_font, bool *is_emoji_presentation) {
|
||||
*is_main_font = false;
|
||||
*is_emoji_presentation = false;
|
||||
START_ALLOW_CASE_RANGE
|
||||
ssize_t ans;
|
||||
@@ -510,6 +510,7 @@ START_ALLOW_CASE_RANGE
|
||||
case 0x1fba0 ... 0x1fbae:
|
||||
return BOX_FONT;
|
||||
default:
|
||||
*is_emoji_presentation = has_emoji_presentation(cpu_cell, gpu_cell);
|
||||
ans = in_symbol_maps(fg, cpu_cell->ch);
|
||||
if (ans > -1) return ans;
|
||||
switch(gpu_cell->attrs.bold | (gpu_cell->attrs.italic << 1)) {
|
||||
@@ -523,9 +524,7 @@ START_ALLOW_CASE_RANGE
|
||||
ans = fg->bi_font_idx; break;
|
||||
}
|
||||
if (ans < 0) ans = fg->medium_font_idx;
|
||||
*is_emoji_presentation = has_emoji_presentation(cpu_cell, gpu_cell);
|
||||
if (!*is_emoji_presentation && has_cell_text(fg->fonts + ans, cpu_cell)) return ans;
|
||||
*is_fallback_font = true;
|
||||
if (!*is_emoji_presentation && has_cell_text(fg->fonts + ans, cpu_cell)) { *is_main_font = true; return ans; }
|
||||
return fallback_font(fg, cpu_cell, gpu_cell);
|
||||
}
|
||||
END_ALLOW_CASE_RANGE
|
||||
@@ -852,6 +851,19 @@ detect_spacer_strategy(hb_font_t *hbf, Font *font) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If spacer_strategy is still default, check ### glyph to to confirm strategy
|
||||
// https://github.com/kovidgoyal/kitty/issues/4721
|
||||
if (font->spacer_strategy == SPACERS_BEFORE) {
|
||||
cpu_cells[0].ch = '#'; cpu_cells[1].ch = '#'; cpu_cells[2].ch = '#';
|
||||
shape(cpu_cells, gpu_cells, arraysz(cpu_cells), hbf, font, false);
|
||||
if (G(num_glyphs) > 1) {
|
||||
glyph_index glyph_id = G(info)[G(num_glyphs) - 1].codepoint;
|
||||
bool is_special = is_special_glyph(glyph_id, font, &G(current_cell_data));
|
||||
bool is_empty = is_special && is_empty_glyph(glyph_id, font);
|
||||
if (is_empty) font->spacer_strategy = SPACERS_AFTER;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static LigatureType
|
||||
@@ -963,7 +975,7 @@ group_normal(Font *font, hb_font_t *hbf) {
|
||||
*
|
||||
* We rely on the cluster numbers from harfbuzz to tell us how many unicode codepoints a glyph corresponds to.
|
||||
* Then we check if the glyph is a ligature glyph (is_special_glyph) and if it is an empty glyph.
|
||||
* We detect if the font uses EMPTY glyphs before or after ligature glyphs (1. or 3. above) by checking what it does for ===.
|
||||
* We detect if the font uses EMPTY glyphs before or after ligature glyphs (1. or 3. above) by checking what it does for === and ###.
|
||||
* Finally we look at the glyph name. These five datapoints give us enough information to satisfy the constraint above,
|
||||
* for a wide variety of fonts.
|
||||
*/
|
||||
@@ -1220,6 +1232,17 @@ is_non_emoji_dingbat(char_type ch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static unsigned int
|
||||
cell_cap_for_codepoint(const char_type cp) {
|
||||
unsigned int ans = UINT_MAX;
|
||||
for (size_t i = 0; i < num_narrow_symbols; i++) {
|
||||
SymbolMap *sm = narrow_symbols + i;
|
||||
if (sm->left <= cp && cp <= sm->right) ans = sm->font_idx;
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
render_line(FONTS_DATA_HANDLE fg_, Line *line, index_type lnum, Cursor *cursor, DisableLigature disable_ligature_strategy) {
|
||||
#define RENDER if (run_font_idx != NO_FONT && i > first_cell_in_run) { \
|
||||
@@ -1237,12 +1260,12 @@ render_line(FONTS_DATA_HANDLE fg_, Line *line, index_type lnum, Cursor *cursor,
|
||||
if (prev_width == 2) { prev_width = 0; continue; }
|
||||
CPUCell *cpu_cell = line->cpu_cells + i;
|
||||
GPUCell *gpu_cell = line->gpu_cells + i;
|
||||
bool is_fallback_font, is_emoji_presentation;
|
||||
ssize_t cell_font_idx = font_for_cell(fg, cpu_cell, gpu_cell, &is_fallback_font, &is_emoji_presentation);
|
||||
bool is_main_font, is_emoji_presentation;
|
||||
ssize_t cell_font_idx = font_for_cell(fg, cpu_cell, gpu_cell, &is_main_font, &is_emoji_presentation);
|
||||
|
||||
if (
|
||||
cell_font_idx != MISSING_FONT &&
|
||||
((is_fallback_font && !is_emoji_presentation && is_symbol(cpu_cell->ch)) || (cell_font_idx != BOX_FONT && (is_private_use(cpu_cell->ch))) || is_non_emoji_dingbat(cpu_cell->ch))
|
||||
((!is_main_font && !is_emoji_presentation && is_symbol(cpu_cell->ch)) || (cell_font_idx != BOX_FONT && (is_private_use(cpu_cell->ch))) || is_non_emoji_dingbat(cpu_cell->ch))
|
||||
) {
|
||||
unsigned int desired_cells = 1;
|
||||
if (cell_font_idx > 0) {
|
||||
@@ -1252,6 +1275,7 @@ render_line(FONTS_DATA_HANDLE fg_, Line *line, index_type lnum, Cursor *cursor,
|
||||
int width = get_glyph_width(font->face, glyph_id);
|
||||
desired_cells = (unsigned int)ceilf((float)width / fg->cell_width);
|
||||
}
|
||||
desired_cells = MIN(desired_cells, cell_cap_for_codepoint(cpu_cell->ch));
|
||||
|
||||
unsigned int num_spaces = 0;
|
||||
while (
|
||||
@@ -1304,6 +1328,7 @@ render_simple_text(FONTS_DATA_HANDLE fg_, const char *text) {
|
||||
static void
|
||||
clear_symbol_maps(void) {
|
||||
if (symbol_maps) { free(symbol_maps); symbol_maps = NULL; num_symbol_maps = 0; }
|
||||
if (narrow_symbols) { free(narrow_symbols); narrow_symbols = NULL; num_narrow_symbols = 0; }
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
@@ -1312,26 +1337,33 @@ typedef struct {
|
||||
|
||||
DescriptorIndices descriptor_indices = {0};
|
||||
|
||||
static PyObject*
|
||||
set_font_data(PyObject UNUSED *m, PyObject *args) {
|
||||
PyObject *sm;
|
||||
Py_CLEAR(box_drawing_function); Py_CLEAR(prerender_function); Py_CLEAR(descriptor_for_idx); Py_CLEAR(font_feature_settings);
|
||||
if (!PyArg_ParseTuple(args, "OOOIIIIO!dO",
|
||||
&box_drawing_function, &prerender_function, &descriptor_for_idx,
|
||||
&descriptor_indices.bold, &descriptor_indices.italic, &descriptor_indices.bi, &descriptor_indices.num_symbol_fonts,
|
||||
&PyTuple_Type, &sm, &OPT(font_size), &font_feature_settings)) return NULL;
|
||||
Py_INCREF(box_drawing_function); Py_INCREF(prerender_function); Py_INCREF(descriptor_for_idx); Py_INCREF(font_feature_settings);
|
||||
free_font_groups();
|
||||
clear_symbol_maps();
|
||||
num_symbol_maps = PyTuple_GET_SIZE(sm);
|
||||
symbol_maps = calloc(num_symbol_maps, sizeof(SymbolMap));
|
||||
if (symbol_maps == NULL) return PyErr_NoMemory();
|
||||
for (size_t s = 0; s < num_symbol_maps; s++) {
|
||||
static bool
|
||||
set_symbol_maps(SymbolMap **maps, size_t *num, const PyObject *sm) {
|
||||
*num = PyTuple_GET_SIZE(sm);
|
||||
*maps = calloc(*num, sizeof(SymbolMap));
|
||||
if (*maps == NULL) { PyErr_NoMemory(); return false; }
|
||||
for (size_t s = 0; s < *num; s++) {
|
||||
unsigned int left, right, font_idx;
|
||||
SymbolMap *x = symbol_maps + s;
|
||||
SymbolMap *x = *maps + s;
|
||||
if (!PyArg_ParseTuple(PyTuple_GET_ITEM(sm, s), "III", &left, &right, &font_idx)) return NULL;
|
||||
x->left = left; x->right = right; x->font_idx = font_idx;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
set_font_data(PyObject UNUSED *m, PyObject *args) {
|
||||
PyObject *sm, *ns;
|
||||
Py_CLEAR(box_drawing_function); Py_CLEAR(prerender_function); Py_CLEAR(descriptor_for_idx); Py_CLEAR(font_feature_settings);
|
||||
if (!PyArg_ParseTuple(args, "OOOIIIIO!dOO!",
|
||||
&box_drawing_function, &prerender_function, &descriptor_for_idx,
|
||||
&descriptor_indices.bold, &descriptor_indices.italic, &descriptor_indices.bi, &descriptor_indices.num_symbol_fonts,
|
||||
&PyTuple_Type, &sm, &OPT(font_size), &font_feature_settings, &PyTuple_Type, &ns)) return NULL;
|
||||
Py_INCREF(box_drawing_function); Py_INCREF(prerender_function); Py_INCREF(descriptor_for_idx); Py_INCREF(font_feature_settings);
|
||||
free_font_groups();
|
||||
clear_symbol_maps();
|
||||
set_symbol_maps(&symbol_maps, &num_symbol_maps, sm);
|
||||
set_symbol_maps(&narrow_symbols, &num_narrow_symbols, ns);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
@@ -1398,6 +1430,11 @@ initialize_font_group(FontGroup *fg) {
|
||||
}
|
||||
#undef I
|
||||
calc_cell_metrics(fg);
|
||||
// rescale the symbol_map faces for the desired cell height, this is how fallback fonts are sized as well
|
||||
for (size_t i = 0; i < descriptor_indices.num_symbol_fonts; i++) {
|
||||
Font *font = fg->fonts + i + fg->first_symbol_font_idx;
|
||||
set_size_for_face(font->face, fg->cell_height, true, (FONTS_DATA_HANDLE)fg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -115,8 +115,13 @@ def find_best_match(family: str, bold: bool = False, italic: bool = False, monos
|
||||
return candidates[0]
|
||||
|
||||
# Use fc-match to see if we can find a monospaced font that matches family
|
||||
for spacing in (FC_MONO, FC_DUAL):
|
||||
possibility = fc_match(family, False, False, spacing)
|
||||
# When aliases are defined, spacing can cause the incorrect font to be
|
||||
# returned, so check with and without spacing and use the one that matches.
|
||||
mono_possibility = fc_match(family, False, False, FC_MONO)
|
||||
dual_possibility = fc_match(family, False, False, FC_DUAL)
|
||||
any_possibility = fc_match(family, False, False, 0)
|
||||
tries = (dual_possibility, mono_possibility) if any_possibility == dual_possibility else (mono_possibility, dual_possibility)
|
||||
for possibility in tries:
|
||||
for key, map_key in (('postscript_name', 'ps_map'), ('full_name', 'full_map'), ('family', 'family_map')):
|
||||
val: Optional[str] = cast(Optional[str], possibility.get(key))
|
||||
if val:
|
||||
|
||||
@@ -14,13 +14,14 @@ from kitty.constants import is_macos
|
||||
from kitty.fast_data_types import (
|
||||
Screen, create_test_font_group, get_fallback_font, set_font_data,
|
||||
set_options, set_send_sprite_to_gpu, sprite_map_set_limits,
|
||||
test_render_line, test_shape
|
||||
test_render_line, test_shape, NUM_UNDERLINE_STYLES
|
||||
)
|
||||
from kitty.fonts.box_drawing import (
|
||||
BufType, distribute_dots, render_box_char, render_missing_glyph
|
||||
)
|
||||
from kitty.options.types import Options, defaults
|
||||
from kitty.typing import CoreTextFont, FontConfigPattern
|
||||
from kitty.types import _T
|
||||
from kitty.utils import log_error
|
||||
|
||||
if is_macos:
|
||||
@@ -50,10 +51,9 @@ def font_for_family(family: str) -> Tuple[FontObject, bool, bool]:
|
||||
return font_for_family_fontconfig(family)
|
||||
|
||||
|
||||
Range = Tuple[Tuple[int, int], str]
|
||||
|
||||
|
||||
def merge_ranges(a: Range, b: Range, priority_map: Dict[Tuple[int, int], int]) -> Generator[Range, None, None]:
|
||||
def merge_ranges(
|
||||
a: Tuple[Tuple[int, int], _T], b: Tuple[Tuple[int, int], _T], priority_map: Dict[Tuple[int, int], int]
|
||||
) -> Generator[Tuple[Tuple[int, int], _T], None, None]:
|
||||
a_start, a_end = a[0]
|
||||
b_start, b_end = b[0]
|
||||
a_val, b_val = a[1], b[1]
|
||||
@@ -95,7 +95,7 @@ def merge_ranges(a: Range, b: Range, priority_map: Dict[Tuple[int, int], int]) -
|
||||
after_range = ((b_end + 1, a_end), a_val)
|
||||
after_range_prio = a_prio
|
||||
# check if the before, mid and after ranges can be coalesced
|
||||
ranges: List[Range] = []
|
||||
ranges: List[Tuple[Tuple[int, int], _T]] = []
|
||||
priorities: List[int] = []
|
||||
for rq, prio in ((before_range, before_range_prio), (mid_range, mid_range_prio), (after_range, after_range_prio)):
|
||||
if rq is None:
|
||||
@@ -117,7 +117,7 @@ def merge_ranges(a: Range, b: Range, priority_map: Dict[Tuple[int, int], int]) -
|
||||
yield from ranges
|
||||
|
||||
|
||||
def coalesce_symbol_maps(maps: Dict[Tuple[int, int], str]) -> Dict[Tuple[int, int], str]:
|
||||
def coalesce_symbol_maps(maps: Dict[Tuple[int, int], _T]) -> Dict[Tuple[int, int], _T]:
|
||||
if not maps:
|
||||
return maps
|
||||
priority_map = {r: i for i, r in enumerate(maps.keys())}
|
||||
@@ -155,6 +155,10 @@ def create_symbol_map(opts: Options) -> Tuple[Tuple[int, int, int], ...]:
|
||||
return sm
|
||||
|
||||
|
||||
def create_narrow_symbols(opts: Options) -> Tuple[Tuple[int, int, int], ...]:
|
||||
return tuple((a, b, v) for (a, b), v in coalesce_symbol_maps(opts.narrow_symbols).items())
|
||||
|
||||
|
||||
def descriptor_for_idx(idx: int) -> Tuple[FontObject, bool, bool]:
|
||||
return current_faces[idx]
|
||||
|
||||
@@ -193,6 +197,7 @@ def set_font_family(opts: Optional[Options] = None, override_font_size: Optional
|
||||
current_faces.append((font_map[k], 'b' in k, 'i' in k))
|
||||
before = len(current_faces)
|
||||
sm = create_symbol_map(opts)
|
||||
ns = create_narrow_symbols(opts)
|
||||
num_symbol_fonts = len(current_faces) - before
|
||||
font_features = {}
|
||||
for face, _, _ in current_faces:
|
||||
@@ -203,7 +208,7 @@ def set_font_family(opts: Optional[Options] = None, override_font_size: Optional
|
||||
set_font_data(
|
||||
render_box_drawing, prerender_function, descriptor_for_idx,
|
||||
indices['bold'], indices['italic'], indices['bi'], num_symbol_fonts,
|
||||
sm, sz, font_features
|
||||
sm, sz, font_features, ns
|
||||
)
|
||||
|
||||
|
||||
@@ -400,8 +405,15 @@ def prerender_function(
|
||||
render_cursor, cursor_beam_thickness=cursor_beam_thickness,
|
||||
cursor_underline_thickness=cursor_underline_thickness, cell_width=cell_width,
|
||||
cell_height=cell_height, dpi_x=dpi_x, dpi_y=dpi_y)
|
||||
cells = f(1), f(2), f(3), f(4), f(5), f(0, strikethrough=True), f(missing=True), c(1), c(2), c(3)
|
||||
return tuple(map(ctypes.addressof, cells)), cells
|
||||
# If you change the mapping of these cells you will need to change
|
||||
# NUM_UNDERLINE_STYLES and BEAM_IDX in shader.c and STRIKE_SPRITE_INDEX in
|
||||
# window.py and MISSING_GLYPH in font.c
|
||||
cells = list(map(f, range(1, NUM_UNDERLINE_STYLES + 1))) # underline sprites
|
||||
cells.append(f(0, strikethrough=True)) # strikethrough sprite
|
||||
cells.append(f(missing=True)) # missing glyph
|
||||
cells.extend((c(1), c(2), c(3))) # cursor glyphs
|
||||
tcells = tuple(cells)
|
||||
return tuple(map(ctypes.addressof, tcells)), tcells
|
||||
|
||||
|
||||
def render_box_drawing(codepoint: int, cell_width: int, cell_height: int, dpi: float) -> Tuple[int, CBufType]:
|
||||
|
||||
@@ -143,11 +143,6 @@ static bool
|
||||
set_font_size(Face *self, FT_F26Dot6 char_width, FT_F26Dot6 char_height, FT_UInt xdpi, FT_UInt ydpi, unsigned int desired_height, unsigned int cell_height) {
|
||||
int error = FT_Set_Char_Size(self->face, 0, char_height, xdpi, ydpi);
|
||||
if (!error) {
|
||||
unsigned int ch = calc_cell_height(self, false);
|
||||
if (desired_height && ch != desired_height) {
|
||||
FT_F26Dot6 h = (FT_F26Dot6)floor((double)char_height * (double)desired_height / (double) ch);
|
||||
return set_font_size(self, 0, h, xdpi, ydpi, 0, cell_height);
|
||||
}
|
||||
self->char_width = char_width; self->char_height = char_height; self->xdpi = xdpi; self->ydpi = ydpi;
|
||||
if (self->harfbuzz_font != NULL) hb_ft_font_changed(self->harfbuzz_font);
|
||||
} else {
|
||||
@@ -522,19 +517,20 @@ detect_right_edge(ProcessedBitmap *ans) {
|
||||
static bool
|
||||
render_color_bitmap(Face *self, int glyph_id, ProcessedBitmap *ans, unsigned int cell_width, unsigned int cell_height, unsigned int num_cells, unsigned int baseline UNUSED) {
|
||||
unsigned short best = 0, diff = USHRT_MAX;
|
||||
const short limit = self->face->num_fixed_sizes;
|
||||
for (short i = 0; i < limit; i++) {
|
||||
unsigned short w = self->face->available_sizes[i].width;
|
||||
unsigned short d = w > (unsigned short)cell_width ? w - (unsigned short)cell_width : (unsigned short)cell_width - w;
|
||||
if (d < diff) {
|
||||
diff = d;
|
||||
best = i;
|
||||
if (self->face->num_fixed_sizes > 0) {
|
||||
const short limit = self->face->num_fixed_sizes;
|
||||
for (short i = 0; i < limit; i++) {
|
||||
unsigned short w = self->face->available_sizes[i].width;
|
||||
unsigned short d = w > (unsigned short)cell_width ? w - (unsigned short)cell_width : (unsigned short)cell_width - w;
|
||||
if (d < diff) {
|
||||
diff = d;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
FT_Error error = FT_Select_Size(self->face, best);
|
||||
if (error) { set_freetype_error("Failed to set char size for non-scalable font, with error:", error); return false; }
|
||||
if (!load_glyph(self, glyph_id, FT_LOAD_COLOR)) return false;
|
||||
FT_Set_Char_Size(self->face, 0, self->char_height, self->xdpi, self->ydpi);
|
||||
FT_Error error = FT_Select_Size(self->face, best);
|
||||
if (error) { set_freetype_error("Failed to set char size for non-scalable font, with error:", error); return false; }
|
||||
} else FT_Set_Char_Size(self->face, 0, self->char_height, self->xdpi, self->ydpi);
|
||||
if (!load_glyph(self, glyph_id, FT_LOAD_COLOR | FT_LOAD_RENDER)) return false;
|
||||
FT_Bitmap *bitmap = &self->face->glyph->bitmap;
|
||||
if (bitmap->pixel_mode != FT_PIXEL_MODE_BGRA) return false;
|
||||
ans->buf = bitmap->buffer;
|
||||
|
||||
@@ -134,7 +134,7 @@ choose_bitmap_size(FT_Face face, FT_UInt desired_height) {
|
||||
static void
|
||||
set_pixel_size(RenderCtx *ctx, Face *face, FT_UInt sz, bool get_metrics UNUSED) {
|
||||
if (sz != face->pixel_size) {
|
||||
if (FT_HAS_COLOR(face->freetype)) sz = choose_bitmap_size(face->freetype, font_units_to_pixels_y(main_face.freetype, main_face.freetype->height));
|
||||
if (face->freetype->num_fixed_sizes > 0 && FT_HAS_COLOR(face->freetype)) choose_bitmap_size(face->freetype, font_units_to_pixels_y(main_face.freetype, main_face.freetype->height));
|
||||
else FT_Set_Pixel_Sizes(face->freetype, sz, sz);
|
||||
hb_ft_font_changed(face->hb);
|
||||
hb_ft_font_set_load_flags(face->hb, get_load_flags(face->hinting, face->hintstyle, FT_LOAD_DEFAULT));
|
||||
@@ -304,7 +304,7 @@ render_run(RenderCtx *ctx, RenderState *rs) {
|
||||
hb_glyph_info_t *info = hb_buffer_get_glyph_infos(hb_buffer, NULL);
|
||||
hb_glyph_position_t *positions = hb_buffer_get_glyph_positions(hb_buffer, NULL);
|
||||
int baseline = font_units_to_pixels_y(face, face->ascender);
|
||||
int load_flags = get_load_flags(rs->current_face->hinting, rs->current_face->hintstyle, has_color ? FT_LOAD_COLOR : FT_LOAD_RENDER);
|
||||
int load_flags = get_load_flags(rs->current_face->hinting, rs->current_face->hintstyle, FT_LOAD_RENDER | (has_color ? FT_LOAD_COLOR : 0));
|
||||
float pos = rs->x;
|
||||
unsigned int limit = len;
|
||||
for (unsigned int i = 0; i < len; i++) {
|
||||
|
||||
6
kitty/glfw-wrapper.c
generated
6
kitty/glfw-wrapper.c
generated
@@ -1,4 +1,6 @@
|
||||
|
||||
// generated by glfw.py DO NOT edit
|
||||
|
||||
#define GFW_EXTERN
|
||||
#include "data-types.h"
|
||||
#include "glfw-wrapper.h"
|
||||
@@ -405,8 +407,8 @@ load_glfw(const char* path) {
|
||||
*(void **) (&glfwSetCocoaTextInputFilter_impl) = dlsym(handle, "glfwSetCocoaTextInputFilter");
|
||||
if (glfwSetCocoaTextInputFilter_impl == NULL) dlerror(); // clear error indicator
|
||||
|
||||
*(void **) (&glfwSetCocoaFileOpenCallback_impl) = dlsym(handle, "glfwSetCocoaFileOpenCallback");
|
||||
if (glfwSetCocoaFileOpenCallback_impl == NULL) dlerror(); // clear error indicator
|
||||
*(void **) (&glfwSetCocoaURLOpenCallback_impl) = dlsym(handle, "glfwSetCocoaURLOpenCallback");
|
||||
if (glfwSetCocoaURLOpenCallback_impl == NULL) dlerror(); // clear error indicator
|
||||
|
||||
*(void **) (&glfwSetCocoaToggleFullscreenIntercept_impl) = dlsym(handle, "glfwSetCocoaToggleFullscreenIntercept");
|
||||
if (glfwSetCocoaToggleFullscreenIntercept_impl == NULL) dlerror(); // clear error indicator
|
||||
|
||||
8
kitty/glfw-wrapper.h
generated
8
kitty/glfw-wrapper.h
generated
@@ -1597,7 +1597,7 @@ typedef struct GLFWgamepadstate
|
||||
|
||||
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int,unsigned long);
|
||||
typedef bool (* GLFWapplicationshouldhandlereopenfun)(int);
|
||||
typedef bool (* GLFWhandlefileopen)(const char*);
|
||||
typedef bool (* GLFWhandleurlopen)(const char*);
|
||||
typedef void (* GLFWapplicationwillfinishlaunchingfun)(void);
|
||||
typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
|
||||
typedef void (* GLFWcocoarenderframefun)(GLFWwindow*);
|
||||
@@ -2124,9 +2124,9 @@ typedef GLFWcocoatextinputfilterfun (*glfwSetCocoaTextInputFilter_func)(GLFWwind
|
||||
GFW_EXTERN glfwSetCocoaTextInputFilter_func glfwSetCocoaTextInputFilter_impl;
|
||||
#define glfwSetCocoaTextInputFilter glfwSetCocoaTextInputFilter_impl
|
||||
|
||||
typedef GLFWhandlefileopen (*glfwSetCocoaFileOpenCallback_func)(GLFWhandlefileopen);
|
||||
GFW_EXTERN glfwSetCocoaFileOpenCallback_func glfwSetCocoaFileOpenCallback_impl;
|
||||
#define glfwSetCocoaFileOpenCallback glfwSetCocoaFileOpenCallback_impl
|
||||
typedef GLFWhandleurlopen (*glfwSetCocoaURLOpenCallback_func)(GLFWhandleurlopen);
|
||||
GFW_EXTERN glfwSetCocoaURLOpenCallback_func glfwSetCocoaURLOpenCallback_impl;
|
||||
#define glfwSetCocoaURLOpenCallback glfwSetCocoaURLOpenCallback_impl
|
||||
|
||||
typedef GLFWcocoatogglefullscreenfun (*glfwSetCocoaToggleFullscreenIntercept_func)(GLFWwindow*, GLFWcocoatogglefullscreenfun);
|
||||
GFW_EXTERN glfwSetCocoaToggleFullscreenIntercept_func glfwSetCocoaToggleFullscreenIntercept_impl;
|
||||
|
||||
77
kitty/glfw.c
77
kitty/glfw.c
@@ -25,6 +25,7 @@ extern void cocoa_set_titlebar_appearance(void *w, unsigned int theme);
|
||||
extern void cocoa_set_titlebar_color(void *w, color_type color);
|
||||
extern bool cocoa_alt_option_key_pressed(unsigned long);
|
||||
extern void cocoa_toggle_secure_keyboard_entry(void);
|
||||
extern void cocoa_update_menu_bar_title(PyObject*);
|
||||
extern size_t cocoa_get_workspace_ids(void *w, size_t *workspace_ids, size_t array_sz);
|
||||
extern monotonic_t cocoa_cursor_blink_interval(void);
|
||||
|
||||
@@ -46,6 +47,43 @@ get_platform_dependent_config_values(void *glfw_window) {
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
strip_csi_(const char *title, char *buf, size_t bufsz) {
|
||||
enum { NORMAL, IN_ESC, IN_CSI} state = NORMAL;
|
||||
char *dest = buf, *last = &buf[bufsz-1];
|
||||
*dest = 0; *last = 0;
|
||||
|
||||
for (; *title && dest < last; title++) {
|
||||
const char ch = *title;
|
||||
switch (state) {
|
||||
case NORMAL: {
|
||||
if (ch == 0x1b) { state = IN_ESC; }
|
||||
else *(dest++) = ch;
|
||||
} break;
|
||||
case IN_ESC: {
|
||||
if (ch == '[') { state = IN_CSI; }
|
||||
else { state = NORMAL; }
|
||||
} break;
|
||||
case IN_CSI: {
|
||||
if (!(('0' <= ch && ch <= '9') || ch == ';' || ch == ':')) state = NORMAL;
|
||||
} break;
|
||||
}
|
||||
}
|
||||
*dest = 0;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
update_menu_bar_title(PyObject *title UNUSED) {
|
||||
#ifdef __APPLE__
|
||||
static char buf[2048];
|
||||
strip_csi_(PyUnicode_AsUTF8(title), buf, arraysz(buf));
|
||||
DECREF_AFTER_FUNCTION PyObject *stitle = PyUnicode_FromString(buf);
|
||||
cocoa_update_menu_bar_title(stitle);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
request_tick_callback(void) {
|
||||
glfwPostEmptyEvent();
|
||||
@@ -429,8 +467,8 @@ static void get_window_dpi(GLFWwindow *w, double *x, double *y);
|
||||
|
||||
#ifdef __APPLE__
|
||||
static bool
|
||||
apple_file_open_callback(const char* filepath) {
|
||||
set_cocoa_pending_action(OPEN_FILE, filepath);
|
||||
apple_url_open_callback(const char* url) {
|
||||
set_cocoa_pending_action(LAUNCH_URLS, url);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -438,7 +476,9 @@ extern bool cocoa_render_line_of_text(const char *text, const color_type fg, con
|
||||
|
||||
bool
|
||||
draw_window_title(OSWindow *window UNUSED, const char *text, color_type fg, color_type bg, uint8_t *output_buf, size_t width, size_t height) {
|
||||
return cocoa_render_line_of_text(text, fg, bg, output_buf, width, height);
|
||||
static char buf[2048];
|
||||
strip_csi_(text, buf, arraysz(buf));
|
||||
return cocoa_render_line_of_text(buf, fg, bg, output_buf, width, height);
|
||||
}
|
||||
|
||||
extern uint8_t* render_single_ascii_char_as_mask(const char ch, size_t *result_width, size_t *result_height);
|
||||
@@ -484,10 +524,12 @@ draw_text_callback(GLFWwindow *window, const char *text, uint32_t fg, uint32_t b
|
||||
bool
|
||||
draw_window_title(OSWindow *window, const char *text, color_type fg, color_type bg, uint8_t *output_buf, size_t width, size_t height) {
|
||||
if (!ensure_csd_title_render_ctx()) return false;
|
||||
static char buf[2048];
|
||||
strip_csi_(text, buf, arraysz(buf));
|
||||
unsigned px_sz = (unsigned)(window->fonts_data->font_sz_in_pts * window->fonts_data->logical_dpi_y / 72.);
|
||||
px_sz = MIN(px_sz, 3 * height / 4);
|
||||
#define RGB2BGR(x) (x & 0xFF000000) | ((x & 0xFF0000) >> 16) | (x & 0x00FF00) | ((x & 0x0000FF) << 16)
|
||||
bool ok = render_single_line(csd_title_render_ctx, text, px_sz, RGB2BGR(fg), RGB2BGR(bg), output_buf, width, height, 0, 0, 0);
|
||||
bool ok = render_single_line(csd_title_render_ctx, buf, px_sz, RGB2BGR(fg), RGB2BGR(bg), output_buf, width, height, 0, 0, 0);
|
||||
#undef RGB2BGR
|
||||
if (!ok && PyErr_Occurred()) PyErr_Print();
|
||||
return ok;
|
||||
@@ -1003,7 +1045,7 @@ glfw_init(PyObject UNUSED *self, PyObject *args) {
|
||||
PyObject *ans = glfwInit(monotonic_start_time) ? Py_True: Py_False;
|
||||
if (ans == Py_True) {
|
||||
#ifdef __APPLE__
|
||||
glfwSetCocoaFileOpenCallback(apple_file_open_callback);
|
||||
glfwSetCocoaURLOpenCallback(apple_url_open_callback);
|
||||
#else
|
||||
glfwSetDrawTextFunction(draw_text_callback);
|
||||
#endif
|
||||
@@ -1286,31 +1328,6 @@ request_window_attention(id_type kitty_window_id, bool audio_bell) {
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
strip_csi_(const char *title, char *buf, size_t bufsz) {
|
||||
enum { NORMAL, IN_ESC, IN_CSI} state = NORMAL;
|
||||
char *dest = buf, *last = &buf[bufsz-1];
|
||||
*dest = 0; *last = 0;
|
||||
|
||||
for (; *title && dest < last; title++) {
|
||||
const char ch = *title;
|
||||
switch (state) {
|
||||
case NORMAL: {
|
||||
if (ch == 0x1b) { state = IN_ESC; }
|
||||
else *(dest++) = ch;
|
||||
} break;
|
||||
case IN_ESC: {
|
||||
if (ch == '[') { state = IN_CSI; }
|
||||
else { state = NORMAL; }
|
||||
} break;
|
||||
case IN_CSI: {
|
||||
if (!(('0' <= ch && ch <= '9') || ch == ';' || ch == ':')) state = NORMAL;
|
||||
} break;
|
||||
}
|
||||
}
|
||||
*dest = 0;
|
||||
}
|
||||
|
||||
void
|
||||
set_os_window_title(OSWindow *w, const char *title) {
|
||||
static char buf[2048];
|
||||
|
||||
@@ -242,7 +242,7 @@ pagerhist_push(HistoryBuf *self, ANSIBuf *as_ansi_buf) {
|
||||
const GPUCell *prev_cell = NULL;
|
||||
Line l = {.xnum=self->xnum};
|
||||
init_line(self, self->start_of_data, &l);
|
||||
line_as_ansi(&l, as_ansi_buf, &prev_cell);
|
||||
line_as_ansi(&l, as_ansi_buf, &prev_cell, 0, l.xnum, 0);
|
||||
if (ringbuf_bytes_used(ph->ringbuf) && !l.attrs.continued) pagerhist_write_bytes(ph, (const uint8_t*)"\n", 1);
|
||||
pagerhist_write_bytes(ph, (const uint8_t*)"\x1b[m", 3);
|
||||
if (pagerhist_write_ucs4(ph, as_ansi_buf->buf, as_ansi_buf->len)) pagerhist_write_bytes(ph, (const uint8_t*)"\r", 1);
|
||||
@@ -324,7 +324,7 @@ as_ansi(HistoryBuf *self, PyObject *callback) {
|
||||
if (i < self->count - 1) {
|
||||
l.attrs.continued = attrptr(self, index_of(self, i + 1))->continued;
|
||||
} else l.attrs.continued = false;
|
||||
line_as_ansi(&l, &output, &prev_cell);
|
||||
line_as_ansi(&l, &output, &prev_cell, 0, l.xnum, 0);
|
||||
if (!l.attrs.continued) {
|
||||
ensure_space_for(&output, buf, Py_UCS4, output.len + 1, capacity, 2048, false);
|
||||
output.buf[output.len++] = 10; // 10 = \n
|
||||
@@ -418,8 +418,21 @@ pagerhist_write(HistoryBuf *self, PyObject *what) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static const uint8_t*
|
||||
reverse_find(const uint8_t *haystack, size_t haystack_sz, const uint8_t *needle) {
|
||||
const size_t needle_sz = strlen((const char*)needle);
|
||||
if (!needle_sz || needle_sz > haystack_sz) return NULL;
|
||||
const uint8_t *p = haystack + haystack_sz - (needle_sz - 1);
|
||||
while (--p >= haystack) {
|
||||
if (*p == needle[0] && memcmp(p, needle, MIN(needle_sz, haystack_sz - (p - haystack))) == 0) return p;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
pagerhist_as_bytes(HistoryBuf *self, PyObject *args UNUSED) {
|
||||
pagerhist_as_bytes(HistoryBuf *self, PyObject *args) {
|
||||
int upto_output_start = 0;
|
||||
if (!PyArg_ParseTuple(args, "|p", &upto_output_start)) return NULL;
|
||||
#define ph self->pagerhist
|
||||
if (!ph || !ringbuf_bytes_used(ph->ringbuf)) return PyBytes_FromStringAndSize("", 0);
|
||||
pagerhist_ensure_start_is_valid_utf8(ph);
|
||||
@@ -433,6 +446,13 @@ pagerhist_as_bytes(HistoryBuf *self, PyObject *args UNUSED) {
|
||||
uint8_t *buf = (uint8_t*)PyBytes_AS_STRING(ans);
|
||||
ringbuf_memcpy_from(buf, ph->ringbuf, sz);
|
||||
if (!l.attrs.continued) buf[sz-1] = '\n';
|
||||
if (upto_output_start) {
|
||||
const uint8_t *p = reverse_find(buf, sz, (const uint8_t*)"\x1b]133;C\x1b\\");
|
||||
if (p) {
|
||||
PyObject *t = PyBytes_FromStringAndSize((const char*)p, sz - (p - buf));
|
||||
Py_DECREF(ans); ans = t;
|
||||
}
|
||||
}
|
||||
return ans;
|
||||
#undef ph
|
||||
}
|
||||
@@ -501,8 +521,8 @@ static PyMethodDef methods[] = {
|
||||
METHOD(as_ansi, METH_O)
|
||||
METHODB(pagerhist_write, METH_O),
|
||||
METHODB(pagerhist_rewrap, METH_O),
|
||||
METHODB(pagerhist_as_text, METH_NOARGS),
|
||||
METHODB(pagerhist_as_bytes, METH_NOARGS),
|
||||
METHODB(pagerhist_as_text, METH_VARARGS),
|
||||
METHODB(pagerhist_as_bytes, METH_VARARGS),
|
||||
METHODB(as_text, METH_VARARGS),
|
||||
METHOD(dirty_lines, METH_NOARGS)
|
||||
METHOD(push, METH_VARARGS)
|
||||
|
||||
@@ -11,7 +11,7 @@ from .cli_stub import LaunchCLIOptions
|
||||
from .constants import kitty_exe, shell_path
|
||||
from .fast_data_types import patch_color_profiles, set_clipboard_string
|
||||
from .options.utils import env as parse_env
|
||||
from .tabs import Tab
|
||||
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
|
||||
@@ -102,16 +102,17 @@ kitty remote control feature with :code:`kitty @launch --copy-env`.
|
||||
--location
|
||||
type=choices
|
||||
default=default
|
||||
choices=first,after,before,neighbor,last,vsplit,hsplit,default
|
||||
choices=first,after,before,neighbor,last,vsplit,hsplit,split,default
|
||||
Where to place the newly created window when it is added to a tab which
|
||||
already has existing windows in it. :code:`after` and :code:`before` place the new
|
||||
window before or after the active window. :code:`neighbor` is a synonym for :code:`after`.
|
||||
Also applies to creating a new tab, where the value of :code:`after`
|
||||
will cause the new tab to be placed next to the current tab instead of at the end.
|
||||
The values of :code:`vsplit` and :code:`hsplit` are only used by the :code:`splits`
|
||||
layout and control if the new window is placed in a vertical or horizontal split
|
||||
with the currently active window. The default is to place the window in a
|
||||
layout dependent manner, typically, after the currently active window.
|
||||
The values of :code:`vsplit`, :code:`hsplit` and :code:`split` are only used by the
|
||||
:code:`splits` layout and control if the new window is placed in a vertical,
|
||||
horizontal or automatic split with the currently active window. The default is
|
||||
to place the window in a layout dependent manner, typically, after the
|
||||
currently active window.
|
||||
|
||||
|
||||
--allow-remote-control
|
||||
@@ -227,25 +228,26 @@ def get_env(opts: LaunchCLIOptions, active_child: Optional[Child]) -> Dict[str,
|
||||
|
||||
|
||||
def tab_for_window(boss: Boss, opts: LaunchCLIOptions, target_tab: Optional[Tab] = None) -> Optional[Tab]:
|
||||
|
||||
def create_tab(tm: Optional[TabManager] = None) -> Tab:
|
||||
if tm is None:
|
||||
oswid = boss.add_os_window(wclass=opts.os_window_class, wname=opts.os_window_name, override_title=opts.os_window_title or None)
|
||||
tm = boss.os_window_map[oswid]
|
||||
tab = tm.new_tab(empty_tab=True, location=opts.location)
|
||||
if opts.tab_title:
|
||||
tab.set_title(opts.tab_title)
|
||||
return tab
|
||||
|
||||
if opts.type == 'tab':
|
||||
if target_tab is not None:
|
||||
tm = target_tab.tab_manager_ref() or boss.active_tab_manager
|
||||
else:
|
||||
tm = boss.active_tab_manager
|
||||
if tm:
|
||||
tab: Optional[Tab] = tm.new_tab(empty_tab=True, location=opts.location)
|
||||
if opts.tab_title and tab is not None:
|
||||
tab.set_title(opts.tab_title)
|
||||
else:
|
||||
tab = None
|
||||
tab = create_tab(tm)
|
||||
elif opts.type == 'os-window':
|
||||
oswid = boss.add_os_window(wclass=opts.os_window_class, wname=opts.os_window_name, override_title=opts.os_window_title or None)
|
||||
tm = boss.os_window_map[oswid]
|
||||
tab = tm.new_tab(empty_tab=True)
|
||||
if opts.tab_title and tab is not None:
|
||||
tab.set_title(opts.tab_title)
|
||||
tab = create_tab()
|
||||
else:
|
||||
tab = target_tab or boss.active_tab
|
||||
tab = target_tab or boss.active_tab or create_tab()
|
||||
|
||||
return tab
|
||||
|
||||
@@ -397,7 +399,7 @@ def launch(
|
||||
elif x == '@line-count':
|
||||
if 'lines' in pipe_data:
|
||||
x = str(pipe_data['lines'])
|
||||
elif x in ('@cursor-x', '@cursor-y', '@scrolled-by'):
|
||||
elif x in ('@cursor-x', '@cursor-y', '@scrolled-by', '@first-line-on-screen', '@last-line-on-screen'):
|
||||
if active is not None:
|
||||
screen = active.screen
|
||||
if x == '@scrolled-by':
|
||||
@@ -406,6 +408,10 @@ def launch(
|
||||
x = str(screen.cursor.x + 1)
|
||||
elif x == '@cursor-y':
|
||||
x = str(screen.cursor.y + 1)
|
||||
elif x == '@first-line-on-screen':
|
||||
x = str(screen.visual_line(0) or '')
|
||||
elif x == '@last-line-on-screen':
|
||||
x = str(screen.visual_line(screen.lines - 1) or '')
|
||||
final_cmd.append(x)
|
||||
exe = which(final_cmd[0])
|
||||
if exe:
|
||||
|
||||
@@ -450,13 +450,12 @@ class Splits(Layout):
|
||||
) -> None:
|
||||
horizontal = self.default_axis_is_horizontal
|
||||
after = True
|
||||
if location is not None:
|
||||
if location == 'vsplit':
|
||||
horizontal = True
|
||||
elif location == 'hsplit':
|
||||
horizontal = False
|
||||
if location in ('before', 'first'):
|
||||
after = False
|
||||
if location == 'vsplit':
|
||||
horizontal = True
|
||||
elif location == 'hsplit':
|
||||
horizontal = False
|
||||
elif location in ('before', 'first'):
|
||||
after = False
|
||||
aw = all_windows.active_window
|
||||
if aw is not None:
|
||||
ag = all_windows.active_group
|
||||
@@ -464,6 +463,10 @@ class Splits(Layout):
|
||||
group_id = ag.id
|
||||
pair = self.pairs_root.pair_for_window(group_id)
|
||||
if pair is not None:
|
||||
if location == 'split':
|
||||
wwidth = aw.geometry.right - aw.geometry.left
|
||||
wheight = aw.geometry.bottom - aw.geometry.top
|
||||
horizontal = wwidth >= wheight
|
||||
target_group = all_windows.add_window(window, next_to=aw, before=not after)
|
||||
pair.split_and_add(group_id, target_group.id, horizontal, after)
|
||||
return
|
||||
|
||||
@@ -413,7 +413,7 @@ as_ansi(LineBuf *self, PyObject *callback) {
|
||||
ANSIBuf output = {0};
|
||||
do {
|
||||
init_line(self, (&l), self->line_map[ylimit]);
|
||||
line_as_ansi(&l, &output, &prev_cell);
|
||||
line_as_ansi(&l, &output, &prev_cell, 0, l.xnum, 0);
|
||||
if (output.len) break;
|
||||
ylimit--;
|
||||
} while(ylimit > 0);
|
||||
@@ -421,7 +421,7 @@ as_ansi(LineBuf *self, PyObject *callback) {
|
||||
for(index_type i = 0; i <= ylimit; i++) {
|
||||
l.attrs.continued = self->line_attrs[(i + 1 < self->ynum) ? i+1 : i].continued;
|
||||
init_line(self, (&l), self->line_map[i]);
|
||||
line_as_ansi(&l, &output, &prev_cell);
|
||||
line_as_ansi(&l, &output, &prev_cell, 0, l.xnum, 0);
|
||||
if (!l.attrs.continued) {
|
||||
ensure_space_for(&output, buf, Py_UCS4, output.len + 1, capacity, 2048, false);
|
||||
output.buf[output.len++] = 10; // 10 = \n
|
||||
|
||||
47
kitty/line.c
47
kitty/line.c
@@ -9,6 +9,7 @@
|
||||
#include "unicode-data.h"
|
||||
#include "lineops.h"
|
||||
#include "charsets.h"
|
||||
#include "wcwidth-std.h"
|
||||
|
||||
extern PyTypeObject Cursor_Type;
|
||||
|
||||
@@ -300,24 +301,50 @@ write_hyperlink(hyperlink_id_type hid, ANSIBuf *output) {
|
||||
#undef W
|
||||
}
|
||||
|
||||
static void
|
||||
write_mark(const char *mark, ANSIBuf *output) {
|
||||
#define W(c) output->buf[output->len++] = c
|
||||
W(0x1b); W(']'); W('1'); W('3'); W('3'); W(';');
|
||||
for (size_t i = 0; mark[i] != 0 && i < 32; i++) W(mark[i]);
|
||||
W(0x1b); W('\\');
|
||||
#undef W
|
||||
|
||||
void
|
||||
line_as_ansi(Line *self, ANSIBuf *output, const GPUCell** prev_cell) {
|
||||
}
|
||||
|
||||
bool
|
||||
line_as_ansi(Line *self, ANSIBuf *output, const GPUCell** prev_cell, index_type start_at, index_type stop_before, char_type prefix_char) {
|
||||
#define ENSURE_SPACE(extra) ensure_space_for(output, buf, Py_UCS4, output->len + extra, capacity, 2048, false);
|
||||
#define WRITE_SGR(val) { ENSURE_SPACE(128); write_sgr(val, output); }
|
||||
#define WRITE_SGR(val) { ENSURE_SPACE(128); escape_code_written = true; write_sgr(val, output); }
|
||||
#define WRITE_CH(val) { ENSURE_SPACE(1); output->buf[output->len++] = val; }
|
||||
#define WRITE_HYPERLINK(val) { ENSURE_SPACE(2256); write_hyperlink(val, output); }
|
||||
#define WRITE_HYPERLINK(val) { ENSURE_SPACE(2256); escape_code_written = true; write_hyperlink(val, output); }
|
||||
#define WRITE_MARK(val) { ENSURE_SPACE(64); escape_code_written = true; write_mark(val, output); }
|
||||
bool escape_code_written = false;
|
||||
output->len = 0;
|
||||
index_type limit = xlimit_for_line(self);
|
||||
if (limit == 0) return;
|
||||
index_type limit = MIN(stop_before, xlimit_for_line(self));
|
||||
char_type previous_width = 0;
|
||||
if (prefix_char) { WRITE_CH(prefix_char); previous_width = wcwidth_std(prefix_char); }
|
||||
|
||||
switch (self->attrs.prompt_kind) {
|
||||
case UNKNOWN_PROMPT_KIND:
|
||||
break;
|
||||
case PROMPT_START:
|
||||
WRITE_MARK("A");
|
||||
break;
|
||||
case SECONDARY_PROMPT:
|
||||
WRITE_MARK("A;k=s");
|
||||
break;
|
||||
case OUTPUT_START:
|
||||
WRITE_MARK("C");
|
||||
break;
|
||||
}
|
||||
if (limit <= start_at) return escape_code_written;
|
||||
|
||||
static const GPUCell blank_cell = { 0 };
|
||||
GPUCell *cell;
|
||||
if (*prev_cell == NULL) *prev_cell = &blank_cell;
|
||||
const CellAttrs mask_for_sgr = {.val=SGR_MASK};
|
||||
|
||||
for (index_type pos=0; pos < limit; pos++) {
|
||||
for (index_type pos=start_at; pos < limit; pos++) {
|
||||
char_type ch = self->cpu_cells[pos].ch;
|
||||
if (ch == 0) {
|
||||
if (previous_width == 2) { previous_width = 0; continue; }
|
||||
@@ -352,12 +379,14 @@ line_as_ansi(Line *self, ANSIBuf *output, const GPUCell** prev_cell) {
|
||||
}
|
||||
previous_width = cell->attrs.width;
|
||||
}
|
||||
return escape_code_written;
|
||||
#undef CMP_ATTRS
|
||||
#undef CMP
|
||||
#undef WRITE_SGR
|
||||
#undef WRITE_CH
|
||||
#undef ENSURE_SPACE
|
||||
#undef WRITE_HYPERLINK
|
||||
#undef WRITE_MARK
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
@@ -365,7 +394,7 @@ as_ansi(Line* self, PyObject *a UNUSED) {
|
||||
#define as_ansi_doc "Return the line's contents with ANSI (SGR) escape codes for formatting"
|
||||
const GPUCell *prev_cell = NULL;
|
||||
ANSIBuf output = {0};
|
||||
line_as_ansi(self, &output, &prev_cell);
|
||||
line_as_ansi(self, &output, &prev_cell, 0, self->xnum, 0);
|
||||
PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len);
|
||||
free(output.buf);
|
||||
return ans;
|
||||
@@ -834,7 +863,7 @@ as_text_generic(PyObject *args, void *container, get_line_func get_line, index_t
|
||||
// get around to writing a nice pager kitten.
|
||||
// see https://github.com/kovidgoyal/kitty/issues/2381
|
||||
prev_cell = NULL;
|
||||
line_as_ansi(line, ansibuf, &prev_cell);
|
||||
line_as_ansi(line, ansibuf, &prev_cell, 0, line->xnum, 0);
|
||||
t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, ansibuf->buf, ansibuf->len);
|
||||
if (t && ansibuf->len > 0) APPEND(sgr_reset);
|
||||
} else {
|
||||
|
||||
@@ -84,7 +84,7 @@ void line_add_combining_char(Line *, uint32_t , unsigned int );
|
||||
index_type line_url_start_at(Line *self, index_type x);
|
||||
index_type line_url_end_at(Line *self, index_type x, bool, char_type, bool);
|
||||
bool line_startswith_url_chars(Line*);
|
||||
void line_as_ansi(Line *self, ANSIBuf *output, const GPUCell**) __attribute__((nonnull));
|
||||
bool line_as_ansi(Line *self, ANSIBuf *output, const GPUCell**, index_type start_at, index_type stop_before, char_type prefix_char) __attribute__((nonnull));
|
||||
unsigned int line_length(Line *self);
|
||||
size_t cell_as_unicode(CPUCell *cell, bool include_cc, Py_UCS4 *buf, char_type);
|
||||
size_t cell_as_unicode_for_fallback(CPUCell *cell, Py_UCS4 *buf);
|
||||
|
||||
@@ -30,7 +30,6 @@ from .options.types import Options
|
||||
from .options.utils import DELETE_ENV_VAR
|
||||
from .os_window_size import initial_window_size_func
|
||||
from .session import get_os_window_sizing_data
|
||||
from .shell_integration import setup_shell_integration
|
||||
from .types import SingleKey
|
||||
from .utils import (
|
||||
detach, expandvars, log_error, single_instance,
|
||||
@@ -60,7 +59,7 @@ def talk_to_instance(args: CLIOptions) -> None:
|
||||
stdin = ''
|
||||
if args.session == '-':
|
||||
stdin = sys.stdin.read()
|
||||
data = {'cmd': 'new_instance', 'args': tuple(sys.argv),
|
||||
data = {'cmd': 'new_instance', 'args': tuple(sys.argv), 'cmdline_args_for_open': getattr(sys, 'cmdline_args_for_open', []),
|
||||
'startup_id': os.environ.get('DESKTOP_STARTUP_ID'),
|
||||
'cwd': os.getcwd(), 'stdin': stdin}
|
||||
notify_socket = None
|
||||
@@ -151,13 +150,16 @@ def _run_app(opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = ())
|
||||
func_map[parts].append(k)
|
||||
|
||||
for ac in ('new_os_window', 'close_os_window', 'close_tab', 'edit_config_file', 'previous_tab',
|
||||
'next_tab', 'new_tab', 'new_window', 'close_window', 'toggle_macos_secure_keyboard_entry'):
|
||||
'next_tab', 'new_tab', 'new_window', 'close_window', 'toggle_macos_secure_keyboard_entry', 'toggle_fullscreen'):
|
||||
val = get_macos_shortcut_for(func_map, ac)
|
||||
if val is not None:
|
||||
global_shortcuts[ac] = val
|
||||
val = get_macos_shortcut_for(func_map, 'clear_terminal reset active', lookup_name='reset_terminal')
|
||||
if val is not None:
|
||||
global_shortcuts['reset_terminal'] = val
|
||||
val = get_macos_shortcut_for(func_map, 'clear_terminal to_cursor active', lookup_name='clear_terminal_and_scrollback')
|
||||
if val is not None:
|
||||
global_shortcuts['clear_terminal_and_scrollback'] = val
|
||||
val = get_macos_shortcut_for(func_map, 'load_config_file', lookup_name='reload_config')
|
||||
if val is not None:
|
||||
global_shortcuts['reload_config'] = val
|
||||
@@ -319,7 +321,6 @@ def setup_environment(opts: Options, cli_opts: CLIOptions) -> None:
|
||||
os.environ['KITTY_LISTEN_ON'] = cli_opts.listen_on
|
||||
env = opts.env.copy()
|
||||
ensure_kitty_in_path()
|
||||
setup_shell_integration(opts, env)
|
||||
kitty_path = shutil.which('kitty')
|
||||
if kitty_path:
|
||||
child_path = env.get('PATH')
|
||||
@@ -364,8 +365,21 @@ def _main() -> None:
|
||||
cwd_ok = False
|
||||
if not cwd_ok:
|
||||
os.chdir(os.path.expanduser('~'))
|
||||
cli_opts, rest = parse_args(args=args, result_class=CLIOptions)
|
||||
cli_opts.args = rest
|
||||
if getattr(sys, 'cmdline_args_for_open', False):
|
||||
usage: Optional[str] = 'file_or_url ...'
|
||||
appname: Optional[str] = 'kitty +open'
|
||||
msg: Optional[str] = (
|
||||
'Run kitty and open the specified files or URLs in it, using launch-actions.conf. For details'
|
||||
' see https://sw.kovidgoyal.net/kitty/open_actions/#scripting-the-opening-of-files-with-kitty-on-macos'
|
||||
'\n\nAll the normal kitty options can be used.')
|
||||
else:
|
||||
usage = msg = appname = None
|
||||
cli_opts, rest = parse_args(args=args, result_class=CLIOptions, usage=usage, message=msg, appname=appname)
|
||||
if getattr(sys, 'cmdline_args_for_open', False):
|
||||
setattr(sys, 'cmdline_args_for_open', rest)
|
||||
cli_opts.args = []
|
||||
else:
|
||||
cli_opts.args = rest
|
||||
if cli_opts.detach:
|
||||
if cli_opts.session == '-':
|
||||
from .session import PreReadSession
|
||||
|
||||
@@ -141,6 +141,13 @@ static bool
|
||||
dispatch_mouse_event(Window *w, int button, int count, int modifiers, bool grabbed) {
|
||||
bool handled = false;
|
||||
if (w->render_data.screen && w->render_data.screen->callbacks != Py_None) {
|
||||
PyObject *callback_ret = PyObject_CallMethod(w->render_data.screen->callbacks, "on_mouse_event", "{si si si sO}",
|
||||
"button", button, "repeat_count", count, "mods", modifiers, "grabbed", grabbed ? Py_True : Py_False);
|
||||
if (callback_ret == NULL) PyErr_Print();
|
||||
else {
|
||||
handled = callback_ret == Py_True;
|
||||
Py_DECREF(callback_ret);
|
||||
}
|
||||
if (OPT(debug_keyboard)) {
|
||||
const char *evname = "move";
|
||||
switch(count) {
|
||||
@@ -162,14 +169,7 @@ dispatch_mouse_event(Window *w, int button, int count, int modifiers, bool grabb
|
||||
case GLFW_MOUSE_BUTTON_7: bname = "b7"; break;
|
||||
case GLFW_MOUSE_BUTTON_8: bname = "b8"; break;
|
||||
}
|
||||
debug("\x1b[33mon_mouse_input\x1b[m: %s button: %s %sgrabbed: %d\n", evname, bname, format_mods(modifiers), grabbed);
|
||||
}
|
||||
PyObject *callback_ret = PyObject_CallMethod(w->render_data.screen->callbacks, "on_mouse_event", "{si si si sO}",
|
||||
"button", button, "repeat_count", count, "mods", modifiers, "grabbed", grabbed ? Py_True : Py_False);
|
||||
if (callback_ret == NULL) PyErr_Print();
|
||||
else {
|
||||
handled = callback_ret == Py_True;
|
||||
Py_DECREF(callback_ret);
|
||||
debug("\x1b[33mon_mouse_input\x1b[m: %s button: %s %sgrabbed: %d handled_in_kitty: %d\n", evname, bname, format_mods(modifiers), grabbed, handled);
|
||||
}
|
||||
}
|
||||
return handled;
|
||||
@@ -377,18 +377,22 @@ clear_click_queue(Window *w, int button) {
|
||||
|
||||
#define N(n) (q->clicks[q->length - n])
|
||||
|
||||
static double
|
||||
radius_for_multiclick(void) {
|
||||
return 0.5 * (global_state.callback_os_window ? global_state.callback_os_window->fonts_data->cell_height : 8);
|
||||
}
|
||||
|
||||
static bool
|
||||
release_is_click(Window *w, int button) {
|
||||
ClickQueue *q = &w->click_queues[button];
|
||||
double click_allowed_radius = 1.2 * (global_state.callback_os_window ? global_state.callback_os_window->fonts_data->cell_height : 20);
|
||||
release_is_click(const Window *w, int button) {
|
||||
const ClickQueue *q = &w->click_queues[button];
|
||||
monotonic_t now = monotonic();
|
||||
return (q->length > 0 && distance(N(1).x, N(1).y, MAX(0, w->mouse_pos.global_x), MAX(0, w->mouse_pos.global_y)) <= click_allowed_radius && now - N(1).at < OPT(click_interval));
|
||||
return (q->length > 0 && distance(N(1).x, N(1).y, MAX(0, w->mouse_pos.global_x), MAX(0, w->mouse_pos.global_y)) <= radius_for_multiclick() && now - N(1).at < OPT(click_interval));
|
||||
}
|
||||
|
||||
static unsigned
|
||||
multi_click_count(Window *w, int button) {
|
||||
ClickQueue *q = &w->click_queues[button];
|
||||
double multi_click_allowed_radius = 1.2 * (global_state.callback_os_window ? global_state.callback_os_window->fonts_data->cell_height : 20);
|
||||
multi_click_count(const Window *w, int button) {
|
||||
const ClickQueue *q = &w->click_queues[button];
|
||||
double multi_click_allowed_radius = radius_for_multiclick();
|
||||
if (q->length > 2) {
|
||||
// possible triple-click
|
||||
if (
|
||||
@@ -414,7 +418,8 @@ add_press(Window *w, int button, int modifiers) {
|
||||
ClickQueue *q = &w->click_queues[button];
|
||||
if (q->length == CLICK_QUEUE_SZ) { memmove(q->clicks, q->clicks + 1, sizeof(Click) * (CLICK_QUEUE_SZ - 1)); q->length--; }
|
||||
monotonic_t now = monotonic();
|
||||
N(0).at = now; N(0).button = button; N(0).modifiers = modifiers; N(0).x = MAX(0, w->mouse_pos.global_x); N(0).y = MAX(0, w->mouse_pos.global_y);
|
||||
static unsigned long num = 0;
|
||||
N(0).at = now; N(0).button = button; N(0).modifiers = modifiers; N(0).x = MAX(0, w->mouse_pos.global_x); N(0).y = MAX(0, w->mouse_pos.global_y); N(0).num = ++num;
|
||||
q->length++;
|
||||
Screen *screen = w->render_data.screen;
|
||||
int count = multi_click_count(w, button);
|
||||
@@ -459,6 +464,8 @@ typedef struct PendingClick {
|
||||
bool grabbed;
|
||||
monotonic_t at;
|
||||
MousePosition mouse_pos;
|
||||
unsigned long press_num;
|
||||
double radius_for_multiclick;
|
||||
} PendingClick;
|
||||
|
||||
static void
|
||||
@@ -467,14 +474,24 @@ free_pending_click(id_type timer_id UNUSED, void *pc) { free(pc); }
|
||||
void
|
||||
send_pending_click_to_window(Window *w, void *data) {
|
||||
PendingClick *pc = (PendingClick*)data;
|
||||
ClickQueue *q = &w->click_queues[pc->button];
|
||||
// only send click if no presses have happened since the release that triggered the click
|
||||
if (q->length && q->clicks[q->length - 1].at <= pc->at) {
|
||||
const ClickQueue *q = &w->click_queues[pc->button];
|
||||
// only send click if no presses have happened since the release that triggered
|
||||
// the click or if the subsequent press is too far or too late for a double click
|
||||
if (!q->length) return;
|
||||
#define press(n) q->clicks[q->length - n]
|
||||
if (
|
||||
press(1).at <= pc->at || // latest press is before click release
|
||||
(q->length > 1 && press(2).num == pc->press_num && ( // penultimate press is the press that belongs to this click
|
||||
press(1).at - press(2).at > OPT(click_interval) || // too long between the presses for it to be a double click
|
||||
distance(press(1).x, press(1).y, press(2).x, press(2).y) > pc->radius_for_multiclick // presses are too far apart
|
||||
))
|
||||
) {
|
||||
MousePosition current_pos = w->mouse_pos;
|
||||
w->mouse_pos = pc->mouse_pos;
|
||||
dispatch_mouse_event(w, pc->button, pc->count, pc->modifiers, pc->grabbed);
|
||||
w->mouse_pos = current_pos;
|
||||
}
|
||||
#undef press
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -484,6 +501,8 @@ dispatch_possible_click(Window *w, int button, int modifiers) {
|
||||
if (release_is_click(w, button)) {
|
||||
PendingClick *pc = calloc(sizeof(PendingClick), 1);
|
||||
if (pc) {
|
||||
const ClickQueue *q = &w->click_queues[button];
|
||||
pc->press_num = q->length ? q->clicks[q->length - 1].num : 0;
|
||||
pc->window_id = w->id;
|
||||
pc->mouse_pos = w->mouse_pos;
|
||||
pc->at = monotonic();
|
||||
@@ -491,6 +510,7 @@ dispatch_possible_click(Window *w, int button, int modifiers) {
|
||||
pc->count = count == 2 ? -3 : -2;
|
||||
pc->modifiers = modifiers;
|
||||
pc->grabbed = screen->modes.mouse_tracking_mode != 0;
|
||||
pc->radius_for_multiclick = radius_for_multiclick();
|
||||
add_main_loop_timer(OPT(click_interval), false, send_pending_click_to_window_id, pc, free_pending_click);
|
||||
}
|
||||
}
|
||||
@@ -701,7 +721,10 @@ mouse_event(const int button, int modifiers, int action) {
|
||||
bool in_tab_bar;
|
||||
unsigned int window_idx = 0;
|
||||
Window *w = NULL;
|
||||
debug("%s mouse_button: %d %s", action == GLFW_RELEASE ? "\x1b[32mRelease\x1b[m" : (button < 0 ? "\x1b[36mMove\x1b[m" : "\x1b[31mPress\x1b[m"), button, format_mods(modifiers));
|
||||
if (OPT(debug_keyboard)) {
|
||||
if (button < 0) { debug("%s x: %.1f y: %.1f ", "\x1b[36mMove\x1b[m", global_state.callback_os_window->mouse_x, global_state.callback_os_window->mouse_y); }
|
||||
else { debug("%s mouse_button: %d %s", action == GLFW_RELEASE ? "\x1b[32mRelease\x1b[m" : "\x1b[31mPress\x1b[m", button, format_mods(modifiers)); }
|
||||
}
|
||||
if (global_state.redirect_mouse_handling) {
|
||||
w = window_for_event(&window_idx, &in_tab_bar);
|
||||
call_boss(mouse_event, "OK iiii dd",
|
||||
@@ -760,9 +783,9 @@ mouse_event(const int button, int modifiers, int action) {
|
||||
clamp_to_window = true;
|
||||
Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab;
|
||||
for (window_idx = 0; window_idx < t->num_windows && t->windows[window_idx].id != w->id; window_idx++);
|
||||
debug("sent to child as drag end\n");
|
||||
handle_button_event(w, button, modifiers, window_idx);
|
||||
clamp_to_window = false;
|
||||
debug("sent to child as drag end\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -851,11 +874,10 @@ scroll_event(double UNUSED xoffset, double yoffset, int flags, int modifiers) {
|
||||
int s;
|
||||
bool is_high_resolution = flags & 1;
|
||||
|
||||
// scale the scroll by the multiplier unless the mouse is grabbed. If the mouse is grabbed only change direction.
|
||||
#define SCALE_SCROLL(which) { double scale = OPT(which); if (screen->modes.mouse_tracking_mode) scale /= fabs(scale); yoffset *= scale; }
|
||||
if (is_high_resolution) {
|
||||
yoffset *= OPT(touch_scroll_multiplier);
|
||||
if (yoffset * screen->pending_scroll_pixels < 0) {
|
||||
screen->pending_scroll_pixels = 0; // change of direction
|
||||
}
|
||||
SCALE_SCROLL(touch_scroll_multiplier);
|
||||
double pixels = screen->pending_scroll_pixels + yoffset;
|
||||
if (fabs(pixels) < global_state.callback_os_window->fonts_data->cell_height) {
|
||||
screen->pending_scroll_pixels = pixels;
|
||||
@@ -864,20 +886,20 @@ scroll_event(double UNUSED xoffset, double yoffset, int flags, int modifiers) {
|
||||
s = (int)round(pixels) / (int)global_state.callback_os_window->fonts_data->cell_height;
|
||||
screen->pending_scroll_pixels = pixels - s * (int) global_state.callback_os_window->fonts_data->cell_height;
|
||||
} else {
|
||||
if (!screen->modes.mouse_tracking_mode) {
|
||||
// Dont use multiplier if we are sending events to the application
|
||||
yoffset *= OPT(wheel_scroll_multiplier);
|
||||
} else if (OPT(wheel_scroll_multiplier) < 0) {
|
||||
// ensure that changing scroll direction still works, even though
|
||||
// we are not using wheel_scroll_multiplier
|
||||
yoffset *= -1;
|
||||
}
|
||||
SCALE_SCROLL(wheel_scroll_multiplier);
|
||||
s = (int) round(yoffset);
|
||||
// apparently on cocoa some mice generate really small yoffset values
|
||||
// when scrolling slowly https://github.com/kovidgoyal/kitty/issues/1238
|
||||
if (s == 0 && yoffset != 0) s = yoffset > 0 ? 1 : -1;
|
||||
if (yoffset != 0) {
|
||||
const int min_lines = screen->modes.mouse_tracking_mode ? 1 : OPT(wheel_scroll_min_lines);
|
||||
if (min_lines > 0 && abs(s) < min_lines) s = yoffset > 0 ? min_lines : -min_lines;
|
||||
// Always add the minimum number of lines when it is negative
|
||||
else if (min_lines < 0) s = yoffset > 0 ? s - min_lines : s + min_lines;
|
||||
// apparently on cocoa some mice generate really small yoffset values
|
||||
// when scrolling slowly https://github.com/kovidgoyal/kitty/issues/1238
|
||||
if (s == 0) s = yoffset > 0 ? 1 : -1;
|
||||
}
|
||||
screen->pending_scroll_pixels = 0;
|
||||
}
|
||||
#undef SCALE_SCROLL
|
||||
if (s == 0) return;
|
||||
bool upwards = s > 0;
|
||||
if (screen->modes.mouse_tracking_mode) {
|
||||
|
||||
@@ -9,7 +9,7 @@ from contextlib import suppress
|
||||
from typing import (
|
||||
Any, Dict, Iterable, Iterator, List, NamedTuple, Optional, Tuple, cast
|
||||
)
|
||||
from urllib.parse import ParseResult, quote, unquote, urlparse
|
||||
from urllib.parse import ParseResult, unquote, urlparse
|
||||
|
||||
from .conf.utils import KeyAction, to_cmdline_implementation
|
||||
from .constants import config_dir
|
||||
@@ -243,6 +243,10 @@ action launch --type=os-window $EDITOR $FILE_PATH
|
||||
protocol file
|
||||
mime image/*
|
||||
action launch --type=os-window kitty +kitten icat --hold $FILE_PATH
|
||||
|
||||
# Open ssh URLs with ssh command
|
||||
protocol ssh
|
||||
action launch --type=os-window ssh $URL
|
||||
'''.splitlines()))
|
||||
|
||||
|
||||
@@ -254,8 +258,10 @@ def actions_for_url(url: str, actions_spec: Optional[str] = None) -> Iterator[Ke
|
||||
yield from actions_for_url_from_list(url, actions)
|
||||
|
||||
|
||||
def actions_for_launch(path: str) -> Iterator[KeyAction]:
|
||||
url = f'file://{quote(path)}'
|
||||
def actions_for_launch(url: str) -> Iterator[KeyAction]:
|
||||
# Custom launch actions using kitty URL scheme needs to be prefixed with `kitty:///launch/`
|
||||
if url.startswith('kitty://') and not url.startswith('kitty:///launch/'):
|
||||
return
|
||||
found = False
|
||||
for action in actions_for_url_from_list(url, load_launch_actions()):
|
||||
found = True
|
||||
|
||||
@@ -33,7 +33,7 @@ and even specify special fonts for particular characters.
|
||||
opt('font_family', 'monospace',
|
||||
long_text='''
|
||||
You can specify different fonts for the bold/italic/bold-italic variants.
|
||||
To get a full list of supported fonts use the `kitty list-fonts` command.
|
||||
To get a full list of supported fonts use the `kitty +list-fonts` command.
|
||||
By default they are derived automatically, by the OSes font system. When
|
||||
bold_font or bold_italic_font is set to :code:`auto` on macOS, the priority of
|
||||
bold fonts is semi-bold, bold, heavy. Setting them manually is useful for font
|
||||
@@ -124,6 +124,22 @@ Syntax is::
|
||||
'''
|
||||
)
|
||||
|
||||
opt('+narrow_symbols', 'U+E0A0-U+E0A3,U+E0C0-U+E0C7 1',
|
||||
option_type='narrow_symbols',
|
||||
add_to_default=False,
|
||||
long_text='''
|
||||
Usually, for Private Use Unicode characters and some symbol/dingbat characters,
|
||||
if the character is followed by one or more spaces, kitty will use those extra cells
|
||||
to render the character larger, if the character in the font has a wide aspect ratio.
|
||||
Using this setting you can force kitty to restrict the specified code points to render in
|
||||
the specified number of cells (defaulting to one cell).
|
||||
Syntax is::
|
||||
|
||||
narrow_symbols codepoints Optionally the number of cells
|
||||
'''
|
||||
)
|
||||
|
||||
|
||||
opt('disable_ligatures', 'never',
|
||||
option_type='disable_ligatures', ctype='int',
|
||||
long_text='''
|
||||
@@ -166,11 +182,11 @@ disable a feature in the italic font but not in the regular font.
|
||||
On Linux, these are read from the FontConfig database first and then this,
|
||||
setting is applied, so they can be configured in a single, central place.
|
||||
|
||||
To get the PostScript name for a font, use :code:`kitty + list-fonts --psnames`:
|
||||
To get the PostScript name for a font, use :code:`kitty +list-fonts --psnames`:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ kitty + list-fonts --psnames | grep Fira
|
||||
$ kitty +list-fonts --psnames | grep Fira
|
||||
Fira Code
|
||||
Fira Code Bold (FiraCode-Bold)
|
||||
Fira Code Light (FiraCode-Light)
|
||||
@@ -310,7 +326,7 @@ Separate scrollback history size, used only for browsing the scrollback buffer
|
||||
(in MB). This separate buffer is not available for interactive scrolling but
|
||||
will be piped to the pager program when viewing scrollback buffer in a separate
|
||||
window. The current implementation stores the data in UTF-8, so approximatively
|
||||
10000 lines per megabyte at 100 chars per line, for pure ASCII text, unformatted
|
||||
10000 lines per megabyte at 100 chars per line, for pure ASCII, unformatted
|
||||
text. A value of zero or less disables this feature. The maximum allowed size is
|
||||
4GB. Note that on config reload if this
|
||||
is changed it will only affect newly created windows, not existing ones.
|
||||
@@ -327,7 +343,18 @@ opt('wheel_scroll_multiplier', '5.0',
|
||||
long_text='''
|
||||
Modify the amount scrolled by the mouse wheel. Note this is only used for low
|
||||
precision scrolling devices, not for high precision scrolling on platforms such
|
||||
as macOS and Wayland. Use negative numbers to change scroll direction.
|
||||
as macOS and Wayland. Use negative numbers to change scroll direction. See also
|
||||
:opt:`wheel_scroll_min_lines`.
|
||||
'''
|
||||
)
|
||||
|
||||
opt('wheel_scroll_min_lines', '1',
|
||||
option_type='int', ctype='int',
|
||||
long_text='''
|
||||
The minimum number of lines scrolled by the mouse wheel. The :opt:`scroll multiplier <wheel_scroll_multiplier>`
|
||||
only takes effect after it reaches this number. Note that this is only used for
|
||||
low precision scrolling devices like wheel mice that scroll by very small amounts when using the wheel.
|
||||
With a negative number, the minimum number of lines will always be added.
|
||||
'''
|
||||
)
|
||||
|
||||
@@ -377,7 +404,7 @@ The program with which to open URLs that are clicked on. The special value
|
||||
'''
|
||||
)
|
||||
|
||||
opt('url_prefixes', 'http https file ftp gemini irc gopher mailto news git',
|
||||
opt('url_prefixes', 'file ftp ftps gemini git gopher http https irc ircs kitty mailto news sftp ssh',
|
||||
option_type='url_prefixes', ctype='!url_prefixes',
|
||||
long_text='''
|
||||
The set of URL prefixes to look for when detecting a URL under the mouse cursor.
|
||||
@@ -947,7 +974,7 @@ windows, via the quit action). Negative values are converted to positive ones,
|
||||
however, with :ref:`shell_integration` enabled, using negative values means
|
||||
windows sitting at a shell prompt are not counted, only windows where some
|
||||
command is currently running. Note that if you want confirmation when closing
|
||||
individual windows, you can map the :ref:`action-close_window_with_confirmation` action.
|
||||
individual windows, you can map the :ac:`close_window_with_confirmation` action.
|
||||
'''
|
||||
)
|
||||
egr() # }}}
|
||||
@@ -995,10 +1022,11 @@ The tab bar style, can be one of:
|
||||
:file:`tab_bar.py` in the kitty config directory. For examples of how to
|
||||
write such a function, see the functions named :code:`draw_tab_with_*` in
|
||||
kitty's source code: :file:`kitty/tab_bar.py`. See also
|
||||
:disc:`this discussion <4447>` for examples from kitty users.
|
||||
:disc:`this discussion <4447>`
|
||||
for examples from kitty users.
|
||||
:code:`hidden`
|
||||
The tab bar is hidden. If you use this, you might want to create a
|
||||
mapping for the :ref:`action-select_tab`
|
||||
mapping for the :ac:`select_tab`
|
||||
action which presents you with a list of tabs and allows for easy
|
||||
switching to a tab.
|
||||
'''
|
||||
@@ -1064,7 +1092,7 @@ surround the text with quotes. See :opt:`tab_title_template` for how this is ren
|
||||
'''
|
||||
)
|
||||
|
||||
opt('tab_title_template', '"{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.default}{title}"',
|
||||
opt('tab_title_template', '"{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.tab}{title}"',
|
||||
option_type='tab_title_template',
|
||||
long_text='''
|
||||
A template to render the tab title. The default just renders the title with optional symbols for bell and activity.
|
||||
@@ -1079,7 +1107,7 @@ is done by Python's string formatting machinery, so you can use, for instance,
|
||||
:code:`{layout_name[:2].upper()}` to show only the first two letters of the
|
||||
layout name, upper-cased. If you want to style the text, you can use styling
|
||||
directives, for example:
|
||||
:code:`{fmt.fg.red}red{fmt.fg.default}normal{fmt.bg._00FF00}green
|
||||
:code:`{fmt.fg.red}red{fmt.fg.tab}normal{fmt.bg._00FF00}green
|
||||
bg{fmt.bg.tab}`. Similarly, for bold and italic:
|
||||
:code:`{fmt.bold}bold{fmt.nobold}normal{fmt.italic}italic{fmt.noitalic}`.
|
||||
Note that for backward compatibility, if :code:`{bell_symbol}` or :code:`{activity_symbol}`
|
||||
@@ -2829,9 +2857,14 @@ Show or hide the window title in the macOS window or menu-bar. A value of
|
||||
the macOS window. A value of :code:`menubar` will show the title of the
|
||||
currently active window in the macOS menu-bar, making use of otherwise wasted
|
||||
space. :code:`all` will show the title everywhere and :code:`none` hides the
|
||||
title in the window and the menu-bar.
|
||||
'''
|
||||
)
|
||||
title in the window and the menu-bar. See :opt`macos_menubar_title_max_length`
|
||||
for how to control the length of the title in the menu bar.
|
||||
''')
|
||||
|
||||
opt('macos_menubar_title_max_length', '0', option_type='positive_int', ctype='int', long_text='''
|
||||
The maximum number of characters from the window title to show in the global menubar.
|
||||
Values less than one mean there is no maximum.
|
||||
''')
|
||||
|
||||
opt('macos_custom_beam_cursor', 'no',
|
||||
option_type='to_bool',
|
||||
@@ -3539,6 +3572,10 @@ agr('shortcuts.misc', 'Miscellaneous')
|
||||
map('Toggle fullscreen',
|
||||
'toggle_fullscreen kitty_mod+f11 toggle_fullscreen',
|
||||
)
|
||||
map('Toggle fullscreen',
|
||||
'toggle_fullscreen ctrl+cmd+f toggle_fullscreen',
|
||||
only='macos',
|
||||
)
|
||||
|
||||
map('Toggle maximized',
|
||||
'toggle_maximized kitty_mod+f10 toggle_maximized',
|
||||
@@ -3627,6 +3664,11 @@ map('Reset the terminal',
|
||||
only='macos',
|
||||
)
|
||||
|
||||
map('Clear up to cursor line',
|
||||
'clear_terminal_and_scrollback cmd+k clear_terminal to_cursor active',
|
||||
only='macos',
|
||||
)
|
||||
|
||||
map('Reload kitty.conf',
|
||||
'reload_config_file kitty_mod+f5 load_config_file',
|
||||
long_text='''
|
||||
|
||||
23
kitty/options/parse.py
generated
23
kitty/options/parse.py
generated
@@ -11,12 +11,12 @@ from kitty.options.utils import (
|
||||
clipboard_control, config_or_absolute_path, copy_on_select, cursor_text_color,
|
||||
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, 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, 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
|
||||
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,
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
@@ -1029,6 +1029,9 @@ class Parser:
|
||||
def macos_hide_from_tasks(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['macos_hide_from_tasks'] = to_bool(val)
|
||||
|
||||
def macos_menubar_title_max_length(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['macos_menubar_title_max_length'] = positive_int(val)
|
||||
|
||||
def macos_option_as_alt(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['macos_option_as_alt'] = macos_option_as_alt(val)
|
||||
|
||||
@@ -1076,6 +1079,10 @@ class Parser:
|
||||
def mouse_hide_wait(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['mouse_hide_wait'] = float(val)
|
||||
|
||||
def narrow_symbols(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
for k, v in narrow_symbols(val):
|
||||
ans["narrow_symbols"][k] = v
|
||||
|
||||
def open_url_with(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['open_url_with'] = to_cmdline(val)
|
||||
|
||||
@@ -1265,6 +1272,9 @@ class Parser:
|
||||
def wayland_titlebar_color(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['wayland_titlebar_color'] = titlebar_color(val)
|
||||
|
||||
def wheel_scroll_min_lines(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['wheel_scroll_min_lines'] = int(val)
|
||||
|
||||
def wheel_scroll_multiplier(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['wheel_scroll_multiplier'] = float(val)
|
||||
|
||||
@@ -1328,6 +1338,7 @@ def create_result_dict() -> typing.Dict[str, typing.Any]:
|
||||
'exe_search_path': {},
|
||||
'font_features': {},
|
||||
'kitten_alias': {},
|
||||
'narrow_symbols': {},
|
||||
'symbol_map': {},
|
||||
'watcher': {},
|
||||
'map': [],
|
||||
|
||||
30
kitty/options/to-c-generated.h
generated
30
kitty/options/to-c-generated.h
generated
@@ -187,6 +187,19 @@ convert_from_opts_wheel_scroll_multiplier(PyObject *py_opts, Options *opts) {
|
||||
Py_DECREF(ret);
|
||||
}
|
||||
|
||||
static void
|
||||
convert_from_python_wheel_scroll_min_lines(PyObject *val, Options *opts) {
|
||||
opts->wheel_scroll_min_lines = PyLong_AsLong(val);
|
||||
}
|
||||
|
||||
static void
|
||||
convert_from_opts_wheel_scroll_min_lines(PyObject *py_opts, Options *opts) {
|
||||
PyObject *ret = PyObject_GetAttrString(py_opts, "wheel_scroll_min_lines");
|
||||
if (ret == NULL) return;
|
||||
convert_from_python_wheel_scroll_min_lines(ret, opts);
|
||||
Py_DECREF(ret);
|
||||
}
|
||||
|
||||
static void
|
||||
convert_from_python_touch_scroll_multiplier(PyObject *val, Options *opts) {
|
||||
opts->touch_scroll_multiplier = PyFloat_AsDouble(val);
|
||||
@@ -980,6 +993,19 @@ convert_from_opts_macos_show_window_title_in(PyObject *py_opts, Options *opts) {
|
||||
Py_DECREF(ret);
|
||||
}
|
||||
|
||||
static void
|
||||
convert_from_python_macos_menubar_title_max_length(PyObject *val, Options *opts) {
|
||||
opts->macos_menubar_title_max_length = PyLong_AsLong(val);
|
||||
}
|
||||
|
||||
static void
|
||||
convert_from_opts_macos_menubar_title_max_length(PyObject *py_opts, Options *opts) {
|
||||
PyObject *ret = PyObject_GetAttrString(py_opts, "macos_menubar_title_max_length");
|
||||
if (ret == NULL) return;
|
||||
convert_from_python_macos_menubar_title_max_length(ret, opts);
|
||||
Py_DECREF(ret);
|
||||
}
|
||||
|
||||
static bool
|
||||
convert_opts_from_python_opts(PyObject *py_opts, Options *opts) {
|
||||
convert_from_opts_font_size(py_opts, opts);
|
||||
@@ -1010,6 +1036,8 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) {
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_wheel_scroll_multiplier(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_wheel_scroll_min_lines(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_touch_scroll_multiplier(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_mouse_hide_wait(py_opts, opts);
|
||||
@@ -1132,5 +1160,7 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) {
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_macos_show_window_title_in(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_macos_menubar_title_max_length(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
13
kitty/options/types.py
generated
13
kitty/options/types.py
generated
@@ -370,6 +370,7 @@ option_names = ( # {{{
|
||||
'listen_on',
|
||||
'macos_custom_beam_cursor',
|
||||
'macos_hide_from_tasks',
|
||||
'macos_menubar_title_max_length',
|
||||
'macos_option_as_alt',
|
||||
'macos_quit_when_last_window_closed',
|
||||
'macos_show_window_title_in',
|
||||
@@ -386,6 +387,7 @@ option_names = ( # {{{
|
||||
'mark3_foreground',
|
||||
'mouse_hide_wait',
|
||||
'mouse_map',
|
||||
'narrow_symbols',
|
||||
'open_url_with',
|
||||
'placement_strategy',
|
||||
'pointer_shape_when_dragging',
|
||||
@@ -435,6 +437,7 @@ option_names = ( # {{{
|
||||
'visual_window_select_characters',
|
||||
'watcher',
|
||||
'wayland_titlebar_color',
|
||||
'wheel_scroll_min_lines',
|
||||
'wheel_scroll_multiplier',
|
||||
'window_alert_on_bell',
|
||||
'window_border_width',
|
||||
@@ -516,6 +519,7 @@ class Options:
|
||||
listen_on: str = 'none'
|
||||
macos_custom_beam_cursor: bool = False
|
||||
macos_hide_from_tasks: bool = False
|
||||
macos_menubar_title_max_length: int = 0
|
||||
macos_option_as_alt: int = 0
|
||||
macos_quit_when_last_window_closed: bool = False
|
||||
macos_show_window_title_in: choices_for_macos_show_window_title_in = 'all'
|
||||
@@ -565,18 +569,19 @@ class Options:
|
||||
tab_powerline_style: choices_for_tab_powerline_style = 'angled'
|
||||
tab_separator: str = ' ┇'
|
||||
tab_switch_strategy: choices_for_tab_switch_strategy = 'previous'
|
||||
tab_title_template: str = '{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.default}{title}'
|
||||
tab_title_template: str = '{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.tab}{title}'
|
||||
term: str = 'xterm-kitty'
|
||||
touch_scroll_multiplier: float = 1.0
|
||||
update_check_interval: float = 24.0
|
||||
url_color: Color = Color(0, 135, 189)
|
||||
url_excluded_characters: str = ''
|
||||
url_prefixes: typing.Tuple[str, ...] = ('http', 'https', 'file', 'ftp', 'gemini', 'irc', 'gopher', 'mailto', 'news', 'git')
|
||||
url_prefixes: typing.Tuple[str, ...] = ('file', 'ftp', 'ftps', 'gemini', 'git', 'gopher', 'http', 'https', 'irc', 'ircs', 'kitty', 'mailto', 'news', 'sftp', 'ssh') # noqa
|
||||
url_style: int = 3
|
||||
visual_bell_color: typing.Optional[kitty.fast_data_types.Color] = None
|
||||
visual_bell_duration: float = 0
|
||||
visual_window_select_characters: str = '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
wayland_titlebar_color: int = 0
|
||||
wheel_scroll_min_lines: int = 1
|
||||
wheel_scroll_multiplier: float = 5.0
|
||||
window_alert_on_bell: bool = True
|
||||
window_border_width: typing.Tuple[float, str] = (0.5, 'pt')
|
||||
@@ -592,6 +597,7 @@ class Options:
|
||||
exe_search_path: typing.Dict[str, str] = {}
|
||||
font_features: typing.Dict[str, typing.Tuple[kitty.fonts.FontFeature, ...]] = {}
|
||||
kitten_alias: typing.Dict[str, str] = {}
|
||||
narrow_symbols: typing.Dict[typing.Tuple[int, int], int] = {}
|
||||
symbol_map: typing.Dict[typing.Tuple[int, int], str] = {}
|
||||
watcher: typing.Dict[str, str] = {}
|
||||
map: typing.List[kitty.options.utils.KeyDefinition] = []
|
||||
@@ -710,6 +716,7 @@ defaults.env = {}
|
||||
defaults.exe_search_path = {}
|
||||
defaults.font_features = {}
|
||||
defaults.kitten_alias = {}
|
||||
defaults.narrow_symbols = {}
|
||||
defaults.symbol_map = {}
|
||||
defaults.watcher = {}
|
||||
defaults.map = [
|
||||
@@ -898,10 +905,12 @@ if is_macos:
|
||||
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=45), definition='change_font_size all -2.0')) # noqa
|
||||
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=45), definition='change_font_size all -2.0')) # noqa
|
||||
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=48), definition='change_font_size all 0')) # noqa
|
||||
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=12, key=102), definition='toggle_fullscreen')) # noqa
|
||||
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=10, key=115), definition='toggle_macos_secure_keyboard_entry')) # noqa
|
||||
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=12, key=32), definition='kitten unicode_input')) # noqa
|
||||
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=44), definition='edit_config_file')) # noqa
|
||||
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=10, key=114), definition='clear_terminal reset active')) # noqa
|
||||
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=107), definition='clear_terminal to_cursor active')) # noqa
|
||||
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=12, key=44), definition='load_config_file')) # noqa
|
||||
defaults.map.append(KeyDefinition(trigger=SingleKey(mods=10, key=44), definition='debug_config')) # noqa
|
||||
defaults.mouse_map = [
|
||||
|
||||
@@ -163,9 +163,13 @@ def clear_terminal(func: str, rest: str) -> FuncArgsType:
|
||||
vals = rest.strip().split(maxsplit=1)
|
||||
if len(vals) != 2:
|
||||
log_error('clear_terminal needs two arguments, using defaults')
|
||||
args: List[Union[str, bool]] = ['reset', 'active']
|
||||
args = ['reset', True]
|
||||
else:
|
||||
args = [vals[0].lower(), vals[1].lower() == 'active']
|
||||
action = vals[0].lower()
|
||||
if action not in ('reset', 'scroll', 'scrollback', 'clear', 'to_cursor',):
|
||||
log_error(f'{action} is unknown for clear_terminal, using reset')
|
||||
action = 'reset'
|
||||
args = [action, vals[1].lower() == 'active']
|
||||
return func, args
|
||||
|
||||
|
||||
@@ -207,7 +211,7 @@ def resize_window(func: str, rest: str) -> FuncArgsType:
|
||||
args = ['wider', 1]
|
||||
else:
|
||||
quality = vals[0].lower()
|
||||
if quality not in ('taller', 'shorter', 'wider', 'narrower'):
|
||||
if quality not in ('reset', 'taller', 'shorter', 'wider', 'narrower'):
|
||||
log_error(f'Invalid quality specification: {quality}')
|
||||
quality = 'wider'
|
||||
increment = 1
|
||||
@@ -814,19 +818,19 @@ def action_alias(val: str) -> Iterable[Tuple[str, str]]:
|
||||
kitten_alias = action_alias
|
||||
|
||||
|
||||
def symbol_map(val: str) -> Iterable[Tuple[Tuple[int, int], str]]:
|
||||
def symbol_map_parser(val: str, min_size: int = 2) -> Iterable[Tuple[Tuple[int, int], str]]:
|
||||
parts = val.split()
|
||||
|
||||
def abort() -> None:
|
||||
log_error(f'Symbol map: {val} is invalid, ignoring')
|
||||
|
||||
if len(parts) < 2:
|
||||
if len(parts) < min_size:
|
||||
return abort()
|
||||
family = ' '.join(parts[1:])
|
||||
|
||||
def to_chr(x: str) -> int:
|
||||
if not x.startswith('U+'):
|
||||
raise ValueError()
|
||||
raise ValueError(f'{x} is not a unicode codepoint of the form U+number')
|
||||
return int(x[2:], 16)
|
||||
|
||||
for x in parts[0].split(','):
|
||||
@@ -841,6 +845,15 @@ def symbol_map(val: str) -> Iterable[Tuple[Tuple[int, int], str]]:
|
||||
yield (a, b), family
|
||||
|
||||
|
||||
def symbol_map(val: str) -> Iterable[Tuple[Tuple[int, int], str]]:
|
||||
yield from symbol_map_parser(val)
|
||||
|
||||
|
||||
def narrow_symbols(val: str) -> Iterable[Tuple[Tuple[int, int], int]]:
|
||||
for x, y in symbol_map_parser(val, min_size=1):
|
||||
yield x, int(y or 1)
|
||||
|
||||
|
||||
def parse_key_action(action: str, action_type: str = 'map') -> KeyAction:
|
||||
parts = action.strip().split(maxsplit=1)
|
||||
func = parts[0]
|
||||
|
||||
@@ -31,7 +31,7 @@ class WindowSizeData(NamedTuple):
|
||||
|
||||
def sanitize_window_size(x: Any) -> int:
|
||||
ans = int(x)
|
||||
return max(20, min(ans, 100000))
|
||||
return max(20, min(ans, 50000))
|
||||
|
||||
|
||||
def initial_window_size_func(opts: WindowSizeData, cached_values: Dict[str, Any]) -> Callable[[int, int, float, float, float, float], Tuple[int, int]]:
|
||||
|
||||
@@ -43,8 +43,7 @@ requires :ref:`shell_integration` to be enabled.
|
||||
--ansi
|
||||
type=bool-set
|
||||
By default, only plain text is returned. If you specify this flag, the text will
|
||||
include the formatting escape codes for colors/bold/italic/etc. Note that when
|
||||
getting the current selection, the result is always plain text.
|
||||
include the formatting escape codes for colors/bold/italic/etc.
|
||||
|
||||
|
||||
--add-cursor
|
||||
@@ -83,7 +82,7 @@ Clear the selection in the matched window, if any
|
||||
from kitty.window import CommandOutput
|
||||
window = self.windows_for_match_payload(boss, window, payload_get)[0]
|
||||
if payload_get('extent') == 'selection':
|
||||
ans = window.text_for_selection()
|
||||
ans = window.text_for_selection(as_ansi=payload_get('ansi'))
|
||||
elif payload_get('extent') == 'first_cmd_output_on_screen':
|
||||
ans = window.cmd_output(
|
||||
CommandOutput.first_on_screen,
|
||||
|
||||
181
kitty/screen.c
181
kitty/screen.c
@@ -1516,8 +1516,7 @@ int
|
||||
screen_cursor_at_a_shell_prompt(const Screen *self) {
|
||||
if (self->cursor->y >= self->lines || self->linebuf != self->main_linebuf || !screen_is_cursor_visible(self)) return -1;
|
||||
for (index_type y=self->cursor->y + 1; y-- > 0; ) {
|
||||
linebuf_init_line(self->linebuf, y);
|
||||
switch(self->linebuf->line->attrs.prompt_kind) {
|
||||
switch(self->linebuf->line_attrs[y].prompt_kind) {
|
||||
case OUTPUT_START:
|
||||
return -1;
|
||||
case PROMPT_START:
|
||||
@@ -1538,8 +1537,9 @@ screen_fake_move_cursor_to_position(Screen *self, index_type start_x, index_type
|
||||
else { start = &b; end = &a; key = GLFW_FKEY_RIGHT; }
|
||||
unsigned int count = 0;
|
||||
|
||||
for (unsigned y = start->y, x = start->x; y <= end->y; y++) {
|
||||
for (unsigned y = start->y, x = start->x; y <= end->y && y < self->lines; y++) {
|
||||
unsigned x_limit = y == end->y ? end->x : self->columns;
|
||||
x_limit = MIN(x_limit, self->columns);
|
||||
bool found_non_empty_cell = false;
|
||||
while (x < x_limit) {
|
||||
unsigned int w = linebuf_char_width_at(self->linebuf, x, y);
|
||||
@@ -1616,6 +1616,15 @@ screen_erase_in_line(Screen *self, unsigned int how, bool private) {
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
screen_clear_scrollback(Screen *self) {
|
||||
historybuf_clear(self->historybuf);
|
||||
if (self->scrolled_by != 0) {
|
||||
self->scrolled_by = 0;
|
||||
self->scroll_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
screen_erase_in_display(Screen *self, unsigned int how, bool private) {
|
||||
/* Erases display in a specific way.
|
||||
@@ -1662,11 +1671,7 @@ screen_erase_in_display(Screen *self, unsigned int how, bool private) {
|
||||
if (how == 1) linebuf_clear_attrs_and_dirty(self->linebuf, self->cursor->y);
|
||||
}
|
||||
if (how == 3 && self->linebuf == self->main_linebuf) {
|
||||
historybuf_clear(self->historybuf);
|
||||
if (self->scrolled_by != 0) {
|
||||
self->scrolled_by = 0;
|
||||
self->scroll_changed = true;
|
||||
}
|
||||
screen_clear_scrollback(self);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1682,13 +1687,15 @@ screen_insert_lines(Screen *self, unsigned int count) {
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
screen_scroll_until_cursor(Screen *self) {
|
||||
unsigned int num_lines_to_scroll = MIN(self->margin_bottom, self->cursor->y + 1);
|
||||
index_type y = self->cursor->y;
|
||||
static void
|
||||
screen_scroll_until_cursor_prompt(Screen *self) {
|
||||
int q = screen_cursor_at_a_shell_prompt(self);
|
||||
unsigned int y = q > -1 ? (unsigned int)q : self->cursor->y;
|
||||
unsigned int num_lines_to_scroll = MIN(self->margin_bottom, y);
|
||||
unsigned int final_y = num_lines_to_scroll <= self->cursor->y ? self->cursor->y - num_lines_to_scroll : 0;
|
||||
self->cursor->y = self->margin_bottom;
|
||||
while (num_lines_to_scroll--) screen_index(self);
|
||||
self->cursor->y = y;
|
||||
self->cursor->y = final_y;
|
||||
}
|
||||
|
||||
void
|
||||
@@ -2328,6 +2335,7 @@ iteration_data(const Screen *self, const Selection *sel, IterationData *ans, int
|
||||
ans->y += self->scrolled_by; ans->y_limit += self->scrolled_by;
|
||||
}
|
||||
ans->y = MAX(ans->y, min_y);
|
||||
ans->y_limit = MAX(ans->y, ans->y_limit); // iteration is from y to y_limit
|
||||
}
|
||||
|
||||
static XRange
|
||||
@@ -2397,8 +2405,25 @@ screen_apply_selection(Screen *self, void *address, size_t size) {
|
||||
self->url_ranges.last_rendered_count = self->url_ranges.count;
|
||||
}
|
||||
|
||||
static index_type
|
||||
limit_without_trailing_whitespace(const Line *line, index_type limit) {
|
||||
if (!limit) return limit;
|
||||
if (limit > line->xnum) limit = line->xnum;
|
||||
while (limit > 0) {
|
||||
CPUCell *cell = line->cpu_cells + limit - 1;
|
||||
if (cell->cc_idx[0]) break;
|
||||
switch(cell->ch) {
|
||||
case ' ': case '\t': case '\n': case '\r': case 0: break;
|
||||
default:
|
||||
return limit;
|
||||
}
|
||||
limit--;
|
||||
}
|
||||
return limit;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
text_for_range(Screen *self, const Selection *sel, bool insert_newlines) {
|
||||
text_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool strip_trailing_whitespace) {
|
||||
IterationData idata;
|
||||
iteration_data(self, sel, &idata, -self->historybuf->count, false);
|
||||
int limit = MIN((int)self->lines, idata.y_limit);
|
||||
@@ -2408,13 +2433,67 @@ text_for_range(Screen *self, const Selection *sel, bool insert_newlines) {
|
||||
Line *line = range_line_(self, y);
|
||||
XRange xr = xrange_for_iteration(&idata, y, line);
|
||||
char leading_char = (i > 0 && insert_newlines && !line->attrs.continued) ? '\n' : 0;
|
||||
PyObject *text = unicode_in_range(line, xr.x, xr.x_limit, true, leading_char, false);
|
||||
index_type x_limit = xr.x_limit;
|
||||
if (strip_trailing_whitespace) {
|
||||
index_type new_limit = limit_without_trailing_whitespace(line, x_limit);
|
||||
if (new_limit != x_limit) {
|
||||
x_limit = new_limit;
|
||||
if (!x_limit) {
|
||||
PyObject *text = PyUnicode_FromString("\n");
|
||||
if (text == NULL) { Py_DECREF(ans); return PyErr_NoMemory(); }
|
||||
PyTuple_SET_ITEM(ans, i, text);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
PyObject *text = unicode_in_range(line, xr.x, x_limit, true, leading_char, false);
|
||||
if (text == NULL) { Py_DECREF(ans); return PyErr_NoMemory(); }
|
||||
PyTuple_SET_ITEM(ans, i, text);
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
ansi_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool strip_trailing_whitespace) {
|
||||
IterationData idata;
|
||||
iteration_data(self, sel, &idata, -self->historybuf->count, false);
|
||||
int limit = MIN((int)self->lines, idata.y_limit);
|
||||
DECREF_AFTER_FUNCTION PyObject *ans = PyTuple_New(limit - idata.y + 1);
|
||||
DECREF_AFTER_FUNCTION PyObject *nl = PyUnicode_FromString("\n");
|
||||
if (!ans || !nl) return NULL;
|
||||
ANSIBuf output = {0};
|
||||
const GPUCell *prev_cell = NULL;
|
||||
bool has_escape_codes = false;
|
||||
for (int i = 0, y = idata.y; y < limit; y++, i++) {
|
||||
Line *line = range_line_(self, y);
|
||||
XRange xr = xrange_for_iteration(&idata, y, line);
|
||||
output.len = 0;
|
||||
char_type prefix_char = 0;
|
||||
if (i > 0 && insert_newlines && !line->attrs.continued) prefix_char = '\n';
|
||||
index_type x_limit = xr.x_limit;
|
||||
if (strip_trailing_whitespace) {
|
||||
index_type new_limit = limit_without_trailing_whitespace(line, x_limit);
|
||||
if (new_limit != x_limit) {
|
||||
x_limit = new_limit;
|
||||
if (!x_limit) {
|
||||
PyTuple_SET_ITEM(ans, i, nl);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (line_as_ansi(line, &output, &prev_cell, xr.x, x_limit, prefix_char)) has_escape_codes = true;
|
||||
PyObject *t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len);
|
||||
if (!t) return NULL;
|
||||
PyTuple_SET_ITEM(ans, i, t);
|
||||
}
|
||||
PyObject *t = PyUnicode_FromFormat("%s%s", has_escape_codes ? "\x1b[m" : "", output.active_hyperlink_id ? "\x1b]8;;\x1b\\" : "");
|
||||
if (!t) return NULL;
|
||||
PyTuple_SET_ITEM(ans, PyTuple_GET_SIZE(ans) - 1, t);
|
||||
Py_INCREF(ans);
|
||||
return ans;
|
||||
}
|
||||
|
||||
|
||||
static hyperlink_id_type
|
||||
hyperlink_id_for_range(Screen *self, const Selection *sel) {
|
||||
IterationData idata;
|
||||
@@ -2431,7 +2510,7 @@ hyperlink_id_for_range(Screen *self, const Selection *sel) {
|
||||
|
||||
static PyObject*
|
||||
extend_tuple(PyObject *a, PyObject *b) {
|
||||
Py_ssize_t bs = PyBytes_GET_SIZE(b);
|
||||
Py_ssize_t bs = PyTuple_GET_SIZE(b);
|
||||
if (bs < 1) return a;
|
||||
Py_ssize_t off = PyTuple_GET_SIZE(a);
|
||||
if (_PyTuple_Resize(&a, off + bs) != 0) return NULL;
|
||||
@@ -2450,7 +2529,7 @@ current_url_text(Screen *self, PyObject *args UNUSED) {
|
||||
for (size_t i = 0; i < self->url_ranges.count; i++) {
|
||||
Selection *s = self->url_ranges.items + i;
|
||||
if (!is_selection_empty(s)) {
|
||||
PyObject *temp = text_for_range(self, s, false);
|
||||
PyObject *temp = text_for_range(self, s, false, false);
|
||||
if (!temp) goto error;
|
||||
PyObject *text = PyUnicode_Join(empty_string, temp);
|
||||
Py_CLEAR(temp);
|
||||
@@ -2659,6 +2738,7 @@ typedef struct OutputOffset {
|
||||
Screen *screen;
|
||||
int start;
|
||||
unsigned num_lines;
|
||||
bool reached_upper_limit;
|
||||
} OutputOffset;
|
||||
|
||||
static Line*
|
||||
@@ -2711,7 +2791,10 @@ find_cmd_output(Screen *self, OutputOffset *oo, index_type start_screen_y, unsig
|
||||
}
|
||||
y1--;
|
||||
}
|
||||
if (y1 < upward_limit) start = upward_limit;
|
||||
if (y1 < upward_limit) {
|
||||
oo->reached_upper_limit = true;
|
||||
start = upward_limit;
|
||||
}
|
||||
found_output = true; found_prompt = true;
|
||||
}
|
||||
|
||||
@@ -2771,8 +2854,12 @@ cmd_output(Screen *self, PyObject *args) {
|
||||
PyErr_Format(PyExc_KeyError, "%u is not a valid type of command", which);
|
||||
return NULL;
|
||||
}
|
||||
if (found) return as_text_generic(as_text_args, &oo, get_line_from_offset, oo.num_lines, &self->as_ansi_buf);
|
||||
Py_RETURN_NONE;
|
||||
if (found) {
|
||||
DECREF_AFTER_FUNCTION PyObject *ret = as_text_generic(as_text_args, &oo, get_line_from_offset, oo.num_lines, &self->as_ansi_buf);
|
||||
if (!ret) return NULL;
|
||||
}
|
||||
if (oo.reached_upper_limit && self->linebuf == self->main_linebuf && OPT(scrollback_pager_history_size) > 0) Py_RETURN_TRUE;
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -2956,7 +3043,8 @@ is_using_alternate_linebuf(Screen *self, PyObject *a UNUSED) {
|
||||
WRAP1E(cursor_back, 1, -1)
|
||||
WRAP1B(erase_in_line, 0)
|
||||
WRAP1B(erase_in_display, 0)
|
||||
WRAP0(scroll_until_cursor)
|
||||
WRAP0(scroll_until_cursor_prompt)
|
||||
WRAP0(clear_scrollback)
|
||||
|
||||
#define MODE_GETSET(name, uname) \
|
||||
static PyObject* name##_get(Screen *self, void UNUSED *closure) { PyObject *ans = self->modes.m##uname ? Py_True : Py_False; Py_INCREF(ans); return ans; } \
|
||||
@@ -3082,10 +3170,10 @@ copy_colors_from(Screen *self, Screen *other) {
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
text_for_selections(Screen *self, Selections *selections) {
|
||||
text_for_selections(Screen *self, Selections *selections, bool ansi, bool strip_trailing_whitespace) {
|
||||
PyObject *lines = NULL;
|
||||
for (size_t i = 0; i < selections->count; i++) {
|
||||
PyObject *temp = text_for_range(self, selections->items + i, true);
|
||||
PyObject *temp = ansi ? ansi_for_range(self, selections->items +i, true, strip_trailing_whitespace) : text_for_range(self, selections->items + i, true, strip_trailing_whitespace);
|
||||
if (temp) {
|
||||
if (lines) {
|
||||
lines = extend_tuple(lines, temp);
|
||||
@@ -3099,13 +3187,17 @@ text_for_selections(Screen *self, Selections *selections) {
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
text_for_selection(Screen *self, PyObject *a UNUSED) {
|
||||
return text_for_selections(self, &self->selections);
|
||||
text_for_selection(Screen *self, PyObject *args) {
|
||||
int ansi = 0, strip_trailing_whitespace = 0;
|
||||
if (!PyArg_ParseTuple(args, "|pp", &ansi, &strip_trailing_whitespace)) return NULL;
|
||||
return text_for_selections(self, &self->selections, ansi, strip_trailing_whitespace);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
text_for_marked_url(Screen *self, PyObject *a UNUSED) {
|
||||
return text_for_selections(self, &self->url_ranges);
|
||||
text_for_marked_url(Screen *self, PyObject *args) {
|
||||
int ansi = 0, strip_trailing_whitespace = 0;
|
||||
if (!PyArg_ParseTuple(args, "|pp", &ansi, &strip_trailing_whitespace)) return NULL;
|
||||
return text_for_selections(self, &self->url_ranges, ansi, strip_trailing_whitespace);
|
||||
}
|
||||
|
||||
|
||||
@@ -3711,6 +3803,33 @@ reverse_scroll(Screen *self, PyObject *args) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
|
||||
static PyObject*
|
||||
scroll_prompt_to_bottom(Screen *self, PyObject *args UNUSED) {
|
||||
if (self->linebuf != self->main_linebuf || !self->historybuf->count) Py_RETURN_NONE;
|
||||
int q = screen_cursor_at_a_shell_prompt(self);
|
||||
index_type limit_y = q > -1 ? (unsigned int)q : self->cursor->y;
|
||||
index_type y = self->lines - 1;
|
||||
// not before prompt or cursor line
|
||||
while (y > limit_y) {
|
||||
Line *line = checked_range_line(self, y);
|
||||
if (!line || line_length(line)) break;
|
||||
y--;
|
||||
}
|
||||
// don't scroll back beyond the history buffer range
|
||||
unsigned int count = MIN(self->lines - (y + 1), self->historybuf->count);
|
||||
if (count > 0) {
|
||||
_reverse_scroll(self, count, true);
|
||||
screen_cursor_down(self, count);
|
||||
}
|
||||
// always scroll to the bottom
|
||||
if (self->scrolled_by != 0) {
|
||||
self->scrolled_by = 0;
|
||||
self->scroll_changed = true;
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
dump_lines_with_attrs(Screen *self, PyObject *accum) {
|
||||
int y = (self->linebuf == self->main_linebuf) ? -self->historybuf->count : 0;
|
||||
@@ -3777,11 +3896,13 @@ static PyMethodDef methods[] = {
|
||||
MND(cursor_back, METH_VARARGS)
|
||||
MND(erase_in_line, METH_VARARGS)
|
||||
MND(erase_in_display, METH_VARARGS)
|
||||
MND(scroll_until_cursor, METH_NOARGS)
|
||||
MND(clear_scrollback, METH_NOARGS)
|
||||
MND(scroll_until_cursor_prompt, METH_NOARGS)
|
||||
MND(hyperlinks_as_list, METH_NOARGS)
|
||||
MND(garbage_collect_hyperlink_pool, METH_NOARGS)
|
||||
MND(hyperlink_for_id, METH_O)
|
||||
MND(reverse_scroll, METH_VARARGS)
|
||||
MND(scroll_prompt_to_bottom, METH_NOARGS)
|
||||
METHOD(current_char_width, METH_NOARGS)
|
||||
MND(insert_lines, METH_VARARGS)
|
||||
MND(delete_lines, METH_VARARGS)
|
||||
@@ -3817,8 +3938,8 @@ static PyMethodDef methods[] = {
|
||||
MND(detect_url, METH_VARARGS)
|
||||
MND(rescale_images, METH_NOARGS)
|
||||
MND(current_key_encoding_flags, METH_NOARGS)
|
||||
MND(text_for_selection, METH_NOARGS)
|
||||
MND(text_for_marked_url, METH_NOARGS)
|
||||
MND(text_for_selection, METH_VARARGS)
|
||||
MND(text_for_marked_url, METH_VARARGS)
|
||||
MND(is_rectangle_select, METH_NOARGS)
|
||||
MND(scroll, METH_VARARGS)
|
||||
MND(scroll_to_prompt, METH_VARARGS)
|
||||
|
||||
@@ -314,7 +314,7 @@ cell_update_uniform_block(ssize_t vao_idx, Screen *screen, int uniform_buffer, c
|
||||
}
|
||||
rd->use_cell_for_selection_bg = IS_SPECIAL_COLOR(highlight_bg) ? 1. : 0.;
|
||||
// Cursor position
|
||||
enum { BLOCK_IDX = 0, BEAM_IDX = 8, UNDERLINE_IDX = 9, UNFOCUSED_IDX = 10 };
|
||||
enum { BLOCK_IDX = 0, BEAM_IDX = NUM_UNDERLINE_STYLES + 3, UNDERLINE_IDX = NUM_UNDERLINE_STYLES + 4, UNFOCUSED_IDX = NUM_UNDERLINE_STYLES + 5 };
|
||||
if (cursor->is_visible) {
|
||||
rd->cursor_x = screen->cursor->x, rd->cursor_y = screen->cursor->y;
|
||||
if (cursor->is_focused) {
|
||||
|
||||
@@ -3,61 +3,16 @@
|
||||
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from contextlib import suppress
|
||||
from typing import Optional, Union, Dict
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from .options.types import Options
|
||||
from .config import atomic_save
|
||||
from .constants import shell_integration_dir
|
||||
from .utils import log_error, resolved_shell
|
||||
|
||||
posix_template = '''\
|
||||
# BEGIN_KITTY_SHELL_INTEGRATION
|
||||
if test -n "$KITTY_INSTALLATION_DIR" -a -e "$KITTY_INSTALLATION_DIR/{path}"; then source "$KITTY_INSTALLATION_DIR/{path}"; fi
|
||||
# END_KITTY_SHELL_INTEGRATION\
|
||||
'''
|
||||
from .options.types import Options
|
||||
from .utils import log_error, which
|
||||
|
||||
|
||||
def atomic_write(path: str, data: Union[str, bytes]) -> None:
|
||||
if isinstance(data, str):
|
||||
data = data.encode('utf-8')
|
||||
atomic_save(data, path)
|
||||
|
||||
|
||||
def safe_read(path: str) -> str:
|
||||
with suppress(FileNotFoundError):
|
||||
with open(path) as f:
|
||||
return f.read()
|
||||
return ''
|
||||
|
||||
|
||||
def setup_integration(shell_name: str, rc_path: str, template: str = posix_template) -> None:
|
||||
import re
|
||||
rc_path = os.path.realpath(rc_path)
|
||||
rc = safe_read(rc_path)
|
||||
integration = template.format(path=f"shell-integration/{shell_name}/kitty.{shell_name}")
|
||||
newrc, num_subs = re.subn(
|
||||
r'^# BEGIN_KITTY_SHELL_INTEGRATION.+?^# END_KITTY_SHELL_INTEGRATION',
|
||||
integration, rc, flags=re.DOTALL | re.MULTILINE)
|
||||
if num_subs < 1:
|
||||
newrc = newrc.rstrip() + '\n\n' + integration
|
||||
if newrc != rc:
|
||||
atomic_write(rc_path, newrc)
|
||||
|
||||
|
||||
def setup_zsh_integration(env: Dict[str, str]) -> None:
|
||||
pass # this is handled in the zsh env modifier
|
||||
|
||||
|
||||
def setup_bash_integration(env: Dict[str, str]) -> None:
|
||||
setup_integration('bash', os.path.expanduser('~/.bashrc'))
|
||||
|
||||
|
||||
def setup_fish_integration(env: Dict[str, str]) -> None:
|
||||
pass # this is handled in the fish env modifier
|
||||
|
||||
|
||||
def setup_fish_env(env: Dict[str, str]) -> None:
|
||||
def setup_fish_env(env: Dict[str, str], argv: List[str]) -> None:
|
||||
val = env.get('XDG_DATA_DIRS')
|
||||
env['KITTY_FISH_XDG_DATA_DIR'] = shell_integration_dir
|
||||
if not val:
|
||||
@@ -68,13 +23,13 @@ def setup_fish_env(env: Dict[str, str]) -> None:
|
||||
env['XDG_DATA_DIRS'] = os.pathsep.join(dirs)
|
||||
|
||||
|
||||
def is_new_zsh_install(env: Dict[str, str]) -> bool:
|
||||
def is_new_zsh_install(env: Dict[str, str], zdotdir: Optional[str]) -> bool:
|
||||
# if ZDOTDIR is empty, zsh will read user rc files from /
|
||||
# if there aren't any, it'll run zsh-newuser-install
|
||||
# the latter will bail if there are rc files in $HOME
|
||||
zdotdir = env.get('ZDOTDIR')
|
||||
if not zdotdir:
|
||||
zdotdir = os.path.expanduser('~')
|
||||
zdotdir = env.get('HOME', os.path.expanduser('~'))
|
||||
assert isinstance(zdotdir, str)
|
||||
if zdotdir == '~':
|
||||
return True
|
||||
for q in ('.zshrc', '.zshenv', '.zprofile', '.zlogin'):
|
||||
@@ -83,12 +38,25 @@ def is_new_zsh_install(env: Dict[str, str]) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def setup_zsh_env(env: Dict[str, str]) -> None:
|
||||
if is_new_zsh_install(env):
|
||||
# dont prevent zsh-newuser-install from running
|
||||
# zsh-newuser-install never runs as root but we assume that it does
|
||||
return
|
||||
def get_zsh_zdotdir_from_global_zshenv(env: Dict[str, str], argv: List[str]) -> Optional[str]:
|
||||
exe = which(argv[0], only_system=True) or 'zsh'
|
||||
with suppress(Exception):
|
||||
return subprocess.check_output([exe, '--norcs', '--interactive', '-c', 'echo -n $ZDOTDIR'], env=env).decode('utf-8')
|
||||
return None
|
||||
|
||||
|
||||
def setup_zsh_env(env: Dict[str, str], argv: List[str]) -> None:
|
||||
zdotdir = env.get('ZDOTDIR')
|
||||
if is_new_zsh_install(env, zdotdir):
|
||||
if zdotdir is None:
|
||||
# Try to get ZDOTDIR from /etc/zshenv, when all startup files are not present
|
||||
zdotdir = get_zsh_zdotdir_from_global_zshenv(env, argv)
|
||||
if zdotdir is None or is_new_zsh_install(env, zdotdir):
|
||||
return
|
||||
else:
|
||||
# dont prevent zsh-newuser-install from running
|
||||
# zsh-newuser-install never runs as root but we assume that it does
|
||||
return
|
||||
if zdotdir is not None:
|
||||
env['KITTY_ORIG_ZDOTDIR'] = zdotdir
|
||||
else:
|
||||
@@ -98,48 +66,62 @@ def setup_zsh_env(env: Dict[str, str]) -> None:
|
||||
env['ZDOTDIR'] = os.path.join(shell_integration_dir, 'zsh')
|
||||
|
||||
|
||||
SUPPORTED_SHELLS = {
|
||||
'zsh': setup_zsh_integration,
|
||||
'bash': setup_bash_integration,
|
||||
'fish': setup_fish_integration,
|
||||
}
|
||||
def setup_bash_env(env: Dict[str, str], argv: List[str]) -> None:
|
||||
inject = {'1'}
|
||||
posix_env = rcfile = ''
|
||||
remove_args = set()
|
||||
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}
|
||||
env['ENV'] = os.path.join(shell_integration_dir, 'bash', 'kitty.bash')
|
||||
env['KITTY_BASH_INJECT'] = ' '.join(inject)
|
||||
if posix_env:
|
||||
env['KITTY_BASH_POSIX_ENV'] = posix_env
|
||||
if rcfile:
|
||||
env['KITTY_BASH_RCFILE'] = rcfile
|
||||
for i in sorted(remove_args, reverse=True):
|
||||
del argv[i]
|
||||
if 'HISTFILE' not in env and 'posix' not in inject:
|
||||
# In POSIX mode the default history file is ~/.sh_history instead of ~/.bash_history
|
||||
env['HISTFILE'] = os.path.expanduser('~/.bash_history')
|
||||
env['KITTY_BASH_UNEXPORT_HISTFILE'] = '1'
|
||||
argv.insert(1, '--posix')
|
||||
|
||||
|
||||
ENV_MODIFIERS = {
|
||||
'fish': setup_fish_env,
|
||||
'zsh': setup_zsh_env,
|
||||
'bash': setup_bash_env,
|
||||
}
|
||||
|
||||
|
||||
def get_supported_shell_name(path: str) -> Optional[str]:
|
||||
name = os.path.basename(path).split('.')[0].lower()
|
||||
name = name.replace('-', '')
|
||||
if name in SUPPORTED_SHELLS:
|
||||
return name
|
||||
return None
|
||||
name = os.path.basename(path)
|
||||
if name.lower().endswith('.exe'):
|
||||
name = name.rpartition('.')[0]
|
||||
if name.startswith('-'):
|
||||
name = name[1:]
|
||||
return name if name in ENV_MODIFIERS else None
|
||||
|
||||
|
||||
def shell_integration_allows_rc_modification(opts: Options) -> bool:
|
||||
return not (opts.shell_integration & {'disabled', 'no-rc'})
|
||||
|
||||
|
||||
def setup_shell_integration(opts: Options, env: Dict[str, str]) -> bool:
|
||||
if not shell_integration_allows_rc_modification(opts):
|
||||
return False
|
||||
shell = get_supported_shell_name(resolved_shell(opts)[0])
|
||||
if shell is None:
|
||||
return False
|
||||
func = SUPPORTED_SHELLS[shell]
|
||||
try:
|
||||
func(env)
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
log_error(f'Failed to setup shell integration for: {shell}')
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def modify_shell_environ(argv0: str, opts: Options, env: Dict[str, str]) -> None:
|
||||
shell = get_supported_shell_name(argv0)
|
||||
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:
|
||||
return
|
||||
env['KITTY_SHELL_INTEGRATION'] = ' '.join(opts.shell_integration)
|
||||
@@ -148,7 +130,7 @@ def modify_shell_environ(argv0: str, opts: Options, env: Dict[str, str]) -> None
|
||||
f = ENV_MODIFIERS.get(shell)
|
||||
if f is not None:
|
||||
try:
|
||||
f(env)
|
||||
f(env, argv)
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -24,6 +24,7 @@ typedef struct {
|
||||
typedef struct {
|
||||
monotonic_t visual_bell_duration, cursor_blink_interval, cursor_stop_blinking_after, mouse_hide_wait, click_interval;
|
||||
double wheel_scroll_multiplier, touch_scroll_multiplier;
|
||||
int wheel_scroll_min_lines;
|
||||
bool enable_audio_bell;
|
||||
CursorShape cursor_shape;
|
||||
float cursor_beam_thickness;
|
||||
@@ -80,6 +81,7 @@ typedef struct {
|
||||
struct {
|
||||
double outer, inner;
|
||||
} tab_bar_margin_height;
|
||||
long macos_menubar_title_max_length;
|
||||
} Options;
|
||||
|
||||
typedef struct WindowLogoRenderData {
|
||||
@@ -104,6 +106,7 @@ typedef struct {
|
||||
monotonic_t at;
|
||||
int button, modifiers;
|
||||
double x, y;
|
||||
unsigned long num;
|
||||
} Click;
|
||||
|
||||
#define CLICK_QUEUE_SZ 3
|
||||
@@ -298,12 +301,14 @@ typedef enum {
|
||||
NEXT_TAB,
|
||||
PREVIOUS_TAB,
|
||||
DETACH_TAB,
|
||||
OPEN_FILE,
|
||||
LAUNCH_URLS,
|
||||
NEW_WINDOW,
|
||||
CLOSE_WINDOW,
|
||||
RESET_TERMINAL,
|
||||
CLEAR_TERMINAL_AND_SCROLLBACK,
|
||||
RELOAD_CONFIG,
|
||||
TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY,
|
||||
TOGGLE_FULLSCREEN,
|
||||
|
||||
NUM_COCOA_PENDING_ACTIONS
|
||||
} CocoaPendingAction;
|
||||
@@ -339,3 +344,4 @@ 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);
|
||||
void set_ignore_os_keyboard_processing(bool enabled);
|
||||
void update_menu_bar_title(PyObject *title UNUSED);
|
||||
|
||||
@@ -203,7 +203,7 @@ def draw_title(draw_data: DrawData, screen: Screen, tab: TabBarData, index: int)
|
||||
if eval_locals['activity_symbol'] and not template_has_field(template, 'activity_symbol'):
|
||||
prefix += '{activity_symbol}'
|
||||
if prefix:
|
||||
template = '{fmt.fg.red}' + prefix + '{fmt.fg.default}' + template
|
||||
template = '{fmt.fg.red}' + prefix + '{fmt.fg.tab}' + template
|
||||
try:
|
||||
title = eval(compile_template(template), {'__builtins__': {}}, eval_locals)
|
||||
except Exception as e:
|
||||
|
||||
@@ -316,6 +316,9 @@ class Tab: # {{{
|
||||
See :ref:`window_resizing` for details.
|
||||
''')
|
||||
def resize_window(self, quality: str, increment: int) -> None:
|
||||
if quality == 'reset':
|
||||
self.reset_window_sizes()
|
||||
return
|
||||
if increment < 1:
|
||||
raise ValueError(increment)
|
||||
is_horizontal = quality in ('wider', 'narrower')
|
||||
@@ -383,12 +386,12 @@ class Tab: # {{{
|
||||
import shlex
|
||||
with suppress(OSError):
|
||||
with open(old_exe) as f:
|
||||
cmd = [kitty_exe(), '+hold']
|
||||
if f.read(2) == '#!':
|
||||
line = f.read(4096).splitlines()[0]
|
||||
cmd += shlex.split(line) + [old_exe]
|
||||
cmd[:0] = shlex.split(line)
|
||||
else:
|
||||
cmd += [resolved_shell(get_options())[0], cmd[0]]
|
||||
cmd[:0] = [resolved_shell(get_options())[0]]
|
||||
cmd[:0] = [kitty_exe(), '+hold']
|
||||
fenv: Dict[str, str] = {}
|
||||
if env:
|
||||
fenv.update(env)
|
||||
|
||||
@@ -9,7 +9,7 @@ import os
|
||||
import re
|
||||
import string
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
from contextlib import contextmanager, suppress
|
||||
from functools import lru_cache
|
||||
from time import monotonic
|
||||
from typing import (
|
||||
@@ -21,10 +21,10 @@ from .constants import (
|
||||
appname, config_dir, is_macos, is_wayland, read_kitty_resource, shell_path,
|
||||
supports_primary_selection
|
||||
)
|
||||
from .fast_data_types import Color, open_tty
|
||||
from .rgb import to_color
|
||||
from .types import run_once
|
||||
from .typing import AddressFamily, PopenType, Socket, StartupCtx
|
||||
from .fast_data_types import Color
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .fast_data_types import OSWindowSize
|
||||
@@ -61,6 +61,13 @@ def sgr_sanitizer_pat(for_splitting: bool = False) -> 're.Pattern[str]':
|
||||
return re.compile(pat)
|
||||
|
||||
|
||||
@run_once
|
||||
def kitty_ansi_sanitizer_pat() -> 're.Pattern[str]':
|
||||
# removes ANSI sequences generated by kitty's ANSI output routines. Not
|
||||
# suitable for stripping general ANSI sequences
|
||||
return re.compile(r'\x1b(?:\[[0-9;:]*?m|\].*?\x1b\\)')
|
||||
|
||||
|
||||
def platform_window_id(os_window_id: int) -> Optional[int]:
|
||||
if is_macos:
|
||||
from .fast_data_types import cocoa_window_id
|
||||
@@ -141,6 +148,19 @@ class ScreenSize(NamedTuple):
|
||||
cell_height: int
|
||||
|
||||
|
||||
def read_screen_size(fd: int = -1) -> ScreenSize:
|
||||
import array
|
||||
import fcntl
|
||||
import termios
|
||||
buf = array.array('H', [0, 0, 0, 0])
|
||||
if fd < 0:
|
||||
fd = sys.stdout.fileno()
|
||||
fcntl.ioctl(fd, termios.TIOCGWINSZ, cast(bytearray, buf))
|
||||
rows, cols, width, height = tuple(buf)
|
||||
cell_width, cell_height = width // (cols or 1), height // (rows or 1)
|
||||
return ScreenSize(rows, cols, width, height, cell_width, cell_height)
|
||||
|
||||
|
||||
class ScreenSizeGetter:
|
||||
changed = True
|
||||
Size = ScreenSize
|
||||
@@ -153,14 +173,7 @@ class ScreenSizeGetter:
|
||||
|
||||
def __call__(self) -> ScreenSize:
|
||||
if self.changed:
|
||||
import array
|
||||
import fcntl
|
||||
import termios
|
||||
buf = array.array('H', [0, 0, 0, 0])
|
||||
fcntl.ioctl(self.fd, termios.TIOCGWINSZ, cast(bytearray, buf))
|
||||
rows, cols, width, height = tuple(buf)
|
||||
cell_width, cell_height = width // (cols or 1), height // (rows or 1)
|
||||
self.ans = ScreenSize(rows, cols, width, height, cell_width, cell_height)
|
||||
self.ans = read_screen_size(self.fd)
|
||||
self.changed = False
|
||||
return cast(ScreenSize, self.ans)
|
||||
|
||||
@@ -459,7 +472,6 @@ class TTYIO:
|
||||
self.read_with_timeout = read_with_timeout
|
||||
|
||||
def __enter__(self) -> 'TTYIO':
|
||||
from .fast_data_types import open_tty
|
||||
self.tty_fd, self.original_termios = open_tty(self.read_with_timeout)
|
||||
return self
|
||||
|
||||
@@ -495,6 +507,21 @@ class TTYIO:
|
||||
break
|
||||
|
||||
|
||||
@contextmanager
|
||||
def no_echo(fd: int = -1) -> Generator[None, None, None]:
|
||||
import termios
|
||||
if fd < 0:
|
||||
fd = sys.stdin.fileno()
|
||||
old = termios.tcgetattr(fd)
|
||||
new = termios.tcgetattr(fd)
|
||||
new[3] = new[3] & ~termios.ECHO
|
||||
try:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, new)
|
||||
yield
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
||||
|
||||
|
||||
def natsort_ints(iterable: Iterable[str]) -> List[str]:
|
||||
|
||||
def convert(text: str) -> Union[int, str]:
|
||||
@@ -654,6 +681,7 @@ def which(name: str, only_system: bool = False) -> Optional[str]:
|
||||
if os.sep in name:
|
||||
return name
|
||||
import shutil
|
||||
|
||||
from .fast_data_types import get_options
|
||||
opts: Optional[Options] = None
|
||||
with suppress(RuntimeError):
|
||||
@@ -837,6 +865,7 @@ def is_kitty_gui_cmdline(*cmd: str) -> bool:
|
||||
|
||||
def reload_conf_in_all_kitties() -> None:
|
||||
import signal
|
||||
|
||||
from kitty.child import cmdline_of_process
|
||||
|
||||
for pid in get_all_processes():
|
||||
@@ -858,21 +887,22 @@ def sanitize_control_codes(text: str, replace_with: str = '') -> str:
|
||||
|
||||
|
||||
def hold_till_enter() -> None:
|
||||
import select
|
||||
import termios
|
||||
|
||||
from kittens.tui.operations import init_state, set_cursor_visible
|
||||
with suppress(BaseException):
|
||||
print(
|
||||
'\n\x1b[1;32mPress Enter to exit',
|
||||
end=init_state(alternate_screen=False, kitty_keyboard_mode=False) + set_cursor_visible(False),
|
||||
flush=True)
|
||||
with suppress(BaseException):
|
||||
fd = sys.stdin.fileno()
|
||||
old = termios.tcgetattr(fd)
|
||||
new = old[:]
|
||||
new[3] &= ~termios.ECHO # 3 == 'lflags'
|
||||
tcsetattr_flags = termios.TCSAFLUSH
|
||||
if hasattr(termios, 'TCSASOFT'):
|
||||
tcsetattr_flags |= getattr(termios, 'TCSASOFT')
|
||||
termios.tcsetattr(fd, tcsetattr_flags, new)
|
||||
with suppress(KeyboardInterrupt):
|
||||
input()
|
||||
fd, original_termios = open_tty()
|
||||
msg = '\n\r\x1b[1;32mPress Enter or Esc to exit\x1b[m'
|
||||
write_all(fd, msg)
|
||||
write_all(fd, init_state(alternate_screen=False, kitty_keyboard_mode=False) + set_cursor_visible(False))
|
||||
with no_echo(fd):
|
||||
termios.tcdrain(fd)
|
||||
while True:
|
||||
rd = select.select([fd], [], [])[0]
|
||||
if not rd:
|
||||
break
|
||||
q = os.read(fd, 1)
|
||||
if q in b'\n\r\x1b':
|
||||
break
|
||||
if q in b'\x03\x04':
|
||||
write_all(fd, msg)
|
||||
|
||||
148
kitty/window.py
148
kitty/window.py
@@ -3,6 +3,7 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import weakref
|
||||
from collections import deque
|
||||
@@ -23,17 +24,17 @@ from .constants import appname, is_macos, wakeup
|
||||
from .fast_data_types import (
|
||||
BGIMAGE_PROGRAM, BLIT_PROGRAM, CELL_BG_PROGRAM, CELL_FG_PROGRAM,
|
||||
CELL_PROGRAM, CELL_SPECIAL_PROGRAM, CURSOR_BEAM, CURSOR_BLOCK,
|
||||
CURSOR_UNDERLINE, DCS, DECORATION, DIM, GLFW_MOD_CONTROL,
|
||||
CURSOR_UNDERLINE, DCS, DECORATION, DECORATION_MASK, DIM, GLFW_MOD_CONTROL,
|
||||
GRAPHICS_ALPHA_MASK_PROGRAM, GRAPHICS_PREMULT_PROGRAM, GRAPHICS_PROGRAM,
|
||||
MARK, MARK_MASK, NO_CURSOR_SHAPE, OSC, REVERSE, SCROLL_FULL, SCROLL_LINE,
|
||||
SCROLL_PAGE, STRIKETHROUGH, TINT_PROGRAM, Color, KeyEvent, Screen,
|
||||
add_timer, add_window, cell_size_for_window, click_mouse_cmd_output,
|
||||
click_mouse_url, compile_program, current_os_window, encode_key_for_tty,
|
||||
get_boss, get_click_interval, get_clipboard_string, get_options,
|
||||
init_cell_program, mark_os_window_dirty, mouse_selection,
|
||||
move_cursor_to_mouse_if_in_prompt, pt_to_px, set_clipboard_string,
|
||||
set_titlebar_color, set_window_logo, set_window_padding,
|
||||
set_window_render_data, update_ime_position_for_window,
|
||||
MARK, MARK_MASK, NO_CURSOR_SHAPE, NUM_UNDERLINE_STYLES, OSC, REVERSE,
|
||||
SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE, STRIKETHROUGH, TINT_PROGRAM, Color,
|
||||
KeyEvent, Screen, add_timer, add_window, cell_size_for_window,
|
||||
click_mouse_cmd_output, click_mouse_url, compile_program,
|
||||
current_os_window, encode_key_for_tty, get_boss, get_click_interval,
|
||||
get_clipboard_string, get_options, init_cell_program, mark_os_window_dirty,
|
||||
mouse_selection, move_cursor_to_mouse_if_in_prompt, pt_to_px,
|
||||
set_clipboard_string, set_titlebar_color, set_window_logo,
|
||||
set_window_padding, set_window_render_data, update_ime_position_for_window,
|
||||
update_window_title, update_window_visibility
|
||||
)
|
||||
from .keys import keyboard_mode_name, mod_mask
|
||||
@@ -44,9 +45,9 @@ from .terminfo import get_capabilities
|
||||
from .types import MouseEvent, WindowGeometry, ac
|
||||
from .typing import BossType, ChildType, EdgeLiteral, TabType, TypedDict
|
||||
from .utils import (
|
||||
get_primary_selection, load_shaders, log_error, open_cmd, open_url,
|
||||
parse_color_set, resolve_custom_file, sanitize_title,
|
||||
set_primary_selection, sgr_sanitizer_pat
|
||||
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
|
||||
)
|
||||
|
||||
MatchPatternType = Union[Pattern[str], Tuple[Pattern[str], Optional[Pattern[str]]]]
|
||||
@@ -56,6 +57,16 @@ if TYPE_CHECKING:
|
||||
from .file_transmission import FileTransmission
|
||||
|
||||
|
||||
def process_title_from_child(title: str, is_base64: bool) -> str:
|
||||
if is_base64:
|
||||
from base64 import standard_b64decode
|
||||
try:
|
||||
title = standard_b64decode(title).decode('utf-8', 'replace')
|
||||
except Exception:
|
||||
title = 'undecodeable title'
|
||||
return sanitize_title(title)
|
||||
|
||||
|
||||
class WindowDict(TypedDict):
|
||||
id: int
|
||||
is_focused: bool
|
||||
@@ -156,6 +167,14 @@ def call_watchers(windowref: Callable[[], Optional['Window']], which: str, data:
|
||||
add_timer(callback, 0, False)
|
||||
|
||||
|
||||
def pagerhist(screen: Screen, as_ansi: bool = False, add_wrap_markers: bool = True, upto_output_start: bool = False) -> str:
|
||||
pht = screen.historybuf.pagerhist_as_text(upto_output_start)
|
||||
if pht and (not as_ansi or not add_wrap_markers):
|
||||
sanitizer = text_sanitizer(as_ansi, add_wrap_markers)
|
||||
pht = sanitizer(pht)
|
||||
return pht
|
||||
|
||||
|
||||
def as_text(
|
||||
screen: Screen,
|
||||
as_ansi: bool = False,
|
||||
@@ -185,13 +204,8 @@ def as_text(
|
||||
ctext += f'\x1b[{code} q'
|
||||
|
||||
if add_history:
|
||||
h: List[str] = []
|
||||
pht = screen.historybuf.pagerhist_as_text()
|
||||
if pht:
|
||||
h.append(pht)
|
||||
if h and (not as_ansi or not add_wrap_markers):
|
||||
sanitizer = text_sanitizer(as_ansi, add_wrap_markers)
|
||||
h = list(map(sanitizer, h))
|
||||
pht = pagerhist(screen, as_ansi, add_wrap_markers)
|
||||
h: List[str] = [pht] if pht else []
|
||||
screen.historybuf.as_text(h.append, as_ansi, add_wrap_markers)
|
||||
if h:
|
||||
if not screen.linebuf.is_continued(0):
|
||||
@@ -208,6 +222,15 @@ def as_text(
|
||||
return ans
|
||||
|
||||
|
||||
def multi_replace(src: str, **replacements: Any) -> str:
|
||||
r = {k: str(v) for k, v in replacements.items()}
|
||||
|
||||
def sub(m: 're.Match[str]') -> str:
|
||||
return r.get(m.group(1), m.group(1))
|
||||
|
||||
return re.sub(r'\{([A-Z_]+)\}', sub, src)
|
||||
|
||||
|
||||
class LoadShaderPrograms:
|
||||
|
||||
def __call__(self, semi_transparent: bool = False) -> None:
|
||||
@@ -220,16 +243,19 @@ class LoadShaderPrograms:
|
||||
'SPECIAL': CELL_SPECIAL_PROGRAM,
|
||||
'FOREGROUND': CELL_FG_PROGRAM,
|
||||
}.items():
|
||||
vv, ff = v.replace('WHICH_PROGRAM', which), f.replace('WHICH_PROGRAM', which)
|
||||
for gln, pyn in {
|
||||
'REVERSE_SHIFT': REVERSE,
|
||||
'STRIKE_SHIFT': STRIKETHROUGH,
|
||||
'DIM_SHIFT': DIM,
|
||||
'DECORATION_SHIFT': DECORATION,
|
||||
'MARK_SHIFT': MARK,
|
||||
'MARK_MASK': MARK_MASK,
|
||||
}.items():
|
||||
vv = vv.replace(f'{{{gln}}}', str(pyn), 1)
|
||||
ff = f.replace('{WHICH_PROGRAM}', which)
|
||||
vv = multi_replace(
|
||||
v,
|
||||
WHICH_PROGRAM=which,
|
||||
REVERSE_SHIFT=REVERSE,
|
||||
STRIKE_SHIFT=STRIKETHROUGH,
|
||||
DIM_SHIFT=DIM,
|
||||
DECORATION_SHIFT=DECORATION,
|
||||
MARK_SHIFT=MARK,
|
||||
MARK_MASK=MARK_MASK,
|
||||
DECORATION_MASK=DECORATION_MASK,
|
||||
STRIKE_SPRITE_INDEX=NUM_UNDERLINE_STYLES + 1,
|
||||
)
|
||||
if semi_transparent:
|
||||
vv = vv.replace('#define NOT_TRANSPARENT', '#define TRANSPARENT')
|
||||
ff = ff.replace('#define NOT_TRANSPARENT', '#define TRANSPARENT')
|
||||
@@ -268,7 +294,7 @@ def setup_colors(screen: Screen, opts: Options) -> None:
|
||||
|
||||
|
||||
def text_sanitizer(as_ansi: bool, add_wrap_markers: bool) -> Callable[[str], str]:
|
||||
pat = sgr_sanitizer_pat()
|
||||
pat = kitty_ansi_sanitizer_pat()
|
||||
ansi, wrap_markers = not as_ansi, not add_wrap_markers
|
||||
|
||||
def remove_wrap_markers(line: str) -> str:
|
||||
@@ -285,6 +311,23 @@ def text_sanitizer(as_ansi: bool, add_wrap_markers: bool) -> Callable[[str], str
|
||||
return remove_wrap_markers
|
||||
|
||||
|
||||
def cmd_output(screen: Screen, which: CommandOutput = CommandOutput.last_run, as_ansi: bool = False, add_wrap_markers: bool = False) -> str:
|
||||
lines: List[str] = []
|
||||
search_in_pager_hist = screen.cmd_output(which, lines.append, as_ansi, add_wrap_markers)
|
||||
if search_in_pager_hist:
|
||||
pht = pagerhist(screen, as_ansi, add_wrap_markers, True)
|
||||
if pht:
|
||||
lines.insert(0, pht)
|
||||
return ''.join(lines)
|
||||
|
||||
|
||||
def process_remote_print(msg: bytes) -> 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'))
|
||||
|
||||
|
||||
class EdgeWidths:
|
||||
left: Optional[float]
|
||||
top: Optional[float]
|
||||
@@ -347,6 +390,7 @@ class Window:
|
||||
else:
|
||||
self.watchers = global_watchers().copy()
|
||||
self.last_focused_at = 0.
|
||||
self.started_at = monotonic()
|
||||
self.current_mouse_event_button = 0
|
||||
self.current_clipboard_read_ask: Optional[bool] = None
|
||||
self.prev_osc99_cmd = NotificationCommand()
|
||||
@@ -486,9 +530,13 @@ class Window:
|
||||
def current_colors(self) -> Dict[str, Optional[int]]:
|
||||
return self.screen.color_profile.as_dict()
|
||||
|
||||
@property
|
||||
def at_prompt(self) -> bool:
|
||||
return self.screen.cursor_at_prompt()
|
||||
|
||||
@property
|
||||
def has_running_program(self) -> bool:
|
||||
return not self.screen.cursor_at_prompt()
|
||||
return not self.at_prompt
|
||||
|
||||
def matches(self, field: str, pat: MatchPatternType) -> bool:
|
||||
if not pat:
|
||||
@@ -696,8 +744,8 @@ class Window:
|
||||
# Cancel IME composition after loses focus
|
||||
update_ime_position_for_window(self.id, False, True)
|
||||
|
||||
def title_changed(self, new_title: Optional[str]) -> None:
|
||||
self.child_title = sanitize_title(new_title or self.default_title)
|
||||
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)
|
||||
if self.override_title is None:
|
||||
self.title_updated()
|
||||
|
||||
@@ -828,11 +876,7 @@ class Window:
|
||||
get_boss().handle_remote_cmd(cmd, self)
|
||||
|
||||
def handle_remote_print(self, msg: bytes) -> None:
|
||||
from base64 import standard_b64decode
|
||||
|
||||
from .cli import green
|
||||
text = standard_b64decode(msg).decode('utf-8')
|
||||
text = text.replace('\x1b', green(r'\e')).replace('\a', green(r'\a')).replace('\0', green(r'\0'))
|
||||
text = process_remote_print(msg)
|
||||
print(text, end='', file=sys.stderr)
|
||||
sys.stderr.flush()
|
||||
|
||||
@@ -1012,12 +1056,10 @@ class Window:
|
||||
self.show_cmd_output(CommandOutput.last_visited, 'Clicked command output')
|
||||
# }}}
|
||||
|
||||
def text_for_selection(self) -> str:
|
||||
lines = self.screen.text_for_selection()
|
||||
def text_for_selection(self, as_ansi: bool = False) -> str:
|
||||
sts = get_options().strip_trailing_spaces
|
||||
if sts == 'always' or (
|
||||
sts == 'smart' and not self.screen.is_rectangle_select()):
|
||||
return ''.join((ln.rstrip() or '\n') for ln in lines)
|
||||
strip_trailing_spaces = sts == 'always' or (sts == 'smart' and not self.screen.is_rectangle_select())
|
||||
lines = self.screen.text_for_selection(as_ansi, strip_trailing_spaces)
|
||||
return ''.join(lines)
|
||||
|
||||
def call_watchers(self, which: Iterable[Watcher], data: Dict[str, Any]) -> None:
|
||||
@@ -1051,9 +1093,7 @@ class Window:
|
||||
return as_text(self.screen, as_ansi, add_history, add_wrap_markers, alternate_screen, add_cursor)
|
||||
|
||||
def cmd_output(self, which: CommandOutput = CommandOutput.last_run, as_ansi: bool = False, add_wrap_markers: bool = False) -> str:
|
||||
lines: List[str] = []
|
||||
self.screen.cmd_output(which, lines.append, as_ansi, add_wrap_markers)
|
||||
return ''.join(lines)
|
||||
return cmd_output(self.screen, which, as_ansi, add_wrap_markers)
|
||||
|
||||
@property
|
||||
def cwd_of_child(self) -> Optional[str]:
|
||||
@@ -1147,6 +1187,12 @@ class Window:
|
||||
if text:
|
||||
set_clipboard_string(text)
|
||||
|
||||
@ac('cp', 'Copy the selected text from the active window to the clipboard with ANSI formatting codes')
|
||||
def copy_ansi_to_clipboard(self) -> None:
|
||||
text = self.text_for_selection(as_ansi=True)
|
||||
if text:
|
||||
set_clipboard_string(text)
|
||||
|
||||
def encoded_key(self, key_event: KeyEvent) -> bytes:
|
||||
return encode_key_for_tty(
|
||||
key=key_event.key, shifted_key=key_event.shifted_key, alternate_key=key_event.alternate_key,
|
||||
@@ -1155,7 +1201,7 @@ class Window:
|
||||
cursor_key_mode=self.screen.cursor_key_mode,
|
||||
).encode('ascii')
|
||||
|
||||
@ac('cp', 'Copy the selected text from the active window to the clipboard, if no selection, send Ctrl-C')
|
||||
@ac('cp', 'Copy the selected text from the active window to the clipboard, if no selection, send SIGINT (aka :kbd:`ctrl+c`)')
|
||||
def copy_or_interrupt(self) -> None:
|
||||
text = self.text_for_selection()
|
||||
if text:
|
||||
@@ -1163,7 +1209,7 @@ class Window:
|
||||
else:
|
||||
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 Ctrl-C')
|
||||
@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`)')
|
||||
def copy_and_clear_or_interrupt(self) -> None:
|
||||
self.copy_or_interrupt()
|
||||
self.screen.clear_selection()
|
||||
@@ -1228,6 +1274,10 @@ 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')
|
||||
def scroll_prompt_to_bottom(self) -> None:
|
||||
self.screen.scroll_prompt_to_bottom()
|
||||
|
||||
@ac('mk', 'Toggle the current marker on/off')
|
||||
def toggle_marker(self, ftype: str, spec: Union[str, Tuple[Tuple[int, str], ...]], flags: int) -> None:
|
||||
from .marks import marker_from_spec
|
||||
|
||||
@@ -1,29 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import fcntl
|
||||
import io
|
||||
import os
|
||||
import select
|
||||
import shlex
|
||||
import struct
|
||||
import sys
|
||||
import termios
|
||||
import time
|
||||
from pty import CHILD, fork
|
||||
from unittest import TestCase
|
||||
|
||||
from kitty.config import finalize_keys, finalize_mouse_mappings
|
||||
from kitty.fast_data_types import (
|
||||
Cursor, HistoryBuf, LineBuf, Screen, set_options
|
||||
Cursor, HistoryBuf, LineBuf, Screen, get_options, parse_bytes, set_options
|
||||
)
|
||||
from kitty.options.parse import merge_result_dicts
|
||||
from kitty.options.types import Options, defaults
|
||||
from kitty.types import MouseEvent
|
||||
from kitty.utils import read_screen_size
|
||||
from kitty.window import process_remote_print, process_title_from_child
|
||||
|
||||
|
||||
class Callbacks:
|
||||
|
||||
def __init__(self, opts) -> None:
|
||||
def __init__(self) -> None:
|
||||
self.clear()
|
||||
self.opts = opts
|
||||
|
||||
def write(self, data) -> None:
|
||||
self.wtcbuf += data
|
||||
|
||||
def title_changed(self, data) -> None:
|
||||
self.titlebuf += data
|
||||
def title_changed(self, data, is_base64=False) -> None:
|
||||
self.titlebuf.append(process_title_from_child(data, is_base64))
|
||||
|
||||
def icon_changed(self, data) -> None:
|
||||
self.iconbuf += data
|
||||
@@ -53,7 +63,8 @@ class Callbacks:
|
||||
|
||||
def clear(self) -> None:
|
||||
self.wtcbuf = b''
|
||||
self.iconbuf = self.titlebuf = self.colorbuf = self.ctbuf = ''
|
||||
self.iconbuf = self.colorbuf = self.ctbuf = ''
|
||||
self.titlebuf = []
|
||||
self.iutf8 = True
|
||||
self.notifications = []
|
||||
self.open_urls = []
|
||||
@@ -68,15 +79,20 @@ class Callbacks:
|
||||
|
||||
def on_mouse_event(self, event):
|
||||
ev = MouseEvent(**event)
|
||||
action_def = self.opts.mousemap.get(ev)
|
||||
opts = get_options()
|
||||
action_def = opts.mousemap.get(ev)
|
||||
if not action_def:
|
||||
return False
|
||||
self.current_mouse_button = ev.button
|
||||
for action in self.opts.alias_map.resolve_aliases(action_def, 'mouse_map'):
|
||||
for action in opts.alias_map.resolve_aliases(action_def, 'mouse_map'):
|
||||
getattr(self, action.func)(*action.args)
|
||||
self.current_mouse_button = 0
|
||||
return True
|
||||
|
||||
def handle_remote_print(self, msg):
|
||||
text = process_remote_print(msg)
|
||||
print(text, file=sys.__stderr__)
|
||||
|
||||
|
||||
def filled_line_buf(ynum=5, xnum=5, cursor=Cursor()):
|
||||
ans = LineBuf(ynum, xnum)
|
||||
@@ -120,12 +136,20 @@ class BaseTest(TestCase):
|
||||
set_options(options)
|
||||
return options
|
||||
|
||||
def cmd_to_run_python_code(self, code):
|
||||
from kitty.constants import kitty_exe
|
||||
return [kitty_exe(), '+runpy', code]
|
||||
|
||||
def create_screen(self, cols=5, lines=5, scrollback=5, cell_width=10, cell_height=20, options=None):
|
||||
opts = self.set_options(options)
|
||||
c = Callbacks(opts)
|
||||
self.set_options(options)
|
||||
c = Callbacks()
|
||||
s = Screen(c, lines, cols, scrollback, cell_width, cell_height, 0, c)
|
||||
return s
|
||||
|
||||
def create_pty(self, argv, cols=80, lines=25, scrollback=100, cell_width=10, cell_height=20, options=None, cwd=None, env=None):
|
||||
self.set_options(options)
|
||||
return PTY(argv, lines, cols, scrollback, cell_width, cell_height, cwd, env)
|
||||
|
||||
def assertEqualAttributes(self, c1, c2):
|
||||
x1, y1, c1.x, c1.y = c1.x, c1.y, 0, 0
|
||||
x2, y2, c2.x, c2.y = c2.x, c2.y, 0, 0
|
||||
@@ -133,3 +157,93 @@ class BaseTest(TestCase):
|
||||
self.assertEqual(c1, c2)
|
||||
finally:
|
||||
c1.x, c1.y, c2.x, c2.y = x1, y1, x2, y2
|
||||
|
||||
|
||||
class PTY:
|
||||
|
||||
def __init__(self, argv, rows=25, columns=80, scrollback=100, cell_width=10, cell_height=20, cwd=None, env=None):
|
||||
if isinstance(argv, str):
|
||||
argv = shlex.split(argv)
|
||||
pid, self.master_fd = fork()
|
||||
self.is_child = pid == CHILD
|
||||
self.write_buf = b''
|
||||
if self.is_child:
|
||||
while read_screen_size().width != columns * cell_width:
|
||||
time.sleep(0.01)
|
||||
if cwd:
|
||||
os.chdir(cwd)
|
||||
os.execvpe(argv[0], argv, env or os.environ)
|
||||
os.set_blocking(self.master_fd, False)
|
||||
self.cell_width = cell_width
|
||||
self.cell_height = cell_height
|
||||
self.set_window_size(rows=rows, columns=columns)
|
||||
self.callbacks = Callbacks()
|
||||
self.screen = Screen(self.callbacks, rows, columns, scrollback, cell_width, cell_height, 0, self.callbacks)
|
||||
self.received_bytes = b''
|
||||
|
||||
def __del__(self):
|
||||
if not self.is_child:
|
||||
fd = self.master_fd
|
||||
del self.master_fd
|
||||
os.close(fd)
|
||||
|
||||
def write_to_child(self, data):
|
||||
if isinstance(data, str):
|
||||
data = data.encode('utf-8')
|
||||
self.write_buf += data
|
||||
|
||||
def send_cmd_to_child(self, cmd):
|
||||
self.write_to_child(cmd + '\r')
|
||||
|
||||
def process_input_from_child(self, timeout=10):
|
||||
rd, wd, err = select.select([self.master_fd], [self.master_fd] if self.write_buf else [], [self.master_fd], timeout)
|
||||
if err:
|
||||
raise OSError('master_fd is in error condition')
|
||||
while wd:
|
||||
try:
|
||||
n = os.write(self.master_fd, self.write_buf)
|
||||
except (BlockingIOError, OSError):
|
||||
n = 0
|
||||
if not n:
|
||||
break
|
||||
self.write_buf = self.write_buf[n:]
|
||||
|
||||
bytes_read = 0
|
||||
while rd:
|
||||
try:
|
||||
data = os.read(self.master_fd, io.DEFAULT_BUFFER_SIZE)
|
||||
except (BlockingIOError, OSError):
|
||||
data = b''
|
||||
if not data:
|
||||
break
|
||||
bytes_read += len(data)
|
||||
self.received_bytes += data
|
||||
parse_bytes(self.screen, data)
|
||||
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))
|
||||
if not q():
|
||||
raise TimeoutError(f'The condition was not met. Screen contents: \n {repr(self.screen_contents())}')
|
||||
|
||||
def set_window_size(self, rows=25, columns=80):
|
||||
if hasattr(self, 'screen'):
|
||||
self.screen.resize(rows, columns)
|
||||
x_pixels = columns * self.cell_width
|
||||
y_pixels = rows * self.cell_height
|
||||
s = struct.pack('HHHH', rows, columns, x_pixels, y_pixels)
|
||||
fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, s)
|
||||
|
||||
def screen_contents(self):
|
||||
lines = []
|
||||
for i in range(self.screen.lines):
|
||||
x = str(self.screen.line(i))
|
||||
if x:
|
||||
lines.append(x)
|
||||
return '\n'.join(lines)
|
||||
|
||||
def last_cmd_output(self, as_ansi=False, add_wrap_markers=False):
|
||||
from kitty.window import cmd_output
|
||||
return cmd_output(self.screen, as_ansi=as_ansi, add_wrap_markers=add_wrap_markers)
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
import os
|
||||
from . import BaseTest
|
||||
|
||||
|
||||
class TestHints(BaseTest):
|
||||
|
||||
def test_url_hints(self):
|
||||
from kittens.hints.main import parse_hints_args, functions_for, mark, convert_text
|
||||
from kittens.hints.main import (
|
||||
Mark, convert_text, functions_for, linenum_marks,
|
||||
linenum_process_result, mark, parse_hints_args
|
||||
)
|
||||
args = parse_hints_args([])[0]
|
||||
pattern, post_processors = functions_for(args)
|
||||
|
||||
def create_marks(text, cols=20):
|
||||
def create_marks(text, cols=20, mark=mark):
|
||||
text = convert_text(text, cols)
|
||||
return tuple(mark(pattern, post_processors, text, args))
|
||||
|
||||
@@ -30,8 +33,27 @@ class TestHints(BaseTest):
|
||||
t(f'`xyz <{u}>`_.', u)
|
||||
t(f'<a href="{u}">moo', u)
|
||||
|
||||
def m(text, path, line, cols=20):
|
||||
|
||||
def adapt(pattern, postprocessors, text, *a):
|
||||
return linenum_marks(text, args, Mark, ())
|
||||
|
||||
marks = create_marks(text, cols, mark=adapt)
|
||||
data = {'groupdicts': [m.groupdict for m in marks], 'match': [m.text for m in marks]}
|
||||
self.ae(linenum_process_result(data), (path, line))
|
||||
|
||||
args = parse_hints_args('--type=linenum'.split())[0]
|
||||
m('file.c:23', 'file.c', 23)
|
||||
m('file.c:23:32', 'file.c', 23)
|
||||
m('file.cpp:23:1', 'file.cpp', 23)
|
||||
m('a/file.c:23', 'a/file.c', 23)
|
||||
m('a/file.c:23:32', 'a/file.c', 23)
|
||||
m('~/file.c:23:32', os.path.expanduser('~/file.c'), 23)
|
||||
|
||||
def test_ip_hints(self):
|
||||
from kittens.hints.main import parse_hints_args, functions_for, mark, convert_text
|
||||
from kittens.hints.main import (
|
||||
convert_text, functions_for, mark, parse_hints_args
|
||||
)
|
||||
args = parse_hints_args(['--type', 'ip'])[0]
|
||||
pattern, post_processors = functions_for(args)
|
||||
|
||||
|
||||
@@ -213,18 +213,18 @@ class TestParser(BaseTest):
|
||||
c = s.callbacks
|
||||
pb('a\033]2;x\\ryz\x9cbcde', 'a', ('set_title', 'x\\ryz'), 'bcde')
|
||||
self.ae(str(s.line(0)), 'abcde')
|
||||
self.ae(c.titlebuf, 'x\\ryz')
|
||||
self.ae(c.titlebuf, ['x\\ryz'])
|
||||
c.clear()
|
||||
pb('\033]\x07', ('set_title', ''), ('set_icon', ''))
|
||||
self.ae(c.titlebuf, ''), self.ae(c.iconbuf, '')
|
||||
self.ae(c.titlebuf, ['']), self.ae(c.iconbuf, '')
|
||||
pb('\033]ab\x07', ('set_title', 'ab'), ('set_icon', 'ab'))
|
||||
self.ae(c.titlebuf, 'ab'), self.ae(c.iconbuf, 'ab')
|
||||
self.ae(c.titlebuf, ['', 'ab']), self.ae(c.iconbuf, 'ab')
|
||||
c.clear()
|
||||
pb('\033]2;;;;\x07', ('set_title', ';;;'))
|
||||
self.ae(c.titlebuf, ';;;')
|
||||
self.ae(c.titlebuf, [';;;'])
|
||||
c.clear()
|
||||
pb('\033]2;\x07', ('set_title', ''))
|
||||
self.ae(c.titlebuf, '')
|
||||
self.ae(c.titlebuf, [''])
|
||||
pb('\033]110\x07', ('set_dynamic_color', 110, ''))
|
||||
self.ae(c.colorbuf, '')
|
||||
c.clear()
|
||||
|
||||
@@ -5,6 +5,7 @@ from kitty.fast_data_types import (
|
||||
DECAWM, DECCOLM, DECOM, IRM, Cursor, parse_bytes
|
||||
)
|
||||
from kitty.marks import marker_from_function, marker_from_regex
|
||||
from kitty.window import pagerhist
|
||||
|
||||
from . import BaseTest
|
||||
|
||||
@@ -530,6 +531,28 @@ class TestScreen(BaseTest):
|
||||
s.scroll(2, True)
|
||||
self.ae(s.text_for_selection(), expected)
|
||||
s.reset()
|
||||
s.draw('ab cd')
|
||||
s.start_selection(0, 0)
|
||||
s.update_selection(1, 3)
|
||||
self.ae(s.text_for_selection(), ('ab ', 'cd'))
|
||||
self.ae(s.text_for_selection(False, True), ('ab', 'cd'))
|
||||
s.reset()
|
||||
s.draw('ab cd')
|
||||
s.start_selection(0, 0)
|
||||
s.update_selection(3, 4)
|
||||
self.ae(s.text_for_selection(), ('ab ', ' ', 'cd'))
|
||||
self.ae(s.text_for_selection(False, True), ('ab', '\n', 'cd'))
|
||||
s.reset()
|
||||
s.draw('a')
|
||||
s.select_graphic_rendition(32)
|
||||
s.draw('b')
|
||||
s.select_graphic_rendition(39)
|
||||
s.draw('c xy')
|
||||
s.start_selection(0, 0)
|
||||
s.update_selection(1, 3)
|
||||
self.ae(s.text_for_selection(), ('abc ', 'xy'))
|
||||
self.ae(s.text_for_selection(True), ('a\x1b[32mb\x1b[39mc ', 'xy', '\x1b[m'))
|
||||
self.ae(s.text_for_selection(True, True), ('a\x1b[32mb\x1b[39mc', 'xy', '\x1b[m'))
|
||||
|
||||
def test_soft_hyphen(self):
|
||||
s = self.create_screen()
|
||||
@@ -974,9 +997,12 @@ class TestScreen(BaseTest):
|
||||
self.assertTrue(s.scroll_to_prompt())
|
||||
self.ae(str(s.visual_line(0)), '$ 1')
|
||||
|
||||
def lco():
|
||||
def lco(as_ansi=False):
|
||||
a = []
|
||||
s.cmd_output(0, a.append)
|
||||
if s.cmd_output(0, a.append, as_ansi):
|
||||
pht = pagerhist(s, as_ansi=as_ansi, upto_output_start=True)
|
||||
if pht:
|
||||
a.insert(0, pht)
|
||||
return ''.join(a)
|
||||
|
||||
def fco():
|
||||
@@ -1003,6 +1029,7 @@ class TestScreen(BaseTest):
|
||||
mark_prompt(), s.draw('$ 1')
|
||||
self.ae(fco(), 'abcd\n12')
|
||||
self.ae(lco(), 'abcd\n12')
|
||||
self.ae(lco(as_ansi=True), '\x1b[m\x1b]133;C\x1b\\abcd\n\x1b[m12')
|
||||
|
||||
def draw_prompt(x):
|
||||
mark_prompt(), s.draw(f'$ {x}'), s.carriage_return(), s.index()
|
||||
@@ -1053,3 +1080,9 @@ class TestScreen(BaseTest):
|
||||
self.ae(lvco(), '0\n1\n2')
|
||||
s.scroll_to_prompt(1)
|
||||
self.ae(lvco(), '0x\n1x')
|
||||
|
||||
# last command output from pager history
|
||||
s = self.create_screen()
|
||||
draw_prompt('p1')
|
||||
draw_output(30)
|
||||
self.ae(tuple(map(int, lco().split())), tuple(range(0, 30)))
|
||||
|
||||
329
kitty_tests/shell_integration.py
Normal file
329
kitty_tests/shell_integration.py
Normal file
@@ -0,0 +1,329 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from contextlib import contextmanager
|
||||
from functools import lru_cache, partial
|
||||
|
||||
from kitty.constants import kitty_base_dir, terminfo_dir
|
||||
from kitty.fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE
|
||||
from kitty.shell_integration import (
|
||||
setup_bash_env, setup_fish_env, setup_zsh_env
|
||||
)
|
||||
|
||||
from . import BaseTest
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def bash_ok():
|
||||
v = shutil.which('bash')
|
||||
if not v:
|
||||
return False
|
||||
o = subprocess.check_output([v, '-c', 'echo "${BASH_VERSION}"']).decode('utf-8').strip()
|
||||
if not o or int(o[0]) < 5:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def basic_shell_env(home_dir):
|
||||
ans = {
|
||||
'PATH': os.environ.get('PATH', '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'),
|
||||
'HOME': home_dir,
|
||||
'TERM': 'xterm-kitty',
|
||||
'TERMINFO': terminfo_dir,
|
||||
'KITTY_SHELL_INTEGRATION': 'enabled',
|
||||
'KITTY_INSTALLATION_DIR': kitty_base_dir,
|
||||
'BASH_SILENCE_DEPRECATION_WARNING': '1',
|
||||
}
|
||||
for x in ('USER', 'LANG'):
|
||||
if os.environ.get(x):
|
||||
ans[x] = os.environ[x]
|
||||
return ans
|
||||
|
||||
|
||||
def safe_env_for_running_shell(argv, home_dir, rc='', shell='zsh'):
|
||||
ans = basic_shell_env(home_dir)
|
||||
if shell == 'zsh':
|
||||
argv.insert(1, '--noglobalrcs')
|
||||
ans['ZLE_RPROMPT_INDENT'] = '0'
|
||||
with open(os.path.join(home_dir, '.zshrc'), 'w') as f:
|
||||
print(rc + '\n', file=f)
|
||||
setup_zsh_env(ans, argv)
|
||||
elif shell == 'fish':
|
||||
conf_dir = os.path.join(home_dir, '.config', 'fish')
|
||||
os.makedirs(conf_dir, exist_ok=True)
|
||||
# Avoid generating unneeded completion scripts
|
||||
os.makedirs(os.path.join(home_dir, '.local', 'share', 'fish', 'generated_completions'), exist_ok=True)
|
||||
with open(os.path.join(conf_dir, 'config.fish'), 'w') as f:
|
||||
print(rc + '\n', file=f)
|
||||
setup_fish_env(ans, argv)
|
||||
elif shell == 'bash':
|
||||
setup_bash_env(ans, argv)
|
||||
ans['KITTY_BASH_INJECT'] += ' posix'
|
||||
ans['KITTY_BASH_POSIX_ENV'] = os.path.join(home_dir, '.bashrc')
|
||||
with open(ans['KITTY_BASH_POSIX_ENV'], 'w') as f:
|
||||
# ensure LINES and COLUMNS are kept up to date
|
||||
print('shopt -s checkwinsize', file=f)
|
||||
if rc:
|
||||
print(rc, file=f)
|
||||
return ans
|
||||
|
||||
|
||||
class ShellIntegration(BaseTest):
|
||||
|
||||
@contextmanager
|
||||
def run_shell(self, shell='zsh', rc='', cmd='', setup_env=None):
|
||||
home_dir = os.path.realpath(tempfile.mkdtemp())
|
||||
cmd = cmd or shell
|
||||
cmd = shlex.split(cmd.format(**locals()))
|
||||
env = (setup_env or safe_env_for_running_shell)(cmd, home_dir, rc=rc, shell=shell)
|
||||
try:
|
||||
pty = self.create_pty(cmd, cwd=home_dir, env=env)
|
||||
i = 10
|
||||
while i > 0 and not pty.screen_contents().strip():
|
||||
pty.process_input_from_child()
|
||||
i -= 1
|
||||
yield pty
|
||||
finally:
|
||||
if os.path.exists(home_dir):
|
||||
shutil.rmtree(home_dir)
|
||||
|
||||
@unittest.skipUnless(shutil.which('zsh'), 'zsh not installed')
|
||||
def test_zsh_integration(self):
|
||||
ps1, rps1 = 'left>', '<right'
|
||||
with self.run_shell(
|
||||
rc=f'''
|
||||
PS1="{ps1}"
|
||||
RPS1="{rps1}"
|
||||
''') as pty:
|
||||
q = ps1 + ' ' * (pty.screen.columns - len(ps1) - len(rps1)) + rps1
|
||||
try:
|
||||
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
|
||||
except TimeoutError as e:
|
||||
raise AssertionError(f'Cursor was not changed to beam. Screen contents: {repr(pty.screen_contents())}') from e
|
||||
self.ae(pty.screen_contents(), q)
|
||||
self.ae(pty.callbacks.titlebuf[-1], '~')
|
||||
pty.callbacks.clear()
|
||||
pty.send_cmd_to_child('mkdir test && ls -a')
|
||||
pty.wait_till(lambda: pty.screen_contents().count(rps1) == 2)
|
||||
self.ae(pty.callbacks.titlebuf[-2:], ['mkdir test && ls -a', '~'])
|
||||
q = '\n'.join(str(pty.screen.line(i)) for i in range(1, pty.screen.cursor.y))
|
||||
self.ae(pty.last_cmd_output(), q)
|
||||
# shrink the screen
|
||||
pty.write_to_child(r'echo $COLUMNS')
|
||||
pty.set_window_size(rows=20, columns=40)
|
||||
q = ps1 + 'echo $COLUMNS' + ' ' * (40 - len(ps1) - len(rps1) - len('echo $COLUMNS')) + rps1
|
||||
pty.process_input_from_child()
|
||||
|
||||
def redrawn():
|
||||
q = pty.screen_contents()
|
||||
return '$COLUMNS' in q and q.count(rps1) == 2 and q.count(ps1) == 2
|
||||
|
||||
pty.wait_till(redrawn)
|
||||
self.ae(q, str(pty.screen.line(pty.screen.cursor.y)))
|
||||
pty.write_to_child('\r')
|
||||
pty.wait_till(lambda: pty.screen_contents().count(rps1) == 3)
|
||||
self.ae('40', str(pty.screen.line(pty.screen.cursor.y - 1)))
|
||||
self.ae(q, str(pty.screen.line(pty.screen.cursor.y - 2)))
|
||||
pty.send_cmd_to_child('clear')
|
||||
q = ps1 + ' ' * (pty.screen.columns - len(ps1) - len(rps1)) + rps1
|
||||
pty.wait_till(lambda: pty.screen_contents() == q)
|
||||
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
|
||||
pty.send_cmd_to_child('cat')
|
||||
pty.wait_till(lambda: pty.screen.cursor.shape == 0)
|
||||
pty.write_to_child('\x04')
|
||||
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
|
||||
with self.run_shell(rc=f'''PS1="{ps1}"''') as pty:
|
||||
pty.callbacks.clear()
|
||||
pty.send_cmd_to_child('printf "%s\x16\a%s" "a" "b"')
|
||||
pty.wait_till(lambda: 'ab' in pty.screen_contents())
|
||||
self.assertIn('%s^G%s', pty.screen_contents())
|
||||
|
||||
@unittest.skipUnless(shutil.which('fish'), 'fish not installed')
|
||||
def test_fish_integration(self):
|
||||
fish_prompt, right_prompt = 'left>', '<right'
|
||||
completions_dir = os.path.join(kitty_base_dir, 'shell-integration', 'fish', 'vendor_completions.d')
|
||||
with self.run_shell(
|
||||
shell='fish',
|
||||
rc=f'''
|
||||
set -g fish_greeting
|
||||
function fish_prompt; echo -n "{fish_prompt}"; end
|
||||
function fish_right_prompt; echo -n "{right_prompt}"; end
|
||||
function _test_comp_path; contains "{completions_dir}" $fish_complete_path; and echo ok; end
|
||||
function _set_key; set -g fish_key_bindings fish_$argv[1]_key_bindings; end
|
||||
function _set_status_prompt; function fish_prompt; echo -n "$pipestatus $status {fish_prompt}"; end; end
|
||||
''') as pty:
|
||||
q = fish_prompt + ' ' * (pty.screen.columns - len(fish_prompt) - len(right_prompt)) + right_prompt
|
||||
pty.wait_till(lambda: pty.screen_contents().count(right_prompt) == 1)
|
||||
self.ae(pty.screen_contents(), q)
|
||||
|
||||
# XDG_DATA_DIRS
|
||||
pty.send_cmd_to_child('set -q XDG_DATA_DIRS; or echo ok')
|
||||
pty.wait_till(lambda: pty.screen_contents().count(right_prompt) == 2)
|
||||
self.ae(str(pty.screen.line(1)), 'ok')
|
||||
|
||||
# completion and prompt marking
|
||||
pty.send_cmd_to_child('clear')
|
||||
pty.send_cmd_to_child('_test_comp_path')
|
||||
pty.wait_till(lambda: pty.screen_contents().count(right_prompt) == 2)
|
||||
q = '\n'.join(str(pty.screen.line(i)) for i in range(1, pty.screen.cursor.y))
|
||||
self.ae(q, 'ok')
|
||||
self.ae(pty.last_cmd_output(), q)
|
||||
|
||||
# resize and redraw (fish_handle_reflow)
|
||||
pty.write_to_child(r'echo $COLUMNS')
|
||||
pty.set_window_size(rows=20, columns=40)
|
||||
q = fish_prompt + 'echo $COLUMNS' + ' ' * (40 - len(fish_prompt) - len(right_prompt) - len('echo $COLUMNS')) + right_prompt
|
||||
pty.process_input_from_child()
|
||||
|
||||
def redrawn():
|
||||
q = pty.screen_contents()
|
||||
return '$COLUMNS' in q and q.count(right_prompt) == 2 and q.count(fish_prompt) == 2
|
||||
|
||||
pty.wait_till(redrawn)
|
||||
self.ae(q, str(pty.screen.line(pty.screen.cursor.y)))
|
||||
pty.write_to_child('\r')
|
||||
pty.wait_till(lambda: pty.screen_contents().count(right_prompt) == 3)
|
||||
self.ae('40', str(pty.screen.line(pty.screen.cursor.y - 1)))
|
||||
self.ae(q, str(pty.screen.line(pty.screen.cursor.y - 2)))
|
||||
|
||||
# cursor shapes
|
||||
pty.send_cmd_to_child('clear')
|
||||
q = fish_prompt + ' ' * (pty.screen.columns - len(fish_prompt) - len(right_prompt)) + right_prompt
|
||||
pty.wait_till(lambda: pty.screen_contents() == q)
|
||||
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
|
||||
pty.send_cmd_to_child('echo; cat')
|
||||
pty.wait_till(lambda: pty.screen.cursor.shape == 0 and pty.screen.cursor.y > 1)
|
||||
pty.write_to_child('\x04')
|
||||
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
|
||||
pty.send_cmd_to_child('_set_key vi')
|
||||
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BLOCK)
|
||||
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
|
||||
pty.write_to_child('\x1b')
|
||||
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BLOCK)
|
||||
pty.write_to_child('r')
|
||||
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_UNDERLINE)
|
||||
pty.write_to_child('\x1b')
|
||||
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BLOCK)
|
||||
pty.write_to_child('i')
|
||||
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
|
||||
pty.send_cmd_to_child('_set_key default')
|
||||
|
||||
# pipestatus
|
||||
pty.send_cmd_to_child('clear;false|true|false')
|
||||
pty.send_cmd_to_child('echo $pipestatus $status')
|
||||
pty.wait_till(lambda: pty.screen_contents().count(right_prompt) == 2)
|
||||
self.ae(pty.last_cmd_output(), '1 0 1 1')
|
||||
pty.send_cmd_to_child('_set_status_prompt')
|
||||
pty.send_cmd_to_child('false|true|false')
|
||||
pty.wait_till(lambda: pty.screen_contents().count(right_prompt) == 4)
|
||||
self.assertTrue(str(pty.screen.line(pty.screen.cursor.y)).startswith(f'1 0 1 1 {fish_prompt}'))
|
||||
|
||||
pty.send_cmd_to_child('exit')
|
||||
|
||||
@unittest.skipUnless(bash_ok(), 'bash not installed or too old')
|
||||
def test_bash_integration(self):
|
||||
ps1 = 'prompt> '
|
||||
with self.run_shell(
|
||||
shell='bash', rc=f'''
|
||||
PS1="{ps1}"
|
||||
''') as pty:
|
||||
try:
|
||||
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
|
||||
except TimeoutError as e:
|
||||
raise AssertionError(f'Cursor was not changed to beam. Screen contents: {repr(pty.screen_contents())}') from e
|
||||
pty.wait_till(lambda: pty.screen_contents().count(ps1) == 1)
|
||||
self.ae(pty.screen_contents(), ps1)
|
||||
pty.wait_till(lambda: pty.callbacks.titlebuf[-1:] == ['~'])
|
||||
self.ae(pty.callbacks.titlebuf[-1], '~')
|
||||
pty.callbacks.clear()
|
||||
pty.send_cmd_to_child('mkdir test && ls -a')
|
||||
pty.wait_till(lambda: pty.callbacks.titlebuf[-2:] == ['mkdir test && ls -a', '~'])
|
||||
pty.wait_till(lambda: pty.screen_contents().count(ps1) == 2)
|
||||
q = '\n'.join(str(pty.screen.line(i)) for i in range(1, pty.screen.cursor.y))
|
||||
self.ae(pty.last_cmd_output(), q)
|
||||
# shrink the screen
|
||||
pty.write_to_child(r'echo $COLUMNS')
|
||||
pty.set_window_size(rows=20, columns=40)
|
||||
pty.process_input_from_child()
|
||||
|
||||
def redrawn():
|
||||
q = pty.screen_contents()
|
||||
return '$COLUMNS' in q and q.count(ps1) == 2
|
||||
|
||||
pty.wait_till(redrawn)
|
||||
self.ae(ps1 + 'echo $COLUMNS', str(pty.screen.line(pty.screen.cursor.y)))
|
||||
pty.write_to_child('\r')
|
||||
pty.wait_till(lambda: pty.screen_contents().count(ps1) == 3)
|
||||
self.ae('40', str(pty.screen.line(pty.screen.cursor.y - 1)))
|
||||
self.ae(ps1 + 'echo $COLUMNS', str(pty.screen.line(pty.screen.cursor.y - 2)))
|
||||
pty.send_cmd_to_child('clear')
|
||||
pty.wait_till(lambda: pty.screen_contents() == ps1)
|
||||
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
|
||||
pty.send_cmd_to_child('cat')
|
||||
pty.wait_till(lambda: pty.screen.cursor.shape == 0)
|
||||
pty.write_to_child('\x04')
|
||||
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
|
||||
pty.write_to_child('\x04')
|
||||
pty.send_cmd_to_child('clear')
|
||||
pty.wait_till(lambda: pty.callbacks.titlebuf)
|
||||
with self.run_shell(shell='bash', rc=f'''PS1="{ps1}"''') as pty:
|
||||
pty.callbacks.clear()
|
||||
pty.send_cmd_to_child('printf "%s\x16\a%s" "a" "b"')
|
||||
pty.wait_till(lambda: pty.screen_contents().count(ps1) == 2)
|
||||
self.ae(pty.screen_contents(), f'{ps1}printf "%s^G%s" "a" "b"\nab{ps1}')
|
||||
pty.send_cmd_to_child('echo $HISTFILE')
|
||||
pty.wait_till(lambda: '.bash_history' in pty.screen_contents())
|
||||
|
||||
for ps1 in ('line1\\nline\\2\\prompt> ', 'line1\nprompt> ', 'line1\\nprompt> ',):
|
||||
with self.subTest(ps1=ps1), self.run_shell(
|
||||
shell='bash', rc=f'''
|
||||
PS1="{ps1}"
|
||||
''') as pty:
|
||||
ps1 = ps1.replace('\\n', '\n')
|
||||
pty.wait_till(lambda: pty.screen_contents().count(ps1) == 1)
|
||||
pty.send_cmd_to_child('echo test')
|
||||
pty.wait_till(lambda: pty.screen_contents().count(ps1) == 2)
|
||||
self.ae(pty.screen_contents(), f'{ps1}echo test\ntest\n{ps1}')
|
||||
pty.write_to_child(r'echo $COLUMNS')
|
||||
pty.set_window_size(rows=20, columns=40)
|
||||
pty.process_input_from_child()
|
||||
pty.wait_till(redrawn)
|
||||
self.ae(ps1.splitlines()[-1] + 'echo $COLUMNS', str(pty.screen.line(pty.screen.cursor.y)))
|
||||
pty.write_to_child('\r')
|
||||
pty.wait_till(lambda: pty.screen_contents().count(ps1) == 3)
|
||||
self.ae('40', str(pty.screen.line(pty.screen.cursor.y - len(ps1.splitlines()))))
|
||||
self.ae(ps1.splitlines()[-1] + 'echo $COLUMNS', str(pty.screen.line(pty.screen.cursor.y - 1 - len(ps1.splitlines()))))
|
||||
|
||||
# test startup file sourcing
|
||||
|
||||
def setup_env(excluded, argv, home_dir, rc='', shell='bash'):
|
||||
ans = basic_shell_env(home_dir)
|
||||
setup_bash_env(ans, argv)
|
||||
for x in {'profile', 'bash.bashrc', '.bash_profile', '.bash_login', '.profile', '.bashrc', 'rcfile'} - excluded:
|
||||
with open(os.path.join(home_dir, x), 'w') as f:
|
||||
print(f'echo {x}', file=f)
|
||||
ans['KITTY_BASH_ETC_LOCATION'] = home_dir
|
||||
return ans
|
||||
|
||||
def run_test(argv, *expected, excluded=()):
|
||||
with self.subTest(argv=argv), self.run_shell(shell='bash', setup_env=partial(setup_env, set(excluded)), cmd=argv) as pty:
|
||||
pty.wait_till(lambda: '$' in pty.screen_contents())
|
||||
q = pty.screen_contents()
|
||||
for x in expected:
|
||||
self.assertIn(x, q)
|
||||
|
||||
run_test('bash', 'bash.bashrc', '.bashrc')
|
||||
run_test('bash --rcfile rcfile', 'bash.bashrc', 'rcfile')
|
||||
run_test('bash --init-file rcfile', 'bash.bashrc', 'rcfile')
|
||||
run_test('bash --norc')
|
||||
run_test('bash -l', 'profile', '.bash_profile')
|
||||
run_test('bash --noprofile -l')
|
||||
run_test('bash -l', 'profile', '.bash_login', excluded=('.bash_profile',))
|
||||
run_test('bash -l', 'profile', '.profile', excluded=('.bash_profile', '.bash_login'))
|
||||
@@ -3,14 +3,27 @@
|
||||
|
||||
|
||||
import os
|
||||
from . import BaseTest
|
||||
|
||||
from kittens.ssh.main import get_connection_data
|
||||
from kitty.utils import SSHConnectionData
|
||||
|
||||
from . import BaseTest
|
||||
|
||||
|
||||
class SSHTest(BaseTest):
|
||||
|
||||
def test_basic_pty_operations(self):
|
||||
pty = self.create_pty('echo hello')
|
||||
pty.process_input_from_child()
|
||||
self.ae(pty.screen_contents(), 'hello')
|
||||
pty = self.create_pty(self.cmd_to_run_python_code('''\
|
||||
import array, fcntl, sys, termios
|
||||
buf = array.array('H', [0, 0, 0, 0])
|
||||
fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, buf)
|
||||
print(' '.join(map(str, buf)))'''), lines=13, cols=77)
|
||||
pty.process_input_from_child()
|
||||
self.ae(pty.screen_contents(), '13 77 770 260')
|
||||
|
||||
def test_ssh_connection_data(self):
|
||||
def t(cmdline, binary='ssh', host='main', port=None, identity_file=''):
|
||||
if identity_file:
|
||||
|
||||
@@ -259,6 +259,7 @@ read_exe_path(char *exe, size_t buf_sz) {
|
||||
#endif // }}}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
if (argc < 1 || !argv) { fprintf(stderr, "Invalid argc/argv\n"); return 1; }
|
||||
char exe[PATH_MAX+1] = {0};
|
||||
char exe_dir_buf[PATH_MAX+1] = {0};
|
||||
FREE_AFTER_FUNCTION const char *lc_ctype = NULL;
|
||||
|
||||
27
publish.py
27
publish.py
@@ -23,6 +23,7 @@ import requests
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
docs_dir = os.path.abspath('docs')
|
||||
publish_dir = os.path.abspath(os.path.join('..', 'kovidgoyal.github.io', 'kitty'))
|
||||
building_nightly = False
|
||||
with open('kitty/constants.py') as f:
|
||||
raw = f.read()
|
||||
nv = re.search(r'^version: Version\s+=\s+Version\((\d+), (\d+), (\d+)\)', raw, flags=re.MULTILINE)
|
||||
@@ -58,18 +59,26 @@ def call(*cmd: str, cwd: Optional[str] = None, echo: bool = False) -> None:
|
||||
|
||||
|
||||
def run_build(args: Any) -> None:
|
||||
for x in ('64', '32', 'arm64'):
|
||||
|
||||
def run_with_retry(cmd: str) -> None:
|
||||
try:
|
||||
call(f'python ../bypy linux --arch {x} program', echo=True)
|
||||
call(cmd, echo=True)
|
||||
except (SystemExit, Exception):
|
||||
if x != 'arm64':
|
||||
needs_retry = 'arm64' in cmd or building_nightly
|
||||
if not needs_retry:
|
||||
raise
|
||||
print('Linux ARM build failed, retrying in a few seconds...', file=sys.stderr)
|
||||
time.sleep(15)
|
||||
call(f'python ../bypy linux --arch {x} program', echo=True)
|
||||
print('Build failed, retrying in a few seconds...', file=sys.stderr)
|
||||
if 'macos' in cmd:
|
||||
call('python ../bypy macos shutdown')
|
||||
time.sleep(25)
|
||||
call(cmd, echo=True)
|
||||
|
||||
for x in ('64', '32', 'arm64'):
|
||||
cmd = f'python ../bypy linux --arch {x} program'
|
||||
run_with_retry(cmd)
|
||||
call(f'python ../bypy linux --arch {x} shutdown', echo=True)
|
||||
call('python ../bypy macos program --sign-installers --notarize', echo=True)
|
||||
call('python ../bypy macos shutdown')
|
||||
cmd = 'python ../bypy macos program --sign-installers --notarize'
|
||||
run_with_retry(cmd)
|
||||
|
||||
|
||||
def run_tag(args: Any) -> None:
|
||||
@@ -464,6 +473,7 @@ def exec_actions(actions: Iterable[str], args: Any) -> None:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
global building_nightly
|
||||
parser = argparse.ArgumentParser(description='Publish kitty')
|
||||
parser.add_argument(
|
||||
'--only',
|
||||
@@ -485,6 +495,7 @@ def main() -> None:
|
||||
require_penv()
|
||||
if args.nightly:
|
||||
with change_to_git_master():
|
||||
building_nightly = True
|
||||
exec_actions(NIGHTLY_ACTIONS, args)
|
||||
return
|
||||
require_git_master()
|
||||
|
||||
136
setup.py
136
setup.py
@@ -967,6 +967,23 @@ TryExec=kitty
|
||||
Exec=kitty
|
||||
Icon=kitty
|
||||
Categories=System;TerminalEmulator;
|
||||
'''
|
||||
)
|
||||
with open(os.path.join(deskdir, 'kitty-open.desktop'), 'w') as f:
|
||||
f.write(
|
||||
'''\
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=kitty URL Launcher
|
||||
GenericName=Terminal emulator
|
||||
Comment=Open URLs with kitty
|
||||
TryExec=kitty
|
||||
Exec=kitty +open %U
|
||||
Icon=kitty
|
||||
Categories=System;TerminalEmulator;
|
||||
NoDisplay=true
|
||||
MimeType=image/*;application/x-sh;inode/directory;text/*;x-scheme-handler/kitty;
|
||||
'''
|
||||
)
|
||||
|
||||
@@ -1006,26 +1023,98 @@ def macos_info_plist() -> bytes:
|
||||
{
|
||||
'CFBundleTypeName': 'Text files',
|
||||
'LSItemContentTypes': ['public.text'],
|
||||
'LSTypeIsPackage': False,
|
||||
'CFBundleTypeRole': 'Editor',
|
||||
'LSHandlerRank': 'Alternate',
|
||||
},
|
||||
{
|
||||
'CFBundleTypeName': 'Image files',
|
||||
'LSItemContentTypes': ['public.image'],
|
||||
'LSTypeIsPackage': False,
|
||||
'CFBundleTypeRole': 'Viewer',
|
||||
'LSHandlerRank': 'Alternate',
|
||||
},
|
||||
# Allows dragging arbitrary files to kitty Dock icon, and list kitty in the Open With context menu.
|
||||
{
|
||||
'CFBundleTypeName': 'All files',
|
||||
'LSItemContentTypes': ['public.content', 'public.data'],
|
||||
'LSItemContentTypes': ['public.archive', 'public.content', 'public.data'],
|
||||
'CFBundleTypeRole': 'Editor',
|
||||
'LSHandlerRank': 'Alternate',
|
||||
},
|
||||
]
|
||||
|
||||
url_schemes = [
|
||||
{
|
||||
'CFBundleURLName': 'File URL',
|
||||
'CFBundleURLSchemes': ['file'],
|
||||
},
|
||||
{
|
||||
'CFBundleURLName': 'FTP URL',
|
||||
'CFBundleURLSchemes': ['ftp', 'ftps'],
|
||||
},
|
||||
{
|
||||
'CFBundleURLName': 'Gemini URL',
|
||||
'CFBundleURLSchemes': ['gemini'],
|
||||
},
|
||||
{
|
||||
'CFBundleURLName': 'Git URL',
|
||||
'CFBundleURLSchemes': ['git'],
|
||||
},
|
||||
{
|
||||
'CFBundleURLName': 'Gopher URL',
|
||||
'CFBundleURLSchemes': ['gopher'],
|
||||
},
|
||||
{
|
||||
'CFBundleURLName': 'HTTP URL',
|
||||
'CFBundleURLSchemes': ['http', 'https'],
|
||||
},
|
||||
{
|
||||
'CFBundleURLName': 'IRC URL',
|
||||
'CFBundleURLSchemes': ['irc', 'irc6', 'ircs'],
|
||||
},
|
||||
{
|
||||
'CFBundleURLName': 'kitty URL',
|
||||
'CFBundleURLSchemes': ['kitty'],
|
||||
'LSHandlerRank': 'Owner',
|
||||
'LSIsAppleDefaultForScheme': True,
|
||||
},
|
||||
{
|
||||
'CFBundleURLName': 'Mail Address URL',
|
||||
'CFBundleURLSchemes': ['mailto'],
|
||||
},
|
||||
{
|
||||
'CFBundleURLName': 'News URL',
|
||||
'CFBundleURLSchemes': ['news', 'nntp'],
|
||||
},
|
||||
{
|
||||
'CFBundleURLName': 'SSH and SFTP URL',
|
||||
'CFBundleURLSchemes': ['ssh', 'sftp'],
|
||||
},
|
||||
{
|
||||
'CFBundleURLName': 'Telnet URL',
|
||||
'CFBundleURLSchemes': ['telnet'],
|
||||
},
|
||||
]
|
||||
|
||||
services = [
|
||||
{
|
||||
'NSMenuItem': {'default': f'New {appname} Tab Here'},
|
||||
'NSMessage': 'openTab',
|
||||
'NSRequiredContext': {'NSTextContent': 'FilePath'},
|
||||
'NSSendTypes': ['NSFilenamesPboardType', 'public.plain-text'],
|
||||
},
|
||||
{
|
||||
'NSMenuItem': {'default': f'New {appname} Window Here'},
|
||||
'NSMessage': 'openOSWindow',
|
||||
'NSRequiredContext': {'NSTextContent': 'FilePath'},
|
||||
'NSSendTypes': ['NSFilenamesPboardType', 'public.plain-text'],
|
||||
},
|
||||
{
|
||||
'NSMenuItem': {'default': f'Open with {appname}'},
|
||||
'NSMessage': 'openFileURLs',
|
||||
'NSRequiredContext': {'NSTextContent': 'FilePath'},
|
||||
'NSSendTypes': ['NSFilenamesPboardType', 'public.plain-text'],
|
||||
},
|
||||
]
|
||||
|
||||
pl = dict(
|
||||
# Naming
|
||||
CFBundleName=appname,
|
||||
@@ -1043,7 +1132,6 @@ def macos_info_plist() -> bytes:
|
||||
# Categorization
|
||||
CFBundlePackageType='APPL',
|
||||
CFBundleSignature='????',
|
||||
CFBundleDocumentTypes=docs,
|
||||
LSApplicationCategoryType='public.app-category.utilities',
|
||||
# App Execution
|
||||
CFBundleExecutable=appname,
|
||||
@@ -1061,21 +1149,11 @@ def macos_info_plist() -> bytes:
|
||||
NSSupportsAutomaticGraphicsSwitching=True,
|
||||
# Needed for dark mode in Mojave when linking against older SDKs
|
||||
NSRequiresAquaSystemAppearance='NO',
|
||||
# Document and URL Types
|
||||
CFBundleDocumentTypes=docs,
|
||||
CFBundleURLTypes=url_schemes,
|
||||
# Services
|
||||
NSServices=[
|
||||
{
|
||||
'NSMenuItem': {'default': f'New {appname} Tab Here'},
|
||||
'NSMessage': 'openTab',
|
||||
'NSRequiredContext': {'NSTextContent': 'FilePath'},
|
||||
'NSSendTypes': ['NSFilenamesPboardType', 'public.plain-text'],
|
||||
},
|
||||
{
|
||||
'NSMenuItem': {'default': f'New {appname} Window Here'},
|
||||
'NSMessage': 'openOSWindow',
|
||||
'NSRequiredContext': {'NSTextContent': 'FilePath'},
|
||||
'NSSendTypes': ['NSFilenamesPboardType', 'public.plain-text'],
|
||||
},
|
||||
],
|
||||
NSServices=services,
|
||||
# Calendar and Reminders
|
||||
NSCalendarsUsageDescription=access('your calendar data.'),
|
||||
NSRemindersUsageDescription=access('your reminders.'),
|
||||
@@ -1413,7 +1491,7 @@ def build_dep() -> None:
|
||||
p.add_argument(
|
||||
'--platform',
|
||||
default='all',
|
||||
choices='all macos linux linux-x86'.split(),
|
||||
choices='all macos linux linux-32 linux-arm64 linux-64'.split(),
|
||||
help='Platforms to build the dep for'
|
||||
)
|
||||
p.add_argument(
|
||||
@@ -1423,15 +1501,25 @@ def build_dep() -> None:
|
||||
help='Names of the dependencies, if none provided, build all'
|
||||
)
|
||||
args = p.parse_args(sys.argv[2:], namespace=Options)
|
||||
linux_platforms = [
|
||||
['linux', '--arch=64'],
|
||||
['linux', '--arch=32'],
|
||||
['linux', '--arch=arm64'],
|
||||
]
|
||||
if args.platform == 'all':
|
||||
platforms = ['linux', 'linux 32', 'macos']
|
||||
elif args.platform == 'linux-x86':
|
||||
platforms = ['linux 32']
|
||||
platforms = linux_platforms + [['macos']]
|
||||
elif args.platform == 'linux':
|
||||
platforms = linux_platforms
|
||||
elif args.platform == 'macos':
|
||||
platforms = [['macos']]
|
||||
elif '-' in args.platform:
|
||||
parts = args.platform.split('-')
|
||||
platforms = [[parts[0], f'--arch={parts[1]}']]
|
||||
else:
|
||||
platforms = [args.platform]
|
||||
raise SystemExit(f'Unknown platform: {args.platform}')
|
||||
base = [sys.executable, '../bypy']
|
||||
for pf in platforms:
|
||||
cmd = base + pf.split() + args.deps
|
||||
cmd = base + pf + ['dependencies'] + args.deps
|
||||
run_tool(cmd)
|
||||
|
||||
|
||||
|
||||
@@ -20,13 +20,58 @@ _ksi_main() {
|
||||
|
||||
_ksi_debug_print() {
|
||||
# print a line to STDOUT of parent kitty process
|
||||
builtin local b=$(command base64 <<< "${@}" | tr -d \\n)
|
||||
builtin printf "\eP@kitty-print|%s\e\\" "$b"
|
||||
# "
|
||||
builtin local b
|
||||
b=$(builtin command base64 <<< "${@}")
|
||||
builtin printf "\eP@kitty-print|%s\e\\" "${b//\\n}"
|
||||
}
|
||||
|
||||
_ksi_set_mark() {
|
||||
_ksi_prompt["${1}_mark"]="\[\e]133;k;${1}_kitty\a\]"
|
||||
_ksi_safe_source() {
|
||||
if [[ -f "$1" && -r "$1" ]]; then
|
||||
builtin source "$1";
|
||||
builtin return 0;
|
||||
fi
|
||||
builtin return 1;
|
||||
}
|
||||
|
||||
if [[ -n "$KITTY_BASH_INJECT" ]]; then
|
||||
builtin unset ENV;
|
||||
if [[ -z "$HOME" ]]; then HOME=~; fi
|
||||
if [[ -z "$KITTY_BASH_ETC_LOCATION" ]]; then KITTY_BASH_ETC_LOCATION="/etc"; fi
|
||||
if [[ "$KITTY_BASH_INJECT" == *"posix"* ]]; then
|
||||
_ksi_safe_source "$KITTY_BASH_POSIX_ENV" && builtin export ENV="$KITTY_BASH_POSIX_ENV";
|
||||
else
|
||||
builtin set +o posix;
|
||||
if [[ -n "$KITTY_BASH_UNEXPORT_HISTFILE" ]]; then
|
||||
export -n HISTFILE;
|
||||
unset KITTY_BASH_UNEXPORT_HISTFILE;
|
||||
fi
|
||||
|
||||
# See run_startup_files() in shell.c in the Bash source code
|
||||
if builtin shopt -q login_shell; then
|
||||
if [[ "$KITTY_BASH_INJECT" != *"no-profile"* ]]; then
|
||||
_ksi_safe_source "$KITTY_BASH_ETC_LOCATION/profile";
|
||||
_ksi_safe_source "$HOME/.bash_profile" || _ksi_safe_source "$HOME/.bash_login" || _ksi_safe_source "$HOME/.profile";
|
||||
fi
|
||||
else
|
||||
if [[ "$KITTY_BASH_INJECT" != *"no-rc"* ]]; then
|
||||
# Linux distros build bash with -DSYS_BASHRC. Unfortunately, there is
|
||||
# no way to to probe bash for it and different distros use different files
|
||||
_ksi_safe_source "$KITTY_BASH_ETC_LOCATION/bash.bashrc" # Arch, Debian, Ubuntu
|
||||
# Fedora uses /etc/bashrc sourced from ~/.bashrc instead of SYS_BASHRC
|
||||
if [[ -z "$KITTY_BASH_RCFILE" ]]; then KITTY_BASH_RCFILE="$HOME/.bashrc"; fi
|
||||
_ksi_safe_source "$KITTY_BASH_RCFILE";
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
builtin unset KITTY_BASH_RCFILE;
|
||||
builtin unset KITTY_BASH_POSIX_ENV;
|
||||
builtin unset KITTY_BASH_INJECT;
|
||||
builtin unset KITTY_BASH_ETC_LOCATION;
|
||||
fi
|
||||
builtin unset -f _ksi_safe_source
|
||||
|
||||
_ksi_set_mark() {
|
||||
_ksi_prompt["${1}_mark"]="\[\e]133;k;${1}_kitty\a\]"
|
||||
}
|
||||
|
||||
_ksi_set_mark start
|
||||
@@ -49,18 +94,31 @@ _ksi_main() {
|
||||
PS0=${PS0//\\\[\\e\]133;k;start_suffix_kitty\\a\\\]*end_suffix_kitty\\a\\\]}
|
||||
PS0="${PS0}${_ksi_prompt[ps0_suffix]}"
|
||||
fi
|
||||
# restore PS1 to its pristine state without our additions
|
||||
if [[ -n "${_ksi_prompt[ps1]}" ]]; then
|
||||
PS1=${PS1//\\\[\\e\]133;k;start_kitty\\a\\\]*end_kitty\\a\\\]}
|
||||
PS1="${_ksi_prompt[ps1]}$PS1"
|
||||
if [[ "${_ksi_prompt[mark]}" == "y" ]]; then
|
||||
# bash does not redraw the leading lines in a multiline prompt so
|
||||
# mark them as secondary prompts
|
||||
PS1=${PS1//\\\[\\e\]133;k;start_secondary_kitty\\a\\\]*end_secondary_kitty\\a\\\]}
|
||||
PS1=${PS1//"\n"/${_ksi_prompt[secondary_prompt]}}
|
||||
fi
|
||||
PS1=${PS1//\\\[\\e\]133;k;start_secondary_kitty\\a\\\]*end_secondary_kitty\\a\\\]}
|
||||
fi
|
||||
if [[ -n "${_ksi_prompt[ps1_suffix]}" ]]; then
|
||||
PS1=${PS1//\\\[\\e\]133;k;start_suffix_kitty\\a\\\]*end_suffix_kitty\\a\\\]}
|
||||
fi
|
||||
if [[ -n "${_ksi_prompt[ps1]}" ]]; then
|
||||
if [[ "${_ksi_prompt[mark]}" == "y" && ( "${PS1}" == *"\n"* || "${PS1}" == *$'\n'* ) ]]; then
|
||||
builtin local oldval
|
||||
oldval=$(builtin shopt -p extglob)
|
||||
builtin shopt -s extglob
|
||||
# bash does not redraw the leading lines in a multiline prompt so
|
||||
# mark the last line as a secondary prompt. Otherwise on resize the
|
||||
# lines before the last line will be erased by kitty.
|
||||
# the first part removes everything from the last \n onwards
|
||||
# the second part appends a newline with the secondary marking
|
||||
# the third part appends everything after the last newline
|
||||
PS1=${PS1%@('\n'|$'\n')*}${_ksi_prompt[secondary_prompt]}${PS1##*@('\n'|$'\n')};
|
||||
builtin eval "$oldval"
|
||||
fi
|
||||
PS1="${_ksi_prompt[ps1]}$PS1"
|
||||
fi
|
||||
if [[ -n "${_ksi_prompt[ps1_suffix]}" ]]; then
|
||||
PS1="${PS1}${_ksi_prompt[ps1_suffix]}"
|
||||
fi
|
||||
if [[ -n "${_ksi_prompt[ps2]}" ]]; then
|
||||
@@ -69,12 +127,12 @@ _ksi_main() {
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "${_ksi_prompt[cursor]}" == "y" ]]; then
|
||||
if [[ "${_ksi_prompt[cursor]}" == "y" ]]; then
|
||||
_ksi_prompt[ps1_suffix]+="\[\e[5 q\]" # blinking bar cursor
|
||||
_ksi_prompt[ps0_suffix]+="\[\e[0 q\]" # blinking default cursor
|
||||
fi
|
||||
|
||||
if [[ "${_ksi_prompt[title]}" == "y" ]]; then
|
||||
if [[ "${_ksi_prompt[title]}" == "y" ]]; then
|
||||
# see https://www.gnu.org/software/bash/manual/html_node/Controlling-the-Prompt.html#Controlling-the-Prompt
|
||||
# we use suffix here because some distros add title setting to their bashrc files by default
|
||||
_ksi_prompt[ps1_suffix]+="\[\e]2;\w\a\]"
|
||||
@@ -82,27 +140,28 @@ _ksi_main() {
|
||||
_ksi_debug_print "ignoreboth or ignorespace present in bash HISTCONTROL setting, showing running command in window title will not be robust"
|
||||
fi
|
||||
_ksi_get_current_command() {
|
||||
local last_cmd=$(HISTTIMEFORMAT= builtin history 1)
|
||||
builtin local last_cmd
|
||||
last_cmd=$(HISTTIMEFORMAT= builtin history 1)
|
||||
last_cmd="${last_cmd#*[[:digit:]]*[[:space:]]}" # remove leading history number
|
||||
last_cmd="${last_cmd#"${last_cmd%%[![:space:]]*}"}" # remove remaining leading whitespace
|
||||
builtin printf "\e]2;%s\a" "${last_cmd}"
|
||||
builtin printf "\e]2;%s\a" "${last_cmd//[[:cntrl:]]}" # remove any control characters
|
||||
}
|
||||
_ksi_prompt[ps0_suffix]+='$(_ksi_get_current_command)'
|
||||
fi
|
||||
|
||||
if [[ "${_ksi_prompt[mark]}" == "y" ]]; then
|
||||
if [[ "${_ksi_prompt[mark]}" == "y" ]]; then
|
||||
_ksi_prompt[ps1]+="\[\e]133;A\a\]"
|
||||
_ksi_prompt[ps2]+="\[\e]133;A;k=s\a\]"
|
||||
_ksi_prompt[ps0]+="\[\e]133;C\a\]"
|
||||
fi
|
||||
|
||||
if [[ "${_ksi_prompt[complete]}" == "y" ]]; then
|
||||
if [[ "${_ksi_prompt[complete]}" == "y" ]]; then
|
||||
_ksi_completions() {
|
||||
builtin local src
|
||||
builtin local limit
|
||||
# Send all words up to the word the cursor is currently on
|
||||
builtin let limit=1+$COMP_CWORD
|
||||
src=$(builtin printf "%s\n" "${COMP_WORDS[@]:0:$limit}" | command kitty +complete bash)
|
||||
src=$(builtin printf "%s\n" "${COMP_WORDS[@]:0:$limit}" | builtin command kitty +complete bash)
|
||||
if [[ $? == 0 ]]; then
|
||||
builtin eval ${src}
|
||||
fi
|
||||
@@ -112,19 +171,19 @@ _ksi_main() {
|
||||
|
||||
# wrap our prompt additions in markers we can use to remove them using
|
||||
# bash's anemic pattern substitution
|
||||
if [[ -n "${_ksi_prompt[ps0]}" ]]; then
|
||||
if [[ -n "${_ksi_prompt[ps0]}" ]]; then
|
||||
_ksi_prompt[ps0]="${_ksi_prompt[start_mark]}${_ksi_prompt[ps0]}${_ksi_prompt[end_mark]}"
|
||||
fi
|
||||
if [[ -n "${_ksi_prompt[ps0_suffix]}" ]]; then
|
||||
if [[ -n "${_ksi_prompt[ps0_suffix]}" ]]; then
|
||||
_ksi_prompt[ps0_suffix]="${_ksi_prompt[start_suffix_mark]}${_ksi_prompt[ps0_suffix]}${_ksi_prompt[end_suffix_mark]}"
|
||||
fi
|
||||
if [[ -n "${_ksi_prompt[ps1]}" ]]; then
|
||||
if [[ -n "${_ksi_prompt[ps1]}" ]]; then
|
||||
_ksi_prompt[ps1]="${_ksi_prompt[start_mark]}${_ksi_prompt[ps1]}${_ksi_prompt[end_mark]}"
|
||||
fi
|
||||
if [[ -n "${_ksi_prompt[ps1_suffix]}" ]]; then
|
||||
if [[ -n "${_ksi_prompt[ps1_suffix]}" ]]; then
|
||||
_ksi_prompt[ps1_suffix]="${_ksi_prompt[start_suffix_mark]}${_ksi_prompt[ps1_suffix]}${_ksi_prompt[end_suffix_mark]}"
|
||||
fi
|
||||
if [[ -n "${_ksi_prompt[ps2]}" ]]; then
|
||||
if [[ -n "${_ksi_prompt[ps2]}" ]]; then
|
||||
_ksi_prompt[ps2]="${_ksi_prompt[start_mark]}${_ksi_prompt[ps2]}${_ksi_prompt[end_mark]}"
|
||||
fi
|
||||
builtin unset _ksi_prompt[start_mark]
|
||||
@@ -135,13 +194,19 @@ _ksi_main() {
|
||||
builtin unset _ksi_prompt[end_secondary_mark]
|
||||
|
||||
# install our prompt command, using an array if it is unset or already an array,
|
||||
# otherwise append a string
|
||||
# otherwise append a string. We check if _ksi_prompt_command exists as some shell
|
||||
# scripts stupidly export PROMPT_COMMAND making it inherited by all programs launched
|
||||
# from the shell
|
||||
builtin local pc
|
||||
pc='builtin declare -F _ksi_prompt_command > /dev/null 2> /dev/null && _ksi_prompt_command'
|
||||
if [[ -z "${PROMPT_COMMAND}" ]]; then
|
||||
PROMPT_COMMAND=([0]="_ksi_prompt_command")
|
||||
PROMPT_COMMAND=([0]="$pc")
|
||||
elif [[ $(builtin declare -p PROMPT_COMMAND 2> /dev/null) =~ 'declare -a PROMPT_COMMAND' ]]; then
|
||||
PROMPT_COMMAND+=("_ksi_prompt_command")
|
||||
PROMPT_COMMAND+=("$pc")
|
||||
else
|
||||
PROMPT_COMMAND+="; _ksi_prompt_command"
|
||||
PROMPT_COMMAND="${PROMPT_COMMAND%% }"
|
||||
PROMPT_COMMAND="${PROMPT_COMMAND%%;}"
|
||||
PROMPT_COMMAND+="; $pc"
|
||||
fi
|
||||
}
|
||||
_ksi_main
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
#!/bin/fish
|
||||
|
||||
status is-interactive || exit 0
|
||||
not functions -q __ksi_schedule || exit 0
|
||||
|
||||
function __ksi_schedule --on-event fish_prompt -d "Setup kitty integration after other scripts have run, we hope"
|
||||
functions --erase __ksi_schedule
|
||||
test -n "$KITTY_SHELL_INTEGRATION" || return 0
|
||||
|
||||
# To use fish's autoloading feature, kitty prepends the vendored integration script directory to XDG_DATA_DIRS.
|
||||
# The original paths needs to be restored here to not affect other programs.
|
||||
# In particular, if the original XDG_DATA_DIRS does not exist, it needs to be removed.
|
||||
if set -q XDG_DATA_DIRS KITTY_FISH_XDG_DATA_DIR
|
||||
# To use fish's autoloading feature, kitty prepends the vendored integration script directory to XDG_DATA_DIRS.
|
||||
# The original paths needs to be restored here to not affect other programs.
|
||||
# In particular, if the original XDG_DATA_DIRS does not exist, it needs to be removed.
|
||||
if set -q KITTY_FISH_XDG_DATA_DIR
|
||||
if set -q XDG_DATA_DIRS
|
||||
set --global --export --path XDG_DATA_DIRS "$XDG_DATA_DIRS"
|
||||
if set -l index (contains -i "$KITTY_FISH_XDG_DATA_DIR" $XDG_DATA_DIRS)
|
||||
set --erase --global XDG_DATA_DIRS[$index]
|
||||
@@ -20,23 +14,32 @@ function __ksi_schedule --on-event fish_prompt -d "Setup kitty integration after
|
||||
set --global --export --unpath XDG_DATA_DIRS "$XDG_DATA_DIRS"
|
||||
end
|
||||
end
|
||||
set --erase KITTY_FISH_XDG_DATA_DIR
|
||||
end
|
||||
|
||||
status is-interactive || exit 0
|
||||
not functions -q __ksi_schedule || exit 0
|
||||
# Check fish version 3.3.0+ efficiently and fallback to check the last working version 3.2.0, exit on outdated versions.
|
||||
# "Warning: Update fish to version 3.3.0+ to enable kitty shell integration.\n"
|
||||
set -q fish_killring || set -q status_generation || string match -qnv "3.1.*" "$version"
|
||||
or echo -en "\eP@kitty-print|V2FybmluZzogVXBkYXRlIGZpc2ggdG8gdmVyc2lvbiAzLjMuMCsgdG8gZW5hYmxlIGtpdHR5IHNoZWxsIGludGVncmF0aW9uLgo=\e\\" && exit 0 || exit 0
|
||||
|
||||
function __ksi_schedule --on-event fish_prompt -d "Setup kitty integration after other scripts have run, we hope"
|
||||
functions --erase __ksi_schedule
|
||||
test -n "$KITTY_SHELL_INTEGRATION" || return 0
|
||||
set --local _ksi (string split " " -- "$KITTY_SHELL_INTEGRATION")
|
||||
set --erase KITTY_SHELL_INTEGRATION KITTY_FISH_XDG_DATA_DIR
|
||||
set --erase KITTY_SHELL_INTEGRATION
|
||||
|
||||
# Enable cursor shape changes for default mode and vi mode
|
||||
if not contains "no-cursor" $_ksi
|
||||
and not functions -q __ksi_set_cursor
|
||||
|
||||
function __ksi_block_cursor --on-event fish_preexec -d "Set cursor shape to blinking default shape before executing command"
|
||||
printf "\e[0 q"
|
||||
end
|
||||
|
||||
function __ksi_set_cursor --on-variable fish_key_bindings -d "Set the cursor shape for different modes when switching key bindings"
|
||||
if test "$fish_key_bindings" = fish_default_key_bindings
|
||||
not functions -q __ksi_bar_cursor || return
|
||||
function __ksi_bar_cursor --on-event fish_prompt -d "Set cursor shape to blinking bar on prompt"
|
||||
printf "\e[5 q"
|
||||
echo -en "\e[5 q"
|
||||
end
|
||||
# Change the cursor shape on first run
|
||||
set -q argv[1]
|
||||
and __ksi_bar_cursor
|
||||
else
|
||||
functions --erase __ksi_bar_cursor
|
||||
contains "$fish_key_bindings" fish_vi_key_bindings fish_hybrid_key_bindings
|
||||
@@ -56,85 +59,54 @@ function __ksi_schedule --on-event fish_prompt -d "Setup kitty integration after
|
||||
end
|
||||
|
||||
# Change the cursor shape for current mode
|
||||
test "$fish_bind_mode" = "insert" && printf "\e[5 q" || printf "\e[1 q"
|
||||
test "$fish_bind_mode" = "insert" && echo -en "\e[5 q" || echo -en "\e[1 q"
|
||||
end
|
||||
|
||||
__ksi_set_cursor
|
||||
functions -q __ksi_bar_cursor
|
||||
and __ksi_bar_cursor
|
||||
function __ksi_default_cursor --on-event fish_preexec -d "Set cursor shape to blinking default shape before executing command"
|
||||
echo -en "\e[0 q"
|
||||
end
|
||||
|
||||
__ksi_set_cursor init
|
||||
end
|
||||
|
||||
# Enable prompt marking with OSC 133
|
||||
if not contains "no-prompt-mark" $_ksi
|
||||
and not set -q __ksi_prompt_state
|
||||
set --global __ksi_prompt_state first-run
|
||||
|
||||
function __ksi_function_is_not_empty -d "Check if the specified function exists and is not empty"
|
||||
functions --no-details $argv[1] | string match -qnvr '^ *(#|function |end$|$)'
|
||||
end
|
||||
|
||||
function __ksi_mark -d "Tell kitty to mark the current cursor position using OSC 133"
|
||||
printf "\e]133;%s\a" "$argv[1]"
|
||||
end
|
||||
|
||||
function __ksi_prompt_start
|
||||
# Preserve the command exit code from $status
|
||||
set --local cmd_status $status
|
||||
if contains "$__ksi_prompt_state" post-exec first-run
|
||||
__ksi_mark D
|
||||
end
|
||||
function __ksi_mark_prompt_start --on-event fish_prompt
|
||||
contains "$__ksi_prompt_state" post-exec pre-exec ""
|
||||
and echo -en "\e]133;D\a"
|
||||
set --global __ksi_prompt_state prompt-start
|
||||
__ksi_mark A
|
||||
return $cmd_status
|
||||
end
|
||||
|
||||
function __ksi_prompt_end
|
||||
set --local cmd_status $status
|
||||
# fish trims one trailing newline from the output of fish_prompt, so
|
||||
# we need to do the same. See https://github.com/kovidgoyal/kitty/issues/4032
|
||||
set --local op (__ksi_original_fish_prompt) # op is a list because fish splits on newlines in command substitution
|
||||
if set -q op[2]
|
||||
printf '%s\n' $op[1..-2] # print all but last element of the list, each followed by a new line
|
||||
end
|
||||
printf '%s' $op[-1] # print the last component without a newline
|
||||
set --global __ksi_prompt_state prompt-end
|
||||
__ksi_mark B
|
||||
return $cmd_status
|
||||
echo -en "\e]133;A\a"
|
||||
end
|
||||
__ksi_mark_prompt_start
|
||||
|
||||
functions -c fish_prompt __ksi_original_fish_prompt
|
||||
|
||||
if __ksi_function_is_not_empty fish_mode_prompt
|
||||
# See https://github.com/starship/starship/issues/1283
|
||||
# for why we have to test for a non-empty fish_mode_prompt
|
||||
functions -c fish_mode_prompt __ksi_original_fish_mode_prompt
|
||||
function fish_mode_prompt
|
||||
__ksi_prompt_start
|
||||
__ksi_original_fish_mode_prompt
|
||||
end
|
||||
function fish_prompt
|
||||
__ksi_prompt_end
|
||||
end
|
||||
else
|
||||
function fish_prompt
|
||||
__ksi_prompt_start
|
||||
__ksi_prompt_end
|
||||
end
|
||||
function fish_prompt
|
||||
# fish trims one trailing newline from the output of fish_prompt, so
|
||||
# we need to do the same. See https://github.com/kovidgoyal/kitty/issues/4032
|
||||
# op is a list because fish splits on newlines in command substitution
|
||||
set --local op (__ksi_original_fish_prompt)
|
||||
# print all but last element of the list, each followed by a new line
|
||||
set -q op[2]
|
||||
and printf '%s\n' $op[1..-2]
|
||||
# print the last component without a newline
|
||||
printf '%s' $op[-1]
|
||||
set --global __ksi_prompt_state prompt-end
|
||||
echo -en "\e]133;B\a"
|
||||
end
|
||||
|
||||
function __ksi_mark_output_start --on-event fish_preexec
|
||||
set --global __ksi_prompt_state pre-exec
|
||||
__ksi_mark C
|
||||
echo -en "\e]133;C\a"
|
||||
end
|
||||
|
||||
function __ksi_mark_output_end --on-event fish_postexec
|
||||
set --global __ksi_prompt_state post-exec
|
||||
__ksi_mark "D;$status"
|
||||
echo -en "\e]133;D;$status\a"
|
||||
end
|
||||
# With prompt marking kitty clears the current prompt on resize so we need
|
||||
# fish to redraw it
|
||||
set --global fish_handle_reflow 1
|
||||
|
||||
functions --erase __ksi_function_is_not_empty
|
||||
# With prompt marking, kitty clears the current prompt on resize,
|
||||
# so we need fish to redraw it.
|
||||
set --global fish_handle_reflow 1
|
||||
end
|
||||
end
|
||||
|
||||
7
test.py
7
test.py
@@ -5,8 +5,8 @@ import importlib
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from tempfile import TemporaryDirectory
|
||||
from contextlib import contextmanager
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Iterator
|
||||
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -32,8 +32,11 @@ def init_env() -> None:
|
||||
|
||||
def main() -> None:
|
||||
warnings.simplefilter('error')
|
||||
current_home = os.path.expanduser('~') + os.sep
|
||||
paths = os.environ.get('PATH', '/usr/local/sbin:/usr/local/bin:/usr/bin').split(os.pathsep)
|
||||
path = os.pathsep.join(x for x in paths if not x.startswith(current_home))
|
||||
with TemporaryDirectory() as tdir, env_vars(
|
||||
PYTHONWARNINGS='error', HOME=tdir, USERPROFILE=tdir,
|
||||
PYTHONWARNINGS='error', HOME=tdir, USERPROFILE=tdir, PATH=path,
|
||||
XDG_CONFIG_HOME=os.path.join(tdir, '.config'),
|
||||
XDG_CONFIG_DIRS=os.path.join(tdir, '.config'),
|
||||
XDG_DATA_DIRS=os.path.join(tdir, '.local', 'xdg'),
|
||||
|
||||
@@ -57,4 +57,4 @@ with tempfile.NamedTemporaryFile(prefix='install-dmg-', suffix='.py') as f:
|
||||
f.write(script.encode('utf-8'))
|
||||
f.flush()
|
||||
run(f'scp bypy/b/macos/dist/{dmg} {f.name} {HOST}:/tmp')
|
||||
run(f'ssh {HOST} python /tmp/{os.path.basename(f.name)} /tmp/{dmg}')
|
||||
run(f'ssh {HOST} python3 /tmp/{os.path.basename(f.name)} /tmp/{dmg}')
|
||||
|
||||
Reference in New Issue
Block a user