Compare commits
240 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
750cf7ad20 | ||
|
|
962acd1537 | ||
|
|
7a44765860 | ||
|
|
007be8e52c | ||
|
|
5e4c98eae6 | ||
|
|
858a6dc27e | ||
|
|
ad5d14c672 | ||
|
|
199685a25b | ||
|
|
d99e243f57 | ||
|
|
64f1211cf8 | ||
|
|
9432f86e43 | ||
|
|
fc99d4d757 | ||
|
|
7fb2b21a22 | ||
|
|
260b300da9 | ||
|
|
f26d5b02cb | ||
|
|
c9ff061feb | ||
|
|
86ad318e6c | ||
|
|
df05339d2a | ||
|
|
ec1303a232 | ||
|
|
c1049734e6 | ||
|
|
99fa79caaf | ||
|
|
761062a6d7 | ||
|
|
11227b3ee1 | ||
|
|
8d63d50aea | ||
|
|
3360aa6c33 | ||
|
|
bfffd98fd4 | ||
|
|
9a86184a45 | ||
|
|
00828cb804 | ||
|
|
ec31a36fd9 | ||
|
|
9003c76261 | ||
|
|
7148f262c0 | ||
|
|
3ab417e291 | ||
|
|
4ced7b7657 | ||
|
|
ad69143573 | ||
|
|
4a71afaf96 | ||
|
|
a1b87f445b | ||
|
|
33e63f000a | ||
|
|
1c8b7955eb | ||
|
|
0bade29c25 | ||
|
|
4cff3e51cb | ||
|
|
d264c3d91e | ||
|
|
099fed07e9 | ||
|
|
091fec0867 | ||
|
|
a670268a6b | ||
|
|
caa44b3d4a | ||
|
|
f3977da8f3 | ||
|
|
7dc3184f31 | ||
|
|
d09c20aa01 | ||
|
|
81411e6b54 | ||
|
|
a8d1c73fec | ||
|
|
1b3742efbb | ||
|
|
e01bb09e8c | ||
|
|
2c9c0751a4 | ||
|
|
e10a7579e8 | ||
|
|
c52a04c7d3 | ||
|
|
79dd98b20e | ||
|
|
2e71429b03 | ||
|
|
4ab299d0af | ||
|
|
c60a941d1b | ||
|
|
e2197c586b | ||
|
|
93e9d3cb5f | ||
|
|
2e7b68bf74 | ||
|
|
6ffb198e9c | ||
|
|
604458810e | ||
|
|
af5ed093b8 | ||
|
|
8f491e7dbb | ||
|
|
c1324da3fc | ||
|
|
0bbc4462fd | ||
|
|
e0261e925e | ||
|
|
46b3f71b8f | ||
|
|
3e598a17cf | ||
|
|
c1b13f2db2 | ||
|
|
a059e49579 | ||
|
|
6d7df1c5e8 | ||
|
|
dd5715ce79 | ||
|
|
43acf3c5b1 | ||
|
|
f7db9e3527 | ||
|
|
1621a67f36 | ||
|
|
248631a1a8 | ||
|
|
b4cb6e10ca | ||
|
|
5fa73e4413 | ||
|
|
49c4b20113 | ||
|
|
43ece23b89 | ||
|
|
f976851442 | ||
|
|
5470dd74bd | ||
|
|
ddd178fa82 | ||
|
|
8d411cac5f | ||
|
|
bfbb85399e | ||
|
|
9f8a120664 | ||
|
|
e1cd6b6037 | ||
|
|
2fd4487922 | ||
|
|
27a459b0dd | ||
|
|
6c344d4ae2 | ||
|
|
d7aa9952d8 | ||
|
|
09093c8f3e | ||
|
|
222bf09df4 | ||
|
|
3041ff8d25 | ||
|
|
bfe1952705 | ||
|
|
97181a39da | ||
|
|
39b3d3de0f | ||
|
|
056d16017f | ||
|
|
5cef0aef92 | ||
|
|
f794de5b9b | ||
|
|
4a67af9b90 | ||
|
|
f178dff4e0 | ||
|
|
ba821cb02f | ||
|
|
b169831810 | ||
|
|
06b3c71304 | ||
|
|
fe94f4cbb4 | ||
|
|
a4daa49f70 | ||
|
|
3b1d534f6d | ||
|
|
c1777b1098 | ||
|
|
c827a29a7b | ||
|
|
253b219d67 | ||
|
|
b702f3daf1 | ||
|
|
76c9f46438 | ||
|
|
c6c203da43 | ||
|
|
00302b74d1 | ||
|
|
32f6f18527 | ||
|
|
4fd0446538 | ||
|
|
082ad61d14 | ||
|
|
0ca4faa25b | ||
|
|
46a0566e2e | ||
|
|
97440d45d6 | ||
|
|
c5afe4e745 | ||
|
|
bc6e819396 | ||
|
|
d650a97dda | ||
|
|
cf88eb9d60 | ||
|
|
0a742ea8d0 | ||
|
|
cb8935746f | ||
|
|
608ac953e5 | ||
|
|
4d0d0b205d | ||
|
|
07b643e24c | ||
|
|
02fb020dfd | ||
|
|
bac6ebdf95 | ||
|
|
b7072d4097 | ||
|
|
d7c7bb00b8 | ||
|
|
0485f0c7ed | ||
|
|
2ade6c0739 | ||
|
|
5eeb19871b | ||
|
|
083a0ae5fc | ||
|
|
ccc370e1c4 | ||
|
|
23b8cafc41 | ||
|
|
d7ab96856c | ||
|
|
3c77229f61 | ||
|
|
fcd206891f | ||
|
|
3bf9130b0a | ||
|
|
4125ac013f | ||
|
|
e089e9c121 | ||
|
|
81af379bbc | ||
|
|
9b07aa6894 | ||
|
|
a75140c6d7 | ||
|
|
f3364cfdc0 | ||
|
|
f64b4e0e56 | ||
|
|
a1356d3bcf | ||
|
|
4c5a1ceefa | ||
|
|
e4b4a35375 | ||
|
|
cc2afef390 | ||
|
|
7c5706ead9 | ||
|
|
bcb739fcd2 | ||
|
|
85ef3724f1 | ||
|
|
cb21422836 | ||
|
|
1c9674cec9 | ||
|
|
6dcc09a96f | ||
|
|
0260c9d3fb | ||
|
|
80c03f28f8 | ||
|
|
eeaf67079a | ||
|
|
c50863c0d5 | ||
|
|
f4ab6542fa | ||
|
|
dfbe1bd234 | ||
|
|
1e89cdc055 | ||
|
|
7a40959f13 | ||
|
|
2b4d55804c | ||
|
|
858dac5601 | ||
|
|
e944945b7a | ||
|
|
920151a460 | ||
|
|
ac2a01fb09 | ||
|
|
a7933018cb | ||
|
|
5ee889eadd | ||
|
|
e811f03011 | ||
|
|
417c81de60 | ||
|
|
212c653165 | ||
|
|
e36f11385f | ||
|
|
abb05f4883 | ||
|
|
2973f33959 | ||
|
|
053c2ed2b9 | ||
|
|
1865570390 | ||
|
|
ba1ee7e6cc | ||
|
|
f6b0fcbc0f | ||
|
|
a802058886 | ||
|
|
bbc1f68966 | ||
|
|
ca0b42c3bd | ||
|
|
9536a475ff | ||
|
|
cfd731c376 | ||
|
|
de1015f6ac | ||
|
|
6c0730fef4 | ||
|
|
c73132610e | ||
|
|
77abaaf8b8 | ||
|
|
2787e86fd7 | ||
|
|
355808b0f6 | ||
|
|
80c13fa75b | ||
|
|
9a6c2aa1ea | ||
|
|
63d76ee837 | ||
|
|
7c48db7da8 | ||
|
|
bd8d5f6288 | ||
|
|
7413b7c4f6 | ||
|
|
80e3c871ce | ||
|
|
74c1e02274 | ||
|
|
efb0f6f24a | ||
|
|
dbf8580dc3 | ||
|
|
f110a01ffe | ||
|
|
eced40a761 | ||
|
|
5ac315bc3a | ||
|
|
32df4daa63 | ||
|
|
5836e456e5 | ||
|
|
dc6ab69387 | ||
|
|
7301c56753 | ||
|
|
3b76d657bd | ||
|
|
2adb6240e4 | ||
|
|
e11af496da | ||
|
|
f6e33406d7 | ||
|
|
6eab138d68 | ||
|
|
33287115de | ||
|
|
8e36056dd8 | ||
|
|
4a34f596a8 | ||
|
|
7616a3e743 | ||
|
|
c64353c324 | ||
|
|
8eac22d37a | ||
|
|
35fab12330 | ||
|
|
a46988bc50 | ||
|
|
26e1d6fe5c | ||
|
|
ec68739585 | ||
|
|
33de0f821f | ||
|
|
86ce11134a | ||
|
|
bef4905416 | ||
|
|
baa8152248 | ||
|
|
34fe103c2b | ||
|
|
0d08014bd5 | ||
|
|
dd0130180b | ||
|
|
accdf9a6a8 |
5
.gitattributes
vendored
5
.gitattributes
vendored
@@ -7,6 +7,11 @@ kitty/rgb.py linguist-generated=true
|
|||||||
kitty/gl-wrapper.* linguist-generated=true
|
kitty/gl-wrapper.* linguist-generated=true
|
||||||
kitty/glfw-wrapper.* linguist-generated=true
|
kitty/glfw-wrapper.* linguist-generated=true
|
||||||
kitty/parse-graphics-command.h linguist-generated=true
|
kitty/parse-graphics-command.h linguist-generated=true
|
||||||
|
kitty/options/types.py linguist-generated=true
|
||||||
|
kitty/options/parse.py linguist-generated=true
|
||||||
|
kitty/options/to-c-generated.h linguist-generated=true
|
||||||
|
kittens/diff/options/types.py linguist-generated=true
|
||||||
|
kittens/diff/options/parse.py linguist-generated=true
|
||||||
glfw/*.c linguist-vendored=true
|
glfw/*.c linguist-vendored=true
|
||||||
glfw/*.h linguist-vendored=true
|
glfw/*.h linguist-vendored=true
|
||||||
kittens/unicode_input/names.h linguist-generated=true
|
kittens/unicode_input/names.h linguist-generated=true
|
||||||
|
|||||||
10
.github/ISSUE_TEMPLATE/bug_report.md
vendored
10
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -17,17 +17,15 @@ Steps to reproduce the behavior:
|
|||||||
3. ZZZ
|
3. ZZZ
|
||||||
4. See error
|
4. See error
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
**Environment details**
|
**Environment details**
|
||||||
OS: Name and version of operating system(s)
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Output of kitty --debug-config
|
Press Ctrl+Shift+F6 in kitty, to copy debug output about kitty and its
|
||||||
|
configuration to the clipboard and paste it here.
|
||||||
|
|
||||||
|
On older versions of kitty, run kitty --debug-config instead
|
||||||
```
|
```
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
|
|||||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -76,7 +76,7 @@ jobs:
|
|||||||
python-version: 3.8
|
python-version: 3.8
|
||||||
|
|
||||||
- name: Install build-only deps
|
- name: Install build-only deps
|
||||||
run: pip install flake8 mypy sphinx
|
run: pip install flake8 mypy types-requests types-docutils sphinx
|
||||||
|
|
||||||
- name: Run pyflakes
|
- name: Run pyflakes
|
||||||
run: python -m flake8 --count .
|
run: python -m flake8 --count .
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# vim:fileencoding=utf-8
|
# vim:fileencoding=utf-8
|
||||||
# License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ def runpy(args: List[str]) -> None:
|
|||||||
|
|
||||||
def hold(args: List[str]) -> None:
|
def hold(args: List[str]) -> None:
|
||||||
import subprocess
|
import subprocess
|
||||||
from contextlib import suppress
|
|
||||||
import tty
|
import tty
|
||||||
|
from contextlib import suppress
|
||||||
ret = subprocess.Popen(args[1:]).wait()
|
ret = subprocess.Popen(args[1:]).wait()
|
||||||
with suppress(BaseException):
|
with suppress(BaseException):
|
||||||
print('\n\x1b[1;32mPress any key to exit', end='', flush=True)
|
print('\n\x1b[1;32mPress any key to exit', end='', flush=True)
|
||||||
@@ -72,7 +72,10 @@ def run_kitten(args: List[str]) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def edit_config_file(args: List[str]) -> None:
|
def edit_config_file(args: List[str]) -> None:
|
||||||
|
from kitty.cli import create_default_opts
|
||||||
|
from kitty.fast_data_types import set_options
|
||||||
from kitty.utils import edit_config_file as f
|
from kitty.utils import edit_config_file as f
|
||||||
|
set_options(create_default_opts())
|
||||||
f()
|
f()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ def copy_python(env):
|
|||||||
for x in bases:
|
for x in bases:
|
||||||
iv['sanitize_source_folder'](os.path.join(env.py_dir, x))
|
iv['sanitize_source_folder'](os.path.join(env.py_dir, x))
|
||||||
py_compile(env.py_dir)
|
py_compile(env.py_dir)
|
||||||
freeze_python(env.py_dir, pdir, env.obj_dir, ext_map, develop_mode_env_var='KITTY_DEVELOP_FROM')
|
freeze_python(env.py_dir, pdir, env.obj_dir, ext_map, develop_mode_env_var='KITTY_DEVELOP_FROM', remove_pyc_files=True)
|
||||||
|
|
||||||
|
|
||||||
def build_launcher(env):
|
def build_launcher(env):
|
||||||
|
|||||||
@@ -348,7 +348,7 @@ class Freeze(object):
|
|||||||
for x in bases:
|
for x in bases:
|
||||||
iv['sanitize_source_folder'](os.path.join(self.python_stdlib, x))
|
iv['sanitize_source_folder'](os.path.join(self.python_stdlib, x))
|
||||||
self.compile_py_modules()
|
self.compile_py_modules()
|
||||||
freeze_python(self.python_stdlib, pdir, self.obj_dir, ext_map, develop_mode_env_var='KITTY_DEVELOP_FROM')
|
freeze_python(self.python_stdlib, pdir, self.obj_dir, ext_map, develop_mode_env_var='KITTY_DEVELOP_FROM', remove_pyc_files=True)
|
||||||
iv['build_frozen_launcher']([path_to_freeze_dir(), self.obj_dir])
|
iv['build_frozen_launcher']([path_to_freeze_dir(), self.obj_dir])
|
||||||
os.rename(join(dirname(self.contents_dir), 'bin', 'kitty'), join(self.contents_dir, 'MacOS', 'kitty'))
|
os.rename(join(dirname(self.contents_dir), 'bin', 'kitty'), join(self.contents_dir, 'MacOS', 'kitty'))
|
||||||
shutil.rmtree(join(dirname(self.contents_dir), 'bin'))
|
shutil.rmtree(join(dirname(self.contents_dir), 'bin'))
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ kitty/glfw-wrapper.c
|
|||||||
kitty/emoji.h
|
kitty/emoji.h
|
||||||
kittens/unicode_input/names.h
|
kittens/unicode_input/names.h
|
||||||
kitty/parse-graphics-command.h
|
kitty/parse-graphics-command.h
|
||||||
|
kitty/options/types.py
|
||||||
|
kitty/options/parse.py
|
||||||
|
kitty/options/to-c-generated.h
|
||||||
|
kittens/diff/options/types.py
|
||||||
|
kittens/diff/options/parse.py
|
||||||
'''
|
'''
|
||||||
|
|
||||||
p = subprocess.Popen([
|
p = subprocess.Popen([
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ Toggle maximized :sc:`toggle_maximized`
|
|||||||
Input unicode character :sc:`input_unicode_character` (also :kbd:`^+⌘+space` on macOS)
|
Input unicode character :sc:`input_unicode_character` (also :kbd:`^+⌘+space` on macOS)
|
||||||
Click URL using the keyboard :sc:`open_url`
|
Click URL using the keyboard :sc:`open_url`
|
||||||
Reset the terminal :sc:`reset_terminal`
|
Reset the terminal :sc:`reset_terminal`
|
||||||
|
Reload :file:`kitty.conf` :sc:`reload_config_file`
|
||||||
|
Debug :file:`kitty.conf` :sc:`debug_config`
|
||||||
Pass current selection to program :sc:`pass_selection_to_program`
|
Pass current selection to program :sc:`pass_selection_to_program`
|
||||||
Edit |kitty| config file :sc:`edit_config_file`
|
Edit |kitty| config file :sc:`edit_config_file`
|
||||||
Open a |kitty| shell :sc:`kitty_shell`
|
Open a |kitty| shell :sc:`kitty_shell`
|
||||||
|
|||||||
@@ -48,6 +48,13 @@ particular desktop, but it should work for most major desktop environments.
|
|||||||
# Update the path to the kitty icon in the kitty.desktop file
|
# 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
|
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/>`_
|
||||||
|
command to manage your manual installations, the following takes care of the
|
||||||
|
above for you (use with :file:`dest=~/.local/stow`)::
|
||||||
|
|
||||||
|
cd ~/.local/stow
|
||||||
|
stow -v kitty.app
|
||||||
|
|
||||||
|
|
||||||
Customizing the installation
|
Customizing the installation
|
||||||
|
|||||||
@@ -6,23 +6,34 @@ Building kitty from source
|
|||||||
:target: https://github.com/kovidgoyal/kitty/actions?query=workflow%3ACI
|
:target: https://github.com/kovidgoyal/kitty/actions?query=workflow%3ACI
|
||||||
|
|
||||||
|
|
||||||
|kitty| is designed to run from source, for easy hackability. Make sure
|
|kitty| is designed to run from source, for easy hack-ability. Make sure
|
||||||
the following dependencies are installed first.
|
the following dependencies are installed first.
|
||||||
|
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
If you are making small changes only to the python parts of kitty, there is no need to
|
||||||
|
build kitty at all, instead, assuming you have installed the official kitty
|
||||||
|
binaries, you can simply set the KITTY_DEVELOP_FROM enviroment variable to
|
||||||
|
point to the directory into which you have checked out the kitty source
|
||||||
|
code. kitty will then load its python code from there. You should use a
|
||||||
|
version of the source that matches the binary version as closely as
|
||||||
|
possible, since the two are tightly coupled.
|
||||||
|
|
||||||
|
|
||||||
Dependencies
|
Dependencies
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
Run-time dependencies:
|
Run-time dependencies:
|
||||||
|
|
||||||
* python >= 3.5
|
* python >= 3.6
|
||||||
* harfbuzz >= 1.5.0
|
* harfbuzz >= 2.2.0
|
||||||
* zlib
|
* zlib
|
||||||
* libpng
|
* libpng
|
||||||
* liblcms2
|
* liblcms2
|
||||||
* freetype (not needed on macOS)
|
* freetype (not needed on macOS)
|
||||||
* fontconfig (not needed on macOS)
|
* fontconfig (not needed on macOS)
|
||||||
* libcanberra (not needed on macOS)
|
* libcanberra (not needed on macOS)
|
||||||
* ImageMagick (optional, needed to use the ``kitty icat`` tool to display images in the terminal)
|
* ImageMagick (optional, needed to use the ``kitty +kitten icat`` tool to display images in the terminal)
|
||||||
* pygments (optional, need for syntax highlighting in ``kitty +kitten diff``)
|
* pygments (optional, need for syntax highlighting in ``kitty +kitten diff``)
|
||||||
|
|
||||||
Build-time dependencies:
|
Build-time dependencies:
|
||||||
@@ -67,8 +78,7 @@ you might have to rebuild the app.
|
|||||||
.. note::
|
.. note::
|
||||||
The released :file:`kitty.dmg` includes all dependencies, unlike the
|
The released :file:`kitty.dmg` includes all dependencies, unlike the
|
||||||
:file:`kitty.app` built above and is built automatically by using the
|
:file:`kitty.app` built above and is built automatically by using the
|
||||||
:file:`kitty` branch of `build-calibre
|
<https://github.com/kovidgoyal/bypy>`_ however, that is designed to
|
||||||
<https://github.com/kovidgoyal/build-calibre>`_ however, that is designed to
|
|
||||||
run on Linux and is not for the faint of heart.
|
run on Linux and is not for the faint of heart.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,99 @@ Changelog
|
|||||||
|kitty| is a feature-rich, cross-platform, *fast*, GPU based terminal.
|
|kitty| is a feature-rich, cross-platform, *fast*, GPU based terminal.
|
||||||
To update |kitty|, :doc:`follow the instructions <binary>`.
|
To update |kitty|, :doc:`follow the instructions <binary>`.
|
||||||
|
|
||||||
|
0.21.0 [2021-06-12]
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
- Allow reloading the :file:`kitty.conf` config file by pressing
|
||||||
|
:sc:`reload_config_file`. (:iss:`1292`)
|
||||||
|
|
||||||
|
- Allow clicking URLs to open them without needing to also hold
|
||||||
|
:kbd:`ctrl+shift`
|
||||||
|
|
||||||
|
- Allow remapping all mouse button press/release events to perform arbitrary
|
||||||
|
actions. :ref:`See details <conf-kitty-mouse.mousemap>`.
|
||||||
|
|
||||||
|
- Support infinite length ligatures (:iss:`3504`)
|
||||||
|
|
||||||
|
- **Backward incompatibility**: The options to control which modifiers keys to
|
||||||
|
press for various mouse actions have been removed, if you used these options,
|
||||||
|
you will need to replace them with configuration using the new
|
||||||
|
:ref:`mouse actions framework <conf-kitty-mouse.mousemap>` as they will be
|
||||||
|
ignored. The options were: ``terminal_select_modifiers``,
|
||||||
|
``rectangle_select_modifiers`` and ``open_url_modifiers``.
|
||||||
|
|
||||||
|
- Add a configurable mouse action (:kbd:`ctrl+alt+triplepress` to select from the
|
||||||
|
clicked point to the end of the line. (:iss:`3585`)
|
||||||
|
|
||||||
|
- Add the ability to un-scroll the screen to the ``kitty @ scroll-window``
|
||||||
|
remote control command (:iss:`3604`)
|
||||||
|
|
||||||
|
- A new option, :opt:`tab_bar_margin_height` to add margins around the
|
||||||
|
top and bottom edges of the tab bar (:iss:`3247`)
|
||||||
|
|
||||||
|
- Unicode input kitten: Fix a regression in 0.20.0 that broke keyboard handling
|
||||||
|
when the NumLock or CapsLock modifiers were engaged. (:iss:`3587`)
|
||||||
|
|
||||||
|
- Fix a regression in 0.20.0 that sent incorrect bytes for the :kbd:`F1-F4` keys
|
||||||
|
in rmkx mode (:iss:`3586`)
|
||||||
|
|
||||||
|
- macOS: When the Apple Color Emoji font lacks an emoji glyph search for it in other
|
||||||
|
installed fonts (:iss:`3591`)
|
||||||
|
|
||||||
|
- macOS: Fix rendering getting stuck on some machines after sleep/screensaver
|
||||||
|
(:iss:`2016`)
|
||||||
|
|
||||||
|
- macOS: Add a new ``Shell`` menu to the global menubar with some commonly used
|
||||||
|
actions (:pull:`3653`)
|
||||||
|
|
||||||
|
- macOS: Fix the baseline for text not matching other CoreText based
|
||||||
|
applications for some fonts (:iss:`2022`)
|
||||||
|
|
||||||
|
- Add a few more special commandline arguments for the launch command. Now all
|
||||||
|
``KITTY_PIPE_DATA`` is also available via command line argument substitution
|
||||||
|
(:iss:`3593`)
|
||||||
|
|
||||||
|
- Fix dynamically changing the background color in a window causing rendering
|
||||||
|
artifacts in the tab bar (:iss:`3595`)
|
||||||
|
|
||||||
|
- Fix passing STDIN to launched background processes causing them to not inherit
|
||||||
|
environment variables (:pull:`3603`)
|
||||||
|
|
||||||
|
- Fix deleting windows that are not the last window via remote control leaving
|
||||||
|
no window focused (:iss:`3619`)
|
||||||
|
|
||||||
|
- Add an option :option:`kitty @ get-text --add-cursor` to also get the current
|
||||||
|
cursor position and state as ANSI escape codes (:iss:`3625`)
|
||||||
|
|
||||||
|
- Add an option :option:`kitty @ get-text --add-wrap-markers` to add line wrap
|
||||||
|
markers to the output (:pull:`3633`)
|
||||||
|
|
||||||
|
- Improve rendering of curly underlines on HiDPI screens (:pull:`3637`)
|
||||||
|
|
||||||
|
- ssh kitten: Mimic behavior of ssh command line client more closely by
|
||||||
|
executing any command specified on the command line via the users' shell
|
||||||
|
just as ssh does (:iss:`3638`)
|
||||||
|
|
||||||
|
- Fix trailing parentheses in URLs not being detected (:iss:`3688`)
|
||||||
|
|
||||||
|
- Tab bar: Use a lower contrast color for tab separators (:pull:`3666`)
|
||||||
|
|
||||||
|
- Fix a regression that caused using the ``title`` command in session files
|
||||||
|
to stop working (:iss:`3676`)
|
||||||
|
|
||||||
|
- macOS: Fix a rare crash on exit (:iss:`3686`)
|
||||||
|
|
||||||
|
- Fix ligatures not working with the `Iosevka
|
||||||
|
<https://github.com/be5invis/Iosevka>`_ font (requires Iosevka >= 7.0.4)
|
||||||
|
(:iss:`297`)
|
||||||
|
|
||||||
|
- Remote control: Allow matching tabs by index number in currently active OS
|
||||||
|
Window (:iss:`3708`)
|
||||||
|
|
||||||
|
- ssh kitten: Fix non-standard properties in terminfo such as the ones used for
|
||||||
|
true color not being copied (:iss:`312`)
|
||||||
|
|
||||||
|
|
||||||
0.20.3 [2021-05-06]
|
0.20.3 [2021-05-06]
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
@@ -680,7 +773,7 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
|
|||||||
beam and underline cursors (:iss:`2337` and :pull:`2342`)
|
beam and underline cursors (:iss:`2337` and :pull:`2342`)
|
||||||
|
|
||||||
- When the application running in the terminal grabs the mouse, pass middle
|
- When the application running in the terminal grabs the mouse, pass middle
|
||||||
clicks to the application unless :opt:`terminal_select_modifiers` are
|
clicks to the application unless `terminal_select_modifiers` are
|
||||||
pressed (:iss:`2368`)
|
pressed (:iss:`2368`)
|
||||||
|
|
||||||
- A new ``copy_and_clear_or_interrupt`` function (:iss:`2403`)
|
- A new ``copy_and_clear_or_interrupt`` function (:iss:`2403`)
|
||||||
@@ -788,7 +881,7 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
|
|||||||
- macOS: Add a :code:`titlebar-only` setting to
|
- macOS: Add a :code:`titlebar-only` setting to
|
||||||
:opt:`hide_window_decorations` to only hide the title bar (:pull:`2286`)
|
:opt:`hide_window_decorations` to only hide the title bar (:pull:`2286`)
|
||||||
|
|
||||||
- Fix a segfault when using :option:`kitty --debug-config` with maps (:iss:`2270`)
|
- Fix a segfault when using ``--debug-config`` with maps (:iss:`2270`)
|
||||||
|
|
||||||
- ``goto_tab`` now maps numbers larger than the last tab to the last tab
|
- ``goto_tab`` now maps numbers larger than the last tab to the last tab
|
||||||
(:iss:`2291`)
|
(:iss:`2291`)
|
||||||
@@ -960,7 +1053,7 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
|
|||||||
the mouse pointer shape when the terminal programs grabs the pointer
|
the mouse pointer shape when the terminal programs grabs the pointer
|
||||||
(:iss:`1808`)
|
(:iss:`1808`)
|
||||||
|
|
||||||
- Add an option :opt:`terminal_select_modifiers` to control which modifiers
|
- Add an option `terminal_select_modifiers` to control which modifiers
|
||||||
are used to override mouse selection even when a terminal application has
|
are used to override mouse selection even when a terminal application has
|
||||||
grabbed the mouse (:iss:`1774`)
|
grabbed the mouse (:iss:`1774`)
|
||||||
|
|
||||||
@@ -1664,8 +1757,8 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
|
|||||||
|
|
||||||
- Support "-T" as an alias for "--title" (:pull:`659`)
|
- Support "-T" as an alias for "--title" (:pull:`659`)
|
||||||
|
|
||||||
- Fix a regression in the previous release that broke using :option:`kitty
|
- Fix a regression in the previous release that broke using
|
||||||
--debug-config` with custom key mappings (:iss:`695`)
|
``--debug-config`` with custom key mappings (:iss:`695`)
|
||||||
|
|
||||||
|
|
||||||
0.11.1 [2018-06-17]
|
0.11.1 [2018-06-17]
|
||||||
|
|||||||
117
docs/conf.py
117
docs/conf.py
@@ -13,10 +13,7 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import (
|
from typing import Any, Callable, Dict, Iterable, List, Match, Optional, Tuple
|
||||||
Any, Callable, Dict, Iterable, List, Match, Optional, Sequence, Tuple,
|
|
||||||
Union
|
|
||||||
)
|
|
||||||
|
|
||||||
from docutils import nodes
|
from docutils import nodes
|
||||||
from docutils.parsers.rst.roles import set_classes
|
from docutils.parsers.rst.roles import set_classes
|
||||||
@@ -32,10 +29,9 @@ kitty_src = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|||||||
if kitty_src not in sys.path:
|
if kitty_src not in sys.path:
|
||||||
sys.path.insert(0, kitty_src)
|
sys.path.insert(0, kitty_src)
|
||||||
|
|
||||||
from kitty.conf.definition import Option, Shortcut # noqa
|
from kitty.conf.types import Definition # noqa
|
||||||
from kitty.constants import str_version # noqa
|
from kitty.constants import str_version # noqa
|
||||||
|
|
||||||
|
|
||||||
# config {{{
|
# config {{{
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
@@ -248,8 +244,8 @@ def add_html_context(app: Any, pagename: str, templatename: str, context: Any, *
|
|||||||
|
|
||||||
# CLI docs {{{
|
# CLI docs {{{
|
||||||
def write_cli_docs(all_kitten_names: Iterable[str]) -> None:
|
def write_cli_docs(all_kitten_names: Iterable[str]) -> None:
|
||||||
from kitty.launch import options_spec as launch_options_spec
|
|
||||||
from kitty.cli import option_spec_as_rst
|
from kitty.cli import option_spec_as_rst
|
||||||
|
from kitty.launch import options_spec as launch_options_spec
|
||||||
with open('generated/launch.rst', 'w') as f:
|
with open('generated/launch.rst', 'w') as f:
|
||||||
f.write(option_spec_as_rst(
|
f.write(option_spec_as_rst(
|
||||||
appname='launch', ospec=launch_options_spec, heading_char='_',
|
appname='launch', ospec=launch_options_spec, heading_char='_',
|
||||||
@@ -264,7 +260,7 @@ if you specify a program-to-run you can use the special placeholder
|
|||||||
'kitty --to', 'kitty @ --to'))
|
'kitty --to', 'kitty @ --to'))
|
||||||
as_rst = partial(option_spec_as_rst, heading_char='_')
|
as_rst = partial(option_spec_as_rst, heading_char='_')
|
||||||
from kitty.rc.base import all_command_names, command_for_name
|
from kitty.rc.base import all_command_names, command_for_name
|
||||||
from kitty.remote_control import global_options_spec, cli_msg
|
from kitty.remote_control import cli_msg, global_options_spec
|
||||||
with open('generated/cli-kitty-at.rst', 'w') as f:
|
with open('generated/cli-kitty-at.rst', 'w') as f:
|
||||||
p = partial(print, file=f)
|
p = partial(print, file=f)
|
||||||
p('kitty @\n' + '-' * 80)
|
p('kitty @\n' + '-' * 80)
|
||||||
@@ -293,7 +289,9 @@ if you specify a program-to-run you can use the special placeholder
|
|||||||
|
|
||||||
|
|
||||||
def write_remote_control_protocol_docs() -> None: # {{{
|
def write_remote_control_protocol_docs() -> None: # {{{
|
||||||
from kitty.rc.base import all_command_names, command_for_name, RemoteCommand
|
from kitty.rc.base import (
|
||||||
|
RemoteCommand, all_command_names, command_for_name
|
||||||
|
)
|
||||||
field_pat = re.compile(r'\s*([a-zA-Z0-9_+]+)\s*:\s*(.+)')
|
field_pat = re.compile(r'\s*([a-zA-Z0-9_+]+)\s*:\s*(.+)')
|
||||||
|
|
||||||
def format_cmd(p: Callable, name: str, cmd: RemoteCommand) -> None:
|
def format_cmd(p: Callable, name: str, cmd: RemoteCommand) -> None:
|
||||||
@@ -390,12 +388,14 @@ class SessionLexer(RegexLexer):
|
|||||||
|
|
||||||
|
|
||||||
def link_role(name: str, rawtext: str, text: str, lineno: int, inliner: Any, options: Any = {}, content: Any = []) -> Tuple[List, List]:
|
def link_role(name: str, rawtext: str, text: str, lineno: int, inliner: Any, options: Any = {}, content: Any = []) -> Tuple[List, List]:
|
||||||
|
text = text.replace('\n', ' ')
|
||||||
m = re.match(r'(.+)\s+<(.+?)>', text)
|
m = re.match(r'(.+)\s+<(.+?)>', text)
|
||||||
if m is None:
|
if m is None:
|
||||||
msg = inliner.reporter.error(f'link "{text}" not recognized', line=lineno)
|
msg = inliner.reporter.error(f'link "{text}" not recognized', line=lineno)
|
||||||
prb = inliner.problematic(rawtext, rawtext, msg)
|
prb = inliner.problematic(rawtext, rawtext, msg)
|
||||||
return [prb], [msg]
|
return [prb], [msg]
|
||||||
text, url = m.group(1, 2)
|
text, url = m.group(1, 2)
|
||||||
|
url = url.replace(' ', '')
|
||||||
set_classes(options)
|
set_classes(options)
|
||||||
node = nodes.reference(rawtext, text, refuri=url, **options)
|
node = nodes.reference(rawtext, text, refuri=url, **options)
|
||||||
return [node], []
|
return [node], []
|
||||||
@@ -447,87 +447,6 @@ def parse_shortcut_node(env: Any, sig: str, signode: Any) -> str:
|
|||||||
return sig
|
return sig
|
||||||
|
|
||||||
|
|
||||||
def render_conf(conf_name: str, all_options: Iterable[Union['Option', Sequence['Shortcut']]]) -> str:
|
|
||||||
from kitty.conf.definition import merged_opts, Option, Group
|
|
||||||
ans = ['.. default-domain:: conf', '']
|
|
||||||
a = ans.append
|
|
||||||
current_group: Optional[Group] = None
|
|
||||||
all_options_ = list(all_options)
|
|
||||||
kitty_mod = 'kitty_mod'
|
|
||||||
|
|
||||||
def render_group(group: Group) -> None:
|
|
||||||
a('')
|
|
||||||
a(f'.. _conf-{conf_name}-{group.name}:')
|
|
||||||
a('')
|
|
||||||
a(group.short_text)
|
|
||||||
heading_level = '+' if '.' in group.name else '^'
|
|
||||||
a(heading_level * (len(group.short_text) + 20))
|
|
||||||
a('')
|
|
||||||
if group.start_text:
|
|
||||||
a(group.start_text)
|
|
||||||
a('')
|
|
||||||
|
|
||||||
def handle_group_end(group: Group) -> None:
|
|
||||||
if group.end_text:
|
|
||||||
assert current_group is not None
|
|
||||||
a(''), a(current_group.end_text)
|
|
||||||
|
|
||||||
def handle_group(new_group: Group, new_group_is_shortcut: bool = False) -> None:
|
|
||||||
nonlocal current_group
|
|
||||||
if new_group is not current_group:
|
|
||||||
if current_group:
|
|
||||||
handle_group_end(current_group)
|
|
||||||
current_group = new_group
|
|
||||||
render_group(current_group)
|
|
||||||
|
|
||||||
def handle_option(i: int, opt: Option) -> None:
|
|
||||||
nonlocal kitty_mod
|
|
||||||
if not opt.long_text or not opt.add_to_docs:
|
|
||||||
return
|
|
||||||
handle_group(opt.group)
|
|
||||||
if opt.name == 'kitty_mod':
|
|
||||||
kitty_mod = opt.defval_as_string
|
|
||||||
mopts = list(merged_opts(all_options_, opt, i))
|
|
||||||
a('.. opt:: ' + ', '.join(conf_name + '.' + mo.name for mo in mopts))
|
|
||||||
a('.. code-block:: conf')
|
|
||||||
a('')
|
|
||||||
sz = max(len(x.name) for x in mopts)
|
|
||||||
for mo in mopts:
|
|
||||||
a((' {:%ds} {}' % sz).format(mo.name, mo.defval_as_string))
|
|
||||||
a('')
|
|
||||||
if opt.long_text:
|
|
||||||
a(expand_opt_references(conf_name, opt.long_text))
|
|
||||||
a('')
|
|
||||||
|
|
||||||
def handle_shortcuts(shortcuts: Sequence[Shortcut]) -> None:
|
|
||||||
sc = shortcuts[0]
|
|
||||||
handle_group(sc.group, True)
|
|
||||||
sc_text = f'{conf_name}.{sc.short_text}'
|
|
||||||
a('.. shortcut:: ' + sc_text)
|
|
||||||
shortcuts = [s for s in shortcuts if s.add_to_default]
|
|
||||||
shortcut_slugs[f'{conf_name}.{sc.name}'] = (sc_text, sc.key.replace('kitty_mod', kitty_mod))
|
|
||||||
if shortcuts:
|
|
||||||
a('.. code-block:: conf')
|
|
||||||
a('')
|
|
||||||
for x in shortcuts:
|
|
||||||
if x.add_to_default:
|
|
||||||
a(' map {} {}'.format(x.key.replace('kitty_mod', kitty_mod), x.action_def))
|
|
||||||
a('')
|
|
||||||
if sc.long_text:
|
|
||||||
a(expand_opt_references(conf_name, sc.long_text))
|
|
||||||
a('')
|
|
||||||
|
|
||||||
for i, opt in enumerate(all_options_):
|
|
||||||
if isinstance(opt, Option):
|
|
||||||
handle_option(i, opt)
|
|
||||||
else:
|
|
||||||
handle_shortcuts(opt)
|
|
||||||
|
|
||||||
if current_group:
|
|
||||||
handle_group_end(current_group)
|
|
||||||
return '\n'.join(ans)
|
|
||||||
|
|
||||||
|
|
||||||
def process_opt_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
|
def process_opt_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
|
||||||
conf_name, opt = target.partition('.')[::2]
|
conf_name, opt = target.partition('.')[::2]
|
||||||
if not opt:
|
if not opt:
|
||||||
@@ -571,26 +490,26 @@ def write_conf_docs(app: Any, all_kitten_names: Iterable[str]) -> None:
|
|||||||
sc_role = app.registry.domain_roles['std']['sc']
|
sc_role = app.registry.domain_roles['std']['sc']
|
||||||
sc_role.warn_dangling = True
|
sc_role.warn_dangling = True
|
||||||
sc_role.process_link = process_shortcut_link
|
sc_role.process_link = process_shortcut_link
|
||||||
|
shortcut_slugs.clear()
|
||||||
|
|
||||||
def generate_default_config(all_options: Dict[str, Union[Option, Sequence[Shortcut]]], name: str) -> None:
|
def generate_default_config(definition: Definition, name: str) -> None:
|
||||||
from kitty.conf.definition import as_conf_file
|
|
||||||
with open(f'generated/conf-{name}.rst', 'w', encoding='utf-8') as f:
|
with open(f'generated/conf-{name}.rst', 'w', encoding='utf-8') as f:
|
||||||
print('.. highlight:: conf\n', file=f)
|
print('.. highlight:: conf\n', file=f)
|
||||||
f.write(render_conf(name, all_options.values()))
|
f.write('\n'.join(definition.as_rst(name, shortcut_slugs)))
|
||||||
|
|
||||||
conf_name = re.sub(r'^kitten-', '', name) + '.conf'
|
conf_name = re.sub(r'^kitten-', '', name) + '.conf'
|
||||||
with open(f'generated/conf/{conf_name}', 'w', encoding='utf-8') as f:
|
with open(f'generated/conf/{conf_name}', 'w', encoding='utf-8') as f:
|
||||||
text = '\n'.join(as_conf_file(all_options.values()))
|
text = '\n'.join(definition.as_conf())
|
||||||
print(text, file=f)
|
print(text, file=f)
|
||||||
|
|
||||||
from kitty.config_data import all_options
|
from kitty.options.definition import definition
|
||||||
generate_default_config(all_options, 'kitty')
|
generate_default_config(definition, 'kitty')
|
||||||
|
|
||||||
from kittens.runner import get_kitten_conf_docs
|
from kittens.runner import get_kitten_conf_docs
|
||||||
for kitten in all_kitten_names:
|
for kitten in all_kitten_names:
|
||||||
all_options = get_kitten_conf_docs(kitten)
|
definition = get_kitten_conf_docs(kitten)
|
||||||
if all_options:
|
if definition:
|
||||||
generate_default_config(all_options, f'kitten-{kitten}')
|
generate_default_config(definition, f'kitten-{kitten}')
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ Configuring kitty
|
|||||||
|
|
||||||
.. highlight:: conf
|
.. highlight:: conf
|
||||||
|
|
||||||
|kitty| is highly customizable, everything from keyboard shortcuts, to painting
|
|kitty| is highly customizable, everything from keyboard shortcuts, to
|
||||||
frames-per-second. See below for an overview of all customization
|
rendering frames-per-second. See below for an overview of all customization
|
||||||
possibilities.
|
possibilities.
|
||||||
|
|
||||||
You can open the config file within kitty by pressing :sc:`edit_config_file`.
|
You can open the config file within kitty by pressing :sc:`edit_config_file`.
|
||||||
You can also display the current configuration by running ``kitty
|
You can reload the config file within kitty by pressing
|
||||||
--debug-config``.
|
:sc:`reload_config_file` or sending kitty the ``SIGUSR1`` signal. You can also
|
||||||
|
display the current configuration by pressing the :sc:`debug_config` key.
|
||||||
|
|
||||||
.. _confloc:
|
.. _confloc:
|
||||||
|
|
||||||
|
|||||||
17
docs/faq.rst
17
docs/faq.rst
@@ -64,7 +64,7 @@ server, use the following one-liner instead (it
|
|||||||
is slower as it needs to ssh into the server twice, but will work with most
|
is slower as it needs to ssh into the server twice, but will work with most
|
||||||
servers)::
|
servers)::
|
||||||
|
|
||||||
infocmp xterm-kitty | ssh myserver tic -x -o \~/.terminfo /dev/stdin
|
infocmp -a xterm-kitty | ssh myserver tic -x -o \~/.terminfo /dev/stdin
|
||||||
|
|
||||||
If you are behind a proxy (like Balabit) that prevents this, you must redirect the
|
If you are behind a proxy (like Balabit) that prevents this, you must redirect the
|
||||||
1st command to a file, copy that to the server and run ``tic`` manually. If you
|
1st command to a file, copy that to the server and run ``tic`` manually. If you
|
||||||
@@ -170,6 +170,19 @@ You can, of course, also run |kitty| from a terminal with command line options,
|
|||||||
And within |kitty| itself, you can always run |kitty| using just `kitty` as it
|
And within |kitty| itself, you can always run |kitty| using just `kitty` as it
|
||||||
cleverly adds itself to the ``PATH``.
|
cleverly adds itself to the ``PATH``.
|
||||||
|
|
||||||
|
I catted a binary file and now kitty is hung?
|
||||||
|
-----------------------------------------------
|
||||||
|
|
||||||
|
**Never** output unknown binary data directly into a terminal.
|
||||||
|
|
||||||
|
Terminals have a single channel for both data and control. Certain bytes
|
||||||
|
are control codes. Some of these control codes are of arbitrary length, so
|
||||||
|
if the binary data you output into the terminal happens to contain the starting
|
||||||
|
sequence for one of these control codes, the terminal will hang waiting for
|
||||||
|
the closing sequence. Press :kbd:`ctrl+shift+delete` to reset the terminal.
|
||||||
|
|
||||||
|
If you do want to cat unknown data, use ``cat -v``.
|
||||||
|
|
||||||
|
|
||||||
kitty is not able to use my favorite font?
|
kitty is not able to use my favorite font?
|
||||||
---------------------------------------------
|
---------------------------------------------
|
||||||
@@ -227,7 +240,7 @@ How do I map key presses in kitty to different keys in the terminal program?
|
|||||||
This is accomplished by using ``map`` with :sc:`send_text <send_text>` in :file:`kitty.conf`.
|
This is accomplished by using ``map`` with :sc:`send_text <send_text>` in :file:`kitty.conf`.
|
||||||
For example::
|
For example::
|
||||||
|
|
||||||
map alt+s send_text all \x13
|
map alt+s send_text normal,application \x13
|
||||||
|
|
||||||
This maps :kbd:`alt+s` to :kbd:`ctrl+s`. To figure out what bytes to use for
|
This maps :kbd:`alt+s` to :kbd:`ctrl+s`. To figure out what bytes to use for
|
||||||
the :sc:`send_text <send_text>` you can use the ``show_key`` kitten. Run::
|
the :sc:`send_text <send_text>` you can use the ``show_key`` kitten. Run::
|
||||||
|
|||||||
@@ -226,8 +226,12 @@ Value of `t` Meaning
|
|||||||
is in a known temporary directory, such as :file:`/tmp`,
|
is in a known temporary directory, such as :file:`/tmp`,
|
||||||
:file:`/dev/shm`, :file:`TMPDIR env var if present` and any platform
|
:file:`/dev/shm`, :file:`TMPDIR env var if present` and any platform
|
||||||
specific temporary directories.
|
specific temporary directories.
|
||||||
``s`` A `POSIX shared memory object <http://man7.org/linux/man-pages/man7/shm_overview.7.html>`_.
|
``s`` A *shared memory object*, which on POSIX systems is a `POSIX shared memory object
|
||||||
The terminal emulator will delete it after reading the pixel data
|
<http://man7.org/linux/man-pages/man7/shm_overview.7.html>`_ and on Windows is a
|
||||||
|
`Named shared memory object <https://docs.microsoft.com/en-us/windows/win32/memory/creating-named-shared-memory>`_.
|
||||||
|
The terminal emulator must read the data from the memory
|
||||||
|
object and then unlink and close it on POSIX and just
|
||||||
|
close it on Windows.
|
||||||
================== ============
|
================== ============
|
||||||
|
|
||||||
Local client
|
Local client
|
||||||
@@ -312,7 +316,7 @@ While as of May 2020, kitty is the only terminal emulator to support this
|
|||||||
graphics protocol, we intend that any terminal emulator that wishes to support
|
graphics protocol, we intend that any terminal emulator that wishes to support
|
||||||
it can. To check if a terminal emulator supports the graphics protocol the best way
|
it can. To check if a terminal emulator supports the graphics protocol the best way
|
||||||
is to send the above *query action* followed by a request for the
|
is to send the above *query action* followed by a request for the
|
||||||
`primary device attributes <https://vt100.net/docs/vt510-rm/DA1.html>`. If you
|
`primary device attributes <https://vt100.net/docs/vt510-rm/DA1.html>`_. If you
|
||||||
get back an answer for the device attributes without getting back an answer for
|
get back an answer for the device attributes without getting back an answer for
|
||||||
the *query action* the terminal emulator does not support the graphics
|
the *query action* the terminal emulator does not support the graphics
|
||||||
protocol.
|
protocol.
|
||||||
@@ -472,7 +476,7 @@ Requesting image ids from the terminal
|
|||||||
If you are writing a program that is going to share the screen with other
|
If you are writing a program that is going to share the screen with other
|
||||||
programs and you still want to use image ids, it is not possible to know
|
programs and you still want to use image ids, it is not possible to know
|
||||||
what image ids are free to use. In this case, instead of using the ``i``
|
what image ids are free to use. In this case, instead of using the ``i``
|
||||||
key to specify and image id use the ``I`` key to specify and image number
|
key to specify an image id use the ``I`` key to specify an image number
|
||||||
instead. These numbers are not unique.
|
instead. These numbers are not unique.
|
||||||
When creating a new image, even if an existing image has the same number a new
|
When creating a new image, even if an existing image has the same number a new
|
||||||
one is created. And the terminal will reply with the id of the newly created
|
one is created. And the terminal will reply with the id of the newly created
|
||||||
|
|||||||
@@ -61,21 +61,17 @@ Quickstart
|
|||||||
--------------
|
--------------
|
||||||
|
|
||||||
Pre-built binaries of |kitty| are available for both macOS and Linux.
|
Pre-built binaries of |kitty| are available for both macOS and Linux.
|
||||||
See the :doc:`binary install instructions </binary>`. You can
|
See the :doc:`binary install instructions </binary>`. You can also
|
||||||
:doc:`build from source </build>`.
|
:doc:`build from source </build>`.
|
||||||
|
|
||||||
You can also use your favorite package manager to install the |kitty| package.
|
Additionally, you can use your favorite package manager to install the |kitty|
|
||||||
|kitty| packages are available for:
|
package, but note that some Linux distribution packages are woefully outdated.
|
||||||
`macOS with Homebrew (Cask) <https://formulae.brew.sh/cask/kitty>`_,
|
|kitty| is available in a vast number of package repositories for macOS
|
||||||
`macOS and Linux with Nix <https://search.nixos.org/packages?channel=unstable&show=kitty&sort=relevance&query=kitty>`_,
|
and Linux.
|
||||||
`Ubuntu <https://launchpad.net/ubuntu/+source/kitty>`_,
|
|
||||||
`Debian <https://packages.debian.org/buster/kitty>`_,
|
.. image:: https://repology.org/badge/tiny-repos/kitty.svg
|
||||||
`openSUSE <https://build.opensuse.org/package/show/X11:terminals/kitty>`_,
|
:target: https://repology.org/project/kitty/versions
|
||||||
`Arch Linux <https://www.archlinux.org/packages/community/x86_64/kitty/>`_,
|
:alt: Number of repositories kitty is available in
|
||||||
`Gentoo <https://packages.gentoo.org/packages/x11-terms/kitty>`_,
|
|
||||||
`Fedora <https://src.fedoraproject.org/rpms/kitty>`_,
|
|
||||||
`Void Linux <https://github.com/void-linux/void-packages/blob/master/srcpkgs/kitty/template>`_,
|
|
||||||
and `Solus <https://dev.getsol.us/source/kitty/>`_.
|
|
||||||
|
|
||||||
See :doc:`Configuring kitty <conf>` for help on configuring |kitty| and
|
See :doc:`Configuring kitty <conf>` for help on configuring |kitty| and
|
||||||
:doc:`Invocation <invocation>` for the command line arguments |kitty| supports.
|
:doc:`Invocation <invocation>` for the command line arguments |kitty| supports.
|
||||||
@@ -225,8 +221,7 @@ For example:
|
|||||||
# vim in it
|
# vim in it
|
||||||
launch --env FOO=BAR vim
|
launch --env FOO=BAR vim
|
||||||
# Set the title for the next window
|
# Set the title for the next window
|
||||||
title Chat with x
|
launch --title "Chat with x" irssi --profile x
|
||||||
launch irssi --profile x
|
|
||||||
|
|
||||||
# Create a new tab (the part after new_tab is the optional tab
|
# Create a new tab (the part after new_tab is the optional tab
|
||||||
# name which will be displayed in the tab bar, if omitted, the
|
# name which will be displayed in the tab bar, if omitted, the
|
||||||
@@ -258,17 +253,23 @@ For example:
|
|||||||
Mouse features
|
Mouse features
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
* You can hold down :kbd:`ctrl+shift` and click on a URL to open it in a browser.
|
* You can click on a URL to open it in a browser.
|
||||||
* You can double click to select a word and then drag to select more words.
|
* You can double click to select a word and then drag to select more words.
|
||||||
* You can triple click to select a line and then drag to select more lines.
|
* You can triple click to select a line and then drag to select more lines.
|
||||||
|
* You can triple click while holding :kbd:`ctrl+alt` to select from clicked
|
||||||
|
point to end of line.
|
||||||
* You can right click to extend a previous selection.
|
* You can right click to extend a previous selection.
|
||||||
* You can hold down :kbd:`ctrl+alt` and drag with the mouse to select in
|
* You can hold down :kbd:`ctrl+alt` and drag with the mouse to select in
|
||||||
columns (see also :opt:`rectangle_select_modifiers`).
|
columns.
|
||||||
* Selecting text automatically copies it to the primary clipboard (on
|
* Selecting text automatically copies it to the primary clipboard (on
|
||||||
platforms with a primary clipboard).
|
platforms with a primary clipboard).
|
||||||
|
* You can middle click to paste from the primary clipboard (on platforms
|
||||||
|
with a primary clipboard).
|
||||||
* You can select text with kitty even when a terminal program has grabbed
|
* You can select text with kitty even when a terminal program has grabbed
|
||||||
the mouse by holding down the :kbd:`shift` key (see also
|
the mouse by holding down the :kbd:`shift` key.
|
||||||
:opt:`terminal_select_modifiers`).
|
|
||||||
|
All these actions can be customized in :file:`kitty.conf` as described
|
||||||
|
:ref:`here <conf-kitty-mouse.mousemap>`.
|
||||||
|
|
||||||
|
|
||||||
Font control
|
Font control
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ System and data visualisation tools
|
|||||||
`KittyTerminalImages.jl <https://github.com/simonschoelly/KittyTerminalImages.jl>`_
|
`KittyTerminalImages.jl <https://github.com/simonschoelly/KittyTerminalImages.jl>`_
|
||||||
show images from Julia directly in kitty
|
show images from Julia directly in kitty
|
||||||
|
|
||||||
|
`euporie <https://github.com/joouha/euporie>`_
|
||||||
|
a text-based user interface for running and editing Jupyter notebooks,
|
||||||
|
powered by kitty's graphics protocol for displaying plots
|
||||||
|
|
||||||
`gnuplot <http://www.gnuplot.info/>`_
|
`gnuplot <http://www.gnuplot.info/>`_
|
||||||
a graphing and data visualization tool that can be made to display its
|
a graphing and data visualization tool that can be made to display its
|
||||||
output in kitty with the following bash snippet::
|
output in kitty with the following bash snippet::
|
||||||
@@ -82,6 +86,10 @@ System and data visualisation tools
|
|||||||
`onefetch <https://github.com/o2sh/onefetch>`_
|
`onefetch <https://github.com/o2sh/onefetch>`_
|
||||||
a tool to fetch information about your git repositories
|
a tool to fetch information about your git repositories
|
||||||
|
|
||||||
|
`patat <https://github.com/jaspervdj/patat>`_
|
||||||
|
terminal based presentations using pandoc and kitty's image protocol for
|
||||||
|
images
|
||||||
|
|
||||||
`wttr.in <https://github.com/chubin/wttr.in>`_
|
`wttr.in <https://github.com/chubin/wttr.in>`_
|
||||||
a tool to display weather information in your terminal with curl
|
a tool to display weather information in your terminal with curl
|
||||||
|
|
||||||
@@ -111,6 +119,9 @@ such a split windows, previews, REPLs etc.
|
|||||||
`vim-test <https://github.com/vim-test/vim-test>`_
|
`vim-test <https://github.com/vim-test/vim-test>`_
|
||||||
allows easily running tests in a terminal window
|
allows easily running tests in a terminal window
|
||||||
|
|
||||||
|
`hologram.nvim <https://github.com/edluffy/hologram.nvim>`_
|
||||||
|
terminal image viewer for nvim
|
||||||
|
|
||||||
|
|
||||||
Scrollback manipulation
|
Scrollback manipulation
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ decimal number. For example, the :kbd:`A` key is represented as ``97`` which is
|
|||||||
the unicode code for lowercase ``a``. Note that the codepoint used is *always*
|
the unicode code for lowercase ``a``. Note that the codepoint used is *always*
|
||||||
the lower-case (or more technically, un-shifted) version of the key. If the
|
the lower-case (or more technically, un-shifted) version of the key. If the
|
||||||
user presses, for example, :kbd:`ctrl+shift+a` the escape code would be ``CSI
|
user presses, for example, :kbd:`ctrl+shift+a` the escape code would be ``CSI
|
||||||
97;modifiers u``. It *must not* by ``CSI 65; modifiers u``.
|
97;modifiers u``. It *must not* be ``CSI 65; modifiers u``.
|
||||||
|
|
||||||
If *alternate key reporting* is requested by the program running in the
|
If *alternate key reporting* is requested by the program running in the
|
||||||
terminal, the terminal can send two additional Unicode codepoints, the
|
terminal, the terminal can send two additional Unicode codepoints, the
|
||||||
@@ -296,7 +296,7 @@ represented in one of the following two forms::
|
|||||||
|
|
||||||
This makes it very easy to parse key events in an application. In particular,
|
This makes it very easy to parse key events in an application. In particular,
|
||||||
:kbd:`ctrl+c` will no longer generate the ``SIGINT`` signal, but instead be
|
:kbd:`ctrl+c` will no longer generate the ``SIGINT`` signal, but instead be
|
||||||
delivers as a ``CSI u`` escape code. This has the nice side effect of making it
|
delivered as a ``CSI u`` escape code. This has the nice side effect of making it
|
||||||
much easier to integrate into the application event loop. The only exceptions
|
much easier to integrate into the application event loop. The only exceptions
|
||||||
are the :kbd:`Enter, Tab and Backspace` keys which still generate the same
|
are the :kbd:`Enter, Tab and Backspace` keys which still generate the same
|
||||||
bytes as in legacy mode this is to allow the user to type and execute commands
|
bytes as in legacy mode this is to allow the user to type and execute commands
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ also type a space followed by a period and the index for the match if you don't
|
|||||||
like to use arrow keys.
|
like to use arrow keys.
|
||||||
|
|
||||||
You can switch between modes using either the function keys or by pressing
|
You can switch between modes using either the function keys or by pressing
|
||||||
:kbd:`Ctrl+[` and :kbd:`Ctrl+]`.
|
:kbd:`Ctrl+[` and :kbd:`Ctrl+]` or by pressing :kbd:`Ctrl+Tab` and
|
||||||
|
:kbd:`Ctrl+Shift+Tab`.
|
||||||
|
|
||||||
|
|
||||||
Command Line Interface
|
Command Line Interface
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ launch::
|
|||||||
|
|
||||||
map f1 launch vim path/to/some/file
|
map f1 launch vim path/to/some/file
|
||||||
|
|
||||||
|
|
||||||
To open a new window with the same working directory as the currently
|
To open a new window with the same working directory as the currently
|
||||||
active window::
|
active window::
|
||||||
|
|
||||||
@@ -29,6 +28,10 @@ To open the new window in a new tab::
|
|||||||
|
|
||||||
map f1 launch --type=tab
|
map f1 launch --type=tab
|
||||||
|
|
||||||
|
To run multiple commands in a shell, use::
|
||||||
|
|
||||||
|
map f1 launch sh -c "ls && zsh"
|
||||||
|
|
||||||
To pass the contents of the current screen and scrollback to the started process::
|
To pass the contents of the current screen and scrollback to the started process::
|
||||||
|
|
||||||
map f1 launch --stdin-source=@screen_scrollback less
|
map f1 launch --stdin-source=@screen_scrollback less
|
||||||
@@ -54,9 +57,34 @@ Special arguments
|
|||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
There are a few special placeholder arguments that can be specified as part of
|
There are a few special placeholder arguments that can be specified as part of
|
||||||
the command line. Namely ``@selection`` which is replaced by the current
|
the command line:
|
||||||
selection and ``@active-kitty-window-id`` which is replaced by the id of the
|
|
||||||
currently active kitty window. For example::
|
|
||||||
|
``@selection``
|
||||||
|
replaced by the currently selected text
|
||||||
|
|
||||||
|
``@active-kitty-window-id``
|
||||||
|
replaced by the id of the currently active kitty window
|
||||||
|
|
||||||
|
``@line-count``
|
||||||
|
replaced by the number of lines in STDIN. Only present when passing some
|
||||||
|
data to STDIN
|
||||||
|
|
||||||
|
``@input-line-number``
|
||||||
|
replaced the number of lines a pager should scroll to match the current
|
||||||
|
scroll position in kitty. See :opt:`scrollback_pager` for details
|
||||||
|
|
||||||
|
``@scrolled-by``
|
||||||
|
replaced by the number of lines kitty is currently scrolled by
|
||||||
|
|
||||||
|
``@cursor-x``
|
||||||
|
replaced by the current cursor x position with 1 being the leftmost cell
|
||||||
|
|
||||||
|
``@cursor-y``
|
||||||
|
replaced by the current cursor y position with 1 being the topmost cell
|
||||||
|
|
||||||
|
|
||||||
|
For example::
|
||||||
|
|
||||||
map f1 launch my-program @active-kitty-window-id
|
map f1 launch my-program @active-kitty-window-id
|
||||||
|
|
||||||
|
|||||||
@@ -179,9 +179,7 @@ screen. This escape code allows that text to be restored.
|
|||||||
If the scrollback buffer is empty or there is no scrollback buffer, such as for
|
If the scrollback buffer is empty or there is no scrollback buffer, such as for
|
||||||
the alternate screen, then the newly inserted lines must be empty, just as with
|
the alternate screen, then the newly inserted lines must be empty, just as with
|
||||||
the original ``SD`` escape code. The maximum number of lines that can be
|
the original ``SD`` escape code. The maximum number of lines that can be
|
||||||
scrolled down is the height of the screen, i.e. the number of lines on the screen.
|
scrolled down is implementation defined, but must be at least one screen worth.
|
||||||
If the escape code requests more than that number of lines, the terminal *must*
|
|
||||||
clip the request to the current screen size.
|
|
||||||
|
|
||||||
The syntax of the escape code is identical to that of ``SD`` except that it has
|
The syntax of the escape code is identical to that of ``SD`` except that it has
|
||||||
a trailing ``+`` modifier. This is legal under the `ECMA 48 standard
|
a trailing ``+`` modifier. This is legal under the `ECMA 48 standard
|
||||||
|
|||||||
435
gen-config.py
Executable file
435
gen-config.py
Executable file
@@ -0,0 +1,435 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
import re
|
||||||
|
import textwrap
|
||||||
|
from typing import (
|
||||||
|
Any, Callable, Dict, Iterator, List, Set, Tuple, Union, get_type_hints
|
||||||
|
)
|
||||||
|
|
||||||
|
from kitty.conf.types import Definition, MultiOption, Option, unset
|
||||||
|
|
||||||
|
|
||||||
|
def chunks(lst: List, n: int) -> Iterator[List]:
|
||||||
|
for i in range(0, len(lst), n):
|
||||||
|
yield lst[i:i + n]
|
||||||
|
|
||||||
|
|
||||||
|
def atoi(text: str) -> str:
|
||||||
|
return f'{int(text):08d}' if text.isdigit() else text
|
||||||
|
|
||||||
|
|
||||||
|
def natural_keys(text: str) -> Tuple[str, ...]:
|
||||||
|
return tuple(atoi(c) for c in re.split(r'(\d+)', text))
|
||||||
|
|
||||||
|
|
||||||
|
def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
|
||||||
|
class_lines: List[str] = []
|
||||||
|
tc_lines: List[str] = []
|
||||||
|
a = class_lines.append
|
||||||
|
t = tc_lines.append
|
||||||
|
a('class Options:')
|
||||||
|
t('class Parser:')
|
||||||
|
choices = {}
|
||||||
|
imports: Set[Tuple[str, str]] = set()
|
||||||
|
tc_imports: Set[Tuple[str, str]] = set()
|
||||||
|
|
||||||
|
def type_name(x: type) -> str:
|
||||||
|
ans = x.__name__
|
||||||
|
if x.__module__ and x.__module__ != 'builtins':
|
||||||
|
imports.add((x.__module__, x.__name__))
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def option_type_as_str(x: Any) -> str:
|
||||||
|
if hasattr(x, '__name__'):
|
||||||
|
return type_name(x)
|
||||||
|
ans = repr(x)
|
||||||
|
ans = ans.replace('NoneType', 'None')
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def option_type_data(option: Union[Option, MultiOption]) -> Tuple[Callable, str]:
|
||||||
|
func = option.parser_func
|
||||||
|
if func.__module__ == 'builtins':
|
||||||
|
return func, func.__name__
|
||||||
|
th = get_type_hints(func)
|
||||||
|
rettype = th['return']
|
||||||
|
typ = option_type_as_str(rettype)
|
||||||
|
if isinstance(option, MultiOption):
|
||||||
|
typ = typ[typ.index('[') + 1:-1]
|
||||||
|
typ = typ.replace('Tuple', 'Dict', 1)
|
||||||
|
return func, typ
|
||||||
|
|
||||||
|
is_mutiple_vars = {}
|
||||||
|
option_names = set()
|
||||||
|
color_table = list(map(str, range(256)))
|
||||||
|
|
||||||
|
def parser_function_declaration(option_name: str) -> None:
|
||||||
|
t('')
|
||||||
|
t(f' def {option_name}(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:')
|
||||||
|
|
||||||
|
for option in sorted(defn.iter_all_options(), key=lambda a: natural_keys(a.name)):
|
||||||
|
option_names.add(option.name)
|
||||||
|
parser_function_declaration(option.name)
|
||||||
|
if isinstance(option, MultiOption):
|
||||||
|
mval: Dict[str, Dict[str, Any]] = {'macos': {}, 'linux': {}, '': {}}
|
||||||
|
func, typ = option_type_data(option)
|
||||||
|
for val in option:
|
||||||
|
if val.add_to_default:
|
||||||
|
gr = mval[val.only]
|
||||||
|
for k, v in func(val.defval_as_str):
|
||||||
|
gr[k] = v
|
||||||
|
is_mutiple_vars[option.name] = typ, mval
|
||||||
|
sig = inspect.signature(func)
|
||||||
|
tc_imports.add((func.__module__, func.__name__))
|
||||||
|
if len(sig.parameters) == 1:
|
||||||
|
t(f' for k, v in {func.__name__}(val):')
|
||||||
|
t(f' ans["{option.name}"][k] = v')
|
||||||
|
else:
|
||||||
|
t(f' for k, v in {func.__name__}(val, ans["{option.name}"]):')
|
||||||
|
t(f' ans["{option.name}"][k] = v')
|
||||||
|
continue
|
||||||
|
|
||||||
|
if option.choices:
|
||||||
|
typ = 'typing.Literal[{}]'.format(', '.join(repr(x) for x in option.choices))
|
||||||
|
ename = f'choices_for_{option.name}'
|
||||||
|
choices[ename] = typ
|
||||||
|
typ = ename
|
||||||
|
func = str
|
||||||
|
elif defn.has_color_table and option.is_color_table_color:
|
||||||
|
func, typ = option_type_data(option)
|
||||||
|
t(f' ans[{option.name!r}] = {func.__name__}(val)')
|
||||||
|
tc_imports.add((func.__module__, func.__name__))
|
||||||
|
cnum = int(option.name[5:])
|
||||||
|
color_table[cnum] = '0x{:06x}'.format(func(option.defval_as_string).__int__())
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
func, typ = option_type_data(option)
|
||||||
|
try:
|
||||||
|
params = inspect.signature(func).parameters
|
||||||
|
except Exception:
|
||||||
|
params = {}
|
||||||
|
if 'dict_with_parse_results' in params:
|
||||||
|
t(f' {func.__name__}(val, ans)')
|
||||||
|
else:
|
||||||
|
t(f' ans[{option.name!r}] = {func.__name__}(val)')
|
||||||
|
if func.__module__ != 'builtins':
|
||||||
|
tc_imports.add((func.__module__, func.__name__))
|
||||||
|
|
||||||
|
defval = repr(func(option.defval_as_string))
|
||||||
|
if option.macos_defval is not unset:
|
||||||
|
md = repr(func(option.macos_defval))
|
||||||
|
defval = f'{md} if is_macos else {defval}'
|
||||||
|
imports.add(('kitty.constants', 'is_macos'))
|
||||||
|
a(f' {option.name}: {typ} = {defval}')
|
||||||
|
if option.choices:
|
||||||
|
t(' val = val.lower()')
|
||||||
|
t(f' if val not in self.choices_for_{option.name}:')
|
||||||
|
t(f' raise ValueError(f"The value {{val}} is not a valid choice for {option.name}")')
|
||||||
|
t(f' ans["{option.name}"] = val')
|
||||||
|
t('')
|
||||||
|
t(f' choices_for_{option.name} = frozenset({option.choices!r})')
|
||||||
|
|
||||||
|
for option_name, (typ, mval) in is_mutiple_vars.items():
|
||||||
|
a(f' {option_name}: {typ} = ' '{}')
|
||||||
|
|
||||||
|
for parser, aliases in defn.deprecations.items():
|
||||||
|
for alias in aliases:
|
||||||
|
parser_function_declaration(alias)
|
||||||
|
tc_imports.add((parser.__module__, parser.__name__))
|
||||||
|
t(f' {parser.__name__}({alias!r}, val, ans)')
|
||||||
|
|
||||||
|
action_parsers = {}
|
||||||
|
|
||||||
|
def resolve_import(ftype: str) -> str:
|
||||||
|
if '.' in ftype:
|
||||||
|
fmod, ftype = ftype.rpartition('.')[::2]
|
||||||
|
else:
|
||||||
|
fmod = f'{loc}.options.utils'
|
||||||
|
imports.add((fmod, ftype))
|
||||||
|
return ftype
|
||||||
|
|
||||||
|
for aname, action in defn.actions.items():
|
||||||
|
option_names.add(aname)
|
||||||
|
action_parsers[aname] = func = action.parser_func
|
||||||
|
th = get_type_hints(func)
|
||||||
|
rettype = th['return']
|
||||||
|
typ = option_type_as_str(rettype)
|
||||||
|
typ = typ[typ.index('[') + 1:-1]
|
||||||
|
a(f' {aname}: typing.List[{typ}] = []')
|
||||||
|
for imp in action.imports:
|
||||||
|
resolve_import(imp)
|
||||||
|
for fname, ftype in action.fields.items():
|
||||||
|
ftype = resolve_import(ftype)
|
||||||
|
a(f' {fname}: {ftype} = ' '{}')
|
||||||
|
parser_function_declaration(aname)
|
||||||
|
t(f' for k in {func.__name__}(val):')
|
||||||
|
t(f' ans[{aname!r}].append(k)')
|
||||||
|
tc_imports.add((func.__module__, func.__name__))
|
||||||
|
|
||||||
|
if defn.has_color_table:
|
||||||
|
imports.add(('array', 'array'))
|
||||||
|
a(' color_table: array = array("L", (')
|
||||||
|
for grp in chunks(color_table, 8):
|
||||||
|
a(' ' + ', '.join(grp) + ',')
|
||||||
|
a(' ))')
|
||||||
|
|
||||||
|
a(' config_paths: typing.Tuple[str, ...] = ()')
|
||||||
|
a(' config_overrides: typing.Tuple[str, ...] = ()')
|
||||||
|
a('')
|
||||||
|
a(' def __init__(self, options_dict: typing.Optional[typing.Dict[str, typing.Any]] = None) -> None:')
|
||||||
|
a(' if options_dict is not None:')
|
||||||
|
a(' for key in option_names:')
|
||||||
|
a(' setattr(self, key, options_dict[key])')
|
||||||
|
|
||||||
|
a('')
|
||||||
|
a(' @property')
|
||||||
|
a(' def _fields(self) -> typing.Tuple[str, ...]:')
|
||||||
|
a(' return option_names')
|
||||||
|
|
||||||
|
a('')
|
||||||
|
a(' def __iter__(self) -> typing.Iterator[str]:')
|
||||||
|
a(' return iter(self._fields)')
|
||||||
|
|
||||||
|
a('')
|
||||||
|
a(' def __len__(self) -> int:')
|
||||||
|
a(' return len(self._fields)')
|
||||||
|
|
||||||
|
a('')
|
||||||
|
a(' def _copy_of_val(self, name: str) -> typing.Any:')
|
||||||
|
a(' ans = getattr(self, name)')
|
||||||
|
a(' if isinstance(ans, dict):\n ans = ans.copy()')
|
||||||
|
a(' elif isinstance(ans, list):\n ans = ans[:]')
|
||||||
|
a(' return ans')
|
||||||
|
|
||||||
|
a('')
|
||||||
|
a(' def _asdict(self) -> typing.Dict[str, typing.Any]:')
|
||||||
|
a(' return {k: self._copy_of_val(k) for k in self}')
|
||||||
|
|
||||||
|
a('')
|
||||||
|
a(' def _replace(self, **kw: typing.Any) -> "Options":')
|
||||||
|
a(' ans = Options()')
|
||||||
|
a(' for name in self:')
|
||||||
|
a(' setattr(ans, name, self._copy_of_val(name))')
|
||||||
|
a(' for name, val in kw.items():')
|
||||||
|
a(' setattr(ans, name, val)')
|
||||||
|
a(' return ans')
|
||||||
|
|
||||||
|
a('')
|
||||||
|
a(' def __getitem__(self, key: typing.Union[int, str]) -> typing.Any:')
|
||||||
|
a(' k = option_names[key] if isinstance(key, int) else key')
|
||||||
|
a(' try:')
|
||||||
|
a(' return getattr(self, k)')
|
||||||
|
a(' except AttributeError:')
|
||||||
|
a(' pass')
|
||||||
|
a(' raise KeyError(f"No option named: {k}")')
|
||||||
|
|
||||||
|
if defn.has_color_table:
|
||||||
|
a('')
|
||||||
|
a(' def __getattr__(self, key: str) -> typing.Any:')
|
||||||
|
a(' if key.startswith("color"):')
|
||||||
|
a(' q = key[5:]')
|
||||||
|
a(' if q.isdigit():')
|
||||||
|
a(' k = int(q)')
|
||||||
|
a(' if 0 <= k <= 255:')
|
||||||
|
a(' x = self.color_table[k]')
|
||||||
|
a(' return Color((x >> 16) & 255, (x >> 8) & 255, x & 255)')
|
||||||
|
a(' raise AttributeError(key)')
|
||||||
|
a('')
|
||||||
|
a(' def __setattr__(self, key: str, val: typing.Any) -> typing.Any:')
|
||||||
|
a(' if key.startswith("color"):')
|
||||||
|
a(' q = key[5:]')
|
||||||
|
a(' if q.isdigit():')
|
||||||
|
a(' k = int(q)')
|
||||||
|
a(' if 0 <= k <= 255:')
|
||||||
|
a(' self.color_table[k] = int(val)')
|
||||||
|
a(' return')
|
||||||
|
a(' object.__setattr__(self, key, val)')
|
||||||
|
|
||||||
|
a('')
|
||||||
|
a('')
|
||||||
|
a('defaults = Options()')
|
||||||
|
for option_name, (typ, mval) in is_mutiple_vars.items():
|
||||||
|
a(f'defaults.{option_name} = {mval[""]!r}')
|
||||||
|
if mval['macos']:
|
||||||
|
imports.add(('kitty.constants', 'is_macos'))
|
||||||
|
a('if is_macos:')
|
||||||
|
a(f' defaults.{option_name}.update({mval["macos"]!r}')
|
||||||
|
if mval['macos']:
|
||||||
|
imports.add(('kitty.constants', 'is_macos'))
|
||||||
|
a('if not is_macos:')
|
||||||
|
a(f' defaults.{option_name}.update({mval["linux"]!r}')
|
||||||
|
|
||||||
|
for aname, func in action_parsers.items():
|
||||||
|
a(f'defaults.{aname} = [')
|
||||||
|
only: Dict[str, List[Tuple[str, Callable]]] = {}
|
||||||
|
for sc in defn.iter_all_maps(aname):
|
||||||
|
if not sc.add_to_default:
|
||||||
|
continue
|
||||||
|
text = sc.parseable_text
|
||||||
|
if sc.only:
|
||||||
|
only.setdefault(sc.only, []).append((text, func))
|
||||||
|
else:
|
||||||
|
for val in func(text):
|
||||||
|
a(f' # {sc.name}')
|
||||||
|
a(f' {val!r},')
|
||||||
|
a(']')
|
||||||
|
if only:
|
||||||
|
imports.add(('kitty.constants', 'is_macos'))
|
||||||
|
for cond, items in only.items():
|
||||||
|
cond = 'is_macos' if cond == 'macos' else 'not is_macos'
|
||||||
|
a(f'if {cond}:')
|
||||||
|
for (text, func) in items:
|
||||||
|
for val in func(text):
|
||||||
|
a(f' defaults.{aname}.append({val!r})')
|
||||||
|
|
||||||
|
t('')
|
||||||
|
t('')
|
||||||
|
t('def create_result_dict() -> typing.Dict[str, typing.Any]:')
|
||||||
|
t(' return {')
|
||||||
|
for oname in is_mutiple_vars:
|
||||||
|
t(f' {oname!r}: {{}},')
|
||||||
|
for aname in defn.actions:
|
||||||
|
t(f' {aname!r}: [],')
|
||||||
|
t(' }')
|
||||||
|
|
||||||
|
t('')
|
||||||
|
t('')
|
||||||
|
t(f'actions = frozenset({tuple(defn.actions)!r})')
|
||||||
|
t('')
|
||||||
|
t('')
|
||||||
|
t('def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:')
|
||||||
|
t(' ans = {}')
|
||||||
|
t(' for k, v in defaults.items():')
|
||||||
|
t(' if isinstance(v, dict):')
|
||||||
|
t(' ans[k] = merge_dicts(v, vals.get(k, {}))')
|
||||||
|
t(' elif k in actions:')
|
||||||
|
t(' ans[k] = v + vals.get(k, [])')
|
||||||
|
t(' else:')
|
||||||
|
t(' ans[k] = vals.get(k, v)')
|
||||||
|
t(' return ans')
|
||||||
|
tc_imports.add(('kitty.conf.utils', 'merge_dicts'))
|
||||||
|
|
||||||
|
t('')
|
||||||
|
t('')
|
||||||
|
t('parser = Parser()')
|
||||||
|
t('')
|
||||||
|
t('')
|
||||||
|
t('def parse_conf_item(key: str, val: str, ans: typing.Dict[str, typing.Any]) -> bool:')
|
||||||
|
t(' func = getattr(parser, key, None)')
|
||||||
|
t(' if func is not None:')
|
||||||
|
t(' func(val, ans)')
|
||||||
|
t(' return True')
|
||||||
|
t(' return False')
|
||||||
|
|
||||||
|
preamble = ['# generated by gen-config.py DO NOT edit', '# vim:fileencoding=utf-8', '']
|
||||||
|
a = preamble.append
|
||||||
|
|
||||||
|
def output_imports(imports: Set, add_module_imports: bool = True) -> None:
|
||||||
|
a('import typing')
|
||||||
|
seen_mods = {'typing'}
|
||||||
|
mmap: Dict[str, List[str]] = {}
|
||||||
|
for mod, name in imports:
|
||||||
|
mmap.setdefault(mod, []).append(name)
|
||||||
|
for mod in sorted(mmap):
|
||||||
|
names = sorted(mmap[mod])
|
||||||
|
lines = textwrap.wrap(', '.join(names), 100)
|
||||||
|
if len(lines) == 1:
|
||||||
|
s = lines[0]
|
||||||
|
else:
|
||||||
|
s = '\n '.join(lines)
|
||||||
|
s = f'(\n {s}\n)'
|
||||||
|
a(f'from {mod} import {s}')
|
||||||
|
if add_module_imports and mod not in seen_mods and mod != s:
|
||||||
|
a(f'import {mod}')
|
||||||
|
seen_mods.add(mod)
|
||||||
|
|
||||||
|
output_imports(imports)
|
||||||
|
a('')
|
||||||
|
if choices:
|
||||||
|
a('if typing.TYPE_CHECKING:')
|
||||||
|
for name, cdefn in choices.items():
|
||||||
|
a(f' {name} = {cdefn}')
|
||||||
|
a('else:')
|
||||||
|
for name in choices:
|
||||||
|
a(f' {name} = str')
|
||||||
|
|
||||||
|
a('')
|
||||||
|
a('option_names = ( # {{''{')
|
||||||
|
a(' ' + pprint.pformat(tuple(sorted(option_names, key=natural_keys)))[1:] + ' # }}''}')
|
||||||
|
class_def = '\n'.join(preamble + ['', ''] + class_lines)
|
||||||
|
|
||||||
|
preamble = ['# generated by gen-config.py DO NOT edit', '# vim:fileencoding=utf-8', '']
|
||||||
|
a = preamble.append
|
||||||
|
output_imports(tc_imports, False)
|
||||||
|
|
||||||
|
return class_def, '\n'.join(preamble + ['', ''] + tc_lines)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_c_conversion(loc: str, ctypes: List[Option]) -> str:
|
||||||
|
lines: List[str] = []
|
||||||
|
basic_converters = {
|
||||||
|
'int': 'PyLong_AsLong', 'uint': 'PyLong_AsUnsignedLong', 'bool': 'PyObject_IsTrue',
|
||||||
|
'float': 'PyFloat_AsFloat', 'double': 'PyFloat_AsDouble',
|
||||||
|
'time': 'parse_s_double_to_monotonic_t', 'time-ms': 'parse_ms_long_to_monotonic_t'
|
||||||
|
}
|
||||||
|
|
||||||
|
for opt in ctypes:
|
||||||
|
lines.append('')
|
||||||
|
lines.append(f'static void\nconvert_from_python_{opt.name}(PyObject *val, Options *opts) ''{')
|
||||||
|
is_special = opt.ctype.startswith('!')
|
||||||
|
if is_special:
|
||||||
|
func = opt.ctype[1:]
|
||||||
|
lines.append(f' {func}(val, opts);')
|
||||||
|
else:
|
||||||
|
func = basic_converters.get(opt.ctype, opt.ctype)
|
||||||
|
lines.append(f' opts->{opt.name} = {func}(val);')
|
||||||
|
lines.append('}')
|
||||||
|
lines.append('')
|
||||||
|
lines.append(f'static void\nconvert_from_opts_{opt.name}(PyObject *py_opts, Options *opts) ''{')
|
||||||
|
lines.append(f' PyObject *ret = PyObject_GetAttrString(py_opts, "{opt.name}");')
|
||||||
|
lines.append(' if (ret == NULL) return;')
|
||||||
|
lines.append(f' convert_from_python_{opt.name}(ret, opts);')
|
||||||
|
lines.append(' Py_DECREF(ret);')
|
||||||
|
lines.append('}')
|
||||||
|
|
||||||
|
lines.append('')
|
||||||
|
lines.append('static bool\nconvert_opts_from_python_opts(PyObject *py_opts, Options *opts) ''{')
|
||||||
|
for opt in ctypes:
|
||||||
|
lines.append(f' convert_from_opts_{opt.name}(py_opts, opts);')
|
||||||
|
lines.append(' if (PyErr_Occurred()) return false;')
|
||||||
|
lines.append(' return true;')
|
||||||
|
lines.append('}')
|
||||||
|
|
||||||
|
preamble = ['// generated by gen-config.py DO NOT edit', '// vim:fileencoding=utf-8', '#pragma once', '#include "to-c.h"']
|
||||||
|
return '\n'.join(preamble + ['', ''] + lines)
|
||||||
|
|
||||||
|
|
||||||
|
def write_output(loc: str, defn: Definition) -> None:
|
||||||
|
cls, tc = generate_class(defn, loc)
|
||||||
|
with open(os.path.join(*loc.split('.'), 'options', 'types.py'), 'w') as f:
|
||||||
|
f.write(cls + '\n')
|
||||||
|
with open(os.path.join(*loc.split('.'), 'options', 'parse.py'), 'w') as f:
|
||||||
|
f.write(tc + '\n')
|
||||||
|
ctypes = []
|
||||||
|
for opt in defn.root_group.iter_all_non_groups():
|
||||||
|
if isinstance(opt, Option) and opt.ctype:
|
||||||
|
ctypes.append(opt)
|
||||||
|
if ctypes:
|
||||||
|
c = generate_c_conversion(loc, ctypes)
|
||||||
|
with open(os.path.join(*loc.split('.'), 'options', 'to-c-generated.h'), 'w') as f:
|
||||||
|
f.write(c + '\n')
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
from kitty.options.definition import definition
|
||||||
|
write_output('kitty', definition)
|
||||||
|
from kittens.diff.options.definition import definition as kd
|
||||||
|
write_output('kittens.diff', kd)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -316,6 +316,7 @@ void _glfwClearDisplayLinks() {
|
|||||||
CVDisplayLinkRelease(_glfw.ns.displayLinks.entries[i].displayLink);
|
CVDisplayLinkRelease(_glfw.ns.displayLinks.entries[i].displayLink);
|
||||||
_glfw.ns.displayLinks.entries[i].displayLink = nil;
|
_glfw.ns.displayLinks.entries[i].displayLink = nil;
|
||||||
_glfw.ns.displayLinks.entries[i].lastRenderFrameRequestedAt = 0;
|
_glfw.ns.displayLinks.entries[i].lastRenderFrameRequestedAt = 0;
|
||||||
|
_glfw.ns.displayLinks.entries[i].first_unserviced_render_frame_request_at = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_glfw.ns.displayLinks.count = 0;
|
_glfw.ns.displayLinks.count = 0;
|
||||||
@@ -333,7 +334,14 @@ static CVReturn displayLinkCallback(
|
|||||||
return kCVReturnSuccess;
|
return kCVReturnSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void createDisplayLink(CGDirectDisplayID displayID) {
|
void
|
||||||
|
_glfw_create_cv_display_link(_GLFWDisplayLinkNS *entry) {
|
||||||
|
CVDisplayLinkCreateWithCGDisplay(entry->displayID, &entry->displayLink);
|
||||||
|
CVDisplayLinkSetOutputCallback(entry->displayLink, &displayLinkCallback, (void*)(uintptr_t)entry->displayID);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
createDisplayLink(CGDirectDisplayID displayID) {
|
||||||
if (_glfw.ns.displayLinks.count >= sizeof(_glfw.ns.displayLinks.entries)/sizeof(_glfw.ns.displayLinks.entries[0]) - 1) return;
|
if (_glfw.ns.displayLinks.count >= sizeof(_glfw.ns.displayLinks.entries)/sizeof(_glfw.ns.displayLinks.entries[0]) - 1) return;
|
||||||
for (size_t i = 0; i < _glfw.ns.displayLinks.count; i++) {
|
for (size_t i = 0; i < _glfw.ns.displayLinks.count; i++) {
|
||||||
if (_glfw.ns.displayLinks.entries[i].displayID == displayID) return;
|
if (_glfw.ns.displayLinks.entries[i].displayID == displayID) return;
|
||||||
@@ -341,8 +349,7 @@ static inline void createDisplayLink(CGDirectDisplayID displayID) {
|
|||||||
_GLFWDisplayLinkNS *entry = &_glfw.ns.displayLinks.entries[_glfw.ns.displayLinks.count++];
|
_GLFWDisplayLinkNS *entry = &_glfw.ns.displayLinks.entries[_glfw.ns.displayLinks.count++];
|
||||||
memset(entry, 0, sizeof(_GLFWDisplayLinkNS));
|
memset(entry, 0, sizeof(_GLFWDisplayLinkNS));
|
||||||
entry->displayID = displayID;
|
entry->displayID = displayID;
|
||||||
CVDisplayLinkCreateWithCGDisplay(displayID, &entry->displayLink);
|
_glfw_create_cv_display_link(entry);
|
||||||
CVDisplayLinkSetOutputCallback(entry->displayLink, &displayLinkCallback, (void*)(uintptr_t)displayID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Poll for changes in the set of connected monitors
|
// Poll for changes in the set of connected monitors
|
||||||
|
|||||||
3
glfw/cocoa_platform.h
vendored
3
glfw/cocoa_platform.h
vendored
@@ -158,7 +158,7 @@ typedef struct _GLFWDisplayLinkNS
|
|||||||
{
|
{
|
||||||
CVDisplayLinkRef displayLink;
|
CVDisplayLinkRef displayLink;
|
||||||
CGDirectDisplayID displayID;
|
CGDirectDisplayID displayID;
|
||||||
monotonic_t lastRenderFrameRequestedAt;
|
monotonic_t lastRenderFrameRequestedAt, first_unserviced_render_frame_request_at;
|
||||||
} _GLFWDisplayLinkNS;
|
} _GLFWDisplayLinkNS;
|
||||||
|
|
||||||
// Cocoa-specific global data
|
// Cocoa-specific global data
|
||||||
@@ -246,3 +246,4 @@ void _glfwDispatchTickCallback(void);
|
|||||||
void _glfwDispatchRenderFrame(CGDirectDisplayID);
|
void _glfwDispatchRenderFrame(CGDirectDisplayID);
|
||||||
void _glfwShutdownCVDisplayLink(unsigned long long, void*);
|
void _glfwShutdownCVDisplayLink(unsigned long long, void*);
|
||||||
void _glfwCocoaPostEmptyEvent(void);
|
void _glfwCocoaPostEmptyEvent(void);
|
||||||
|
void _glfw_create_cv_display_link(_GLFWDisplayLinkNS *entry);
|
||||||
|
|||||||
@@ -317,6 +317,7 @@ _glfwShutdownCVDisplayLink(unsigned long long timer_id UNUSED, void *user_data U
|
|||||||
_GLFWDisplayLinkNS *dl = &_glfw.ns.displayLinks.entries[i];
|
_GLFWDisplayLinkNS *dl = &_glfw.ns.displayLinks.entries[i];
|
||||||
if (dl->displayLink) CVDisplayLinkStop(dl->displayLink);
|
if (dl->displayLink) CVDisplayLinkStop(dl->displayLink);
|
||||||
dl->lastRenderFrameRequestedAt = 0;
|
dl->lastRenderFrameRequestedAt = 0;
|
||||||
|
dl->first_unserviced_render_frame_request_at = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,10 +341,22 @@ requestRenderFrame(_GLFWwindow *w, GLFWcocoarenderframefun callback) {
|
|||||||
_GLFWDisplayLinkNS *dl = &_glfw.ns.displayLinks.entries[i];
|
_GLFWDisplayLinkNS *dl = &_glfw.ns.displayLinks.entries[i];
|
||||||
if (dl->displayID == displayID) {
|
if (dl->displayID == displayID) {
|
||||||
dl->lastRenderFrameRequestedAt = now;
|
dl->lastRenderFrameRequestedAt = now;
|
||||||
|
if (!dl->first_unserviced_render_frame_request_at) dl->first_unserviced_render_frame_request_at = now;
|
||||||
if (!CVDisplayLinkIsRunning(dl->displayLink)) CVDisplayLinkStart(dl->displayLink);
|
if (!CVDisplayLinkIsRunning(dl->displayLink)) CVDisplayLinkStart(dl->displayLink);
|
||||||
|
else if (now - dl->first_unserviced_render_frame_request_at > s_to_monotonic_t(1ll)) {
|
||||||
|
// display link is stuck need to recreate it because Apple cant even
|
||||||
|
// get a simple timer right
|
||||||
|
CVDisplayLinkRelease(dl->displayLink); dl->displayLink = nil;
|
||||||
|
dl->first_unserviced_render_frame_request_at = now;
|
||||||
|
_glfw_create_cv_display_link(dl);
|
||||||
|
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||||
|
"CVDisplayLink stuck possibly because of sleep/screensaver + Apple's incompetence, recreating.");
|
||||||
|
if (!CVDisplayLinkIsRunning(dl->displayLink)) CVDisplayLinkStart(dl->displayLink);
|
||||||
|
}
|
||||||
} else if (dl->displayLink && dl->lastRenderFrameRequestedAt && now - dl->lastRenderFrameRequestedAt >= DISPLAY_LINK_SHUTDOWN_CHECK_INTERVAL) {
|
} else if (dl->displayLink && dl->lastRenderFrameRequestedAt && now - dl->lastRenderFrameRequestedAt >= DISPLAY_LINK_SHUTDOWN_CHECK_INTERVAL) {
|
||||||
CVDisplayLinkStop(dl->displayLink);
|
CVDisplayLinkStop(dl->displayLink);
|
||||||
dl->lastRenderFrameRequestedAt = 0;
|
dl->lastRenderFrameRequestedAt = 0;
|
||||||
|
dl->first_unserviced_render_frame_request_at = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2141,6 +2154,12 @@ _glfwDispatchRenderFrame(CGDirectDisplayID displayID) {
|
|||||||
}
|
}
|
||||||
w = w->next;
|
w = w->next;
|
||||||
}
|
}
|
||||||
|
for (size_t i = 0; i < _glfw.ns.displayLinks.count; i++) {
|
||||||
|
_GLFWDisplayLinkNS *dl = &_glfw.ns.displayLinks.entries[i];
|
||||||
|
if (dl->displayID == displayID) {
|
||||||
|
dl->first_unserviced_render_frame_request_at = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _glfwPlatformGetCursorPos(_GLFWwindow* window, double* xpos, double* ypos)
|
void _glfwPlatformGetCursorPos(_GLFWwindow* window, double* xpos, double* ypos)
|
||||||
|
|||||||
26
glfw/glfw3.h
vendored
26
glfw/glfw3.h
vendored
@@ -523,18 +523,20 @@ typedef enum {
|
|||||||
*
|
*
|
||||||
* @ingroup input
|
* @ingroup input
|
||||||
* @{ */
|
* @{ */
|
||||||
#define GLFW_MOUSE_BUTTON_1 0
|
typedef enum GLFWMouseButton {
|
||||||
#define GLFW_MOUSE_BUTTON_2 1
|
GLFW_MOUSE_BUTTON_1 = 0,
|
||||||
#define GLFW_MOUSE_BUTTON_3 2
|
GLFW_MOUSE_BUTTON_LEFT = 0,
|
||||||
#define GLFW_MOUSE_BUTTON_4 3
|
GLFW_MOUSE_BUTTON_2 = 1,
|
||||||
#define GLFW_MOUSE_BUTTON_5 4
|
GLFW_MOUSE_BUTTON_RIGHT = 1,
|
||||||
#define GLFW_MOUSE_BUTTON_6 5
|
GLFW_MOUSE_BUTTON_3 = 2,
|
||||||
#define GLFW_MOUSE_BUTTON_7 6
|
GLFW_MOUSE_BUTTON_MIDDLE = 2,
|
||||||
#define GLFW_MOUSE_BUTTON_8 7
|
GLFW_MOUSE_BUTTON_4 = 3,
|
||||||
#define GLFW_MOUSE_BUTTON_LAST GLFW_MOUSE_BUTTON_8
|
GLFW_MOUSE_BUTTON_5 = 4,
|
||||||
#define GLFW_MOUSE_BUTTON_LEFT GLFW_MOUSE_BUTTON_1
|
GLFW_MOUSE_BUTTON_6 = 5,
|
||||||
#define GLFW_MOUSE_BUTTON_RIGHT GLFW_MOUSE_BUTTON_2
|
GLFW_MOUSE_BUTTON_7 = 6,
|
||||||
#define GLFW_MOUSE_BUTTON_MIDDLE GLFW_MOUSE_BUTTON_3
|
GLFW_MOUSE_BUTTON_8 = 7,
|
||||||
|
GLFW_MOUSE_BUTTON_LAST = 7
|
||||||
|
} GLFWMouseButton;
|
||||||
/*! @} */
|
/*! @} */
|
||||||
|
|
||||||
/*! @defgroup joysticks Joysticks
|
/*! @defgroup joysticks Joysticks
|
||||||
|
|||||||
4
glfw/window.c
vendored
4
glfw/window.c
vendored
@@ -701,8 +701,8 @@ GLFWAPI void glfwSetWindowSizeIncrements(GLFWwindow* handle, int widthincr, int
|
|||||||
{
|
{
|
||||||
_GLFWwindow* window = (_GLFWwindow*) handle;
|
_GLFWwindow* window = (_GLFWwindow*) handle;
|
||||||
assert(window != NULL);
|
assert(window != NULL);
|
||||||
assert(widthincr >= 0);
|
assert(widthincr >= 0 || widthincr == GLFW_DONT_CARE);
|
||||||
assert(heightincr >= 0);
|
assert(heightincr >= 0 || heightincr == GLFW_DONT_CARE);
|
||||||
|
|
||||||
_GLFW_REQUIRE_INIT();
|
_GLFW_REQUIRE_INIT();
|
||||||
|
|
||||||
|
|||||||
@@ -3,21 +3,16 @@
|
|||||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from typing import Any, Dict, Iterable, Optional, Tuple, Type, Union
|
from typing import Any, Dict, Iterable, Optional
|
||||||
|
|
||||||
from kitty.conf.definition import config_lines
|
from kitty.cli_stub import DiffCLIOptions
|
||||||
from kitty.conf.utils import (
|
from kitty.conf.utils import (
|
||||||
init_config as _init_config, key_func, load_config as _load_config,
|
load_config as _load_config, parse_config_base, resolve_config
|
||||||
merge_dicts, parse_config_base, parse_kittens_key, resolve_config
|
|
||||||
)
|
)
|
||||||
from kitty.constants import config_dir
|
from kitty.constants import config_dir
|
||||||
from kitty.options_stub import DiffOptions
|
|
||||||
from kitty.cli_stub import DiffCLIOptions
|
|
||||||
from kitty.rgb import color_as_sgr
|
from kitty.rgb import color_as_sgr
|
||||||
|
|
||||||
from .config_data import all_options, type_convert
|
from .options.types import Options as DiffOptions, defaults
|
||||||
|
|
||||||
defaults: Optional[DiffOptions] = None
|
|
||||||
|
|
||||||
formats: Dict[str, str] = {
|
formats: Dict[str, str] = {
|
||||||
'title': '',
|
'title': '',
|
||||||
@@ -42,94 +37,30 @@ def set_formats(opts: DiffOptions) -> None:
|
|||||||
formats['added_highlight'] = '48' + color_as_sgr(opts.highlight_added_bg)
|
formats['added_highlight'] = '48' + color_as_sgr(opts.highlight_added_bg)
|
||||||
|
|
||||||
|
|
||||||
func_with_args, args_funcs = key_func()
|
SYSTEM_CONF = '/etc/xdg/kitty/diff.conf'
|
||||||
|
defconf = os.path.join(config_dir, 'diff.conf')
|
||||||
|
|
||||||
@func_with_args('scroll_by')
|
|
||||||
def parse_scroll_by(func: str, rest: str) -> Tuple[str, int]:
|
|
||||||
try:
|
|
||||||
return func, int(rest)
|
|
||||||
except Exception:
|
|
||||||
return func, 1
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('scroll_to')
|
|
||||||
def parse_scroll_to(func: str, rest: str) -> Tuple[str, str]:
|
|
||||||
rest = rest.lower()
|
|
||||||
if rest not in {'start', 'end', 'next-change', 'prev-change', 'next-page', 'prev-page', 'next-match', 'prev-match'}:
|
|
||||||
rest = 'start'
|
|
||||||
return func, rest
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('change_context')
|
|
||||||
def parse_change_context(func: str, rest: str) -> Tuple[str, Union[int, str]]:
|
|
||||||
rest = rest.lower()
|
|
||||||
if rest in {'all', 'default'}:
|
|
||||||
return func, rest
|
|
||||||
try:
|
|
||||||
amount = int(rest)
|
|
||||||
except Exception:
|
|
||||||
amount = 5
|
|
||||||
return func, amount
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('start_search')
|
|
||||||
def parse_start_search(func: str, rest: str) -> Tuple[str, Tuple[bool, bool]]:
|
|
||||||
rest_ = rest.lower().split()
|
|
||||||
is_regex = bool(rest_ and rest_[0] == 'regex')
|
|
||||||
is_backward = bool(len(rest_) > 1 and rest_[1] == 'backward')
|
|
||||||
return func, (is_regex, is_backward)
|
|
||||||
|
|
||||||
|
|
||||||
def special_handling(key: str, val: str, ans: Dict) -> bool:
|
|
||||||
if key == 'map':
|
|
||||||
x = parse_kittens_key(val, args_funcs)
|
|
||||||
if x is not None:
|
|
||||||
action, key_def = x
|
|
||||||
ans['key_definitions'][key_def] = action
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def parse_config(lines: Iterable[str], check_keys: bool = True) -> Dict[str, Any]:
|
|
||||||
ans: Dict[str, Any] = {'key_definitions': {}}
|
|
||||||
parse_config_base(
|
|
||||||
lines,
|
|
||||||
defaults,
|
|
||||||
type_convert,
|
|
||||||
special_handling,
|
|
||||||
ans,
|
|
||||||
check_keys=check_keys
|
|
||||||
)
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
def merge_configs(defaults: Dict, vals: Dict) -> Dict:
|
|
||||||
ans = {}
|
|
||||||
for k, v in defaults.items():
|
|
||||||
if isinstance(v, dict):
|
|
||||||
newvals = vals.get(k, {})
|
|
||||||
ans[k] = merge_dicts(v, newvals)
|
|
||||||
else:
|
|
||||||
ans[k] = vals.get(k, v)
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
def parse_defaults(lines: Iterable[str], check_keys: bool = False) -> Dict[str, Any]:
|
|
||||||
return parse_config(lines, check_keys)
|
|
||||||
|
|
||||||
|
|
||||||
x = _init_config(config_lines(all_options), parse_defaults)
|
|
||||||
Options: Type[DiffOptions] = x[0]
|
|
||||||
defaults = x[1]
|
|
||||||
|
|
||||||
|
|
||||||
def load_config(*paths: str, overrides: Optional[Iterable[str]] = None) -> DiffOptions:
|
def load_config(*paths: str, overrides: Optional[Iterable[str]] = None) -> DiffOptions:
|
||||||
return _load_config(Options, defaults, parse_config, merge_configs, *paths, overrides=overrides)
|
from .options.parse import (
|
||||||
|
create_result_dict, merge_result_dicts, parse_conf_item
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_config(lines: Iterable[str]) -> Dict[str, Any]:
|
||||||
|
ans: Dict[str, Any] = create_result_dict()
|
||||||
|
parse_config_base(
|
||||||
|
lines,
|
||||||
|
parse_conf_item,
|
||||||
|
ans,
|
||||||
|
)
|
||||||
|
return ans
|
||||||
|
|
||||||
SYSTEM_CONF = '/etc/xdg/kitty/diff.conf'
|
overrides = tuple(overrides) if overrides is not None else ()
|
||||||
defconf = os.path.join(config_dir, 'diff.conf')
|
opts_dict, paths = _load_config(defaults, parse_config, merge_result_dicts, *paths, overrides=overrides)
|
||||||
|
opts = DiffOptions(opts_dict)
|
||||||
|
opts.config_paths = paths
|
||||||
|
opts.config_overrides = overrides
|
||||||
|
return opts
|
||||||
|
|
||||||
|
|
||||||
def init_config(args: DiffCLIOptions) -> DiffOptions:
|
def init_config(args: DiffCLIOptions) -> DiffOptions:
|
||||||
@@ -137,4 +68,6 @@ def init_config(args: DiffCLIOptions) -> DiffOptions:
|
|||||||
overrides = (a.replace('=', ' ', 1) for a in args.override or ())
|
overrides = (a.replace('=', ' ', 1) for a in args.override or ())
|
||||||
opts = load_config(*config, overrides=overrides)
|
opts = load_config(*config, overrides=overrides)
|
||||||
set_formats(opts)
|
set_formats(opts)
|
||||||
|
for (sc, action) in opts.map:
|
||||||
|
opts.key_definitions[sc] = action
|
||||||
return opts
|
return opts
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# vim:fileencoding=utf-8
|
|
||||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
|
||||||
|
|
||||||
|
|
||||||
# Utils {{{
|
|
||||||
from functools import partial
|
|
||||||
from gettext import gettext as _
|
|
||||||
from typing import Any, Dict, Sequence, Union
|
|
||||||
|
|
||||||
from kitty.conf.definition import Option, Shortcut, option_func
|
|
||||||
from kitty.conf.utils import python_string, to_color, to_color_or_none
|
|
||||||
from kitty.utils import positive_int
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
all_options: Dict[str, Union[Option, Sequence[Shortcut]]] = {}
|
|
||||||
o, k, g, all_groups = option_func(all_options, {
|
|
||||||
'colors': [_('Colors')],
|
|
||||||
'diff': [_('Diffing'), ],
|
|
||||||
'shortcuts': [_('Keyboard shortcuts')],
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
g('diff')
|
|
||||||
|
|
||||||
|
|
||||||
def syntax_aliases(raw: str) -> Dict[str, str]:
|
|
||||||
ans = {}
|
|
||||||
for x in raw.split():
|
|
||||||
a, b = x.partition(':')[::2]
|
|
||||||
if a and b:
|
|
||||||
ans[a.lower()] = b
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
o('syntax_aliases', 'pyj:py pyi:py recipe:py', option_type=syntax_aliases, long_text=_('''
|
|
||||||
File extension aliases for syntax highlight
|
|
||||||
For example, to syntax highlight :file:`file.xyz` as
|
|
||||||
:file:`file.abc` use a setting of :code:`xyz:abc`
|
|
||||||
'''))
|
|
||||||
|
|
||||||
o('num_context_lines', 3, option_type=positive_int, long_text=_('''
|
|
||||||
The number of lines of context to show around each change.'''))
|
|
||||||
|
|
||||||
o('diff_cmd', 'auto', long_text=_('''
|
|
||||||
The diff command to use. Must contain the placeholder :code:`_CONTEXT_`
|
|
||||||
which will be replaced by the number of lines of context. The default
|
|
||||||
is to search the system for either git or diff and use that, if found.
|
|
||||||
'''))
|
|
||||||
|
|
||||||
o('replace_tab_by', r'\x20\x20\x20\x20', option_type=python_string, long_text=_('''
|
|
||||||
The string to replace tabs with. Default is to use four spaces.'''))
|
|
||||||
|
|
||||||
|
|
||||||
g('colors')
|
|
||||||
|
|
||||||
o('pygments_style', 'default', long_text=_('''
|
|
||||||
The pygments color scheme to use for syntax highlighting.
|
|
||||||
See :link:`pygments colors schemes <https://help.farbox.com/pygments.html>` for a list of schemes.'''))
|
|
||||||
|
|
||||||
|
|
||||||
c = partial(o, option_type=to_color)
|
|
||||||
c('foreground', 'black', long_text=_('Basic colors'))
|
|
||||||
c('background', 'white')
|
|
||||||
|
|
||||||
c('title_fg', 'black', long_text=_('Title colors'))
|
|
||||||
c('title_bg', 'white')
|
|
||||||
|
|
||||||
c('margin_bg', '#fafbfc', long_text=_('Margin colors'))
|
|
||||||
c('margin_fg', '#aaaaaa')
|
|
||||||
|
|
||||||
c('removed_bg', '#ffeef0', long_text=_('Removed text backgrounds'))
|
|
||||||
c('highlight_removed_bg', '#fdb8c0')
|
|
||||||
c('removed_margin_bg', '#ffdce0')
|
|
||||||
|
|
||||||
c('added_bg', '#e6ffed', long_text=_('Added text backgrounds'))
|
|
||||||
c('highlight_added_bg', '#acf2bd')
|
|
||||||
c('added_margin_bg', '#cdffd8')
|
|
||||||
|
|
||||||
c('filler_bg', '#fafbfc', long_text=_('Filler (empty) line background'))
|
|
||||||
c('margin_filler_bg', 'none', option_type=to_color_or_none, long_text=_(
|
|
||||||
'Filler (empty) line background in margins, defaults to the filler background'))
|
|
||||||
|
|
||||||
c('hunk_margin_bg', '#dbedff', long_text=_('Hunk header colors'))
|
|
||||||
c('hunk_bg', '#f1f8ff')
|
|
||||||
|
|
||||||
c('search_bg', '#444', long_text=_('Highlighting'))
|
|
||||||
c('search_fg', 'white')
|
|
||||||
c('select_bg', '#b4d5fe')
|
|
||||||
o('select_fg', 'black', option_type=to_color_or_none)
|
|
||||||
|
|
||||||
g('shortcuts')
|
|
||||||
k('quit', 'q', 'quit', _('Quit'))
|
|
||||||
k('quit', 'esc', 'quit', _('Quit'))
|
|
||||||
|
|
||||||
k('scroll_down', 'j', 'scroll_by 1', _('Scroll down'))
|
|
||||||
k('scroll_down', 'down', 'scroll_by 1', _('Scroll down'))
|
|
||||||
k('scroll_up', 'k', 'scroll_by -1', _('Scroll up'))
|
|
||||||
k('scroll_up', 'up', 'scroll_by -1', _('Scroll up'))
|
|
||||||
|
|
||||||
k('scroll_top', 'home', 'scroll_to start', _('Scroll to top'))
|
|
||||||
k('scroll_bottom', 'end', 'scroll_to end', _('Scroll to bottom'))
|
|
||||||
|
|
||||||
k('scroll_page_down', 'page_down', 'scroll_to next-page', _('Scroll to next page'))
|
|
||||||
k('scroll_page_down', 'space', 'scroll_to next-page', _('Scroll to next page'))
|
|
||||||
k('scroll_page_up', 'page_up', 'scroll_to prev-page', _('Scroll to previous page'))
|
|
||||||
|
|
||||||
k('next_change', 'n', 'scroll_to next-change', _('Scroll to next change'))
|
|
||||||
k('prev_change', 'p', 'scroll_to prev-change', _('Scroll to previous change'))
|
|
||||||
|
|
||||||
k('all_context', 'a', 'change_context all', _('Show all context'))
|
|
||||||
k('default_context', '=', 'change_context default', _('Show default context'))
|
|
||||||
k('increase_context', '+', 'change_context 5', _('Increase context'))
|
|
||||||
k('decrease_context', '-', 'change_context -5', _('Decrease context'))
|
|
||||||
|
|
||||||
k('search_forward', '/', 'start_search regex forward', _('Search forward'))
|
|
||||||
k('search_backward', '?', 'start_search regex backward', _('Search backward'))
|
|
||||||
k('next_match', '.', 'scroll_to next-match', _('Scroll to next search match'))
|
|
||||||
k('prev_match', ',', 'scroll_to prev-match', _('Scroll to previous search match'))
|
|
||||||
k('next_match', '>', 'scroll_to next-match', _('Scroll to next search match'))
|
|
||||||
k('prev_match', '<', 'scroll_to prev-match', _('Scroll to previous search match'))
|
|
||||||
k('search_forward_simple', 'f', 'start_search substring forward', _('Search forward (no regex)'))
|
|
||||||
k('search_backward_simple', 'b', 'start_search substring backward', _('Search backward (no regex)'))
|
|
||||||
|
|
||||||
|
|
||||||
def type_convert(name: str, val: Any) -> Any:
|
|
||||||
o = all_options.get(name)
|
|
||||||
if isinstance(o, Option):
|
|
||||||
val = o.option_type(val)
|
|
||||||
return val
|
|
||||||
@@ -159,13 +159,12 @@ def highlight_collection(collection: Collection, aliases: Optional[Dict[str, str
|
|||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
from .config import defaults
|
|
||||||
# kitty +runpy "from kittens.diff.highlight import main; main()" file
|
# kitty +runpy "from kittens.diff.highlight import main; main()" file
|
||||||
|
from .options.types import defaults
|
||||||
import sys
|
import sys
|
||||||
initialize_highlighter()
|
initialize_highlighter()
|
||||||
if defaults is not None:
|
with open(sys.argv[-1]) as f:
|
||||||
with open(sys.argv[-1]) as f:
|
highlighted = highlight_data(f.read(), f.name, defaults.syntax_aliases)
|
||||||
highlighted = highlight_data(f.read(), f.name, defaults.syntax_aliases)
|
if highlighted is None:
|
||||||
if highlighted is None:
|
raise SystemExit('Unknown filetype: {}'.format(sys.argv[-1]))
|
||||||
raise SystemExit('Unknown filetype: {}'.format(sys.argv[-1]))
|
print(highlighted)
|
||||||
print(highlighted)
|
|
||||||
|
|||||||
@@ -19,11 +19,10 @@ from typing import (
|
|||||||
|
|
||||||
from kitty.cli import CONFIG_HELP, parse_args
|
from kitty.cli import CONFIG_HELP, parse_args
|
||||||
from kitty.cli_stub import DiffCLIOptions
|
from kitty.cli_stub import DiffCLIOptions
|
||||||
from kitty.conf.utils import KittensKeyAction
|
from kitty.conf.utils import KeyAction
|
||||||
from kitty.constants import appname
|
from kitty.constants import appname
|
||||||
from kitty.fast_data_types import wcswidth
|
from kitty.fast_data_types import wcswidth
|
||||||
from kitty.key_encoding import EventType, KeyEvent
|
from kitty.key_encoding import EventType, KeyEvent
|
||||||
from kitty.options_stub import DiffOptions
|
|
||||||
from kitty.utils import ScreenSize
|
from kitty.utils import ScreenSize
|
||||||
|
|
||||||
from ..tui.handler import Handler
|
from ..tui.handler import Handler
|
||||||
@@ -33,10 +32,11 @@ from ..tui.loop import Loop
|
|||||||
from ..tui.operations import styled
|
from ..tui.operations import styled
|
||||||
from . import global_data
|
from . import global_data
|
||||||
from .collect import (
|
from .collect import (
|
||||||
Collection, create_collection, data_for_path, lines_for_path, sanitize,
|
Collection, add_remote_dir, create_collection, data_for_path,
|
||||||
set_highlight_data, add_remote_dir
|
lines_for_path, sanitize, set_highlight_data
|
||||||
)
|
)
|
||||||
from .config import init_config
|
from .config import init_config
|
||||||
|
from .options.types import Options as DiffOptions
|
||||||
from .patch import Differ, Patch, set_diff_command, worker_processes
|
from .patch import Differ, Patch, set_diff_command, worker_processes
|
||||||
from .render import (
|
from .render import (
|
||||||
ImagePlacement, ImageSupportWarning, Line, LineRef, Reference, render_diff
|
ImagePlacement, ImageSupportWarning, Line, LineRef, Reference, render_diff
|
||||||
@@ -95,14 +95,14 @@ class DiffHandler(Handler):
|
|||||||
for key_def, action in self.opts.key_definitions.items():
|
for key_def, action in self.opts.key_definitions.items():
|
||||||
self.add_shortcut(action, key_def)
|
self.add_shortcut(action, key_def)
|
||||||
|
|
||||||
def perform_action(self, action: KittensKeyAction) -> None:
|
def perform_action(self, action: KeyAction) -> None:
|
||||||
func, args = action
|
func, args = action
|
||||||
if func == 'quit':
|
if func == 'quit':
|
||||||
self.quit_loop(0)
|
self.quit_loop(0)
|
||||||
return
|
return
|
||||||
if self.state <= DIFFED:
|
if self.state <= DIFFED:
|
||||||
if func == 'scroll_by':
|
if func == 'scroll_by':
|
||||||
return self.scroll_lines(int(args[0]))
|
return self.scroll_lines(int(args[0] or 0))
|
||||||
if func == 'scroll_to':
|
if func == 'scroll_to':
|
||||||
where = str(args[0])
|
where = str(args[0])
|
||||||
if 'change' in where:
|
if 'change' in where:
|
||||||
@@ -122,7 +122,7 @@ class DiffHandler(Handler):
|
|||||||
elif to == 'default':
|
elif to == 'default':
|
||||||
new_ctx = self.original_context_count
|
new_ctx = self.original_context_count
|
||||||
else:
|
else:
|
||||||
new_ctx += int(to)
|
new_ctx += int(to or 0)
|
||||||
return self.change_context_count(new_ctx)
|
return self.change_context_count(new_ctx)
|
||||||
if func == 'start_search':
|
if func == 'start_search':
|
||||||
self.start_search(bool(args[0]), bool(args[1]))
|
self.start_search(bool(args[0]), bool(args[1]))
|
||||||
@@ -658,5 +658,5 @@ elif __name__ == '__doc__':
|
|||||||
cd['options'] = OPTIONS
|
cd['options'] = OPTIONS
|
||||||
cd['help_text'] = help_text
|
cd['help_text'] = help_text
|
||||||
elif __name__ == '__conf__':
|
elif __name__ == '__conf__':
|
||||||
from .config_data import all_options
|
from .options.definition import definition
|
||||||
sys.all_options = all_options # type: ignore
|
sys.options_definition = definition # type: ignore
|
||||||
|
|||||||
0
kittens/diff/options/__init__.py
Normal file
0
kittens/diff/options/__init__.py
Normal file
247
kittens/diff/options/definition.py
Normal file
247
kittens/diff/options/definition.py
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
# After editing this file run ./gen-config.py to apply the changes
|
||||||
|
|
||||||
|
from kitty.conf.types import Action, Definition
|
||||||
|
|
||||||
|
|
||||||
|
definition = Definition(
|
||||||
|
'kittens.diff',
|
||||||
|
Action('map', 'parse_map', {'key_definitions': 'kitty.conf.utils.KittensKeyMap'}, ['kitty.types.ParsedShortcut', 'kitty.conf.utils.KeyAction']),
|
||||||
|
)
|
||||||
|
|
||||||
|
agr = definition.add_group
|
||||||
|
egr = definition.end_group
|
||||||
|
opt = definition.add_option
|
||||||
|
map = definition.add_map
|
||||||
|
mma = definition.add_mouse_map
|
||||||
|
|
||||||
|
# diff {{{
|
||||||
|
agr('diff', 'Diffing')
|
||||||
|
|
||||||
|
opt('syntax_aliases', 'pyj:py pyi:py recipe:py',
|
||||||
|
option_type='syntax_aliases',
|
||||||
|
long_text='''
|
||||||
|
File extension aliases for syntax highlight For example, to syntax highlight
|
||||||
|
:file:`file.xyz` as :file:`file.abc` use a setting of :code:`xyz:abc`
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('num_context_lines', '3',
|
||||||
|
option_type='positive_int',
|
||||||
|
long_text='The number of lines of context to show around each change.'
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('diff_cmd', 'auto',
|
||||||
|
long_text='''
|
||||||
|
The diff command to use. Must contain the placeholder :code:`_CONTEXT_` which
|
||||||
|
will be replaced by the number of lines of context. The default is to search the
|
||||||
|
system for either git or diff and use that, if found.
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('replace_tab_by', '\\x20\\x20\\x20\\x20',
|
||||||
|
option_type='python_string',
|
||||||
|
long_text='The string to replace tabs with. Default is to use four spaces.'
|
||||||
|
)
|
||||||
|
egr() # }}}
|
||||||
|
|
||||||
|
# colors {{{
|
||||||
|
agr('colors', 'Colors')
|
||||||
|
|
||||||
|
opt('pygments_style', 'default',
|
||||||
|
long_text='''
|
||||||
|
The pygments color scheme to use for syntax highlighting. See :link:`pygments
|
||||||
|
colors schemes <https://help.farbox.com/pygments.html>` for a list of schemes.
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('foreground', 'black',
|
||||||
|
option_type='to_color',
|
||||||
|
long_text='Basic colors'
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('background', 'white',
|
||||||
|
option_type='to_color',
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('title_fg', 'black',
|
||||||
|
option_type='to_color',
|
||||||
|
long_text='Title colors'
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('title_bg', 'white',
|
||||||
|
option_type='to_color',
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('margin_bg', '#fafbfc',
|
||||||
|
option_type='to_color',
|
||||||
|
long_text='Margin colors'
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('margin_fg', '#aaaaaa',
|
||||||
|
option_type='to_color',
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('removed_bg', '#ffeef0',
|
||||||
|
option_type='to_color',
|
||||||
|
long_text='Removed text backgrounds'
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('highlight_removed_bg', '#fdb8c0',
|
||||||
|
option_type='to_color',
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('removed_margin_bg', '#ffdce0',
|
||||||
|
option_type='to_color',
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('added_bg', '#e6ffed',
|
||||||
|
option_type='to_color',
|
||||||
|
long_text='Added text backgrounds'
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('highlight_added_bg', '#acf2bd',
|
||||||
|
option_type='to_color',
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('added_margin_bg', '#cdffd8',
|
||||||
|
option_type='to_color',
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('filler_bg', '#fafbfc',
|
||||||
|
option_type='to_color',
|
||||||
|
long_text='Filler (empty) line background'
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('margin_filler_bg', 'none',
|
||||||
|
option_type='to_color_or_none',
|
||||||
|
long_text='Filler (empty) line background in margins, defaults to the filler background'
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('hunk_margin_bg', '#dbedff',
|
||||||
|
option_type='to_color',
|
||||||
|
long_text='Hunk header colors'
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('hunk_bg', '#f1f8ff',
|
||||||
|
option_type='to_color',
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('search_bg', '#444',
|
||||||
|
option_type='to_color',
|
||||||
|
long_text='Highlighting'
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('search_fg', 'white',
|
||||||
|
option_type='to_color',
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('select_bg', '#b4d5fe',
|
||||||
|
option_type='to_color',
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('select_fg', 'black',
|
||||||
|
option_type='to_color_or_none',
|
||||||
|
)
|
||||||
|
egr() # }}}
|
||||||
|
|
||||||
|
# shortcuts {{{
|
||||||
|
agr('shortcuts', 'Keyboard shortcuts')
|
||||||
|
|
||||||
|
map('Quit',
|
||||||
|
'quit q quit',
|
||||||
|
)
|
||||||
|
map('Quit',
|
||||||
|
'quit esc quit',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Scroll down',
|
||||||
|
'scroll_down j scroll_by 1',
|
||||||
|
)
|
||||||
|
map('Scroll down',
|
||||||
|
'scroll_down down scroll_by 1',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Scroll up',
|
||||||
|
'scroll_up k scroll_by -1',
|
||||||
|
)
|
||||||
|
map('Scroll up',
|
||||||
|
'scroll_up up scroll_by -1',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Scroll to top',
|
||||||
|
'scroll_top home scroll_to start',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Scroll to bottom',
|
||||||
|
'scroll_bottom end scroll_to end',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Scroll to next page',
|
||||||
|
'scroll_page_down page_down scroll_to next-page',
|
||||||
|
)
|
||||||
|
map('Scroll to next page',
|
||||||
|
'scroll_page_down space scroll_to next-page',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Scroll to previous page',
|
||||||
|
'scroll_page_up page_up scroll_to prev-page',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Scroll to next change',
|
||||||
|
'next_change n scroll_to next-change',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Scroll to previous change',
|
||||||
|
'prev_change p scroll_to prev-change',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Show all context',
|
||||||
|
'all_context a change_context all',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Show default context',
|
||||||
|
'default_context = change_context default',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Increase context',
|
||||||
|
'increase_context + change_context 5',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Decrease context',
|
||||||
|
'decrease_context - change_context -5',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Search forward',
|
||||||
|
'search_forward / start_search regex forward',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Search backward',
|
||||||
|
'search_backward ? start_search regex backward',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Scroll to next search match',
|
||||||
|
'next_match . scroll_to next-match',
|
||||||
|
)
|
||||||
|
map('Scroll to next search match',
|
||||||
|
'next_match > scroll_to next-match',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Scroll to previous search match',
|
||||||
|
'prev_match , scroll_to prev-match',
|
||||||
|
)
|
||||||
|
map('Scroll to previous search match',
|
||||||
|
'prev_match < scroll_to prev-match',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Search forward (no regex)',
|
||||||
|
'search_forward_simple f start_search substring forward',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Search backward (no regex)',
|
||||||
|
'search_backward_simple b start_search substring backward',
|
||||||
|
)
|
||||||
|
egr() # }}}
|
||||||
120
kittens/diff/options/parse.py
generated
Normal file
120
kittens/diff/options/parse.py
generated
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# generated by gen-config.py DO NOT edit
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
|
||||||
|
import typing
|
||||||
|
from kittens.diff.options.utils import parse_map, syntax_aliases
|
||||||
|
from kitty.conf.utils import merge_dicts, positive_int, python_string, to_color, to_color_or_none
|
||||||
|
|
||||||
|
|
||||||
|
class Parser:
|
||||||
|
|
||||||
|
def added_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['added_bg'] = to_color(val)
|
||||||
|
|
||||||
|
def added_margin_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['added_margin_bg'] = to_color(val)
|
||||||
|
|
||||||
|
def background(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['background'] = to_color(val)
|
||||||
|
|
||||||
|
def diff_cmd(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['diff_cmd'] = str(val)
|
||||||
|
|
||||||
|
def filler_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['filler_bg'] = to_color(val)
|
||||||
|
|
||||||
|
def foreground(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['foreground'] = to_color(val)
|
||||||
|
|
||||||
|
def highlight_added_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['highlight_added_bg'] = to_color(val)
|
||||||
|
|
||||||
|
def highlight_removed_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['highlight_removed_bg'] = to_color(val)
|
||||||
|
|
||||||
|
def hunk_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['hunk_bg'] = to_color(val)
|
||||||
|
|
||||||
|
def hunk_margin_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['hunk_margin_bg'] = to_color(val)
|
||||||
|
|
||||||
|
def margin_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['margin_bg'] = to_color(val)
|
||||||
|
|
||||||
|
def margin_fg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['margin_fg'] = to_color(val)
|
||||||
|
|
||||||
|
def margin_filler_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['margin_filler_bg'] = to_color_or_none(val)
|
||||||
|
|
||||||
|
def num_context_lines(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['num_context_lines'] = positive_int(val)
|
||||||
|
|
||||||
|
def pygments_style(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['pygments_style'] = str(val)
|
||||||
|
|
||||||
|
def removed_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['removed_bg'] = to_color(val)
|
||||||
|
|
||||||
|
def removed_margin_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['removed_margin_bg'] = to_color(val)
|
||||||
|
|
||||||
|
def replace_tab_by(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['replace_tab_by'] = python_string(val)
|
||||||
|
|
||||||
|
def search_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['search_bg'] = to_color(val)
|
||||||
|
|
||||||
|
def search_fg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['search_fg'] = to_color(val)
|
||||||
|
|
||||||
|
def select_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['select_bg'] = to_color(val)
|
||||||
|
|
||||||
|
def select_fg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['select_fg'] = to_color_or_none(val)
|
||||||
|
|
||||||
|
def syntax_aliases(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['syntax_aliases'] = syntax_aliases(val)
|
||||||
|
|
||||||
|
def title_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['title_bg'] = to_color(val)
|
||||||
|
|
||||||
|
def title_fg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
ans['title_fg'] = to_color(val)
|
||||||
|
|
||||||
|
def map(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
for k in parse_map(val):
|
||||||
|
ans['map'].append(k)
|
||||||
|
|
||||||
|
|
||||||
|
def create_result_dict() -> typing.Dict[str, typing.Any]:
|
||||||
|
return {
|
||||||
|
'map': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
actions = frozenset(('map',))
|
||||||
|
|
||||||
|
|
||||||
|
def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:
|
||||||
|
ans = {}
|
||||||
|
for k, v in defaults.items():
|
||||||
|
if isinstance(v, dict):
|
||||||
|
ans[k] = merge_dicts(v, vals.get(k, {}))
|
||||||
|
elif k in actions:
|
||||||
|
ans[k] = v + vals.get(k, [])
|
||||||
|
else:
|
||||||
|
ans[k] = vals.get(k, v)
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
parser = Parser()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_conf_item(key: str, val: str, ans: typing.Dict[str, typing.Any]) -> bool:
|
||||||
|
func = getattr(parser, key, None)
|
||||||
|
if func is not None:
|
||||||
|
func(val, ans)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
168
kittens/diff/options/types.py
generated
Normal file
168
kittens/diff/options/types.py
generated
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# generated by gen-config.py DO NOT edit
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
|
||||||
|
import typing
|
||||||
|
from kitty.conf.utils import KeyAction, KittensKeyMap
|
||||||
|
import kitty.conf.utils
|
||||||
|
from kitty.rgb import Color
|
||||||
|
import kitty.rgb
|
||||||
|
from kitty.types import ParsedShortcut
|
||||||
|
import kitty.types
|
||||||
|
|
||||||
|
|
||||||
|
option_names = ( # {{{
|
||||||
|
'added_bg',
|
||||||
|
'added_margin_bg',
|
||||||
|
'background',
|
||||||
|
'diff_cmd',
|
||||||
|
'filler_bg',
|
||||||
|
'foreground',
|
||||||
|
'highlight_added_bg',
|
||||||
|
'highlight_removed_bg',
|
||||||
|
'hunk_bg',
|
||||||
|
'hunk_margin_bg',
|
||||||
|
'map',
|
||||||
|
'margin_bg',
|
||||||
|
'margin_fg',
|
||||||
|
'margin_filler_bg',
|
||||||
|
'num_context_lines',
|
||||||
|
'pygments_style',
|
||||||
|
'removed_bg',
|
||||||
|
'removed_margin_bg',
|
||||||
|
'replace_tab_by',
|
||||||
|
'search_bg',
|
||||||
|
'search_fg',
|
||||||
|
'select_bg',
|
||||||
|
'select_fg',
|
||||||
|
'syntax_aliases',
|
||||||
|
'title_bg',
|
||||||
|
'title_fg') # }}}
|
||||||
|
|
||||||
|
|
||||||
|
class Options:
|
||||||
|
added_bg: Color = Color(red=230, green=255, blue=237)
|
||||||
|
added_margin_bg: Color = Color(red=205, green=255, blue=216)
|
||||||
|
background: Color = Color(red=255, green=255, blue=255)
|
||||||
|
diff_cmd: str = 'auto'
|
||||||
|
filler_bg: Color = Color(red=250, green=251, blue=252)
|
||||||
|
foreground: Color = Color(red=0, green=0, blue=0)
|
||||||
|
highlight_added_bg: Color = Color(red=172, green=242, blue=189)
|
||||||
|
highlight_removed_bg: Color = Color(red=253, green=184, blue=192)
|
||||||
|
hunk_bg: Color = Color(red=241, green=248, blue=255)
|
||||||
|
hunk_margin_bg: Color = Color(red=219, green=237, blue=255)
|
||||||
|
margin_bg: Color = Color(red=250, green=251, blue=252)
|
||||||
|
margin_fg: Color = Color(red=170, green=170, blue=170)
|
||||||
|
margin_filler_bg: typing.Optional[kitty.rgb.Color] = None
|
||||||
|
num_context_lines: int = 3
|
||||||
|
pygments_style: str = 'default'
|
||||||
|
removed_bg: Color = Color(red=255, green=238, blue=240)
|
||||||
|
removed_margin_bg: Color = Color(red=255, green=220, blue=224)
|
||||||
|
replace_tab_by: str = ' '
|
||||||
|
search_bg: Color = Color(red=68, green=68, blue=68)
|
||||||
|
search_fg: Color = Color(red=255, green=255, blue=255)
|
||||||
|
select_bg: Color = Color(red=180, green=213, blue=254)
|
||||||
|
select_fg: typing.Optional[kitty.rgb.Color] = Color(red=0, green=0, blue=0)
|
||||||
|
syntax_aliases: typing.Dict[str, str] = {'pyj': 'py', 'pyi': 'py', 'recipe': 'py'}
|
||||||
|
title_bg: Color = Color(red=255, green=255, blue=255)
|
||||||
|
title_fg: Color = Color(red=0, green=0, blue=0)
|
||||||
|
map: typing.List[typing.Tuple[kitty.types.ParsedShortcut, kitty.conf.utils.KeyAction]] = []
|
||||||
|
key_definitions: KittensKeyMap = {}
|
||||||
|
config_paths: typing.Tuple[str, ...] = ()
|
||||||
|
config_overrides: typing.Tuple[str, ...] = ()
|
||||||
|
|
||||||
|
def __init__(self, options_dict: typing.Optional[typing.Dict[str, typing.Any]] = None) -> None:
|
||||||
|
if options_dict is not None:
|
||||||
|
for key in option_names:
|
||||||
|
setattr(self, key, options_dict[key])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _fields(self) -> typing.Tuple[str, ...]:
|
||||||
|
return option_names
|
||||||
|
|
||||||
|
def __iter__(self) -> typing.Iterator[str]:
|
||||||
|
return iter(self._fields)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self._fields)
|
||||||
|
|
||||||
|
def _copy_of_val(self, name: str) -> typing.Any:
|
||||||
|
ans = getattr(self, name)
|
||||||
|
if isinstance(ans, dict):
|
||||||
|
ans = ans.copy()
|
||||||
|
elif isinstance(ans, list):
|
||||||
|
ans = ans[:]
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def _asdict(self) -> typing.Dict[str, typing.Any]:
|
||||||
|
return {k: self._copy_of_val(k) for k in self}
|
||||||
|
|
||||||
|
def _replace(self, **kw: typing.Any) -> "Options":
|
||||||
|
ans = Options()
|
||||||
|
for name in self:
|
||||||
|
setattr(ans, name, self._copy_of_val(name))
|
||||||
|
for name, val in kw.items():
|
||||||
|
setattr(ans, name, val)
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def __getitem__(self, key: typing.Union[int, str]) -> typing.Any:
|
||||||
|
k = option_names[key] if isinstance(key, int) else key
|
||||||
|
try:
|
||||||
|
return getattr(self, k)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
raise KeyError(f"No option named: {k}")
|
||||||
|
|
||||||
|
|
||||||
|
defaults = Options()
|
||||||
|
defaults.map = [
|
||||||
|
# quit
|
||||||
|
(ParsedShortcut(mods=0, key_name='q'), KeyAction('quit')),
|
||||||
|
# quit
|
||||||
|
(ParsedShortcut(mods=0, key_name='ESCAPE'), KeyAction('quit')),
|
||||||
|
# scroll_down
|
||||||
|
(ParsedShortcut(mods=0, key_name='j'), KeyAction('scroll_by', (1,))),
|
||||||
|
# scroll_down
|
||||||
|
(ParsedShortcut(mods=0, key_name='DOWN'), KeyAction('scroll_by', (1,))),
|
||||||
|
# scroll_up
|
||||||
|
(ParsedShortcut(mods=0, key_name='k'), KeyAction('scroll_by', (-1,))),
|
||||||
|
# scroll_up
|
||||||
|
(ParsedShortcut(mods=0, key_name='UP'), KeyAction('scroll_by', (-1,))),
|
||||||
|
# scroll_top
|
||||||
|
(ParsedShortcut(mods=0, key_name='HOME'), KeyAction('scroll_to', ('start',))),
|
||||||
|
# scroll_bottom
|
||||||
|
(ParsedShortcut(mods=0, key_name='END'), KeyAction('scroll_to', ('end',))),
|
||||||
|
# scroll_page_down
|
||||||
|
(ParsedShortcut(mods=0, key_name='PAGE_DOWN'), KeyAction('scroll_to', ('next-page',))),
|
||||||
|
# scroll_page_down
|
||||||
|
(ParsedShortcut(mods=0, key_name=' '), KeyAction('scroll_to', ('next-page',))),
|
||||||
|
# scroll_page_up
|
||||||
|
(ParsedShortcut(mods=0, key_name='PAGE_UP'), KeyAction('scroll_to', ('prev-page',))),
|
||||||
|
# next_change
|
||||||
|
(ParsedShortcut(mods=0, key_name='n'), KeyAction('scroll_to', ('next-change',))),
|
||||||
|
# prev_change
|
||||||
|
(ParsedShortcut(mods=0, key_name='p'), KeyAction('scroll_to', ('prev-change',))),
|
||||||
|
# all_context
|
||||||
|
(ParsedShortcut(mods=0, key_name='a'), KeyAction('change_context', ('all',))),
|
||||||
|
# default_context
|
||||||
|
(ParsedShortcut(mods=0, key_name='='), KeyAction('change_context', ('default',))),
|
||||||
|
# increase_context
|
||||||
|
(ParsedShortcut(mods=0, key_name='+'), KeyAction('change_context', (5,))),
|
||||||
|
# decrease_context
|
||||||
|
(ParsedShortcut(mods=0, key_name='-'), KeyAction('change_context', (-5,))),
|
||||||
|
# search_forward
|
||||||
|
(ParsedShortcut(mods=0, key_name='/'), KeyAction('start_search', (True, False))),
|
||||||
|
# search_backward
|
||||||
|
(ParsedShortcut(mods=0, key_name='?'), KeyAction('start_search', (True, True))),
|
||||||
|
# next_match
|
||||||
|
(ParsedShortcut(mods=0, key_name='.'), KeyAction('scroll_to', ('next-match',))),
|
||||||
|
# next_match
|
||||||
|
(ParsedShortcut(mods=0, key_name='>'), KeyAction('scroll_to', ('next-match',))),
|
||||||
|
# prev_match
|
||||||
|
(ParsedShortcut(mods=0, key_name=','), KeyAction('scroll_to', ('prev-match',))),
|
||||||
|
# prev_match
|
||||||
|
(ParsedShortcut(mods=0, key_name='<'), KeyAction('scroll_to', ('prev-match',))),
|
||||||
|
# search_forward_simple
|
||||||
|
(ParsedShortcut(mods=0, key_name='f'), KeyAction('start_search', (False, False))),
|
||||||
|
# search_backward_simple
|
||||||
|
(ParsedShortcut(mods=0, key_name='b'), KeyAction('start_search', (False, True))),
|
||||||
|
]
|
||||||
62
kittens/diff/options/utils.py
Normal file
62
kittens/diff/options/utils.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
|
||||||
|
from typing import Any, Dict, Iterable, Sequence, Tuple, Union
|
||||||
|
|
||||||
|
from kitty.conf.utils import KittensKeyDefinition, key_func, parse_kittens_key
|
||||||
|
|
||||||
|
func_with_args, args_funcs = key_func()
|
||||||
|
FuncArgsType = Tuple[str, Sequence[Any]]
|
||||||
|
|
||||||
|
|
||||||
|
@func_with_args('scroll_by')
|
||||||
|
def parse_scroll_by(func: str, rest: str) -> Tuple[str, int]:
|
||||||
|
try:
|
||||||
|
return func, int(rest)
|
||||||
|
except Exception:
|
||||||
|
return func, 1
|
||||||
|
|
||||||
|
|
||||||
|
@func_with_args('scroll_to')
|
||||||
|
def parse_scroll_to(func: str, rest: str) -> Tuple[str, str]:
|
||||||
|
rest = rest.lower()
|
||||||
|
if rest not in {'start', 'end', 'next-change', 'prev-change', 'next-page', 'prev-page', 'next-match', 'prev-match'}:
|
||||||
|
rest = 'start'
|
||||||
|
return func, rest
|
||||||
|
|
||||||
|
|
||||||
|
@func_with_args('change_context')
|
||||||
|
def parse_change_context(func: str, rest: str) -> Tuple[str, Union[int, str]]:
|
||||||
|
rest = rest.lower()
|
||||||
|
if rest in {'all', 'default'}:
|
||||||
|
return func, rest
|
||||||
|
try:
|
||||||
|
amount = int(rest)
|
||||||
|
except Exception:
|
||||||
|
amount = 5
|
||||||
|
return func, amount
|
||||||
|
|
||||||
|
|
||||||
|
@func_with_args('start_search')
|
||||||
|
def parse_start_search(func: str, rest: str) -> Tuple[str, Tuple[bool, bool]]:
|
||||||
|
rest_ = rest.lower().split()
|
||||||
|
is_regex = bool(rest_ and rest_[0] == 'regex')
|
||||||
|
is_backward = bool(len(rest_) > 1 and rest_[1] == 'backward')
|
||||||
|
return func, (is_regex, is_backward)
|
||||||
|
|
||||||
|
|
||||||
|
def syntax_aliases(raw: str) -> Dict[str, str]:
|
||||||
|
ans = {}
|
||||||
|
for x in raw.split():
|
||||||
|
a, b = x.partition(':')[::2]
|
||||||
|
if a and b:
|
||||||
|
ans[a.lower()] = b
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
def parse_map(val: str) -> Iterable[KittensKeyDefinition]:
|
||||||
|
x = parse_kittens_key(val, args_funcs)
|
||||||
|
if x is not None:
|
||||||
|
yield x
|
||||||
@@ -6,7 +6,7 @@ import re
|
|||||||
from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Tuple
|
from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Tuple
|
||||||
|
|
||||||
from kitty.fast_data_types import wcswidth
|
from kitty.fast_data_types import wcswidth
|
||||||
from kitty.options_stub import DiffOptions
|
from .options.types import Options as DiffOptions
|
||||||
|
|
||||||
from ..tui.operations import styled
|
from ..tui.operations import styled
|
||||||
|
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ class ControlMaster:
|
|||||||
cp = subprocess.run(self.batch_cmd_prefix + [self.conn_data.hostname, 'hostname', '-f'], stdout=subprocess.PIPE,
|
cp = subprocess.run(self.batch_cmd_prefix + [self.conn_data.hostname, 'hostname', '-f'], stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
|
stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
|
||||||
if cp.returncode == 0:
|
if cp.returncode == 0:
|
||||||
q = cp.stdout.decode('utf-8').strip()
|
q = tuple(filter(None, cp.stdout.decode('utf-8').strip().splitlines()))[-1]
|
||||||
if not hostname_matches(self.cli_opts.hostname or '', q):
|
if not hostname_matches(self.cli_opts.hostname or '', q):
|
||||||
print(reset_terminal(), end='')
|
print(reset_terminal(), end='')
|
||||||
print(f'The remote hostname {styled(q, fg="green")} does not match the')
|
print(f'The remote hostname {styled(q, fg="green")} does not match the')
|
||||||
@@ -317,7 +317,8 @@ def handle_action(action: str, cli_opts: RemoteFileCLIOptions) -> Result:
|
|||||||
@result_handler()
|
@result_handler()
|
||||||
def handle_result(args: List[str], data: Result, target_window_id: int, boss: BossType) -> None:
|
def handle_result(args: List[str], data: Result, target_window_id: int, boss: BossType) -> None:
|
||||||
if data:
|
if data:
|
||||||
cmd = command_for_open(boss.opts.open_url_with)
|
from kitty.fast_data_types import get_options
|
||||||
|
cmd = command_for_open(get_options().open_url_with)
|
||||||
open_cmd(cmd, data)
|
open_cmd(cmd, data)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class Resize(Handler):
|
|||||||
if key_event.matches('esc'):
|
if key_event.matches('esc'):
|
||||||
self.quit_loop(0)
|
self.quit_loop(0)
|
||||||
return
|
return
|
||||||
if key_event.key in ('w', 'n', 't', 's') and key_event.mods == CTRL:
|
if key_event.key in ('w', 'n', 't', 's') and key_event.mods_without_locks == CTRL:
|
||||||
self.do_window_resize(is_decrease=key_event.key in 'ns', is_horizontal=key_event.key in 'wn', multiplier=2)
|
self.do_window_resize(is_decrease=key_event.key in 'ns', is_horizontal=key_event.key in 'wn', multiplier=2)
|
||||||
|
|
||||||
def on_resize(self, new_size: ScreenSize) -> None:
|
def on_resize(self, new_size: ScreenSize) -> None:
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ import importlib
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import Any, Dict, FrozenSet, List
|
from typing import Any, Dict, FrozenSet, List, TYPE_CHECKING, cast
|
||||||
|
|
||||||
from kitty.types import run_once
|
from kitty.types import run_once
|
||||||
|
|
||||||
aliases = {'url_hints': 'hints'}
|
aliases = {'url_hints': 'hints'}
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from kitty.conf.types import Definition
|
||||||
|
else:
|
||||||
|
Definition = object
|
||||||
|
|
||||||
|
|
||||||
def resolved_kitten(k: str) -> str:
|
def resolved_kitten(k: str) -> str:
|
||||||
@@ -117,7 +121,7 @@ def run_kitten(kitten: str, run_name: str = '__main__') -> None:
|
|||||||
for kitten in all_kitten_names():
|
for kitten in all_kitten_names():
|
||||||
print(kitten, file=sys.stderr)
|
print(kitten, file=sys.stderr)
|
||||||
raise SystemExit('No kitten named {}'.format(original_kitten_name))
|
raise SystemExit('No kitten named {}'.format(original_kitten_name))
|
||||||
m = runpy.run_path(path, init_globals={'sys': sys, 'os': os}, run_name='__run_kitten__') # type: ignore
|
m = runpy.run_path(path, init_globals={'sys': sys, 'os': os}, run_name='__run_kitten__')
|
||||||
m['main'](sys.argv)
|
m['main'](sys.argv)
|
||||||
|
|
||||||
|
|
||||||
@@ -151,12 +155,12 @@ def get_kitten_cli_docs(kitten: str) -> Any:
|
|||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
def get_kitten_conf_docs(kitten: str) -> Any:
|
def get_kitten_conf_docs(kitten: str) -> Definition:
|
||||||
setattr(sys, 'all_options', None)
|
setattr(sys, 'options_definition', None)
|
||||||
run_kitten(kitten, run_name='__conf__')
|
run_kitten(kitten, run_name='__conf__')
|
||||||
ans = getattr(sys, 'all_options')
|
ans = getattr(sys, 'options_definition')
|
||||||
delattr(sys, 'all_options')
|
delattr(sys, 'options_definition')
|
||||||
return ans
|
return cast(Definition, ans)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ rc=$?
|
|||||||
rm $tmp
|
rm $tmp
|
||||||
if [ "$rc" != "0" ]; then echo "$tic_out"; exit 1; fi
|
if [ "$rc" != "0" ]; then echo "$tic_out"; exit 1; fi
|
||||||
if [ -z "$USER" ]; then export USER=$(whoami); fi
|
if [ -z "$USER" ]; then export USER=$(whoami); fi
|
||||||
EXEC_CMD
|
|
||||||
shell_name=$(basename $0)
|
shell_name=$(basename $0)
|
||||||
|
EXEC_CMD
|
||||||
|
|
||||||
# We need to pass the first argument to the executed program with a leading -
|
# We need to pass the first argument to the executed program with a leading -
|
||||||
# to make sure the shell executes as a login shell. Note that not all shells
|
# to make sure the shell executes as a login shell. Note that not all shells
|
||||||
@@ -41,7 +41,6 @@ case "dash" in
|
|||||||
exec $python -c "import os; os.execlp('$0', '-' '$shell_name')"
|
exec $python -c "import os; os.execlp('$0', '-' '$shell_name')"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
exec -a "-$shell_name" "$0"
|
exec -a "-$shell_name" "$0"
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -62,13 +61,13 @@ with NamedTemporaryFile() as tmp:
|
|||||||
getattr(sys.stderr, 'buffer', sys.stderr).write(stdout + stderr)
|
getattr(sys.stderr, 'buffer', sys.stderr).write(stdout + stderr)
|
||||||
raise SystemExit('Failed to compile terminfo using tic')
|
raise SystemExit('Failed to compile terminfo using tic')
|
||||||
command_to_execute = json.loads(binascii.unhexlify('{command_to_execute}'))
|
command_to_execute = json.loads(binascii.unhexlify('{command_to_execute}'))
|
||||||
if command_to_execute:
|
|
||||||
os.execlp(command_to_execute[0], *command_to_execute)
|
|
||||||
try:
|
try:
|
||||||
shell_path = pwd.getpwuid(os.geteuid()).pw_shell or '/bin/sh'
|
shell_path = pwd.getpwuid(os.geteuid()).pw_shell or '/bin/sh'
|
||||||
except KeyError:
|
except KeyError:
|
||||||
shell_path = '/bin/sh'
|
shell_path = '/bin/sh'
|
||||||
shell_name = '-' + os.path.basename(shell_path)
|
shell_name = '-' + os.path.basename(shell_path)
|
||||||
|
if command_to_execute:
|
||||||
|
os.execlp(shell_path, shell_path, '-c', command_to_execute)
|
||||||
os.execlp(shell_path, shell_name)
|
os.execlp(shell_path, shell_name)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -179,11 +178,13 @@ def quote(x: str) -> str:
|
|||||||
|
|
||||||
def get_posix_cmd(terminfo: str, remote_args: List[str]) -> List[str]:
|
def get_posix_cmd(terminfo: str, remote_args: List[str]) -> List[str]:
|
||||||
sh_script = SHELL_SCRIPT.replace('TERMINFO', terminfo, 1)
|
sh_script = SHELL_SCRIPT.replace('TERMINFO', terminfo, 1)
|
||||||
|
command_to_execute = ''
|
||||||
if remote_args:
|
if remote_args:
|
||||||
command_to_executeg = (quote(c) for c in remote_args)
|
# ssh simply concatenates multiple commands using a space see
|
||||||
command_to_execute = 'exec ' + ' '.join(command_to_executeg)
|
# line 1129 of ssh.c and on the remote side sshd.c runs the
|
||||||
else:
|
# concatenated command as shell -c cmd
|
||||||
command_to_execute = ''
|
args = [c.replace("'", """'"'"'""") for c in remote_args]
|
||||||
|
command_to_execute = "exec $0 -c '{}'".format(' '.join(args))
|
||||||
sh_script = sh_script.replace('EXEC_CMD', command_to_execute)
|
sh_script = sh_script.replace('EXEC_CMD', command_to_execute)
|
||||||
return [sh_script] + remote_args
|
return [sh_script] + remote_args
|
||||||
|
|
||||||
@@ -192,7 +193,7 @@ def get_python_cmd(terminfo: str, command_to_execute: List[str]) -> List[str]:
|
|||||||
import json
|
import json
|
||||||
script = PYTHON_SCRIPT.format(
|
script = PYTHON_SCRIPT.format(
|
||||||
terminfo=terminfo.encode('utf-8').hex(),
|
terminfo=terminfo.encode('utf-8').hex(),
|
||||||
command_to_execute=json.dumps(command_to_execute).encode('utf-8').hex()
|
command_to_execute=json.dumps(' '.join(command_to_execute)).encode('utf-8').hex()
|
||||||
)
|
)
|
||||||
return [f'python -c "{script}"']
|
return [f'python -c "{script}"']
|
||||||
|
|
||||||
@@ -210,7 +211,7 @@ def main(args: List[str]) -> NoReturn:
|
|||||||
else:
|
else:
|
||||||
hostname, remote_args = server_args[0], server_args[1:]
|
hostname, remote_args = server_args[0], server_args[1:]
|
||||||
cmd += ['-t', hostname]
|
cmd += ['-t', hostname]
|
||||||
terminfo = subprocess.check_output(['infocmp']).decode('utf-8')
|
terminfo = subprocess.check_output(['infocmp', '-a']).decode('utf-8')
|
||||||
f = get_posix_cmd if use_posix else get_python_cmd
|
f = get_posix_cmd if use_posix else get_python_cmd
|
||||||
cmd += f(terminfo, remote_args)
|
cmd += f(terminfo, remote_args)
|
||||||
os.execvp('ssh', cmd)
|
os.execvp('ssh', cmd)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from typing import (
|
|||||||
from kitty.types import ParsedShortcut
|
from kitty.types import ParsedShortcut
|
||||||
from kitty.typing import (
|
from kitty.typing import (
|
||||||
AbstractEventLoop, BossType, Debug, ImageManagerType, KeyEventType,
|
AbstractEventLoop, BossType, Debug, ImageManagerType, KeyEventType,
|
||||||
KittensKeyActionType, LoopType, MouseEvent, ScreenSize, TermManagerType
|
KeyActionType, LoopType, MouseEvent, ScreenSize, TermManagerType
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -46,15 +46,15 @@ class Handler:
|
|||||||
def asyncio_loop(self) -> AbstractEventLoop:
|
def asyncio_loop(self) -> AbstractEventLoop:
|
||||||
return self._tui_loop.asycio_loop
|
return self._tui_loop.asycio_loop
|
||||||
|
|
||||||
def add_shortcut(self, action: KittensKeyActionType, spec: Union[str, ParsedShortcut]) -> None:
|
def add_shortcut(self, action: KeyActionType, spec: Union[str, ParsedShortcut]) -> None:
|
||||||
if not hasattr(self, '_key_shortcuts'):
|
if not hasattr(self, '_key_shortcuts'):
|
||||||
self._key_shortcuts: Dict[ParsedShortcut, KittensKeyActionType] = {}
|
self._key_shortcuts: Dict[ParsedShortcut, KeyActionType] = {}
|
||||||
if isinstance(spec, str):
|
if isinstance(spec, str):
|
||||||
from kitty.key_encoding import parse_shortcut
|
from kitty.key_encoding import parse_shortcut
|
||||||
spec = parse_shortcut(spec)
|
spec = parse_shortcut(spec)
|
||||||
self._key_shortcuts[spec] = action
|
self._key_shortcuts[spec] = action
|
||||||
|
|
||||||
def shortcut_action(self, key_event: KeyEventType) -> Optional[KittensKeyActionType]:
|
def shortcut_action(self, key_event: KeyEventType) -> Optional[KeyActionType]:
|
||||||
for sc, action in self._key_shortcuts.items():
|
for sc, action in self._key_shortcuts.items():
|
||||||
if key_event.matches(sc):
|
if key_event.matches(sc):
|
||||||
return action
|
return action
|
||||||
|
|||||||
@@ -15,13 +15,12 @@ from typing import (
|
|||||||
Sequence, Tuple, Union
|
Sequence, Tuple, Union
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from kitty.conf.utils import positive_float, positive_int
|
||||||
from kitty.fast_data_types import create_canvas
|
from kitty.fast_data_types import create_canvas
|
||||||
from kitty.typing import (
|
from kitty.typing import (
|
||||||
CompletedProcess, GRT_a, GRT_d, GRT_f, GRT_m, GRT_o, GRT_t, HandlerType
|
CompletedProcess, GRT_a, GRT_d, GRT_f, GRT_m, GRT_o, GRT_t, HandlerType
|
||||||
)
|
)
|
||||||
from kitty.utils import (
|
from kitty.utils import ScreenSize, find_exe, fit_image
|
||||||
ScreenSize, find_exe, fit_image, positive_float, positive_int
|
|
||||||
)
|
|
||||||
|
|
||||||
from .operations import cursor
|
from .operations import cursor
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ class CMD:
|
|||||||
|
|
||||||
def generate_stub() -> None:
|
def generate_stub() -> None:
|
||||||
from kittens.tui.operations import as_type_stub
|
from kittens.tui.operations import as_type_stub
|
||||||
from kitty.conf.definition import save_type_stub
|
from kitty.conf.utils import save_type_stub
|
||||||
text = as_type_stub()
|
text = as_type_stub()
|
||||||
save_type_stub(text, __file__)
|
save_type_stub(text, __file__)
|
||||||
|
|||||||
@@ -416,7 +416,7 @@ class UnicodeInput(Handler):
|
|||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def on_key(self, key_event: KeyEvent) -> None:
|
def on_key(self, key_event: KeyEvent) -> None:
|
||||||
if self.mode is HEX and key_event.type is not EventType.RELEASE and not key_event.mods:
|
if self.mode is HEX and key_event.type is not EventType.RELEASE and not key_event.has_mods:
|
||||||
try:
|
try:
|
||||||
val = int(self.line_edit.current_input, 16)
|
val = int(self.line_edit.current_input, 16)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -434,7 +434,7 @@ class UnicodeInput(Handler):
|
|||||||
self.line_edit.current_input = hex(val - 1)[2:]
|
self.line_edit.current_input = hex(val - 1)[2:]
|
||||||
self.refresh()
|
self.refresh()
|
||||||
return
|
return
|
||||||
if self.mode is NAME and key_event.type is not EventType.RELEASE and not key_event.mods:
|
if self.mode is NAME and key_event.type is not EventType.RELEASE and not key_event.has_mods:
|
||||||
if key_event.matches('shift+tab'):
|
if key_event.matches('shift+tab'):
|
||||||
self.table.move_current(cols=-1)
|
self.table.move_current(cols=-1)
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ from itertools import chain
|
|||||||
from typing import Sequence, Tuple
|
from typing import Sequence, Tuple
|
||||||
|
|
||||||
from .fast_data_types import (
|
from .fast_data_types import (
|
||||||
BORDERS_PROGRAM, add_borders_rect, compile_program, init_borders_program,
|
BORDERS_PROGRAM, add_borders_rect, compile_program, get_options,
|
||||||
os_window_has_background_image
|
init_borders_program, os_window_has_background_image
|
||||||
)
|
)
|
||||||
from .options_stub import Options
|
|
||||||
from .typing import LayoutType
|
from .typing import LayoutType
|
||||||
from .utils import load_shaders
|
from .utils import load_shaders
|
||||||
from .window_list import WindowGroup, WindowList
|
from .window_list import WindowGroup, WindowList
|
||||||
@@ -65,11 +64,9 @@ def load_borders_program() -> None:
|
|||||||
|
|
||||||
class Borders:
|
class Borders:
|
||||||
|
|
||||||
def __init__(self, os_window_id: int, tab_id: int, opts: Options):
|
def __init__(self, os_window_id: int, tab_id: int):
|
||||||
self.os_window_id = os_window_id
|
self.os_window_id = os_window_id
|
||||||
self.tab_id = tab_id
|
self.tab_id = tab_id
|
||||||
self.draw_active_borders = opts.active_border_color is not None
|
|
||||||
self.draw_minimal_borders = opts.draw_minimal_borders and max(opts.window_margin_width) < 1
|
|
||||||
|
|
||||||
def __call__(
|
def __call__(
|
||||||
self,
|
self,
|
||||||
@@ -78,6 +75,9 @@ class Borders:
|
|||||||
extra_blank_rects: Sequence[Tuple[int, int, int, int]],
|
extra_blank_rects: Sequence[Tuple[int, int, int, int]],
|
||||||
draw_window_borders: bool = True,
|
draw_window_borders: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
opts = get_options()
|
||||||
|
draw_active_borders = opts.active_border_color is not None
|
||||||
|
draw_minimal_borders = opts.draw_minimal_borders and max(opts.window_margin_width) < 1
|
||||||
add_borders_rect(self.os_window_id, self.tab_id, 0, 0, 0, 0, BorderColor.default_bg)
|
add_borders_rect(self.os_window_id, self.tab_id, 0, 0, 0, 0, BorderColor.default_bg)
|
||||||
has_background_image = os_window_has_background_image(self.os_window_id)
|
has_background_image = os_window_has_background_image(self.os_window_id)
|
||||||
if not has_background_image:
|
if not has_background_image:
|
||||||
@@ -94,9 +94,9 @@ class Borders:
|
|||||||
for i, wg in enumerate(groups):
|
for i, wg in enumerate(groups):
|
||||||
window_bg = wg.default_bg
|
window_bg = wg.default_bg
|
||||||
window_bg = (window_bg << 8) | BorderColor.window_bg
|
window_bg = (window_bg << 8) | BorderColor.window_bg
|
||||||
if draw_borders and not self.draw_minimal_borders:
|
if draw_borders and not draw_minimal_borders:
|
||||||
# Draw the border rectangles
|
# Draw the border rectangles
|
||||||
if wg is active_group and self.draw_active_borders:
|
if wg is active_group and draw_active_borders:
|
||||||
color = BorderColor.active
|
color = BorderColor.active
|
||||||
else:
|
else:
|
||||||
color = BorderColor.bell if wg.needs_attention else BorderColor.inactive
|
color = BorderColor.bell if wg.needs_attention else BorderColor.inactive
|
||||||
@@ -106,7 +106,7 @@ class Borders:
|
|||||||
colors = window_bg, window_bg, window_bg, window_bg
|
colors = window_bg, window_bg, window_bg, window_bg
|
||||||
draw_edges(self.os_window_id, self.tab_id, colors, wg)
|
draw_edges(self.os_window_id, self.tab_id, colors, wg)
|
||||||
|
|
||||||
if self.draw_minimal_borders:
|
if draw_minimal_borders:
|
||||||
for border_line in current_layout.minimal_borders(all_windows):
|
for border_line in current_layout.get_minimal_borders(all_windows):
|
||||||
left, top, right, bottom = border_line.edges
|
left, top, right, bottom = border_line.edges
|
||||||
add_borders_rect(self.os_window_id, self.tab_id, left, top, right, bottom, border_line.color)
|
add_borders_rect(self.os_window_id, self.tab_id, left, top, right, bottom, border_line.color)
|
||||||
|
|||||||
235
kitty/boss.py
235
kitty/boss.py
@@ -15,33 +15,34 @@ from typing import (
|
|||||||
)
|
)
|
||||||
from weakref import WeakValueDictionary
|
from weakref import WeakValueDictionary
|
||||||
|
|
||||||
from .child import cached_process_data, cwd_of_process
|
from .child import (
|
||||||
|
cached_process_data, cwd_of_process, default_env, set_default_env
|
||||||
|
)
|
||||||
from .cli import create_opts, parse_args
|
from .cli import create_opts, parse_args
|
||||||
from .cli_stub import CLIOptions
|
from .cli_stub import CLIOptions
|
||||||
from .conf.utils import BadLine, to_cmdline
|
from .conf.utils import BadLine, KeyAction, to_cmdline
|
||||||
from .config import (
|
from .config import common_opts_as_dict, prepare_config_file_for_editing
|
||||||
KeyAction, SubSequenceMap, common_opts_as_dict,
|
|
||||||
prepare_config_file_for_editing
|
|
||||||
)
|
|
||||||
from .config_data import MINIMUM_FONT_SIZE
|
|
||||||
from .constants import (
|
from .constants import (
|
||||||
appname, config_dir, is_macos, kitty_exe, supports_primary_selection
|
appname, config_dir, is_macos, is_wayland, kitty_exe,
|
||||||
|
supports_primary_selection
|
||||||
)
|
)
|
||||||
from .fast_data_types import (
|
from .fast_data_types import (
|
||||||
CLOSE_BEING_CONFIRMED, IMPERATIVE_CLOSE_REQUESTED, NO_CLOSE_REQUESTED,
|
CLOSE_BEING_CONFIRMED, IMPERATIVE_CLOSE_REQUESTED, NO_CLOSE_REQUESTED,
|
||||||
ChildMonitor, KeyEvent, add_timer, background_opacity_of,
|
ChildMonitor, KeyEvent, add_timer, apply_options_update,
|
||||||
change_background_opacity, change_os_window_state, cocoa_set_menubar_title,
|
background_opacity_of, change_background_opacity, change_os_window_state,
|
||||||
create_os_window, current_application_quit_request, current_os_window,
|
cocoa_set_menubar_title, create_os_window,
|
||||||
destroy_global_data, focus_os_window, get_clipboard_string,
|
current_application_quit_request, current_os_window, destroy_global_data,
|
||||||
global_font_size, mark_os_window_for_close, os_window_font_size,
|
focus_os_window, get_clipboard_string, get_options, global_font_size,
|
||||||
patch_global_colors, safe_pipe, set_application_quit_request,
|
mark_os_window_for_close, os_window_font_size, patch_global_colors,
|
||||||
set_background_image, set_boss, set_clipboard_string, set_in_sequence_mode,
|
safe_pipe, set_application_quit_request, set_background_image, set_boss,
|
||||||
thread_write, toggle_fullscreen, toggle_maximized
|
set_clipboard_string, set_in_sequence_mode, set_options, thread_write,
|
||||||
|
toggle_fullscreen, toggle_maximized
|
||||||
)
|
)
|
||||||
from .keys import get_shortcut, shortcut_matches
|
from .keys import get_shortcut, shortcut_matches
|
||||||
from .layout.base import set_layout_options
|
from .layout.base import set_layout_options
|
||||||
from .notify import notification_activated
|
from .notify import notification_activated
|
||||||
from .options_stub import Options
|
from .options.types import Options
|
||||||
|
from .options.utils import MINIMUM_FONT_SIZE, SubSequenceMap
|
||||||
from .os_window_size import initial_window_size_func
|
from .os_window_size import initial_window_size_func
|
||||||
from .rgb import Color, color_from_int
|
from .rgb import Color, color_from_int
|
||||||
from .session import Session, create_sessions, get_os_window_sizing_data
|
from .session import Session, create_sessions, get_os_window_sizing_data
|
||||||
@@ -53,8 +54,8 @@ from .typing import PopenType, TypedDict
|
|||||||
from .utils import (
|
from .utils import (
|
||||||
func_name, get_editor, get_primary_selection, is_path_in_temp_dir,
|
func_name, get_editor, get_primary_selection, is_path_in_temp_dir,
|
||||||
log_error, open_url, parse_address_spec, parse_uri_list,
|
log_error, open_url, parse_address_spec, parse_uri_list,
|
||||||
platform_window_id, remove_socket_file, safe_print, set_primary_selection,
|
platform_window_id, read_shell_environment, remove_socket_file, safe_print,
|
||||||
single_instance, startup_notification_handler
|
set_primary_selection, single_instance, startup_notification_handler
|
||||||
)
|
)
|
||||||
from .window import MatchPatternType, Window
|
from .window import MatchPatternType, Window
|
||||||
|
|
||||||
@@ -167,19 +168,23 @@ class Boss:
|
|||||||
talk_fd, listen_fd
|
talk_fd, listen_fd
|
||||||
)
|
)
|
||||||
set_boss(self)
|
set_boss(self)
|
||||||
self.opts, self.args = opts, args
|
self.args = args
|
||||||
self.keymap = self.opts.keymap.copy()
|
|
||||||
self.global_shortcuts_map = {v: KeyAction(k) for k, v in global_shortcuts.items()}
|
self.global_shortcuts_map = {v: KeyAction(k) for k, v in global_shortcuts.items()}
|
||||||
for sc in global_shortcuts.values():
|
self.global_shortcuts = global_shortcuts
|
||||||
self.keymap.pop(sc, None)
|
self.update_keymap()
|
||||||
if is_macos:
|
if is_macos:
|
||||||
from .fast_data_types import (
|
from .fast_data_types import (
|
||||||
cocoa_set_notification_activated_callback
|
cocoa_set_notification_activated_callback
|
||||||
)
|
)
|
||||||
cocoa_set_notification_activated_callback(notification_activated)
|
cocoa_set_notification_activated_callback(notification_activated)
|
||||||
|
|
||||||
|
def update_keymap(self) -> None:
|
||||||
|
self.keymap = get_options().keymap.copy()
|
||||||
|
for sc in self.global_shortcuts.values():
|
||||||
|
self.keymap.pop(sc, None)
|
||||||
|
|
||||||
def startup_first_child(self, os_window_id: Optional[int]) -> None:
|
def startup_first_child(self, os_window_id: Optional[int]) -> None:
|
||||||
startup_sessions = create_sessions(self.opts, self.args, default_session=self.opts.startup_session)
|
startup_sessions = create_sessions(get_options(), self.args, default_session=get_options().startup_session)
|
||||||
for startup_session in startup_sessions:
|
for startup_session in startup_sessions:
|
||||||
self.add_os_window(startup_session, os_window_id=os_window_id)
|
self.add_os_window(startup_session, os_window_id=os_window_id)
|
||||||
os_window_id = None
|
os_window_id = None
|
||||||
@@ -199,7 +204,7 @@ class Boss:
|
|||||||
startup_id: Optional[str] = None
|
startup_id: Optional[str] = None
|
||||||
) -> int:
|
) -> int:
|
||||||
if os_window_id is None:
|
if os_window_id is None:
|
||||||
size_data = get_os_window_sizing_data(opts_for_size or self.opts, startup_session)
|
size_data = get_os_window_sizing_data(opts_for_size or get_options(), startup_session)
|
||||||
wclass = wclass or getattr(startup_session, 'os_window_class', None) or self.args.cls or appname
|
wclass = wclass or getattr(startup_session, 'os_window_class', None) or self.args.cls or appname
|
||||||
wname = wname or self.args.name or wclass
|
wname = wname or self.args.name or wclass
|
||||||
with startup_notification_handler(do_notify=startup_id is not None, startup_id=startup_id) as pre_show_callback:
|
with startup_notification_handler(do_notify=startup_id is not None, startup_id=startup_id) as pre_show_callback:
|
||||||
@@ -210,7 +215,7 @@ class Boss:
|
|||||||
else:
|
else:
|
||||||
wname = self.args.name or self.args.cls or appname
|
wname = self.args.name or self.args.cls or appname
|
||||||
wclass = self.args.cls or appname
|
wclass = self.args.cls or appname
|
||||||
tm = TabManager(os_window_id, self.opts, self.args, wclass, wname, startup_session)
|
tm = TabManager(os_window_id, self.args, wclass, wname, startup_session)
|
||||||
self.os_window_map[os_window_id] = tm
|
self.os_window_map[os_window_id] = tm
|
||||||
return os_window_id
|
return os_window_id
|
||||||
|
|
||||||
@@ -294,6 +299,12 @@ class Boss:
|
|||||||
if q:
|
if q:
|
||||||
found = True
|
found = True
|
||||||
yield q
|
yield q
|
||||||
|
elif field == 'index':
|
||||||
|
tm = self.active_tab_manager
|
||||||
|
if tm is not None and len(tm.tabs) > 0:
|
||||||
|
idx = (int(pat.pattern) + len(tm.tabs)) % len(tm.tabs)
|
||||||
|
found = True
|
||||||
|
yield tm.tabs[idx]
|
||||||
if not found:
|
if not found:
|
||||||
tabs = {self.tab_for_window(w) for w in self.match_windows(match)}
|
tabs = {self.tab_for_window(w) for w in self.match_windows(match)}
|
||||||
for q in tabs:
|
for q in tabs:
|
||||||
@@ -317,7 +328,7 @@ class Boss:
|
|||||||
sw: Optional[SpecialWindowInstance] = args
|
sw: Optional[SpecialWindowInstance] = args
|
||||||
else:
|
else:
|
||||||
sw = self.args_to_special_window(args, cwd_from) if args else None
|
sw = self.args_to_special_window(args, cwd_from) if args else None
|
||||||
startup_session = next(create_sessions(self.opts, special_window=sw, cwd_from=cwd_from))
|
startup_session = next(create_sessions(get_options(), special_window=sw, cwd_from=cwd_from))
|
||||||
return self.add_os_window(startup_session)
|
return self.add_os_window(startup_session)
|
||||||
|
|
||||||
def new_os_window(self, *args: str) -> None:
|
def new_os_window(self, *args: str) -> None:
|
||||||
@@ -347,7 +358,7 @@ class Boss:
|
|||||||
from .remote_control import handle_cmd
|
from .remote_control import handle_cmd
|
||||||
response = None
|
response = None
|
||||||
window = window or None
|
window = window or None
|
||||||
if self.opts.allow_remote_control == 'y' or from_peer or getattr(window, 'allow_remote_control', False):
|
if get_options().allow_remote_control == 'y' or from_peer or getattr(window, 'allow_remote_control', False):
|
||||||
try:
|
try:
|
||||||
response = handle_cmd(self, window, cmd)
|
response = handle_cmd(self, window, cmd)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
@@ -411,7 +422,7 @@ class Boss:
|
|||||||
args.directory = os.path.join(data['cwd'], args.directory)
|
args.directory = os.path.join(data['cwd'], args.directory)
|
||||||
for session in create_sessions(opts, args, respect_cwd=True):
|
for session in create_sessions(opts, args, respect_cwd=True):
|
||||||
os_window_id = self.add_os_window(session, wclass=args.cls, wname=args.name, opts_for_size=opts, startup_id=startup_id)
|
os_window_id = self.add_os_window(session, wclass=args.cls, wname=args.name, opts_for_size=opts, startup_id=startup_id)
|
||||||
if opts.background_opacity != self.opts.background_opacity:
|
if opts.background_opacity != get_options().background_opacity:
|
||||||
self._set_os_window_background_opacity(os_window_id, opts.background_opacity)
|
self._set_os_window_background_opacity(os_window_id, opts.background_opacity)
|
||||||
if data.get('notify_on_os_window_death'):
|
if data.get('notify_on_os_window_death'):
|
||||||
self.os_window_death_actions[os_window_id] = partial(self.notify_on_os_window_death, data['notify_on_os_window_death'])
|
self.os_window_death_actions[os_window_id] = partial(self.notify_on_os_window_death, data['notify_on_os_window_death'])
|
||||||
@@ -484,7 +495,7 @@ class Boss:
|
|||||||
|
|
||||||
def confirm_tab_close(self, tab: Tab) -> None:
|
def confirm_tab_close(self, tab: Tab) -> None:
|
||||||
windows = tuple(tab)
|
windows = tuple(tab)
|
||||||
needs_confirmation = self.opts.confirm_os_window_close > 0 and len(windows) >= self.opts.confirm_os_window_close
|
needs_confirmation = get_options().confirm_os_window_close > 0 and len(windows) >= get_options().confirm_os_window_close
|
||||||
if not needs_confirmation:
|
if not needs_confirmation:
|
||||||
self.close_tab_no_confirm(tab)
|
self.close_tab_no_confirm(tab)
|
||||||
return
|
return
|
||||||
@@ -521,9 +532,9 @@ class Boss:
|
|||||||
self.io_thread_started = True
|
self.io_thread_started = True
|
||||||
self.startup_first_child(first_os_window_id)
|
self.startup_first_child(first_os_window_id)
|
||||||
|
|
||||||
if self.opts.update_check_interval > 0 and not hasattr(self, 'update_check_started'):
|
if get_options().update_check_interval > 0 and not hasattr(self, 'update_check_started'):
|
||||||
from .update_check import run_update_check
|
from .update_check import run_update_check
|
||||||
run_update_check(self.opts.update_check_interval * 60 * 60)
|
run_update_check(get_options().update_check_interval * 60 * 60)
|
||||||
self.update_check_started = True
|
self.update_check_started = True
|
||||||
|
|
||||||
def activate_tab_at(self, os_window_id: int, x: int, is_double: bool = False) -> int:
|
def activate_tab_at(self, os_window_id: int, x: int, is_double: bool = False) -> int:
|
||||||
@@ -561,14 +572,14 @@ class Boss:
|
|||||||
|
|
||||||
def increase_font_size(self) -> None: # legacy
|
def increase_font_size(self) -> None: # legacy
|
||||||
cfs = global_font_size()
|
cfs = global_font_size()
|
||||||
self.set_font_size(min(self.opts.font_size * 5, cfs + 2.0))
|
self.set_font_size(min(get_options().font_size * 5, cfs + 2.0))
|
||||||
|
|
||||||
def decrease_font_size(self) -> None: # legacy
|
def decrease_font_size(self) -> None: # legacy
|
||||||
cfs = global_font_size()
|
cfs = global_font_size()
|
||||||
self.set_font_size(max(MINIMUM_FONT_SIZE, cfs - 2.0))
|
self.set_font_size(max(MINIMUM_FONT_SIZE, cfs - 2.0))
|
||||||
|
|
||||||
def restore_font_size(self) -> None: # legacy
|
def restore_font_size(self) -> None: # legacy
|
||||||
self.set_font_size(self.opts.font_size)
|
self.set_font_size(get_options().font_size)
|
||||||
|
|
||||||
def set_font_size(self, new_size: float) -> None: # legacy
|
def set_font_size(self, new_size: float) -> None: # legacy
|
||||||
self.change_font_size(True, None, new_size)
|
self.change_font_size(True, None, new_size)
|
||||||
@@ -577,13 +588,13 @@ class Boss:
|
|||||||
def calc_new_size(old_size: float) -> float:
|
def calc_new_size(old_size: float) -> float:
|
||||||
new_size = old_size
|
new_size = old_size
|
||||||
if amt == 0:
|
if amt == 0:
|
||||||
new_size = self.opts.font_size
|
new_size = get_options().font_size
|
||||||
else:
|
else:
|
||||||
if increment_operation:
|
if increment_operation:
|
||||||
new_size += (1 if increment_operation == '+' else -1) * amt
|
new_size += (1 if increment_operation == '+' else -1) * amt
|
||||||
else:
|
else:
|
||||||
new_size = amt
|
new_size = amt
|
||||||
new_size = max(MINIMUM_FONT_SIZE, min(new_size, self.opts.font_size * 5))
|
new_size = max(MINIMUM_FONT_SIZE, min(new_size, get_options().font_size * 5))
|
||||||
return new_size
|
return new_size
|
||||||
|
|
||||||
if all_windows:
|
if all_windows:
|
||||||
@@ -633,7 +644,7 @@ class Boss:
|
|||||||
window = self.active_window
|
window = self.active_window
|
||||||
if window is None or not opacity:
|
if window is None or not opacity:
|
||||||
return
|
return
|
||||||
if not self.opts.dynamic_background_opacity:
|
if not get_options().dynamic_background_opacity:
|
||||||
self.show_error(
|
self.show_error(
|
||||||
_('Cannot change background opacity'),
|
_('Cannot change background opacity'),
|
||||||
_('You must set the dynamic_background_opacity option in kitty.conf to be able to change background opacity'))
|
_('You must set the dynamic_background_opacity option in kitty.conf to be able to change background opacity'))
|
||||||
@@ -645,7 +656,7 @@ class Boss:
|
|||||||
return
|
return
|
||||||
fin_opacity = old_opacity + float(opacity)
|
fin_opacity = old_opacity + float(opacity)
|
||||||
elif opacity == 'default':
|
elif opacity == 'default':
|
||||||
fin_opacity = self.opts.background_opacity
|
fin_opacity = get_options().background_opacity
|
||||||
else:
|
else:
|
||||||
fin_opacity = float(opacity)
|
fin_opacity = float(opacity)
|
||||||
self._set_os_window_background_opacity(os_window_id, fin_opacity)
|
self._set_os_window_background_opacity(os_window_id, fin_opacity)
|
||||||
@@ -672,7 +683,7 @@ class Boss:
|
|||||||
# Handles shortcuts, return True if the key was consumed
|
# Handles shortcuts, return True if the key was consumed
|
||||||
key_action = get_shortcut(self.keymap, ev)
|
key_action = get_shortcut(self.keymap, ev)
|
||||||
if key_action is None:
|
if key_action is None:
|
||||||
sequences = get_shortcut(self.opts.sequence_map, ev)
|
sequences = get_shortcut(get_options().sequence_map, ev)
|
||||||
if sequences and not isinstance(sequences, KeyAction):
|
if sequences and not isinstance(sequences, KeyAction):
|
||||||
self.pending_sequences = sequences
|
self.pending_sequences = sequences
|
||||||
set_in_sequence_mode(True)
|
set_in_sequence_mode(True)
|
||||||
@@ -710,8 +721,8 @@ class Boss:
|
|||||||
if w is None:
|
if w is None:
|
||||||
return
|
return
|
||||||
overlay_window = self._run_kitten('resize_window', args=[
|
overlay_window = self._run_kitten('resize_window', args=[
|
||||||
'--horizontal-increment={}'.format(self.opts.window_resize_step_cells),
|
'--horizontal-increment={}'.format(get_options().window_resize_step_cells),
|
||||||
'--vertical-increment={}'.format(self.opts.window_resize_step_lines)
|
'--vertical-increment={}'.format(get_options().window_resize_step_lines)
|
||||||
])
|
])
|
||||||
if overlay_window is not None:
|
if overlay_window is not None:
|
||||||
overlay_window.allow_remote_control = True
|
overlay_window.allow_remote_control = True
|
||||||
@@ -736,27 +747,37 @@ class Boss:
|
|||||||
if t is not None:
|
if t is not None:
|
||||||
t.relayout_borders()
|
t.relayout_borders()
|
||||||
|
|
||||||
def dispatch_action(self, key_action: KeyAction) -> bool:
|
def dispatch_action(
|
||||||
|
self,
|
||||||
|
key_action: KeyAction,
|
||||||
|
window_for_dispatch: Optional[Window] = None,
|
||||||
|
dispatch_type: str = 'KeyPress'
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
|
def report_match(f: Callable) -> None:
|
||||||
|
if self.args.debug_keyboard:
|
||||||
|
print(f'\x1b[35m{dispatch_type}\x1b[m matched action:', func_name(f), flush=True)
|
||||||
|
|
||||||
if key_action is not None:
|
if key_action is not None:
|
||||||
f = getattr(self, key_action.func, None)
|
f = getattr(self, key_action.func, None)
|
||||||
if f is not None:
|
if f is not None:
|
||||||
if self.args.debug_keyboard:
|
report_match(f)
|
||||||
print('Keypress matched action:', func_name(f))
|
|
||||||
passthrough = f(*key_action.args)
|
passthrough = f(*key_action.args)
|
||||||
if passthrough is not True:
|
if passthrough is not True:
|
||||||
return True
|
return True
|
||||||
tab = self.active_tab
|
if window_for_dispatch is None:
|
||||||
if tab is None:
|
tab = self.active_tab
|
||||||
return False
|
window = self.active_window
|
||||||
window = self.active_window
|
else:
|
||||||
if window is None:
|
window = window_for_dispatch
|
||||||
|
tab = window.tabref()
|
||||||
|
if tab is None or window is None:
|
||||||
return False
|
return False
|
||||||
if key_action is not None:
|
if key_action is not None:
|
||||||
f = getattr(tab, key_action.func, getattr(window, key_action.func, None))
|
f = getattr(tab, key_action.func, getattr(window, key_action.func, None))
|
||||||
if f is not None:
|
if f is not None:
|
||||||
passthrough = f(*key_action.args)
|
passthrough = f(*key_action.args)
|
||||||
if self.args.debug_keyboard:
|
report_match(f)
|
||||||
print('Keypress matched action:', func_name(f))
|
|
||||||
if passthrough is not True:
|
if passthrough is not True:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -803,7 +824,7 @@ class Boss:
|
|||||||
|
|
||||||
def confirm_os_window_close(self, os_window_id: int) -> None:
|
def confirm_os_window_close(self, os_window_id: int) -> None:
|
||||||
tm = self.os_window_map.get(os_window_id)
|
tm = self.os_window_map.get(os_window_id)
|
||||||
needs_confirmation = tm is not None and self.opts.confirm_os_window_close > 0 and tm.number_of_windows >= self.opts.confirm_os_window_close
|
needs_confirmation = tm is not None and get_options().confirm_os_window_close > 0 and tm.number_of_windows >= get_options().confirm_os_window_close
|
||||||
if not needs_confirmation:
|
if not needs_confirmation:
|
||||||
mark_os_window_for_close(os_window_id)
|
mark_os_window_for_close(os_window_id)
|
||||||
return
|
return
|
||||||
@@ -840,7 +861,7 @@ class Boss:
|
|||||||
num = 0
|
num = 0
|
||||||
for q in self.os_window_map.values():
|
for q in self.os_window_map.values():
|
||||||
num += q.number_of_windows
|
num += q.number_of_windows
|
||||||
needs_confirmation = tm is not None and self.opts.confirm_os_window_close > 0 and num >= self.opts.confirm_os_window_close
|
needs_confirmation = tm is not None and get_options().confirm_os_window_close > 0 and num >= get_options().confirm_os_window_close
|
||||||
if not needs_confirmation:
|
if not needs_confirmation:
|
||||||
set_application_quit_request(IMPERATIVE_CLOSE_REQUESTED)
|
set_application_quit_request(IMPERATIVE_CLOSE_REQUESTED)
|
||||||
return
|
return
|
||||||
@@ -867,11 +888,29 @@ class Boss:
|
|||||||
s.shutdown(socket.SHUT_RDWR)
|
s.shutdown(socket.SHUT_RDWR)
|
||||||
s.close()
|
s.close()
|
||||||
|
|
||||||
def display_scrollback(self, window: Window, data: Optional[bytes], cmd: List[str]) -> None:
|
def display_scrollback(self, window: Window, data: Union[bytes, str], input_line_number: int = 0, title: str = '') -> None:
|
||||||
|
def prepare_arg(x: str) -> str:
|
||||||
|
x = x.replace('INPUT_LINE_NUMBER', str(input_line_number))
|
||||||
|
x = x.replace('CURSOR_LINE', str(window.screen.cursor.y + 1))
|
||||||
|
x = x.replace('CURSOR_COLUMN', str(window.screen.cursor.x + 1))
|
||||||
|
return x
|
||||||
|
|
||||||
|
cmd = list(map(prepare_arg, get_options().scrollback_pager))
|
||||||
|
if not os.path.isabs(cmd[0]):
|
||||||
|
import shutil
|
||||||
|
exe = shutil.which(cmd[0])
|
||||||
|
if not exe:
|
||||||
|
env = read_shell_environment(get_options())
|
||||||
|
if env and 'PATH' in env:
|
||||||
|
exe = shutil.which(cmd[0], path=env['PATH'])
|
||||||
|
if exe:
|
||||||
|
cmd[0] = exe
|
||||||
|
|
||||||
tab = self.active_tab
|
tab = self.active_tab
|
||||||
if tab is not None:
|
if tab is not None:
|
||||||
|
bdata = data.encode('utf-8') if isinstance(data, str) else data
|
||||||
tab.new_special_window(
|
tab.new_special_window(
|
||||||
SpecialWindow(cmd, data, _('History'), overlay_for=window.id, cwd=window.cwd_of_child),
|
SpecialWindow(cmd, bdata, title or _('History'), overlay_for=window.id, cwd=window.cwd_of_child),
|
||||||
copy_colors_from=self.active_window
|
copy_colors_from=self.active_window
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -879,7 +918,7 @@ class Boss:
|
|||||||
confpath = prepare_config_file_for_editing()
|
confpath = prepare_config_file_for_editing()
|
||||||
# On macOS vim fails to handle SIGWINCH if it occurs early, so add a
|
# On macOS vim fails to handle SIGWINCH if it occurs early, so add a
|
||||||
# small delay.
|
# small delay.
|
||||||
cmd = [kitty_exe(), '+runpy', 'import os, sys, time; time.sleep(0.05); os.execvp(sys.argv[1], sys.argv[1:])'] + get_editor(self.opts) + [confpath]
|
cmd = [kitty_exe(), '+runpy', 'import os, sys, time; time.sleep(0.05); os.execvp(sys.argv[1], sys.argv[1:])'] + get_editor(get_options()) + [confpath]
|
||||||
self.new_os_window(*cmd)
|
self.new_os_window(*cmd)
|
||||||
|
|
||||||
def get_output(self, source_window: Window, num_lines: Optional[int] = 1) -> str:
|
def get_output(self, source_window: Window, num_lines: Optional[int] = 1) -> str:
|
||||||
@@ -931,7 +970,7 @@ class Boss:
|
|||||||
raise ValueError('Unknown type_of_input: {}'.format(type_of_input))
|
raise ValueError('Unknown type_of_input: {}'.format(type_of_input))
|
||||||
else:
|
else:
|
||||||
data = input_data if isinstance(input_data, bytes) else input_data.encode('utf-8')
|
data = input_data if isinstance(input_data, bytes) else input_data.encode('utf-8')
|
||||||
copts = common_opts_as_dict(self.opts)
|
copts = common_opts_as_dict(get_options())
|
||||||
final_args: List[str] = []
|
final_args: List[str] = []
|
||||||
for x in args:
|
for x in args:
|
||||||
if x == '@selection':
|
if x == '@selection':
|
||||||
@@ -1071,7 +1110,7 @@ class Boss:
|
|||||||
if actions:
|
if actions:
|
||||||
self.drain_actions(actions)
|
self.drain_actions(actions)
|
||||||
if not found_action:
|
if not found_action:
|
||||||
open_url(url, program or self.opts.open_url_with, cwd=cwd)
|
open_url(url, program or get_options().open_url_with, cwd=cwd)
|
||||||
|
|
||||||
def drain_actions(self, actions: List) -> None:
|
def drain_actions(self, actions: List) -> None:
|
||||||
|
|
||||||
@@ -1102,8 +1141,14 @@ class Boss:
|
|||||||
text = get_clipboard_string()
|
text = get_clipboard_string()
|
||||||
self.paste_to_active_window(text)
|
self.paste_to_active_window(text)
|
||||||
|
|
||||||
|
def current_primary_selection(self) -> str:
|
||||||
|
return get_primary_selection() if supports_primary_selection else ''
|
||||||
|
|
||||||
|
def current_primary_selection_or_clipboard(self) -> str:
|
||||||
|
return get_primary_selection() if supports_primary_selection else get_clipboard_string()
|
||||||
|
|
||||||
def paste_from_selection(self) -> None:
|
def paste_from_selection(self) -> None:
|
||||||
text = get_primary_selection() if supports_primary_selection else get_clipboard_string()
|
text = self.current_primary_selection_or_clipboard()
|
||||||
self.paste_to_active_window(text)
|
self.paste_to_active_window(text)
|
||||||
|
|
||||||
def set_primary_selection(self) -> None:
|
def set_primary_selection(self) -> None:
|
||||||
@@ -1112,8 +1157,8 @@ class Boss:
|
|||||||
text = w.text_for_selection()
|
text = w.text_for_selection()
|
||||||
if text:
|
if text:
|
||||||
set_primary_selection(text)
|
set_primary_selection(text)
|
||||||
if self.opts.copy_on_select:
|
if get_options().copy_on_select:
|
||||||
self.copy_to_buffer(self.opts.copy_on_select)
|
self.copy_to_buffer(get_options().copy_on_select)
|
||||||
|
|
||||||
def copy_to_buffer(self, buffer_name: str) -> None:
|
def copy_to_buffer(self, buffer_name: str) -> None:
|
||||||
w = self.active_window
|
w = self.active_window
|
||||||
@@ -1160,7 +1205,10 @@ class Boss:
|
|||||||
|
|
||||||
prev_tab = previous_tab
|
prev_tab = previous_tab
|
||||||
|
|
||||||
def process_stdin_source(self, window: Optional[Window] = None, stdin: Optional[str] = None) -> Tuple[Optional[Dict[str, str]], Optional[bytes]]:
|
def process_stdin_source(
|
||||||
|
self, window: Optional[Window] = None,
|
||||||
|
stdin: Optional[str] = None, copy_pipe_data: Optional[Dict] = None
|
||||||
|
) -> Tuple[Optional[Dict[str, str]], Optional[bytes]]:
|
||||||
w = window or self.active_window
|
w = window or self.active_window
|
||||||
if not w:
|
if not w:
|
||||||
return None, None
|
return None, None
|
||||||
@@ -1174,6 +1222,8 @@ class Boss:
|
|||||||
if stdin is not None:
|
if stdin is not None:
|
||||||
pipe_data = w.pipe_data(stdin, has_wrap_markers=add_wrap_markers) if w else None
|
pipe_data = w.pipe_data(stdin, has_wrap_markers=add_wrap_markers) if w else None
|
||||||
if pipe_data:
|
if pipe_data:
|
||||||
|
if copy_pipe_data is not None:
|
||||||
|
copy_pipe_data.update(pipe_data)
|
||||||
env = {
|
env = {
|
||||||
'KITTY_PIPE_DATA':
|
'KITTY_PIPE_DATA':
|
||||||
'{scrolled_by}:{cursor_x},{cursor_y}:{lines},{columns}'.format(**pipe_data)
|
'{scrolled_by}:{cursor_x},{cursor_y}:{lines},{columns}'.format(**pipe_data)
|
||||||
@@ -1216,6 +1266,11 @@ class Boss:
|
|||||||
cwd_from: Optional[int] = None
|
cwd_from: Optional[int] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
import subprocess
|
import subprocess
|
||||||
|
env = env or None
|
||||||
|
if env:
|
||||||
|
env_ = default_env().copy()
|
||||||
|
env_.update(env)
|
||||||
|
env = env_
|
||||||
if cwd_from:
|
if cwd_from:
|
||||||
with suppress(Exception):
|
with suppress(Exception):
|
||||||
cwd = cwd_of_process(cwd_from)
|
cwd = cwd_of_process(cwd_from)
|
||||||
@@ -1375,18 +1430,55 @@ class Boss:
|
|||||||
window.refresh()
|
window.refresh()
|
||||||
|
|
||||||
def patch_colors(self, spec: Dict[str, int], cursor_text_color: Union[bool, int, Color], configured: bool = False) -> None:
|
def patch_colors(self, spec: Dict[str, int], cursor_text_color: Union[bool, int, Color], configured: bool = False) -> None:
|
||||||
|
opts = get_options()
|
||||||
if configured:
|
if configured:
|
||||||
for k, v in spec.items():
|
for k, v in spec.items():
|
||||||
if hasattr(self.opts, k):
|
if hasattr(opts, k):
|
||||||
setattr(self.opts, k, color_from_int(v))
|
setattr(opts, k, color_from_int(v))
|
||||||
if cursor_text_color is not False:
|
if cursor_text_color is not False:
|
||||||
if isinstance(cursor_text_color, int):
|
if isinstance(cursor_text_color, int):
|
||||||
cursor_text_color = color_from_int(cursor_text_color)
|
cursor_text_color = color_from_int(cursor_text_color)
|
||||||
self.opts.cursor_text_color = cursor_text_color
|
opts.cursor_text_color = cursor_text_color
|
||||||
for tm in self.all_tab_managers:
|
for tm in self.all_tab_managers:
|
||||||
tm.tab_bar.patch_colors(spec)
|
tm.tab_bar.patch_colors(spec)
|
||||||
patch_global_colors(spec, configured)
|
patch_global_colors(spec, configured)
|
||||||
|
|
||||||
|
def apply_new_options(self, opts: Options) -> None:
|
||||||
|
from .fonts.box_drawing import set_scale
|
||||||
|
|
||||||
|
# Update options storage
|
||||||
|
set_options(opts, is_wayland(), self.args.debug_rendering, self.args.debug_font_fallback)
|
||||||
|
apply_options_update()
|
||||||
|
set_layout_options(opts)
|
||||||
|
set_default_env(opts.env.copy())
|
||||||
|
# Update font data
|
||||||
|
set_scale(opts.box_drawing_scale)
|
||||||
|
from .fonts.render import set_font_family
|
||||||
|
set_font_family(opts, debug_font_matching=self.args.debug_font_fallback)
|
||||||
|
for os_window_id, tm in self.os_window_map.items():
|
||||||
|
if tm is not None:
|
||||||
|
os_window_font_size(os_window_id, opts.font_size, True)
|
||||||
|
tm.resize()
|
||||||
|
# Update key bindings
|
||||||
|
self.update_keymap()
|
||||||
|
# Update misc options
|
||||||
|
for tm in self.all_tab_managers:
|
||||||
|
tm.apply_options()
|
||||||
|
# Update colors
|
||||||
|
for w in self.all_windows:
|
||||||
|
self.default_bg_changed_for(w.id)
|
||||||
|
w.refresh()
|
||||||
|
|
||||||
|
def load_config_file(self, *paths: str, apply_overrides: bool = True) -> None:
|
||||||
|
from .config import load_config
|
||||||
|
old_opts = get_options()
|
||||||
|
paths = paths or old_opts.config_paths
|
||||||
|
bad_lines: List[BadLine] = []
|
||||||
|
opts = load_config(*paths, overrides=old_opts.config_overrides if apply_overrides else None, accumulate_bad_lines=bad_lines)
|
||||||
|
if bad_lines:
|
||||||
|
self.show_bad_config_lines(bad_lines)
|
||||||
|
self.apply_new_options(opts)
|
||||||
|
|
||||||
def safe_delete_temp_file(self, path: str) -> None:
|
def safe_delete_temp_file(self, path: str) -> None:
|
||||||
if is_path_in_temp_dir(path):
|
if is_path_in_temp_dir(path):
|
||||||
with suppress(FileNotFoundError):
|
with suppress(FileNotFoundError):
|
||||||
@@ -1653,8 +1745,8 @@ class Boss:
|
|||||||
def show_kitty_env_vars(self) -> None:
|
def show_kitty_env_vars(self) -> None:
|
||||||
w = self.active_window
|
w = self.active_window
|
||||||
if w:
|
if w:
|
||||||
output = '\n'.join(f'{k}={v}' for k, v in os.environ.items()).encode('utf-8')
|
output = '\n'.join(f'{k}={v}' for k, v in os.environ.items())
|
||||||
self.display_scrollback(w, output, ['less'])
|
self.display_scrollback(w, output, title=_('Current kitty env vars'))
|
||||||
|
|
||||||
def open_file(self, path: str) -> None:
|
def open_file(self, path: str) -> None:
|
||||||
if path == ":cocoa::application launched::":
|
if path == ":cocoa::application launched::":
|
||||||
@@ -1673,3 +1765,12 @@ class Boss:
|
|||||||
self.new_window(path)
|
self.new_window(path)
|
||||||
if w is not None:
|
if w is not None:
|
||||||
tab.remove_window(w)
|
tab.remove_window(w)
|
||||||
|
|
||||||
|
def debug_config(self) -> None:
|
||||||
|
from .debug_config import debug_config
|
||||||
|
w = self.active_window
|
||||||
|
if w is not None:
|
||||||
|
output = debug_config(get_options())
|
||||||
|
set_clipboard_string(re.sub(r'\x1b.+?m', '', output))
|
||||||
|
output += '\n\x1b[35mThis debug output has been copied to the clipboard\x1b[m'
|
||||||
|
self.display_scrollback(w, output, title=_('Current kitty options'))
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ vec4 blend_onto_opaque_premul(vec3 over, float over_alpha, vec3 under) {
|
|||||||
* 1) Draw background for all cells
|
* 1) Draw background for all cells
|
||||||
* 2) Draw the images that are supposed to be below both the background and text, if any. This happens in the graphics shader
|
* 2) Draw the images that are supposed to be below both the background and text, if any. This happens in the graphics shader
|
||||||
* 3) Draw the background of cells that don't have the default background if any images were drawn in 2 above
|
* 3) Draw the background of cells that don't have the default background if any images were drawn in 2 above
|
||||||
4) Draw the images that are supposed to be below text but not background, again in graphics shader.
|
* 4) Draw the images that are supposed to be below text but not background, again in graphics shader.
|
||||||
* 5) Draw the special cells (selection/cursor). Output is same as from step 1, with bg_alpha 1 for special cells and 0 otherwise
|
* 5) Draw the special cells (selection/cursor). Output is same as from step 1, with bg_alpha 1 for special cells and 0 otherwise
|
||||||
* 6) Draw the foreground -- expected output is color with alpha which is blended using the opaque blend func
|
* 6) Draw the foreground -- expected output is color with alpha which is blended using the opaque blend func
|
||||||
* 7) Draw the images that are supposed to be above text again in the graphics shader
|
* 7) Draw the images that are supposed to be above text again in the graphics shader
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ static unsigned long remove_notify[MAX_CHILDREN] = {0};
|
|||||||
static size_t add_queue_count = 0, remove_queue_count = 0;
|
static size_t add_queue_count = 0, remove_queue_count = 0;
|
||||||
static struct pollfd fds[MAX_CHILDREN + EXTRA_FDS] = {{0}};
|
static struct pollfd fds[MAX_CHILDREN + EXTRA_FDS] = {{0}};
|
||||||
static pthread_mutex_t children_lock, talk_lock;
|
static pthread_mutex_t children_lock, talk_lock;
|
||||||
static bool kill_signal_received = false;
|
static bool kill_signal_received = false, reload_config_signal_received = false;
|
||||||
static ChildMonitor *the_monitor = NULL;
|
static ChildMonitor *the_monitor = NULL;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -333,7 +333,7 @@ static bool
|
|||||||
parse_input(ChildMonitor *self) {
|
parse_input(ChildMonitor *self) {
|
||||||
// Parse all available input that was read in the I/O thread.
|
// Parse all available input that was read in the I/O thread.
|
||||||
size_t count = 0, remove_count = 0;
|
size_t count = 0, remove_count = 0;
|
||||||
bool input_read = false;
|
bool input_read = false, reload_config_called = false;
|
||||||
monotonic_t now = monotonic();
|
monotonic_t now = monotonic();
|
||||||
children_mutex(lock);
|
children_mutex(lock);
|
||||||
while (remove_queue_count) {
|
while (remove_queue_count) {
|
||||||
@@ -343,11 +343,17 @@ parse_input(ChildMonitor *self) {
|
|||||||
FREE_CHILD(remove_queue[remove_queue_count]);
|
FREE_CHILD(remove_queue[remove_queue_count]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (UNLIKELY(kill_signal_received)) {
|
if (UNLIKELY(kill_signal_received || reload_config_signal_received)) {
|
||||||
global_state.quit_request = IMPERATIVE_CLOSE_REQUESTED;
|
if (kill_signal_received) {
|
||||||
global_state.has_pending_closes = true;
|
global_state.quit_request = IMPERATIVE_CLOSE_REQUESTED;
|
||||||
request_tick_callback();
|
global_state.has_pending_closes = true;
|
||||||
kill_signal_received = false;
|
request_tick_callback();
|
||||||
|
kill_signal_received = false;
|
||||||
|
}
|
||||||
|
else if (reload_config_signal_received) {
|
||||||
|
reload_config_signal_received = false;
|
||||||
|
reload_config_called = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
count = self->count;
|
count = self->count;
|
||||||
for (size_t i = 0; i < count; i++) {
|
for (size_t i = 0; i < count; i++) {
|
||||||
@@ -402,6 +408,9 @@ parse_input(ChildMonitor *self) {
|
|||||||
}
|
}
|
||||||
DECREF_CHILD(scratch[i]);
|
DECREF_CHILD(scratch[i]);
|
||||||
}
|
}
|
||||||
|
if (reload_config_called) {
|
||||||
|
call_boss(load_config_file, "");
|
||||||
|
}
|
||||||
return input_read;
|
return input_read;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -997,6 +1006,9 @@ process_global_state(void *data) {
|
|||||||
if (cocoa_pending_actions & NEXT_TAB) { call_boss(next_tab, NULL); }
|
if (cocoa_pending_actions & NEXT_TAB) { call_boss(next_tab, NULL); }
|
||||||
if (cocoa_pending_actions & PREVIOUS_TAB) { call_boss(previous_tab, NULL); }
|
if (cocoa_pending_actions & PREVIOUS_TAB) { call_boss(previous_tab, NULL); }
|
||||||
if (cocoa_pending_actions & DETACH_TAB) { call_boss(detach_tab, NULL); }
|
if (cocoa_pending_actions & DETACH_TAB) { call_boss(detach_tab, NULL); }
|
||||||
|
if (cocoa_pending_actions & NEW_WINDOW) { call_boss(new_window, NULL); }
|
||||||
|
if (cocoa_pending_actions & CLOSE_WINDOW) { call_boss(close_window, NULL); }
|
||||||
|
if (cocoa_pending_actions & RESET_TERMINAL) { call_boss(clear_terminal, "sO", "reset", Py_True ); }
|
||||||
if (cocoa_pending_actions_data.wd) {
|
if (cocoa_pending_actions_data.wd) {
|
||||||
if (cocoa_pending_actions & NEW_OS_WINDOW_WITH_WD) { call_boss(new_os_window_with_wd, "s", cocoa_pending_actions_data.wd); }
|
if (cocoa_pending_actions & NEW_OS_WINDOW_WITH_WD) { call_boss(new_os_window_with_wd, "s", cocoa_pending_actions_data.wd); }
|
||||||
if (cocoa_pending_actions & NEW_TAB_WITH_WD) { call_boss(new_tab_with_wd, "s", cocoa_pending_actions_data.wd); }
|
if (cocoa_pending_actions & NEW_TAB_WITH_WD) { call_boss(new_tab_with_wd, "s", cocoa_pending_actions_data.wd); }
|
||||||
@@ -1142,7 +1154,7 @@ read_bytes(int fd, Screen *screen) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
typedef struct { bool kill_signal, child_died; } SignalSet;
|
typedef struct { bool kill_signal, child_died, reload_config; } SignalSet;
|
||||||
|
|
||||||
static void
|
static void
|
||||||
handle_signal(int signum, void *data) {
|
handle_signal(int signum, void *data) {
|
||||||
@@ -1155,6 +1167,9 @@ handle_signal(int signum, void *data) {
|
|||||||
case SIGCHLD:
|
case SIGCHLD:
|
||||||
ss->child_died = true;
|
ss->child_died = true;
|
||||||
break;
|
break;
|
||||||
|
case SIGUSR1:
|
||||||
|
ss->reload_config = true;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1269,7 +1284,12 @@ io_loop(void *data) {
|
|||||||
SignalSet ss = {0};
|
SignalSet ss = {0};
|
||||||
data_received = true;
|
data_received = true;
|
||||||
read_signals(fds[1].fd, handle_signal, &ss);
|
read_signals(fds[1].fd, handle_signal, &ss);
|
||||||
if (ss.kill_signal) { children_mutex(lock); kill_signal_received = true; children_mutex(unlock); }
|
if (ss.kill_signal || ss.reload_config) {
|
||||||
|
children_mutex(lock);
|
||||||
|
if (ss.kill_signal) kill_signal_received = true;
|
||||||
|
if (ss.reload_config) reload_config_signal_received = true;
|
||||||
|
children_mutex(unlock);
|
||||||
|
}
|
||||||
if (ss.child_died) reap_children(self, OPT(close_on_child_death));
|
if (ss.child_died) reap_children(self, OPT(close_on_child_death));
|
||||||
}
|
}
|
||||||
for (i = 0; i < self->count; i++) {
|
for (i = 0; i < self->count; i++) {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from typing import (
|
|||||||
import kitty.fast_data_types as fast_data_types
|
import kitty.fast_data_types as fast_data_types
|
||||||
|
|
||||||
from .constants import is_macos, shell_path, terminfo_dir
|
from .constants import is_macos, shell_path, terminfo_dir
|
||||||
from .options_stub import Options
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
@@ -187,7 +186,6 @@ class Child:
|
|||||||
self,
|
self,
|
||||||
argv: Sequence[str],
|
argv: Sequence[str],
|
||||||
cwd: str,
|
cwd: str,
|
||||||
opts: Options,
|
|
||||||
stdin: Optional[bytes] = None,
|
stdin: Optional[bytes] = None,
|
||||||
env: Optional[Dict[str, str]] = None,
|
env: Optional[Dict[str, str]] = None,
|
||||||
cwd_from: Optional[int] = None,
|
cwd_from: Optional[int] = None,
|
||||||
@@ -204,7 +202,6 @@ class Child:
|
|||||||
else:
|
else:
|
||||||
cwd = os.path.expandvars(os.path.expanduser(cwd or os.getcwd()))
|
cwd = os.path.expandvars(os.path.expanduser(cwd or os.getcwd()))
|
||||||
self.cwd = os.path.abspath(cwd)
|
self.cwd = os.path.abspath(cwd)
|
||||||
self.opts = opts
|
|
||||||
self.stdin = stdin
|
self.stdin = stdin
|
||||||
self.env = env or {}
|
self.env = env or {}
|
||||||
|
|
||||||
@@ -217,7 +214,7 @@ class Child:
|
|||||||
'lc_ctype_before_python') and not getattr(default_env, 'lc_ctype_set_by_user', False):
|
'lc_ctype_before_python') and not getattr(default_env, 'lc_ctype_set_by_user', False):
|
||||||
del env['LC_CTYPE']
|
del env['LC_CTYPE']
|
||||||
env.update(self.env)
|
env.update(self.env)
|
||||||
env['TERM'] = self.opts.term
|
env['TERM'] = fast_data_types.get_options().term
|
||||||
env['COLORTERM'] = 'truecolor'
|
env['COLORTERM'] = 'truecolor'
|
||||||
if self.cwd:
|
if self.cwd:
|
||||||
# needed in case cwd is a symlink, in which case shells
|
# needed in case cwd is a symlink, in which case shells
|
||||||
|
|||||||
119
kitty/cli.py
119
kitty/cli.py
@@ -2,22 +2,19 @@
|
|||||||
# vim:fileencoding=utf-8
|
# vim:fileencoding=utf-8
|
||||||
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from typing import (
|
from typing import (
|
||||||
Any, Callable, Dict, FrozenSet, Iterable, Iterator, List, Match, Optional,
|
Any, Callable, Dict, FrozenSet, Iterator, List, Match, Optional, Sequence,
|
||||||
Sequence, Set, Tuple, Type, TypeVar, Union, cast
|
Tuple, Type, TypeVar, Union, cast
|
||||||
)
|
)
|
||||||
|
|
||||||
from .cli_stub import CLIOptions
|
from .cli_stub import CLIOptions
|
||||||
from .conf.utils import resolve_config
|
from .conf.utils import resolve_config
|
||||||
from .config import KeyAction
|
from .constants import appname, defconf, is_macos, str_version
|
||||||
from .constants import appname, defconf, is_macos, is_wayland, str_version
|
from .options.types import Options as KittyOpts
|
||||||
from .options_stub import Options as OptionsStub
|
from .typing import BadLineType, TypedDict
|
||||||
from .types import SingleKey
|
|
||||||
from .typing import BadLineType, SequenceMap, TypedDict
|
|
||||||
|
|
||||||
|
|
||||||
class OptionDict(TypedDict):
|
class OptionDict(TypedDict):
|
||||||
@@ -587,7 +584,7 @@ Override individual configuration options, can be specified multiple times.
|
|||||||
Syntax: :italic:`name=value`. For example: :option:`kitty -o` font_size=20
|
Syntax: :italic:`name=value`. For example: :option:`kitty -o` font_size=20
|
||||||
|
|
||||||
|
|
||||||
--directory -d
|
--directory --working-directory -d
|
||||||
default=.
|
default=.
|
||||||
Change to the specified directory when launching
|
Change to the specified directory when launching
|
||||||
|
|
||||||
@@ -690,9 +687,10 @@ instead of ignoring them. Also prints out miscellaneous debug information.
|
|||||||
Useful when debugging rendering problems
|
Useful when debugging rendering problems
|
||||||
|
|
||||||
|
|
||||||
--debug-keyboard
|
--debug-input --debug-keyboard
|
||||||
|
dest=debug_keyboard
|
||||||
type=bool-set
|
type=bool-set
|
||||||
This option will cause kitty to print out key events as they are received
|
This option will cause kitty to print out key and mouse events as they are received
|
||||||
|
|
||||||
|
|
||||||
--debug-font-fallback
|
--debug-font-fallback
|
||||||
@@ -700,11 +698,6 @@ type=bool-set
|
|||||||
Print out information about the selection of fallback fonts for characters not present in the main font.
|
Print out information about the selection of fallback fonts for characters not present in the main font.
|
||||||
|
|
||||||
|
|
||||||
--debug-config
|
|
||||||
type=bool-set
|
|
||||||
Print out information about the system and kitty configuration.
|
|
||||||
|
|
||||||
|
|
||||||
--execute -e
|
--execute -e
|
||||||
type=bool-set
|
type=bool-set
|
||||||
!
|
!
|
||||||
@@ -756,107 +749,17 @@ def parse_args(
|
|||||||
|
|
||||||
|
|
||||||
SYSTEM_CONF = '/etc/xdg/kitty/kitty.conf'
|
SYSTEM_CONF = '/etc/xdg/kitty/kitty.conf'
|
||||||
ShortcutMap = Dict[Tuple[SingleKey, ...], KeyAction]
|
|
||||||
|
|
||||||
|
|
||||||
def print_shortcut(key_sequence: Iterable[SingleKey], action: KeyAction) -> None:
|
def create_opts(args: CLIOptions, accumulate_bad_lines: Optional[List[BadLineType]] = None) -> KittyOpts:
|
||||||
from .fast_data_types import (
|
|
||||||
GLFW_MOD_ALT, GLFW_MOD_CAPS_LOCK, GLFW_MOD_CONTROL, GLFW_MOD_HYPER,
|
|
||||||
GLFW_MOD_META, GLFW_MOD_NUM_LOCK, GLFW_MOD_SHIFT, GLFW_MOD_SUPER,
|
|
||||||
glfw_get_key_name
|
|
||||||
)
|
|
||||||
modmap = {'shift': GLFW_MOD_SHIFT, 'alt': GLFW_MOD_ALT, 'ctrl': GLFW_MOD_CONTROL, ('cmd' if is_macos else 'super'): GLFW_MOD_SUPER,
|
|
||||||
'hyper': GLFW_MOD_HYPER, 'meta': GLFW_MOD_META, 'num_lock': GLFW_MOD_NUM_LOCK, 'caps_lock': GLFW_MOD_CAPS_LOCK}
|
|
||||||
keys = []
|
|
||||||
for key_spec in key_sequence:
|
|
||||||
names = []
|
|
||||||
mods, is_native, key = key_spec
|
|
||||||
for name, val in modmap.items():
|
|
||||||
if mods & val:
|
|
||||||
names.append(name)
|
|
||||||
if key:
|
|
||||||
kname = glfw_get_key_name(0, key) if is_native else glfw_get_key_name(key, 0)
|
|
||||||
names.append(kname or f'{key}')
|
|
||||||
keys.append('+'.join(names))
|
|
||||||
|
|
||||||
print('\t', ' > '.join(keys), action)
|
|
||||||
|
|
||||||
|
|
||||||
def print_shortcut_changes(defns: ShortcutMap, text: str, changes: Set[Tuple[SingleKey, ...]]) -> None:
|
|
||||||
if changes:
|
|
||||||
print(title(text))
|
|
||||||
|
|
||||||
for k in sorted(changes):
|
|
||||||
print_shortcut(k, defns[k])
|
|
||||||
|
|
||||||
|
|
||||||
def compare_keymaps(final: ShortcutMap, initial: ShortcutMap) -> None:
|
|
||||||
added = set(final) - set(initial)
|
|
||||||
removed = set(initial) - set(final)
|
|
||||||
changed = {k for k in set(final) & set(initial) if final[k] != initial[k]}
|
|
||||||
print_shortcut_changes(final, 'Added shortcuts:', added)
|
|
||||||
print_shortcut_changes(initial, 'Removed shortcuts:', removed)
|
|
||||||
print_shortcut_changes(final, 'Changed shortcuts:', changed)
|
|
||||||
|
|
||||||
|
|
||||||
def flatten_sequence_map(m: SequenceMap) -> ShortcutMap:
|
|
||||||
ans: Dict[Tuple[SingleKey, ...], KeyAction] = {}
|
|
||||||
for key_spec, rest_map in m.items():
|
|
||||||
for r, action in rest_map.items():
|
|
||||||
ans[(key_spec,) + (r)] = action
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
def compare_opts(opts: OptionsStub) -> None:
|
|
||||||
from .config import defaults, load_config
|
|
||||||
print('\nConfig options different from defaults:')
|
|
||||||
default_opts = load_config()
|
|
||||||
changed_opts = [
|
|
||||||
f for f in sorted(defaults._fields) # type: ignore
|
|
||||||
if f not in ('key_definitions', 'keymap', 'sequence_map') and getattr(opts, f) != getattr(defaults, f)
|
|
||||||
]
|
|
||||||
field_len = max(map(len, changed_opts)) if changed_opts else 20
|
|
||||||
fmt = '{{:{:d}s}}'.format(field_len)
|
|
||||||
for f in changed_opts:
|
|
||||||
print(title(fmt.format(f)), getattr(opts, f))
|
|
||||||
|
|
||||||
final_, initial_ = opts.keymap, default_opts.keymap
|
|
||||||
final: ShortcutMap = {(k,): v for k, v in final_.items()}
|
|
||||||
initial: ShortcutMap = {(k,): v for k, v in initial_.items()}
|
|
||||||
final_s, initial_s = map(flatten_sequence_map, (opts.sequence_map, default_opts.sequence_map))
|
|
||||||
final.update(final_s)
|
|
||||||
initial.update(initial_s)
|
|
||||||
compare_keymaps(final, initial)
|
|
||||||
|
|
||||||
|
|
||||||
def create_opts(args: CLIOptions, debug_config: bool = False, accumulate_bad_lines: Optional[List[BadLineType]] = None) -> OptionsStub:
|
|
||||||
from .config import load_config
|
from .config import load_config
|
||||||
config = tuple(resolve_config(SYSTEM_CONF, defconf, args.config))
|
config = tuple(resolve_config(SYSTEM_CONF, defconf, args.config))
|
||||||
if debug_config:
|
|
||||||
print(version(add_rev=True))
|
|
||||||
print(' '.join(os.uname()))
|
|
||||||
if is_macos:
|
|
||||||
import subprocess
|
|
||||||
print(' '.join(subprocess.check_output(['sw_vers']).decode('utf-8').splitlines()).strip())
|
|
||||||
if os.path.exists('/etc/issue'):
|
|
||||||
with open('/etc/issue', encoding='utf-8', errors='replace') as f:
|
|
||||||
print(f.read().strip())
|
|
||||||
if os.path.exists('/etc/lsb-release'):
|
|
||||||
with open('/etc/lsb-release', encoding='utf-8', errors='replace') as f:
|
|
||||||
print(f.read().strip())
|
|
||||||
config = tuple(x for x in config if os.path.exists(x))
|
|
||||||
if config:
|
|
||||||
print(green('Loaded config files:'), ', '.join(config))
|
|
||||||
overrides = (a.replace('=', ' ', 1) for a in args.override or ())
|
overrides = (a.replace('=', ' ', 1) for a in args.override or ())
|
||||||
opts = load_config(*config, overrides=overrides, accumulate_bad_lines=accumulate_bad_lines)
|
opts = load_config(*config, overrides=overrides, accumulate_bad_lines=accumulate_bad_lines)
|
||||||
if debug_config:
|
|
||||||
if not is_macos:
|
|
||||||
print('Running under:', green('Wayland' if is_wayland(opts) else 'X11'))
|
|
||||||
compare_opts(opts)
|
|
||||||
return opts
|
return opts
|
||||||
|
|
||||||
|
|
||||||
def create_default_opts() -> OptionsStub:
|
def create_default_opts() -> KittyOpts:
|
||||||
from .config import load_config
|
from .config import load_config
|
||||||
config = tuple(resolve_config(SYSTEM_CONF, defconf, ()))
|
config = tuple(resolve_config(SYSTEM_CONF, defconf, ()))
|
||||||
opts = load_config(*config)
|
opts = load_config(*config)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ QueryTerminalCLIOptions = BroadcastCLIOptions = ShowKeyCLIOptions = CLIOptions
|
|||||||
|
|
||||||
def generate_stub() -> None:
|
def generate_stub() -> None:
|
||||||
from .cli import parse_option_spec, as_type_stub
|
from .cli import parse_option_spec, as_type_stub
|
||||||
from .conf.definition import save_type_stub
|
from .conf.utils import save_type_stub
|
||||||
text = 'import typing\n\n\n'
|
text = 'import typing\n\n\n'
|
||||||
|
|
||||||
def do(otext=None, cls: str = 'CLIOptions', extra_fields: Sequence[str] = ()):
|
def do(otext=None, cls: str = 'CLIOptions', extra_fields: Sequence[str] = ()):
|
||||||
|
|||||||
@@ -88,6 +88,9 @@ PENDING(close_tab, CLOSE_TAB)
|
|||||||
PENDING(new_tab, NEW_TAB)
|
PENDING(new_tab, NEW_TAB)
|
||||||
PENDING(next_tab, NEXT_TAB)
|
PENDING(next_tab, NEXT_TAB)
|
||||||
PENDING(previous_tab, PREVIOUS_TAB)
|
PENDING(previous_tab, PREVIOUS_TAB)
|
||||||
|
PENDING(new_window, NEW_WINDOW)
|
||||||
|
PENDING(close_window, CLOSE_WINDOW)
|
||||||
|
PENDING(reset_terminal, RESET_TERMINAL)
|
||||||
|
|
||||||
- (void)open_kitty_website_url:(id)sender {
|
- (void)open_kitty_website_url:(id)sender {
|
||||||
(void)sender;
|
(void)sender;
|
||||||
@@ -115,7 +118,7 @@ typedef struct {
|
|||||||
} GlobalShortcut;
|
} GlobalShortcut;
|
||||||
typedef struct {
|
typedef struct {
|
||||||
GlobalShortcut new_os_window, close_os_window, close_tab, edit_config_file;
|
GlobalShortcut new_os_window, close_os_window, close_tab, edit_config_file;
|
||||||
GlobalShortcut previous_tab, next_tab, new_tab;
|
GlobalShortcut previous_tab, next_tab, new_tab, new_window, close_window, reset_terminal;
|
||||||
} GlobalShortcuts;
|
} GlobalShortcuts;
|
||||||
static GlobalShortcuts global_shortcuts;
|
static GlobalShortcuts global_shortcuts;
|
||||||
|
|
||||||
@@ -129,6 +132,7 @@ cocoa_set_global_shortcut(PyObject *self UNUSED, PyObject *args) {
|
|||||||
#define Q(x) if (strcmp(name, #x) == 0) gs = &global_shortcuts.x
|
#define Q(x) if (strcmp(name, #x) == 0) gs = &global_shortcuts.x
|
||||||
Q(new_os_window); else Q(close_os_window); else Q(close_tab); else Q(edit_config_file);
|
Q(new_os_window); else Q(close_os_window); else Q(close_tab); else Q(edit_config_file);
|
||||||
else Q(new_tab); else Q(next_tab); else Q(previous_tab);
|
else Q(new_tab); else Q(next_tab); else Q(previous_tab);
|
||||||
|
else Q(new_window); else Q(close_window); else Q(reset_terminal);
|
||||||
#undef Q
|
#undef Q
|
||||||
if (gs == NULL) { PyErr_SetString(PyExc_KeyError, "Unknown shortcut name"); return NULL; }
|
if (gs == NULL) { PyErr_SetString(PyExc_KeyError, "Unknown shortcut name"); return NULL; }
|
||||||
int cocoa_mods;
|
int cocoa_mods;
|
||||||
@@ -145,7 +149,7 @@ get_dock_menu(id self UNUSED, SEL _cmd UNUSED, NSApplication *sender UNUSED) {
|
|||||||
if (!dockMenu) {
|
if (!dockMenu) {
|
||||||
GlobalMenuTarget *global_menu_target = [GlobalMenuTarget shared_instance];
|
GlobalMenuTarget *global_menu_target = [GlobalMenuTarget shared_instance];
|
||||||
dockMenu = [[NSMenu alloc] init];
|
dockMenu = [[NSMenu alloc] init];
|
||||||
[[dockMenu addItemWithTitle:@"New OS window"
|
[[dockMenu addItemWithTitle:@"New OS Window"
|
||||||
action:@selector(new_os_window:)
|
action:@selector(new_os_window:)
|
||||||
keyEquivalent:@""]
|
keyEquivalent:@""]
|
||||||
setTarget:global_menu_target];
|
setTarget:global_menu_target];
|
||||||
@@ -157,9 +161,11 @@ static PyObject *notification_activated_callback = NULL;
|
|||||||
|
|
||||||
static PyObject*
|
static PyObject*
|
||||||
set_notification_activated_callback(PyObject *self UNUSED, PyObject *callback) {
|
set_notification_activated_callback(PyObject *self UNUSED, PyObject *callback) {
|
||||||
if (notification_activated_callback) Py_DECREF(notification_activated_callback);
|
Py_CLEAR(notification_activated_callback);
|
||||||
notification_activated_callback = callback;
|
if (callback != Py_None) {
|
||||||
Py_INCREF(callback);
|
notification_activated_callback = callback;
|
||||||
|
Py_INCREF(callback);
|
||||||
|
}
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +380,6 @@ cocoa_create_global_menu(void) {
|
|||||||
keyEquivalent:@""];
|
keyEquivalent:@""];
|
||||||
[appMenu addItem:[NSMenuItem separatorItem]];
|
[appMenu addItem:[NSMenuItem separatorItem]];
|
||||||
MENU_ITEM(appMenu, @"Preferences…", edit_config_file);
|
MENU_ITEM(appMenu, @"Preferences…", edit_config_file);
|
||||||
MENU_ITEM(appMenu, @"New OS window", new_os_window);
|
|
||||||
|
|
||||||
[appMenu addItemWithTitle:[NSString stringWithFormat:@"Hide %@", app_name]
|
[appMenu addItemWithTitle:[NSString stringWithFormat:@"Hide %@", app_name]
|
||||||
action:@selector(hide:)
|
action:@selector(hide:)
|
||||||
@@ -408,6 +413,23 @@ cocoa_create_global_menu(void) {
|
|||||||
keyEquivalent:@"q"];
|
keyEquivalent:@"q"];
|
||||||
[appMenu release];
|
[appMenu release];
|
||||||
|
|
||||||
|
NSMenuItem* shellMenuItem =
|
||||||
|
[bar addItemWithTitle:@"Shell"
|
||||||
|
action:NULL
|
||||||
|
keyEquivalent:@""];
|
||||||
|
NSMenu* shellMenu = [[NSMenu alloc] initWithTitle:@"Shell"];
|
||||||
|
[shellMenuItem setSubmenu:shellMenu];
|
||||||
|
MENU_ITEM(shellMenu, @"New OS Window", new_os_window);
|
||||||
|
MENU_ITEM(shellMenu, @"New Tab", new_tab);
|
||||||
|
MENU_ITEM(shellMenu, @"New Window", new_window);
|
||||||
|
[shellMenu addItem:[NSMenuItem separatorItem]];
|
||||||
|
MENU_ITEM(shellMenu, @"Close OS Window", close_os_window);
|
||||||
|
MENU_ITEM(shellMenu, @"Close Tab", close_tab);
|
||||||
|
MENU_ITEM(shellMenu, @"Close Window", close_window);
|
||||||
|
[shellMenu addItem:[NSMenuItem separatorItem]];
|
||||||
|
MENU_ITEM(shellMenu, @"Reset", reset_terminal);
|
||||||
|
[shellMenu release];
|
||||||
|
|
||||||
NSMenuItem* windowMenuItem =
|
NSMenuItem* windowMenuItem =
|
||||||
[bar addItemWithTitle:@""
|
[bar addItemWithTitle:@""
|
||||||
action:NULL
|
action:NULL
|
||||||
@@ -427,11 +449,8 @@ cocoa_create_global_menu(void) {
|
|||||||
keyEquivalent:@""];
|
keyEquivalent:@""];
|
||||||
|
|
||||||
[windowMenu addItem:[NSMenuItem separatorItem]];
|
[windowMenu addItem:[NSMenuItem separatorItem]];
|
||||||
MENU_ITEM(windowMenu, @"New Tab", new_tab);
|
|
||||||
MENU_ITEM(windowMenu, @"Show Previous Tab", previous_tab);
|
MENU_ITEM(windowMenu, @"Show Previous Tab", previous_tab);
|
||||||
MENU_ITEM(windowMenu, @"Show Next Tab", next_tab);
|
MENU_ITEM(windowMenu, @"Show Next Tab", next_tab);
|
||||||
MENU_ITEM(windowMenu, @"Close Tab", close_tab);
|
|
||||||
MENU_ITEM(windowMenu, @"Close OS Window", close_os_window);
|
|
||||||
[[windowMenu addItemWithTitle:@"Move Tab to New Window"
|
[[windowMenu addItemWithTitle:@"Move Tab to New Window"
|
||||||
action:@selector(detach_tab:)
|
action:@selector(detach_tab:)
|
||||||
keyEquivalent:@""] setTarget:global_menu_target];
|
keyEquivalent:@""] setTarget:global_menu_target];
|
||||||
@@ -624,7 +643,6 @@ cleanup() {
|
|||||||
|
|
||||||
if (dockMenu) [dockMenu release];
|
if (dockMenu) [dockMenu release];
|
||||||
dockMenu = nil;
|
dockMenu = nil;
|
||||||
Py_CLEAR(notification_activated_callback);
|
|
||||||
|
|
||||||
#ifndef KITTY_USE_DEPRECATED_MACOS_NOTIFICATION_API
|
#ifndef KITTY_USE_DEPRECATED_MACOS_NOTIFICATION_API
|
||||||
drain_pending_notifications(NO);
|
drain_pending_notifications(NO);
|
||||||
|
|||||||
@@ -92,11 +92,11 @@ alloc_color_profile() {
|
|||||||
static PyObject*
|
static PyObject*
|
||||||
update_ansi_color_table(ColorProfile *self, PyObject *val) {
|
update_ansi_color_table(ColorProfile *self, PyObject *val) {
|
||||||
#define update_ansi_color_table_doc "Update the 256 basic colors"
|
#define update_ansi_color_table_doc "Update the 256 basic colors"
|
||||||
if (!PyList_Check(val)) { PyErr_SetString(PyExc_TypeError, "color table must be a list"); return NULL; }
|
if (!PyLong_Check(val)) { PyErr_SetString(PyExc_TypeError, "color table must be a long"); return NULL; }
|
||||||
if (PyList_GET_SIZE(val) != arraysz(FG_BG_256)) { PyErr_SetString(PyExc_TypeError, "color table must have 256 items"); return NULL; }
|
unsigned long *color_table = PyLong_AsVoidPtr(val);
|
||||||
for (size_t i = 0; i < arraysz(FG_BG_256); i++) {
|
for (size_t i = 0; i < arraysz(FG_BG_256); i++) {
|
||||||
self->color_table[i] = PyLong_AsUnsignedLong(PyList_GET_ITEM(val, i));
|
self->color_table[i] = color_table[i];
|
||||||
self->orig_color_table[i] = self->color_table[i];
|
self->orig_color_table[i] = color_table[i];
|
||||||
}
|
}
|
||||||
self->dirty = true;
|
self->dirty = true;
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
|
|||||||
@@ -336,8 +336,14 @@ def complete_remote_command(ans: Completions, cmd_name: str, words: Sequence[str
|
|||||||
return
|
return
|
||||||
args_completer: Optional[CompleteArgsFunc] = None
|
args_completer: Optional[CompleteArgsFunc] = None
|
||||||
args_completion = command_for_name(cmd_name).args_completion
|
args_completion = command_for_name(cmd_name).args_completion
|
||||||
if args_completion and 'files' in args_completion:
|
if args_completion:
|
||||||
args_completer = remote_files_completer(args_completion['files'])
|
if 'files' in args_completion:
|
||||||
|
title, matchers = args_completion['files']
|
||||||
|
if isinstance(matchers, tuple):
|
||||||
|
args_completer = remote_files_completer(title, matchers)
|
||||||
|
elif 'names' in args_completion:
|
||||||
|
title, q = args_completion['names']
|
||||||
|
args_completer = remote_args_completer(title, q() if callable(q) else q)
|
||||||
complete_alias_map(ans, words, new_word, alias_map, complete_args=args_completer)
|
complete_alias_map(ans, words, new_word, alias_map, complete_args=args_completer)
|
||||||
|
|
||||||
|
|
||||||
@@ -379,7 +385,7 @@ def complete_files_and_dirs(
|
|||||||
add_prefix: Optional[str] = None
|
add_prefix: Optional[str] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
dirs, files_ = path_completion(prefix or '')
|
dirs, files_ = path_completion(prefix or '')
|
||||||
files = filter(predicate, files_)
|
files: Iterable[str] = filter(predicate, files_)
|
||||||
if add_prefix:
|
if add_prefix:
|
||||||
dirs = list(add_prefix + x for x in dirs)
|
dirs = list(add_prefix + x for x in dirs)
|
||||||
files = (add_prefix + x for x in files)
|
files = (add_prefix + x for x in files)
|
||||||
@@ -406,8 +412,7 @@ def complete_icat_args(ans: Completions, opt: Optional[OptionDict], prefix: str,
|
|||||||
complete_files_and_dirs(ans, prefix, 'Images', icat_file_predicate)
|
complete_files_and_dirs(ans, prefix, 'Images', icat_file_predicate)
|
||||||
|
|
||||||
|
|
||||||
def remote_files_completer(spec: Tuple[str, Tuple[str, ...]]) -> CompleteArgsFunc:
|
def remote_files_completer(name: str, matchers: Tuple[str, ...]) -> CompleteArgsFunc:
|
||||||
name, matchers = spec
|
|
||||||
|
|
||||||
def complete_files_map(ans: Completions, opt: Optional[OptionDict], prefix: str, unknown_args: Delegate) -> None:
|
def complete_files_map(ans: Completions, opt: Optional[OptionDict], prefix: str, unknown_args: Delegate) -> None:
|
||||||
|
|
||||||
@@ -423,6 +428,16 @@ def remote_files_completer(spec: Tuple[str, Tuple[str, ...]]) -> CompleteArgsFun
|
|||||||
return complete_files_map
|
return complete_files_map
|
||||||
|
|
||||||
|
|
||||||
|
def remote_args_completer(title: str, words: Iterable[str]) -> CompleteArgsFunc:
|
||||||
|
items = sorted(words)
|
||||||
|
|
||||||
|
def complete_names_for_arg(ans: Completions, opt: Optional[OptionDict], prefix: str, unknown_args: Delegate) -> None:
|
||||||
|
if opt is None:
|
||||||
|
ans.match_groups[title] = {c: '' for c in items if c.startswith(prefix)}
|
||||||
|
|
||||||
|
return complete_names_for_arg
|
||||||
|
|
||||||
|
|
||||||
def config_file_predicate(filename: str) -> bool:
|
def config_file_predicate(filename: str) -> bool:
|
||||||
return filename.endswith('.conf')
|
return filename.endswith('.conf')
|
||||||
|
|
||||||
|
|||||||
@@ -1,354 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# vim:fileencoding=utf-8
|
|
||||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
|
||||||
|
|
||||||
import re
|
|
||||||
from functools import partial
|
|
||||||
from typing import (
|
|
||||||
Any, Callable, Dict, Generator, Iterable, List, Match, Optional, Sequence,
|
|
||||||
Set, Tuple, Union, get_type_hints
|
|
||||||
)
|
|
||||||
|
|
||||||
from .utils import Choice, to_bool
|
|
||||||
|
|
||||||
|
|
||||||
def to_string(x: str) -> str:
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
class Group:
|
|
||||||
|
|
||||||
__slots__ = 'name', 'short_text', 'start_text', 'end_text'
|
|
||||||
|
|
||||||
def __init__(self, name: str, short_text: str, start_text: str = '', end_text: str = '') -> None:
|
|
||||||
self.name, self.short_text = name, short_text.strip()
|
|
||||||
self.start_text, self.end_text = start_text.strip(), end_text.strip()
|
|
||||||
|
|
||||||
|
|
||||||
class Option:
|
|
||||||
|
|
||||||
__slots__ = 'name', 'group', 'long_text', 'option_type', 'defval_as_string', 'add_to_default', 'add_to_docs', 'line'
|
|
||||||
|
|
||||||
def __init__(self, name: str, group: Group, defval: str, option_type: Any, long_text: str, add_to_default: bool, add_to_docs: bool):
|
|
||||||
self.name, self.group = name, group
|
|
||||||
self.long_text, self.option_type = long_text.strip(), option_type
|
|
||||||
self.defval_as_string = defval
|
|
||||||
self.add_to_default = add_to_default
|
|
||||||
self.add_to_docs = add_to_docs
|
|
||||||
self.line = self.name + ' ' + self.defval_as_string
|
|
||||||
|
|
||||||
def type_definition(self, is_multiple: bool, imports: Set[Tuple[str, str]]) -> str:
|
|
||||||
|
|
||||||
def type_name(x: type) -> str:
|
|
||||||
ans = x.__name__
|
|
||||||
if x.__module__ and x.__module__ != 'builtins':
|
|
||||||
imports.add((x.__module__, x.__name__))
|
|
||||||
if is_multiple:
|
|
||||||
ans = 'typing.Dict[str, str]'
|
|
||||||
return ans
|
|
||||||
|
|
||||||
def option_type_as_str(x: Any) -> str:
|
|
||||||
if hasattr(x, '__name__'):
|
|
||||||
return type_name(x)
|
|
||||||
ans = repr(x)
|
|
||||||
ans = ans.replace('NoneType', 'None')
|
|
||||||
return ans
|
|
||||||
|
|
||||||
if type(self.option_type) is type:
|
|
||||||
return type_name(self.option_type)
|
|
||||||
if isinstance(self.option_type, Choice):
|
|
||||||
return 'typing.Literal[{}]'.format(','.join(f'{x!r}' for x in self.option_type.all_choices))
|
|
||||||
th = get_type_hints(self.option_type)
|
|
||||||
try:
|
|
||||||
rettype = th['return']
|
|
||||||
except KeyError:
|
|
||||||
raise ValueError('The Option {} has an unknown option_type: {}'.format(self.name, self.option_type))
|
|
||||||
return option_type_as_str(rettype)
|
|
||||||
|
|
||||||
|
|
||||||
class Shortcut:
|
|
||||||
|
|
||||||
__slots__ = 'name', 'group', 'key', 'action_def', 'short_text', 'long_text', 'add_to_default', 'add_to_docs', 'line'
|
|
||||||
|
|
||||||
def __init__(self, name: str, group: Group, key: str, action_def: str, short_text: str, long_text: str, add_to_default: bool, add_to_docs: bool):
|
|
||||||
self.name, self.group, self.key, self.action_def = name, group, key, action_def
|
|
||||||
self.short_text, self.long_text = short_text, long_text
|
|
||||||
self.add_to_default = add_to_default
|
|
||||||
self.add_to_docs = add_to_docs
|
|
||||||
self.line = 'map ' + self.key + ' ' + self.action_def
|
|
||||||
|
|
||||||
|
|
||||||
def option(
|
|
||||||
all_options: Dict[str, Option],
|
|
||||||
group: Sequence[Group],
|
|
||||||
name: str,
|
|
||||||
defval: Any,
|
|
||||||
long_text: str = '',
|
|
||||||
option_type: Callable[[str], Any] = to_string,
|
|
||||||
add_to_default: bool = True,
|
|
||||||
add_to_docs: bool = True
|
|
||||||
) -> Option:
|
|
||||||
is_multiple = name.startswith('+')
|
|
||||||
if is_multiple:
|
|
||||||
name = name[1:]
|
|
||||||
defval_type = type(defval)
|
|
||||||
if defval_type is not str:
|
|
||||||
if option_type is to_string:
|
|
||||||
if defval_type is bool:
|
|
||||||
option_type = to_bool
|
|
||||||
else:
|
|
||||||
option_type = defval_type
|
|
||||||
if defval_type is bool:
|
|
||||||
defval = 'yes' if defval else 'no'
|
|
||||||
else:
|
|
||||||
defval = str(defval)
|
|
||||||
|
|
||||||
key = name
|
|
||||||
if is_multiple:
|
|
||||||
key = name + ' ' + defval.partition(' ')[0]
|
|
||||||
ans = Option(name, group[0], defval, option_type, long_text, add_to_default, add_to_docs)
|
|
||||||
all_options[key] = ans
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
def shortcut(
|
|
||||||
all_options: Dict[str, List[Shortcut]],
|
|
||||||
group: Sequence[Group],
|
|
||||||
action_name: str,
|
|
||||||
key: str,
|
|
||||||
action_def: str,
|
|
||||||
short_text: str = '',
|
|
||||||
long_text: str = '',
|
|
||||||
add_to_default: bool = True,
|
|
||||||
add_to_docs: bool = True
|
|
||||||
) -> Shortcut:
|
|
||||||
ans = Shortcut(action_name, group[0], key, action_def, short_text, long_text, add_to_default, add_to_docs)
|
|
||||||
key = 'sc-' + action_name
|
|
||||||
all_options.setdefault(key, []).append(ans)
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
def option_func(all_options: Dict[str, Any], all_groups: Dict[str, Sequence[str]]) -> Tuple[Callable, Callable, Callable[[str], None], Dict[str, Group]]:
|
|
||||||
all_groups_ = {k: Group(k, *v) for k, v in all_groups.items()}
|
|
||||||
group: List[Optional[Group]] = [None]
|
|
||||||
|
|
||||||
def change_group(name: str) -> None:
|
|
||||||
group[0] = all_groups_[name]
|
|
||||||
|
|
||||||
return partial(option, all_options, group), partial(shortcut, all_options, group), change_group, all_groups_
|
|
||||||
|
|
||||||
|
|
||||||
def merged_opts(all_options: Sequence[Union[Option, Sequence[Shortcut]]], opt: Option, i: int) -> Generator[Option, None, None]:
|
|
||||||
yield opt
|
|
||||||
for k in range(i + 1, len(all_options)):
|
|
||||||
q = all_options[k]
|
|
||||||
if not isinstance(q, Option):
|
|
||||||
break
|
|
||||||
if not q.long_text and q.add_to_docs:
|
|
||||||
yield q
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def remove_markup(text: str) -> str:
|
|
||||||
|
|
||||||
def sub(m: Match) -> str:
|
|
||||||
if m.group(1) == 'ref':
|
|
||||||
return {
|
|
||||||
'layouts': 'https://sw.kovidgoyal.net/kitty/index.html#layouts',
|
|
||||||
'sessions': 'https://sw.kovidgoyal.net/kitty/index.html#sessions',
|
|
||||||
'functional': 'https://sw.kovidgoyal.net/kitty/keyboard-protocol.html#functional-key-definitions',
|
|
||||||
}[m.group(2)]
|
|
||||||
return str(m.group(2))
|
|
||||||
|
|
||||||
return re.sub(r':([a-zA-Z0-9]+):`(.+?)`', sub, text, flags=re.DOTALL)
|
|
||||||
|
|
||||||
|
|
||||||
def iter_blocks(lines: Iterable[str]) -> Generator[Tuple[List[str], int], None, None]:
|
|
||||||
current_block: List[str] = []
|
|
||||||
prev_indent = 0
|
|
||||||
for line in lines:
|
|
||||||
indent_size = len(line) - len(line.lstrip())
|
|
||||||
if indent_size != prev_indent or not line:
|
|
||||||
if current_block:
|
|
||||||
yield current_block, prev_indent
|
|
||||||
current_block = []
|
|
||||||
prev_indent = indent_size
|
|
||||||
if not line:
|
|
||||||
yield [''], 100
|
|
||||||
else:
|
|
||||||
current_block.append(line)
|
|
||||||
if current_block:
|
|
||||||
yield current_block, indent_size
|
|
||||||
|
|
||||||
|
|
||||||
def wrapped_block(lines: Iterable[str]) -> Generator[str, None, None]:
|
|
||||||
wrapper = getattr(wrapped_block, 'wrapper', None)
|
|
||||||
if wrapper is None:
|
|
||||||
import textwrap
|
|
||||||
wrapper = textwrap.TextWrapper(
|
|
||||||
initial_indent='#: ', subsequent_indent='#: ', width=70, break_long_words=False
|
|
||||||
)
|
|
||||||
setattr(wrapped_block, 'wrapper', wrapper)
|
|
||||||
for block, indent_size in iter_blocks(lines):
|
|
||||||
if indent_size > 0:
|
|
||||||
for line in block:
|
|
||||||
if not line:
|
|
||||||
yield line
|
|
||||||
else:
|
|
||||||
yield '#: ' + line
|
|
||||||
else:
|
|
||||||
for line in wrapper.wrap('\n'.join(block)):
|
|
||||||
yield line
|
|
||||||
|
|
||||||
|
|
||||||
def render_block(text: str) -> str:
|
|
||||||
text = remove_markup(text)
|
|
||||||
lines = text.splitlines()
|
|
||||||
return '\n'.join(wrapped_block(lines))
|
|
||||||
|
|
||||||
|
|
||||||
def as_conf_file(all_options: Iterable[Union[Option, Sequence[Shortcut]]]) -> List[str]:
|
|
||||||
ans = ['# vim:fileencoding=utf-8:ft=conf:foldmethod=marker', '']
|
|
||||||
a = ans.append
|
|
||||||
current_group: Optional[Group] = None
|
|
||||||
num_open_folds = 0
|
|
||||||
all_options_ = list(all_options)
|
|
||||||
|
|
||||||
def render_group(group: Group, is_shortcut: bool) -> None:
|
|
||||||
nonlocal num_open_folds
|
|
||||||
if is_shortcut or '.' not in group.name:
|
|
||||||
a('#: ' + group.short_text + ' {{''{')
|
|
||||||
num_open_folds += 1
|
|
||||||
a('')
|
|
||||||
if group.start_text:
|
|
||||||
a(render_block(group.start_text))
|
|
||||||
a('')
|
|
||||||
|
|
||||||
def handle_group_end(group: Group, new_group_name: str = '', new_group_is_shortcut: bool = False) -> None:
|
|
||||||
nonlocal num_open_folds
|
|
||||||
if group.end_text:
|
|
||||||
a(''), a(render_block(group.end_text))
|
|
||||||
is_subgroup = new_group_name.startswith(group.name + '.')
|
|
||||||
if not is_subgroup and num_open_folds > 0:
|
|
||||||
a('#: }}''}'), a('')
|
|
||||||
num_open_folds -= 1
|
|
||||||
|
|
||||||
def handle_group(new_group: Group, is_shortcut: bool = False) -> None:
|
|
||||||
nonlocal current_group
|
|
||||||
if new_group is not current_group:
|
|
||||||
if current_group:
|
|
||||||
handle_group_end(current_group, new_group.name, is_shortcut)
|
|
||||||
current_group = new_group
|
|
||||||
render_group(current_group, is_shortcut)
|
|
||||||
|
|
||||||
def handle_shortcut(shortcuts: Sequence[Shortcut]) -> None:
|
|
||||||
handle_group(shortcuts[0].group, True)
|
|
||||||
for sc in shortcuts:
|
|
||||||
if sc.add_to_default:
|
|
||||||
a('map {} {}'.format(sc.key, sc.action_def))
|
|
||||||
if sc.long_text:
|
|
||||||
a(''), a(render_block(sc.long_text.strip())), a('')
|
|
||||||
|
|
||||||
def handle_option(opt: Option) -> None:
|
|
||||||
if not opt.long_text or not opt.add_to_docs:
|
|
||||||
return
|
|
||||||
handle_group(opt.group)
|
|
||||||
mopts = list(merged_opts(all_options_, opt, i))
|
|
||||||
sz = max(len(x.name) for x in mopts)
|
|
||||||
for mo in mopts:
|
|
||||||
prefix = '' if mo.add_to_default else '# '
|
|
||||||
a('{}{} {}'.format(prefix, mo.name.ljust(sz), mo.defval_as_string))
|
|
||||||
a('')
|
|
||||||
a(render_block(opt.long_text))
|
|
||||||
a('')
|
|
||||||
|
|
||||||
for i, opt in enumerate(all_options_):
|
|
||||||
if isinstance(opt, Option):
|
|
||||||
handle_option(opt)
|
|
||||||
else:
|
|
||||||
handle_shortcut(opt)
|
|
||||||
|
|
||||||
if current_group:
|
|
||||||
handle_group_end(current_group)
|
|
||||||
while num_open_folds > 0:
|
|
||||||
a('# }}''}')
|
|
||||||
num_open_folds -= 1
|
|
||||||
|
|
||||||
map_groups = []
|
|
||||||
start: Optional[int] = None
|
|
||||||
count: Optional[int] = None
|
|
||||||
for i, line in enumerate(ans):
|
|
||||||
if line.startswith('map '):
|
|
||||||
if start is None:
|
|
||||||
start = i
|
|
||||||
count = 1
|
|
||||||
else:
|
|
||||||
if count is not None:
|
|
||||||
count += 1
|
|
||||||
else:
|
|
||||||
if start is not None and count is not None:
|
|
||||||
map_groups.append((start, count))
|
|
||||||
start = count = None
|
|
||||||
for start, count in map_groups:
|
|
||||||
r = range(start, start + count)
|
|
||||||
sz = max(len(ans[i].split(' ', 3)[1]) for i in r)
|
|
||||||
for i in r:
|
|
||||||
line = ans[i]
|
|
||||||
parts = line.split(' ', 3)
|
|
||||||
parts[1] = parts[1].ljust(sz)
|
|
||||||
ans[i] = ' '.join(parts)
|
|
||||||
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
def config_lines(
|
|
||||||
all_options: Dict[str, Union[Option, Sequence[Shortcut]]],
|
|
||||||
) -> Generator[str, None, None]:
|
|
||||||
for opt in all_options.values():
|
|
||||||
if isinstance(opt, Option):
|
|
||||||
if opt.add_to_default:
|
|
||||||
yield opt.line
|
|
||||||
else:
|
|
||||||
for sc in opt:
|
|
||||||
if sc.add_to_default:
|
|
||||||
yield sc.line
|
|
||||||
|
|
||||||
|
|
||||||
def as_type_stub(
|
|
||||||
all_options: Dict[str, Union[Option, Sequence[Shortcut]]],
|
|
||||||
special_types: Optional[Dict[str, str]] = None,
|
|
||||||
preamble_lines: Union[Tuple[str, ...], List[str], Iterable[str]] = (),
|
|
||||||
extra_fields: Union[Tuple[Tuple[str, str], ...], List[Tuple[str, str]], Iterable[Tuple[str, str]]] = (),
|
|
||||||
class_name: str = 'Options'
|
|
||||||
) -> str:
|
|
||||||
ans = ['import typing\n'] + list(preamble_lines) + ['', 'class {}:'.format(class_name)]
|
|
||||||
imports: Set[Tuple[str, str]] = set()
|
|
||||||
overrides = special_types or {}
|
|
||||||
for name, val in all_options.items():
|
|
||||||
if isinstance(val, Option):
|
|
||||||
is_multiple = ' ' in name
|
|
||||||
field_name = name.partition(' ')[0]
|
|
||||||
ans.append(' {}: {}'.format(field_name, overrides.get(field_name, val.type_definition(is_multiple, imports))))
|
|
||||||
for mod, name in imports:
|
|
||||||
ans.insert(0, 'from {} import {}'.format(mod, name))
|
|
||||||
ans.insert(0, 'import {}'.format(mod))
|
|
||||||
for field_name, type_def in extra_fields:
|
|
||||||
ans.append(' {}: {}'.format(field_name, type_def))
|
|
||||||
ans.append(' def __iter__(self) -> typing.Iterator[str]: pass')
|
|
||||||
ans.append(' def __len__(self) -> int: pass')
|
|
||||||
ans.append(' def __getitem__(self, k: typing.Union[int, str]) -> typing.Any: pass')
|
|
||||||
ans.append(' def _replace(self, **kw: typing.Any) -> {}: pass'.format(class_name))
|
|
||||||
return '\n'.join(ans) + '\n\n\n'
|
|
||||||
|
|
||||||
|
|
||||||
def save_type_stub(text: str, fpath: str) -> None:
|
|
||||||
fpath += 'i'
|
|
||||||
preamble = '# Update this file by running: ./test.py mypy\n\n'
|
|
||||||
try:
|
|
||||||
existing = open(fpath).read()
|
|
||||||
except FileNotFoundError:
|
|
||||||
existing = ''
|
|
||||||
current = preamble + text
|
|
||||||
if existing != current:
|
|
||||||
open(fpath, 'w').write(current)
|
|
||||||
649
kitty/conf/types.py
Normal file
649
kitty/conf/types.py
Normal file
@@ -0,0 +1,649 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
import builtins
|
||||||
|
import re
|
||||||
|
import typing
|
||||||
|
from importlib import import_module
|
||||||
|
from typing import (
|
||||||
|
Any, Callable, Dict, Iterable, Iterator, List, Match, Optional, Set, Tuple,
|
||||||
|
Union, cast
|
||||||
|
)
|
||||||
|
|
||||||
|
import kitty.conf.utils as generic_parsers
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
Only = typing.Literal['macos', 'linux', '']
|
||||||
|
else:
|
||||||
|
Only = str
|
||||||
|
|
||||||
|
|
||||||
|
class Unset:
|
||||||
|
def __bool__(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
unset = Unset()
|
||||||
|
|
||||||
|
|
||||||
|
def expand_opt_references(conf_name: str, text: str) -> str:
|
||||||
|
conf_name += '.'
|
||||||
|
|
||||||
|
def expand(m: Match) -> str:
|
||||||
|
ref = m.group(1)
|
||||||
|
if '<' not in ref and '.' not in ref:
|
||||||
|
full_ref = conf_name + ref
|
||||||
|
return ':opt:`{} <{}>`'.format(ref, full_ref)
|
||||||
|
return str(m.group())
|
||||||
|
|
||||||
|
return re.sub(r':opt:`(.+?)`', expand, text)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_markup(text: str) -> str:
|
||||||
|
|
||||||
|
def sub(m: Match) -> str:
|
||||||
|
if m.group(1) == 'ref':
|
||||||
|
return {
|
||||||
|
'layouts': 'https://sw.kovidgoyal.net/kitty/index.html#layouts',
|
||||||
|
'sessions': 'https://sw.kovidgoyal.net/kitty/index.html#sessions',
|
||||||
|
'functional': 'https://sw.kovidgoyal.net/kitty/keyboard-protocol.html#functional-key-definitions',
|
||||||
|
}[m.group(2)]
|
||||||
|
return str(m.group(2))
|
||||||
|
|
||||||
|
return re.sub(r':([a-zA-Z0-9]+):`(.+?)`', sub, text, flags=re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
|
def iter_blocks(lines: Iterable[str]) -> Iterator[Tuple[List[str], int]]:
|
||||||
|
current_block: List[str] = []
|
||||||
|
prev_indent = 0
|
||||||
|
for line in lines:
|
||||||
|
indent_size = len(line) - len(line.lstrip())
|
||||||
|
if indent_size != prev_indent or not line:
|
||||||
|
if current_block:
|
||||||
|
yield current_block, prev_indent
|
||||||
|
current_block = []
|
||||||
|
prev_indent = indent_size
|
||||||
|
if not line:
|
||||||
|
yield [''], 100
|
||||||
|
else:
|
||||||
|
current_block.append(line)
|
||||||
|
if current_block:
|
||||||
|
yield current_block, indent_size
|
||||||
|
|
||||||
|
|
||||||
|
def wrapped_block(lines: Iterable[str]) -> Iterator[str]:
|
||||||
|
wrapper = getattr(wrapped_block, 'wrapper', None)
|
||||||
|
if wrapper is None:
|
||||||
|
import textwrap
|
||||||
|
wrapper = textwrap.TextWrapper(
|
||||||
|
initial_indent='#: ', subsequent_indent='#: ', width=70, break_long_words=False
|
||||||
|
)
|
||||||
|
setattr(wrapped_block, 'wrapper', wrapper)
|
||||||
|
for block, indent_size in iter_blocks(lines):
|
||||||
|
if indent_size > 0:
|
||||||
|
for line in block:
|
||||||
|
if not line:
|
||||||
|
yield line
|
||||||
|
else:
|
||||||
|
yield '#: ' + line
|
||||||
|
else:
|
||||||
|
for line in wrapper.wrap('\n'.join(block)):
|
||||||
|
yield line
|
||||||
|
|
||||||
|
|
||||||
|
def render_block(text: str) -> str:
|
||||||
|
text = remove_markup(text)
|
||||||
|
lines = text.splitlines()
|
||||||
|
return '\n'.join(wrapped_block(lines))
|
||||||
|
|
||||||
|
|
||||||
|
class CoalescedIteratorData:
|
||||||
|
|
||||||
|
option_groups: Dict[int, List['Option']] = {}
|
||||||
|
action_groups: Dict[str, List['Mapping']] = {}
|
||||||
|
coalesced: Set[int] = set()
|
||||||
|
initialized: bool = False
|
||||||
|
kitty_mod: str = 'kitty_mod'
|
||||||
|
|
||||||
|
def initialize(self, root: 'Group') -> None:
|
||||||
|
if self.initialized:
|
||||||
|
return
|
||||||
|
self.root = root
|
||||||
|
option_groups = self.option_groups = {}
|
||||||
|
current_group: List[Option] = []
|
||||||
|
action_groups: Dict[str, List[Mapping]] = {}
|
||||||
|
self.action_groups = action_groups
|
||||||
|
coalesced = self.coalesced = set()
|
||||||
|
self.kitty_mod = 'kitty_mod'
|
||||||
|
for item in root.iter_all_non_groups():
|
||||||
|
if isinstance(item, Option):
|
||||||
|
if item.name == 'kitty_mod':
|
||||||
|
self.kitty_mod = item.defval_as_string
|
||||||
|
if current_group:
|
||||||
|
if item.needs_coalescing:
|
||||||
|
current_group.append(item)
|
||||||
|
coalesced.add(id(item))
|
||||||
|
continue
|
||||||
|
option_groups[id(current_group[0])] = current_group[1:]
|
||||||
|
current_group = [item]
|
||||||
|
else:
|
||||||
|
current_group.append(item)
|
||||||
|
elif isinstance(item, Mapping):
|
||||||
|
if item.name in action_groups:
|
||||||
|
coalesced.add(id(item))
|
||||||
|
action_groups[item.name].append(item)
|
||||||
|
else:
|
||||||
|
action_groups[item.name] = []
|
||||||
|
if current_group:
|
||||||
|
option_groups[id(current_group[0])] = current_group[1:]
|
||||||
|
|
||||||
|
def option_group_for_option(self, opt: 'Option') -> List['Option']:
|
||||||
|
return self.option_groups.get(id(opt), [])
|
||||||
|
|
||||||
|
def action_group_for_action(self, ac: 'Mapping') -> List['Mapping']:
|
||||||
|
return self.action_groups.get(ac.name, [])
|
||||||
|
|
||||||
|
|
||||||
|
class Option:
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, name: str, defval: str, macos_default: Union[Unset, str], parser_func: Callable,
|
||||||
|
long_text: str, documented: bool, group: 'Group', choices: Tuple[str, ...], ctype: str
|
||||||
|
):
|
||||||
|
self.name = name
|
||||||
|
self.ctype = ctype
|
||||||
|
self.defval_as_string = defval
|
||||||
|
self.macos_defval = macos_default
|
||||||
|
self.long_text = long_text
|
||||||
|
self.documented = documented
|
||||||
|
self.group = group
|
||||||
|
self.parser_func = parser_func
|
||||||
|
self.choices = choices
|
||||||
|
|
||||||
|
@property
|
||||||
|
def needs_coalescing(self) -> bool:
|
||||||
|
return self.documented and not self.long_text
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_color_table_color(self) -> bool:
|
||||||
|
return self.name.startswith('color') and self.name[5:].isdigit()
|
||||||
|
|
||||||
|
def as_conf(self, commented: bool = False, level: int = 0, option_group: List['Option'] = []) -> List[str]:
|
||||||
|
ans: List[str] = []
|
||||||
|
a = ans.append
|
||||||
|
if not self.documented:
|
||||||
|
return ans
|
||||||
|
if option_group:
|
||||||
|
sz = max(len(self.name), max(len(o.name) for o in option_group))
|
||||||
|
a(f'{self.name.ljust(sz)} {self.defval_as_string}')
|
||||||
|
for o in option_group:
|
||||||
|
a(f'{o.name.ljust(sz)} {o.defval_as_string}')
|
||||||
|
else:
|
||||||
|
a(f'{self.name} {self.defval_as_string}')
|
||||||
|
if self.long_text:
|
||||||
|
a('')
|
||||||
|
a(render_block(self.long_text))
|
||||||
|
a('')
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def as_rst(
|
||||||
|
self, conf_name: str, shortcut_slugs: Dict[str, Tuple[str, str]],
|
||||||
|
kitty_mod: str, level: int = 0, option_group: List['Option'] = []
|
||||||
|
) -> List[str]:
|
||||||
|
ans: List[str] = []
|
||||||
|
a = ans.append
|
||||||
|
if not self.documented:
|
||||||
|
return ans
|
||||||
|
mopts = [self] + option_group
|
||||||
|
a('.. opt:: ' + ', '.join(conf_name + '.' + mo.name for mo in mopts))
|
||||||
|
a('.. code-block:: conf')
|
||||||
|
a('')
|
||||||
|
sz = max(len(x.name) for x in mopts)
|
||||||
|
for mo in mopts:
|
||||||
|
a((' {:%ds} {}' % sz).format(mo.name, mo.defval_as_string))
|
||||||
|
a('')
|
||||||
|
if self.long_text:
|
||||||
|
a(expand_opt_references(conf_name, self.long_text))
|
||||||
|
a('')
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
class MultiVal:
|
||||||
|
|
||||||
|
def __init__(self, val_as_str: str, add_to_default: bool, documented: bool, only: Only) -> None:
|
||||||
|
self.defval_as_str = val_as_str
|
||||||
|
self.documented = documented
|
||||||
|
self.only = only
|
||||||
|
self.add_to_default = add_to_default
|
||||||
|
|
||||||
|
|
||||||
|
class MultiOption:
|
||||||
|
|
||||||
|
def __init__(self, name: str, parser_func: Callable, long_text: str, group: 'Group', ctype: str):
|
||||||
|
self.name = name
|
||||||
|
self.ctype = ctype
|
||||||
|
self.parser_func = parser_func
|
||||||
|
self.long_text = long_text
|
||||||
|
self.group = group
|
||||||
|
self.items: List[MultiVal] = []
|
||||||
|
|
||||||
|
def add_value(self, val_as_str: str, add_to_default: bool, documented: bool, only: Only) -> None:
|
||||||
|
self.items.append(MultiVal(val_as_str, add_to_default, documented, only))
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[MultiVal]:
|
||||||
|
yield from self.items
|
||||||
|
|
||||||
|
def as_conf(self, commented: bool = False, level: int = 0) -> List[str]:
|
||||||
|
ans: List[str] = []
|
||||||
|
a = ans.append
|
||||||
|
for k in self.items:
|
||||||
|
if k.documented:
|
||||||
|
prefix = '' if k.add_to_default else '# '
|
||||||
|
a(f'{prefix}{self.name} {k.defval_as_str}')
|
||||||
|
if self.long_text:
|
||||||
|
a('')
|
||||||
|
a(render_block(self.long_text))
|
||||||
|
a('')
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def as_rst(self, conf_name: str, shortcut_slugs: Dict[str, Tuple[str, str]], kitty_mod: str, level: int = 0) -> List[str]:
|
||||||
|
ans: List[str] = []
|
||||||
|
a = ans.append
|
||||||
|
a(f'.. opt:: {conf_name}.{self.name}')
|
||||||
|
a('.. code-block:: conf')
|
||||||
|
a('')
|
||||||
|
for k in self.items:
|
||||||
|
if k.documented:
|
||||||
|
a(f' {self.name:s} {k.defval_as_str}')
|
||||||
|
a('')
|
||||||
|
if self.long_text:
|
||||||
|
a(expand_opt_references(conf_name, self.long_text))
|
||||||
|
a('')
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
class Mapping:
|
||||||
|
add_to_default: bool
|
||||||
|
short_text: str
|
||||||
|
long_text: str
|
||||||
|
documented: bool
|
||||||
|
setting_name: str
|
||||||
|
name: str
|
||||||
|
only: Only
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parseable_text(self) -> str:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@property
|
||||||
|
def key_text(self) -> str:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def as_conf(self, commented: bool = False, level: int = 0) -> List[str]:
|
||||||
|
ans: List[str] = []
|
||||||
|
if self.documented:
|
||||||
|
a = ans.append
|
||||||
|
if self.add_to_default:
|
||||||
|
a(self.setting_name + ' ' + self.parseable_text)
|
||||||
|
if self.long_text:
|
||||||
|
a(''), a(render_block(self.long_text.strip())), a('')
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def as_rst(
|
||||||
|
self, conf_name: str, shortcut_slugs: Dict[str, Tuple[str, str]],
|
||||||
|
kitty_mod: str, level: int = 0, action_group: List['Mapping'] = []
|
||||||
|
) -> List[str]:
|
||||||
|
ans: List[str] = []
|
||||||
|
a = ans.append
|
||||||
|
if not self.documented:
|
||||||
|
return ans
|
||||||
|
if not self.short_text:
|
||||||
|
raise ValueError(f'The shortcut for {self.name} has no short_text')
|
||||||
|
sc_text = f'{conf_name}.{self.short_text}'
|
||||||
|
shortcut_slugs[f'{conf_name}.{self.name}'] = (sc_text, self.key_text.replace('kitty_mod', kitty_mod))
|
||||||
|
a('.. shortcut:: ' + sc_text)
|
||||||
|
block_started = False
|
||||||
|
for sc in [self] + action_group:
|
||||||
|
if sc.add_to_default and sc.documented:
|
||||||
|
if not block_started:
|
||||||
|
a('.. code-block:: conf')
|
||||||
|
a('')
|
||||||
|
block_started = True
|
||||||
|
suffix = ''
|
||||||
|
if sc.only == 'macos':
|
||||||
|
suffix = ' 🍎'
|
||||||
|
elif sc.only == 'linux':
|
||||||
|
suffix = ' 🐧'
|
||||||
|
a(f' {sc.setting_name} {sc.parseable_text.replace("kitty_mod", kitty_mod)}{suffix}')
|
||||||
|
a('')
|
||||||
|
if self.long_text:
|
||||||
|
a('')
|
||||||
|
a(expand_opt_references(conf_name, self.long_text))
|
||||||
|
a('')
|
||||||
|
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
class ShortcutMapping(Mapping):
|
||||||
|
setting_name: str = 'map'
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, name: str, key: str, action_def: str, short_text: str, long_text: str, add_to_default: bool, documented: bool, group: 'Group', only: Only
|
||||||
|
):
|
||||||
|
self.name = name
|
||||||
|
self.only = only
|
||||||
|
self.key = key
|
||||||
|
self.action_def = action_def
|
||||||
|
self.short_text = short_text
|
||||||
|
self.long_text = long_text
|
||||||
|
self.documented = documented
|
||||||
|
self.add_to_default = add_to_default
|
||||||
|
self.group = group
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parseable_text(self) -> str:
|
||||||
|
return f'{self.key} {self.action_def}'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def key_text(self) -> str:
|
||||||
|
return self.key
|
||||||
|
|
||||||
|
|
||||||
|
class MouseMapping(Mapping):
|
||||||
|
setting_name: str = 'mouse_map'
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, name: str, button: str, event: str, modes: str, action_def: str,
|
||||||
|
short_text: str, long_text: str, add_to_default: bool, documented: bool, group: 'Group', only: Only
|
||||||
|
):
|
||||||
|
self.name = name
|
||||||
|
self.only = only
|
||||||
|
self.button = button
|
||||||
|
self.event = event
|
||||||
|
self.modes = modes
|
||||||
|
self.action_def = action_def
|
||||||
|
self.short_text = short_text
|
||||||
|
self.long_text = long_text
|
||||||
|
self.documented = documented
|
||||||
|
self.add_to_default = add_to_default
|
||||||
|
self.group = group
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parseable_text(self) -> str:
|
||||||
|
return f'{self.button} {self.event} {self.modes} {self.action_def}'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def key_text(self) -> str:
|
||||||
|
return self.button
|
||||||
|
|
||||||
|
|
||||||
|
NonGroups = Union[Option, MultiOption, ShortcutMapping, MouseMapping]
|
||||||
|
GroupItem = Union[NonGroups, 'Group']
|
||||||
|
|
||||||
|
|
||||||
|
class Group:
|
||||||
|
|
||||||
|
def __init__(self, name: str, title: str, coalesced_iterator_data: CoalescedIteratorData, start_text: str = '', parent: Optional['Group'] = None):
|
||||||
|
self.name = name
|
||||||
|
self.coalesced_iterator_data = coalesced_iterator_data
|
||||||
|
self.title = title
|
||||||
|
self.start_text = start_text
|
||||||
|
self.end_text = ''
|
||||||
|
self.items: List[GroupItem] = []
|
||||||
|
self.parent = parent
|
||||||
|
|
||||||
|
def append(self, item: GroupItem) -> None:
|
||||||
|
self.items.append(item)
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[GroupItem]:
|
||||||
|
return iter(self.items)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self.items)
|
||||||
|
|
||||||
|
def iter_with_coalesced_options(self) -> Iterator[GroupItem]:
|
||||||
|
for item in self:
|
||||||
|
if id(item) not in self.coalesced_iterator_data.coalesced:
|
||||||
|
yield item
|
||||||
|
|
||||||
|
def iter_all(self) -> Iterator[GroupItem]:
|
||||||
|
for x in self:
|
||||||
|
yield x
|
||||||
|
if isinstance(x, Group):
|
||||||
|
yield from x.iter_all()
|
||||||
|
|
||||||
|
def iter_all_non_groups(self) -> Iterator[NonGroups]:
|
||||||
|
for x in self:
|
||||||
|
if isinstance(x, Group):
|
||||||
|
yield from x.iter_all_non_groups()
|
||||||
|
else:
|
||||||
|
yield x
|
||||||
|
|
||||||
|
def as_rst(self, conf_name: str, shortcut_slugs: Dict[str, Tuple[str, str]], kitty_mod: str = 'kitty_mod', level: int = 0) -> List[str]:
|
||||||
|
ans: List[str] = []
|
||||||
|
a = ans.append
|
||||||
|
if level:
|
||||||
|
a('')
|
||||||
|
a(f'.. _conf-{conf_name}-{self.name}:')
|
||||||
|
a('')
|
||||||
|
a(self.title)
|
||||||
|
heading_level = '+' if level > 1 else '^'
|
||||||
|
a(heading_level * (len(self.title) + 20))
|
||||||
|
a('')
|
||||||
|
if self.start_text:
|
||||||
|
a(self.start_text)
|
||||||
|
a('')
|
||||||
|
else:
|
||||||
|
ans.extend(('.. default-domain:: conf', ''))
|
||||||
|
|
||||||
|
kitty_mod = self.coalesced_iterator_data.kitty_mod
|
||||||
|
for item in self.iter_with_coalesced_options():
|
||||||
|
if isinstance(item, Option):
|
||||||
|
lines = item.as_rst(conf_name, shortcut_slugs, kitty_mod, option_group=self.coalesced_iterator_data.option_group_for_option(item))
|
||||||
|
elif isinstance(item, Mapping):
|
||||||
|
lines = item.as_rst(conf_name, shortcut_slugs, kitty_mod, level + 1, action_group=self.coalesced_iterator_data.action_group_for_action(item))
|
||||||
|
else:
|
||||||
|
lines = item.as_rst(conf_name, shortcut_slugs, kitty_mod, level + 1)
|
||||||
|
ans.extend(lines)
|
||||||
|
|
||||||
|
if level:
|
||||||
|
if self.end_text:
|
||||||
|
a('')
|
||||||
|
a(self.end_text)
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def as_conf(self, commented: bool = False, level: int = 0) -> List[str]:
|
||||||
|
ans: List[str] = []
|
||||||
|
a = ans.append
|
||||||
|
if level:
|
||||||
|
a('#: ' + self.title + ' {{''{')
|
||||||
|
a('')
|
||||||
|
if self.start_text:
|
||||||
|
a(render_block(self.start_text))
|
||||||
|
a('')
|
||||||
|
else:
|
||||||
|
ans.extend(('# vim:fileencoding=utf-8:ft=conf:foldmethod=marker', ''))
|
||||||
|
|
||||||
|
for item in self.iter_with_coalesced_options():
|
||||||
|
if isinstance(item, Option):
|
||||||
|
lines = item.as_conf(option_group=self.coalesced_iterator_data.option_group_for_option(item))
|
||||||
|
else:
|
||||||
|
lines = item.as_conf(commented, level + 1)
|
||||||
|
ans.extend(lines)
|
||||||
|
|
||||||
|
if level:
|
||||||
|
if self.end_text:
|
||||||
|
a('')
|
||||||
|
a(render_block(self.end_text))
|
||||||
|
a('#: }}''}')
|
||||||
|
a('')
|
||||||
|
else:
|
||||||
|
map_groups = []
|
||||||
|
start: Optional[int] = None
|
||||||
|
count: Optional[int] = None
|
||||||
|
for i, line in enumerate(ans):
|
||||||
|
if line.startswith('map ') or line.startswith('mouse_map '):
|
||||||
|
if start is None:
|
||||||
|
start = i
|
||||||
|
count = 1
|
||||||
|
else:
|
||||||
|
if count is not None:
|
||||||
|
count += 1
|
||||||
|
else:
|
||||||
|
if start is not None and count is not None:
|
||||||
|
map_groups.append((start, count))
|
||||||
|
start = count = None
|
||||||
|
for start, count in map_groups:
|
||||||
|
r = range(start, start + count)
|
||||||
|
sz = max(len(ans[i].split(' ', 3)[1]) for i in r)
|
||||||
|
for i in r:
|
||||||
|
line = ans[i]
|
||||||
|
parts = line.split(' ', 3)
|
||||||
|
parts[1] = parts[1].ljust(sz)
|
||||||
|
ans[i] = ' '.join(parts)
|
||||||
|
|
||||||
|
if commented:
|
||||||
|
ans = [x if x.startswith('#') or not x.strip() else ('# ' + x) for x in ans]
|
||||||
|
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_import(name: str, module: Any = None) -> Callable:
|
||||||
|
ans = None
|
||||||
|
if name.count('.') > 1:
|
||||||
|
m = import_module(name.rpartition('.')[0])
|
||||||
|
ans = getattr(m, name.rpartition('.')[2])
|
||||||
|
else:
|
||||||
|
ans = getattr(builtins, name, None)
|
||||||
|
if not callable(ans):
|
||||||
|
ans = getattr(generic_parsers, name, None)
|
||||||
|
if not callable(ans):
|
||||||
|
ans = getattr(module, name)
|
||||||
|
if not callable(ans):
|
||||||
|
raise TypeError(f'{name} is not a function')
|
||||||
|
return cast(Callable, ans)
|
||||||
|
|
||||||
|
|
||||||
|
class Action:
|
||||||
|
|
||||||
|
def __init__(self, name: str, option_type: str, fields: Dict[str, str], imports: Iterable[str]):
|
||||||
|
self.name = name
|
||||||
|
self._parser_func = option_type
|
||||||
|
self.fields = fields
|
||||||
|
self.imports = frozenset(imports)
|
||||||
|
|
||||||
|
def resolve_imports(self, module: Any) -> 'Action':
|
||||||
|
self.parser_func = resolve_import(self._parser_func, module)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class Definition:
|
||||||
|
|
||||||
|
def __init__(self, package: str, *actions: Action, has_color_table: bool = False) -> None:
|
||||||
|
self.module_for_parsers = import_module(f'{package}.options.utils')
|
||||||
|
self.has_color_table = has_color_table
|
||||||
|
self.package = package
|
||||||
|
self.coalesced_iterator_data = CoalescedIteratorData()
|
||||||
|
self.root_group = Group('', '', self.coalesced_iterator_data)
|
||||||
|
self.current_group = self.root_group
|
||||||
|
self.option_map: Dict[str, Option] = {}
|
||||||
|
self.multi_option_map: Dict[str, MultiOption] = {}
|
||||||
|
self.shortcut_map: Dict[str, List[ShortcutMapping]] = {}
|
||||||
|
self.mouse_map: Dict[str, List[MouseMapping]] = {}
|
||||||
|
self.actions = {a.name: a.resolve_imports(self.module_for_parsers) for a in actions}
|
||||||
|
self.deprecations: Dict[Callable, Tuple[str, ...]] = {}
|
||||||
|
|
||||||
|
def iter_all_non_groups(self) -> Iterator[NonGroups]:
|
||||||
|
yield from self.root_group.iter_all_non_groups()
|
||||||
|
|
||||||
|
def iter_all_options(self) -> Iterator[Union[Option, MultiOption]]:
|
||||||
|
for x in self.iter_all_non_groups():
|
||||||
|
if isinstance(x, (Option, MultiOption)):
|
||||||
|
yield x
|
||||||
|
|
||||||
|
def iter_all_maps(self, which: str = 'map') -> Iterator[Union[ShortcutMapping, MouseMapping]]:
|
||||||
|
for x in self.iter_all_non_groups():
|
||||||
|
if isinstance(x, ShortcutMapping) and which == 'map':
|
||||||
|
yield x
|
||||||
|
elif isinstance(x, MouseMapping) and which == 'mouse_map':
|
||||||
|
yield x
|
||||||
|
|
||||||
|
def parser_func(self, name: str) -> Callable:
|
||||||
|
ans = getattr(builtins, name, None)
|
||||||
|
if callable(ans):
|
||||||
|
return cast(Callable, ans)
|
||||||
|
ans = getattr(generic_parsers, name, None)
|
||||||
|
if callable(ans):
|
||||||
|
return cast(Callable, ans)
|
||||||
|
ans = getattr(self.module_for_parsers, name)
|
||||||
|
if not callable(ans):
|
||||||
|
raise TypeError(f'{name} is not a function')
|
||||||
|
return cast(Callable, ans)
|
||||||
|
|
||||||
|
def add_group(self, name: str, title: str = '', start_text: str = '') -> None:
|
||||||
|
self.current_group = Group(name, title or name, self.coalesced_iterator_data, start_text.strip(), self.current_group)
|
||||||
|
if self.current_group.parent is not None:
|
||||||
|
self.current_group.parent.append(self.current_group)
|
||||||
|
|
||||||
|
def end_group(self, end_text: str = '') -> None:
|
||||||
|
self.current_group.end_text = end_text.strip()
|
||||||
|
if self.current_group.parent is not None:
|
||||||
|
self.current_group = self.current_group.parent
|
||||||
|
|
||||||
|
def add_option(
|
||||||
|
self, name: str, defval: Union[str, float, int, bool],
|
||||||
|
option_type: str = 'str', long_text: str = '',
|
||||||
|
documented: bool = True, add_to_default: bool = False,
|
||||||
|
only: Only = '', macos_default: Union[Unset, str] = unset,
|
||||||
|
choices: Tuple[str, ...] = (),
|
||||||
|
ctype: str = '',
|
||||||
|
) -> None:
|
||||||
|
if isinstance(defval, bool):
|
||||||
|
defval = 'yes' if defval else 'no'
|
||||||
|
else:
|
||||||
|
defval = str(defval)
|
||||||
|
is_multiple = name.startswith('+')
|
||||||
|
long_text = long_text.strip()
|
||||||
|
if is_multiple:
|
||||||
|
name = name[1:]
|
||||||
|
if macos_default is not unset:
|
||||||
|
raise TypeError(f'Cannot specify macos_default for is_multiple option: {name} use only instead')
|
||||||
|
is_new = name not in self.multi_option_map
|
||||||
|
if is_new:
|
||||||
|
self.multi_option_map[name] = MultiOption(name, self.parser_func(option_type), long_text, self.current_group, ctype)
|
||||||
|
mopt = self.multi_option_map[name]
|
||||||
|
if is_new:
|
||||||
|
self.current_group.append(mopt)
|
||||||
|
mopt.add_value(defval, add_to_default, documented, only)
|
||||||
|
return
|
||||||
|
opt = Option(name, defval, macos_default, self.parser_func(option_type), long_text, documented, self.current_group, choices, ctype)
|
||||||
|
self.current_group.append(opt)
|
||||||
|
self.option_map[name] = opt
|
||||||
|
|
||||||
|
def add_map(
|
||||||
|
self, short_text: str, defn: str, long_text: str = '', add_to_default: bool = True, documented: bool = True, only: Only = ''
|
||||||
|
) -> None:
|
||||||
|
name, key, action_def = defn.split(maxsplit=2)
|
||||||
|
sc = ShortcutMapping(name, key, action_def, short_text, long_text.strip(), add_to_default, documented, self.current_group, only)
|
||||||
|
self.current_group.append(sc)
|
||||||
|
self.shortcut_map.setdefault(name, []).append(sc)
|
||||||
|
|
||||||
|
def add_mouse_map(
|
||||||
|
self, short_text: str, defn: str, long_text: str = '', add_to_default: bool = True, documented: bool = True, only: Only = ''
|
||||||
|
) -> None:
|
||||||
|
name, button, event, modes, action_def = defn.split(maxsplit=4)
|
||||||
|
mm = MouseMapping(name, button, event, modes, action_def, short_text, long_text.strip(), add_to_default, documented, self.current_group, only)
|
||||||
|
self.current_group.append(mm)
|
||||||
|
self.mouse_map.setdefault(name, []).append(mm)
|
||||||
|
|
||||||
|
def add_deprecation(self, parser_name: str, *aliases: str) -> None:
|
||||||
|
self.deprecations[self.parser_func(parser_name)] = aliases
|
||||||
|
|
||||||
|
def as_conf(self, commented: bool = False) -> List[str]:
|
||||||
|
self.coalesced_iterator_data.initialize(self.root_group)
|
||||||
|
return self.root_group.as_conf(commented)
|
||||||
|
|
||||||
|
def as_rst(self, conf_name: str, shortcut_slugs: Dict[str, Tuple[str, str]]) -> List[str]:
|
||||||
|
self.coalesced_iterator_data.initialize(self.root_group)
|
||||||
|
return self.root_group.as_rst(conf_name, shortcut_slugs)
|
||||||
@@ -6,24 +6,40 @@ import os
|
|||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
from typing import (
|
from typing import (
|
||||||
Any, Callable, Dict, FrozenSet, Generator, Iterable, Iterator, List,
|
Any, Callable, Dict, Generator, Iterable, List, NamedTuple, Optional,
|
||||||
NamedTuple, Optional, Sequence, Tuple, Type, TypeVar, Union
|
Sequence, Set, Tuple, TypeVar, Union
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..rgb import Color, to_color as as_color
|
from ..rgb import Color, to_color as as_color
|
||||||
from ..types import ParsedShortcut, ConvertibleToNumbers
|
from ..types import ConvertibleToNumbers, ParsedShortcut
|
||||||
|
from ..typing import Protocol
|
||||||
from ..utils import expandvars, log_error
|
from ..utils import expandvars, log_error
|
||||||
|
|
||||||
key_pat = re.compile(r'([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$')
|
key_pat = re.compile(r'([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$')
|
||||||
|
ItemParser = Callable[[str, str, Dict[str, Any]], bool]
|
||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
|
||||||
|
class OptionsProtocol(Protocol):
|
||||||
|
|
||||||
|
def _asdict(self) -> Dict[str, Any]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BadLine(NamedTuple):
|
class BadLine(NamedTuple):
|
||||||
number: int
|
number: int
|
||||||
line: str
|
line: str
|
||||||
exception: Exception
|
exception: Exception
|
||||||
|
|
||||||
|
|
||||||
|
def positive_int(x: ConvertibleToNumbers) -> int:
|
||||||
|
return max(0, int(x))
|
||||||
|
|
||||||
|
|
||||||
|
def positive_float(x: ConvertibleToNumbers) -> float:
|
||||||
|
return max(0, float(x))
|
||||||
|
|
||||||
|
|
||||||
def to_color(x: str) -> Color:
|
def to_color(x: str) -> Color:
|
||||||
ans = as_color(x, validate=True)
|
ans = as_color(x, validate=True)
|
||||||
if ans is None: # this is only for type-checking
|
if ans is None: # this is only for type-checking
|
||||||
@@ -72,7 +88,11 @@ class ToCmdline:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
to_cmdline = ToCmdline()
|
to_cmdline_implementation = ToCmdline()
|
||||||
|
|
||||||
|
|
||||||
|
def to_cmdline(x: str) -> List[str]:
|
||||||
|
return to_cmdline_implementation(x)
|
||||||
|
|
||||||
|
|
||||||
def python_string(text: str) -> str:
|
def python_string(text: str) -> str:
|
||||||
@@ -90,7 +110,7 @@ class Choice:
|
|||||||
def __call__(self, x: str) -> str:
|
def __call__(self, x: str) -> str:
|
||||||
x = x.lower()
|
x = x.lower()
|
||||||
if x not in self.all_choices:
|
if x not in self.all_choices:
|
||||||
x = self.defval
|
raise ValueError(f'The value {x} is not a known choice')
|
||||||
return x
|
return x
|
||||||
|
|
||||||
|
|
||||||
@@ -100,10 +120,10 @@ def choices(*choices: str) -> Choice:
|
|||||||
|
|
||||||
def parse_line(
|
def parse_line(
|
||||||
line: str,
|
line: str,
|
||||||
type_convert: Callable[[str, Any], Any],
|
parse_conf_item: ItemParser,
|
||||||
special_handling: Callable,
|
ans: Dict[str, Any],
|
||||||
ans: Dict[str, Any], all_keys: Optional[FrozenSet[str]],
|
base_path_for_includes: str,
|
||||||
base_path_for_includes: str
|
accumulate_bad_lines: Optional[List[BadLine]] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if not line or line.startswith('#'):
|
if not line or line.startswith('#'):
|
||||||
@@ -113,15 +133,13 @@ def parse_line(
|
|||||||
log_error('Ignoring invalid config line: {}'.format(line))
|
log_error('Ignoring invalid config line: {}'.format(line))
|
||||||
return
|
return
|
||||||
key, val = m.groups()
|
key, val = m.groups()
|
||||||
if special_handling(key, val, ans):
|
|
||||||
return
|
|
||||||
if key == 'include':
|
if key == 'include':
|
||||||
val = os.path.expandvars(os.path.expanduser(val.strip()))
|
val = os.path.expandvars(os.path.expanduser(val.strip()))
|
||||||
if not os.path.isabs(val):
|
if not os.path.isabs(val):
|
||||||
val = os.path.join(base_path_for_includes, val)
|
val = os.path.join(base_path_for_includes, val)
|
||||||
try:
|
try:
|
||||||
with open(val, encoding='utf-8', errors='replace') as include:
|
with open(val, encoding='utf-8', errors='replace') as include:
|
||||||
_parse(include, type_convert, special_handling, ans, all_keys)
|
_parse(include, parse_conf_item, ans, accumulate_bad_lines)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
log_error(
|
log_error(
|
||||||
'Could not find included config file: {}, ignoring'.
|
'Could not find included config file: {}, ignoring'.
|
||||||
@@ -133,18 +151,14 @@ def parse_line(
|
|||||||
format(val)
|
format(val)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if all_keys is not None and key not in all_keys:
|
if not parse_conf_item(key, val, ans):
|
||||||
log_error('Ignoring unknown config key: {}'.format(key))
|
log_error('Ignoring unknown config key: {}'.format(key))
|
||||||
return
|
|
||||||
ans[key] = type_convert(key, val)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse(
|
def _parse(
|
||||||
lines: Iterable[str],
|
lines: Iterable[str],
|
||||||
type_convert: Callable[[str, Any], Any],
|
parse_conf_item: ItemParser,
|
||||||
special_handling: Callable,
|
|
||||||
ans: Dict[str, Any],
|
ans: Dict[str, Any],
|
||||||
all_keys: Optional[FrozenSet[str]],
|
|
||||||
accumulate_bad_lines: Optional[List[BadLine]] = None
|
accumulate_bad_lines: Optional[List[BadLine]] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
name = getattr(lines, 'name', None)
|
name = getattr(lines, 'name', None)
|
||||||
@@ -156,8 +170,7 @@ def _parse(
|
|||||||
for i, line in enumerate(lines):
|
for i, line in enumerate(lines):
|
||||||
try:
|
try:
|
||||||
parse_line(
|
parse_line(
|
||||||
line, type_convert, special_handling, ans, all_keys,
|
line, parse_conf_item, ans, base_path_for_includes, accumulate_bad_lines
|
||||||
base_path_for_includes
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if accumulate_bad_lines is None:
|
if accumulate_bad_lines is None:
|
||||||
@@ -167,64 +180,15 @@ def _parse(
|
|||||||
|
|
||||||
def parse_config_base(
|
def parse_config_base(
|
||||||
lines: Iterable[str],
|
lines: Iterable[str],
|
||||||
defaults: Any,
|
parse_conf_item: ItemParser,
|
||||||
type_convert: Callable[[str, Any], Any],
|
|
||||||
special_handling: Callable,
|
|
||||||
ans: Dict[str, Any],
|
ans: Dict[str, Any],
|
||||||
check_keys: bool = True,
|
|
||||||
accumulate_bad_lines: Optional[List[BadLine]] = None
|
accumulate_bad_lines: Optional[List[BadLine]] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
all_keys: Optional[FrozenSet[str]] = defaults._asdict() if check_keys else None
|
|
||||||
_parse(
|
_parse(
|
||||||
lines, type_convert, special_handling, ans, all_keys, accumulate_bad_lines
|
lines, parse_conf_item, ans, accumulate_bad_lines
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_options_class(all_keys: Iterable[str]) -> Type:
|
|
||||||
keys = tuple(sorted(all_keys))
|
|
||||||
slots = keys + ('_fields', )
|
|
||||||
|
|
||||||
def __init__(self: Any, kw: Dict[str, Any]) -> None:
|
|
||||||
for k, v in kw.items():
|
|
||||||
setattr(self, k, v)
|
|
||||||
|
|
||||||
def __iter__(self: Any) -> Iterator[str]:
|
|
||||||
return iter(keys)
|
|
||||||
|
|
||||||
def __len__(self: Any) -> int:
|
|
||||||
return len(keys)
|
|
||||||
|
|
||||||
def __getitem__(self: Any, i: Union[int, str]) -> Any:
|
|
||||||
if isinstance(i, int):
|
|
||||||
i = keys[i]
|
|
||||||
try:
|
|
||||||
return getattr(self, i)
|
|
||||||
except AttributeError:
|
|
||||||
raise KeyError('No option named: {}'.format(i))
|
|
||||||
|
|
||||||
def _asdict(self: Any) -> Dict[str, Any]:
|
|
||||||
return {k: getattr(self, k) for k in self._fields}
|
|
||||||
|
|
||||||
def _replace(self: Any, **kw: Dict) -> Any:
|
|
||||||
ans = self._asdict()
|
|
||||||
ans.update(kw)
|
|
||||||
return self.__class__(ans)
|
|
||||||
|
|
||||||
ans = type(
|
|
||||||
'Options', (), {
|
|
||||||
'__slots__': slots,
|
|
||||||
'__init__': __init__,
|
|
||||||
'_asdict': _asdict,
|
|
||||||
'_replace': _replace,
|
|
||||||
'__iter__': __iter__,
|
|
||||||
'__len__': __len__,
|
|
||||||
'__getitem__': __getitem__
|
|
||||||
}
|
|
||||||
)
|
|
||||||
ans._fields = keys # type: ignore
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
def merge_dicts(defaults: Dict, newvals: Dict) -> Dict:
|
def merge_dicts(defaults: Dict, newvals: Dict) -> Dict:
|
||||||
ans = defaults.copy()
|
ans = defaults.copy()
|
||||||
ans.update(newvals)
|
ans.update(newvals)
|
||||||
@@ -243,14 +207,14 @@ def resolve_config(SYSTEM_CONF: str, defconf: str, config_files_on_cmd_line: Seq
|
|||||||
|
|
||||||
|
|
||||||
def load_config(
|
def load_config(
|
||||||
Options: Type[T],
|
defaults: OptionsProtocol,
|
||||||
defaults: Any,
|
parse_config: Callable[[Iterable[str]], Dict[str, Any]],
|
||||||
parse_config: Callable[[Iterable[str]], Dict[str, Any]],
|
merge_configs: Callable[[Dict, Dict], Dict],
|
||||||
merge_configs: Callable[[Dict, Dict], Dict],
|
*paths: str,
|
||||||
*paths: str,
|
overrides: Optional[Iterable[str]] = None
|
||||||
overrides: Optional[Iterable[str]] = None
|
) -> Tuple[Dict[str, Any], Tuple[str, ...]]:
|
||||||
) -> T:
|
ans = defaults._asdict()
|
||||||
ans: Dict = defaults._asdict()
|
found_paths = []
|
||||||
for path in paths:
|
for path in paths:
|
||||||
if not path:
|
if not path:
|
||||||
continue
|
continue
|
||||||
@@ -259,18 +223,12 @@ def load_config(
|
|||||||
vals = parse_config(f)
|
vals = parse_config(f)
|
||||||
except (FileNotFoundError, PermissionError):
|
except (FileNotFoundError, PermissionError):
|
||||||
continue
|
continue
|
||||||
|
found_paths.append(path)
|
||||||
ans = merge_configs(ans, vals)
|
ans = merge_configs(ans, vals)
|
||||||
if overrides is not None:
|
if overrides is not None:
|
||||||
vals = parse_config(overrides)
|
vals = parse_config(overrides)
|
||||||
ans = merge_configs(ans, vals)
|
ans = merge_configs(ans, vals)
|
||||||
return Options(ans) # type: ignore
|
return ans, tuple(found_paths)
|
||||||
|
|
||||||
|
|
||||||
def init_config(default_config_lines: Iterable[str], parse_config: Callable) -> Tuple[Type, Any]:
|
|
||||||
defaults = parse_config(default_config_lines, check_keys=False)
|
|
||||||
Options = create_options_class(defaults.keys())
|
|
||||||
defaults = Options(defaults)
|
|
||||||
return Options, defaults
|
|
||||||
|
|
||||||
|
|
||||||
def key_func() -> Tuple[Callable[..., Callable], Dict[str, Callable]]:
|
def key_func() -> Tuple[Callable[..., Callable], Dict[str, Callable]]:
|
||||||
@@ -291,14 +249,21 @@ def key_func() -> Tuple[Callable[..., Callable], Dict[str, Callable]]:
|
|||||||
return func_with_args, ans
|
return func_with_args, ans
|
||||||
|
|
||||||
|
|
||||||
KittensKeyAction = Tuple[str, Tuple[str, ...]]
|
class KeyAction(NamedTuple):
|
||||||
|
func: str
|
||||||
|
args: Tuple[Union[str, float, bool, int, None], ...] = ()
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
if self.args:
|
||||||
|
return f'KeyAction({self.func!r}, {self.args!r})'
|
||||||
|
return f'KeyAction({self.func!r})'
|
||||||
|
|
||||||
|
|
||||||
def parse_kittens_func_args(action: str, args_funcs: Dict[str, Callable]) -> KittensKeyAction:
|
def parse_kittens_func_args(action: str, args_funcs: Dict[str, Callable]) -> KeyAction:
|
||||||
parts = action.strip().split(' ', 1)
|
parts = action.strip().split(' ', 1)
|
||||||
func = parts[0]
|
func = parts[0]
|
||||||
if len(parts) == 1:
|
if len(parts) == 1:
|
||||||
return func, ()
|
return KeyAction(func, ())
|
||||||
rest = parts[1]
|
rest = parts[1]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -317,15 +282,38 @@ def parse_kittens_func_args(action: str, args_funcs: Dict[str, Callable]) -> Kit
|
|||||||
if not isinstance(args, (list, tuple)):
|
if not isinstance(args, (list, tuple)):
|
||||||
args = (args, )
|
args = (args, )
|
||||||
|
|
||||||
return func, tuple(args)
|
return KeyAction(func, tuple(args))
|
||||||
|
|
||||||
|
|
||||||
|
KittensKeyDefinition = Tuple[ParsedShortcut, KeyAction]
|
||||||
|
KittensKeyMap = Dict[ParsedShortcut, KeyAction]
|
||||||
|
|
||||||
|
|
||||||
def parse_kittens_key(
|
def parse_kittens_key(
|
||||||
val: str, funcs_with_args: Dict[str, Callable]
|
val: str, funcs_with_args: Dict[str, Callable]
|
||||||
) -> Optional[Tuple[KittensKeyAction, ParsedShortcut]]:
|
) -> Optional[KittensKeyDefinition]:
|
||||||
from ..key_encoding import parse_shortcut
|
from ..key_encoding import parse_shortcut
|
||||||
sc, action = val.partition(' ')[::2]
|
sc, action = val.partition(' ')[::2]
|
||||||
if not sc or not action:
|
if not sc or not action:
|
||||||
return None
|
return None
|
||||||
ans = parse_kittens_func_args(action, funcs_with_args)
|
ans = parse_kittens_func_args(action, funcs_with_args)
|
||||||
return ans, parse_shortcut(sc)
|
return parse_shortcut(sc), ans
|
||||||
|
|
||||||
|
|
||||||
|
def uniq(vals: Iterable[T]) -> List[T]:
|
||||||
|
seen: Set[T] = set()
|
||||||
|
seen_add = seen.add
|
||||||
|
return [x for x in vals if x not in seen and not seen_add(x)]
|
||||||
|
|
||||||
|
|
||||||
|
def save_type_stub(text: str, fpath: str) -> None:
|
||||||
|
fpath += 'i'
|
||||||
|
preamble = '# Update this file by running: ./test.py mypy\n\n'
|
||||||
|
try:
|
||||||
|
existing = open(fpath).read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
existing = ''
|
||||||
|
current = preamble + text
|
||||||
|
if existing != current:
|
||||||
|
with open(fpath, 'w') as f:
|
||||||
|
f.write(current)
|
||||||
|
|||||||
720
kitty/config.py
720
kitty/config.py
@@ -4,653 +4,34 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from contextlib import contextmanager, suppress
|
from contextlib import contextmanager, suppress
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import (
|
from typing import Any, Dict, Generator, Iterable, List, Optional, Tuple
|
||||||
Any, Callable, Dict, Generator, Iterable, List, NamedTuple, Optional,
|
|
||||||
Sequence, Set, Tuple, Type, Union
|
|
||||||
)
|
|
||||||
|
|
||||||
from . import fast_data_types as defines
|
from .conf.utils import BadLine, load_config as _load_config, parse_config_base
|
||||||
from .conf.definition import as_conf_file, config_lines
|
from .constants import cache_dir, defconf
|
||||||
from .conf.utils import (
|
from .options.types import Options, defaults, option_names
|
||||||
BadLine, init_config, key_func, load_config as _load_config, merge_dicts,
|
from .options.utils import (
|
||||||
parse_config_base, python_string, to_bool, to_cmdline
|
KeyDefinition, KeyMap, MouseMap, MouseMapping, SequenceMap
|
||||||
)
|
)
|
||||||
from .config_data import InvalidMods, all_options, parse_shortcut, type_convert
|
|
||||||
from .constants import cache_dir, defconf, is_macos
|
|
||||||
from .fonts import FontFeature
|
|
||||||
from .options_stub import Options as OptionsStub
|
|
||||||
from .types import SingleKey
|
|
||||||
from .typing import TypedDict
|
from .typing import TypedDict
|
||||||
from .utils import expandvars, log_error
|
from .utils import log_error
|
||||||
|
|
||||||
KeyMap = Dict[SingleKey, 'KeyAction']
|
|
||||||
KeySequence = Tuple[SingleKey, ...]
|
|
||||||
SubSequenceMap = Dict[KeySequence, 'KeyAction']
|
|
||||||
SequenceMap = Dict[SingleKey, SubSequenceMap]
|
|
||||||
|
|
||||||
|
def option_names_for_completion() -> Tuple[str, ...]:
|
||||||
|
return option_names
|
||||||
|
|
||||||
class KeyAction(NamedTuple):
|
|
||||||
func: str
|
|
||||||
args: Sequence[str] = ()
|
|
||||||
|
|
||||||
|
|
||||||
func_with_args, args_funcs = key_func()
|
|
||||||
FuncArgsType = Tuple[str, Sequence[Any]]
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args(
|
|
||||||
'pass_selection_to_program', 'new_window', 'new_tab', 'new_os_window',
|
|
||||||
'new_window_with_cwd', 'new_tab_with_cwd', 'new_os_window_with_cwd',
|
|
||||||
'launch'
|
|
||||||
)
|
|
||||||
def shlex_parse(func: str, rest: str) -> FuncArgsType:
|
|
||||||
return func, to_cmdline(rest)
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('combine')
|
|
||||||
def combine_parse(func: str, rest: str) -> FuncArgsType:
|
|
||||||
sep, rest = rest.split(maxsplit=1)
|
|
||||||
parts = re.split(r'\s*' + re.escape(sep) + r'\s*', rest)
|
|
||||||
args = tuple(map(parse_key_action, filter(None, parts)))
|
|
||||||
return func, args
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('send_text')
|
|
||||||
def send_text_parse(func: str, rest: str) -> FuncArgsType:
|
|
||||||
args = rest.split(maxsplit=1)
|
|
||||||
mode = ''
|
|
||||||
data = b''
|
|
||||||
if len(args) > 1:
|
|
||||||
mode = args[0]
|
|
||||||
try:
|
|
||||||
data = parse_send_text_bytes(args[1])
|
|
||||||
except Exception:
|
|
||||||
log_error('Ignoring invalid send_text string: ' + args[1])
|
|
||||||
return func, [mode, data]
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('run_kitten', 'run_simple_kitten', 'kitten')
|
|
||||||
def kitten_parse(func: str, rest: str) -> FuncArgsType:
|
|
||||||
if func == 'kitten':
|
|
||||||
args = rest.split(maxsplit=1)
|
|
||||||
else:
|
|
||||||
args = rest.split(maxsplit=2)[1:]
|
|
||||||
func = 'kitten'
|
|
||||||
return func, args
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('goto_tab')
|
|
||||||
def goto_tab_parse(func: str, rest: str) -> FuncArgsType:
|
|
||||||
args = (max(0, int(rest)), )
|
|
||||||
return func, args
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('detach_window')
|
|
||||||
def detach_window_parse(func: str, rest: str) -> FuncArgsType:
|
|
||||||
if rest not in ('new', 'new-tab', 'ask'):
|
|
||||||
log_error('Ignoring invalid detach_window argument: {}'.format(rest))
|
|
||||||
rest = 'new'
|
|
||||||
return func, (rest,)
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('detach_tab')
|
|
||||||
def detach_tab_parse(func: str, rest: str) -> FuncArgsType:
|
|
||||||
if rest not in ('new', 'ask'):
|
|
||||||
log_error('Ignoring invalid detach_tab argument: {}'.format(rest))
|
|
||||||
rest = 'new'
|
|
||||||
return func, (rest,)
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('set_background_opacity', 'goto_layout', 'kitty_shell')
|
|
||||||
def simple_parse(func: str, rest: str) -> FuncArgsType:
|
|
||||||
return func, [rest]
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('set_font_size')
|
|
||||||
def float_parse(func: str, rest: str) -> FuncArgsType:
|
|
||||||
return func, (float(rest),)
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('signal_child')
|
|
||||||
def signal_child_parse(func: str, rest: str) -> FuncArgsType:
|
|
||||||
import signal
|
|
||||||
signals = []
|
|
||||||
for q in rest.split():
|
|
||||||
try:
|
|
||||||
signum = getattr(signal, q.upper())
|
|
||||||
except AttributeError:
|
|
||||||
log_error(f'Unknown signal: {rest} ignoring')
|
|
||||||
else:
|
|
||||||
signals.append(signum)
|
|
||||||
return func, tuple(signals)
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('change_font_size')
|
|
||||||
def parse_change_font_size(func: str, rest: str) -> Tuple[str, Tuple[bool, Optional[str], float]]:
|
|
||||||
vals = rest.strip().split(maxsplit=1)
|
|
||||||
if len(vals) != 2:
|
|
||||||
log_error('Invalid change_font_size specification: {}, treating it as default'.format(rest))
|
|
||||||
return func, (True, None, 0)
|
|
||||||
c_all = vals[0].lower() == 'all'
|
|
||||||
sign: Optional[str] = None
|
|
||||||
amt = vals[1]
|
|
||||||
if amt[0] in '+-':
|
|
||||||
sign = amt[0]
|
|
||||||
amt = amt[1:]
|
|
||||||
return func, (c_all, sign, float(amt.strip()))
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('clear_terminal')
|
|
||||||
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']
|
|
||||||
else:
|
|
||||||
args = [vals[0].lower(), vals[1].lower() == 'active']
|
|
||||||
return func, args
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('copy_to_buffer')
|
|
||||||
def copy_to_buffer(func: str, rest: str) -> FuncArgsType:
|
|
||||||
return func, [rest]
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('paste_from_buffer')
|
|
||||||
def paste_from_buffer(func: str, rest: str) -> FuncArgsType:
|
|
||||||
return func, [rest]
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('neighboring_window')
|
|
||||||
def neighboring_window(func: str, rest: str) -> FuncArgsType:
|
|
||||||
rest = rest.lower()
|
|
||||||
rest = {'up': 'top', 'down': 'bottom'}.get(rest, rest)
|
|
||||||
if rest not in ('left', 'right', 'top', 'bottom'):
|
|
||||||
log_error('Invalid neighbor specification: {}'.format(rest))
|
|
||||||
rest = 'right'
|
|
||||||
return func, [rest]
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('resize_window')
|
|
||||||
def resize_window(func: str, rest: str) -> FuncArgsType:
|
|
||||||
vals = rest.strip().split(maxsplit=1)
|
|
||||||
if len(vals) > 2:
|
|
||||||
log_error('resize_window needs one or two arguments, using defaults')
|
|
||||||
args = ['wider', 1]
|
|
||||||
else:
|
|
||||||
quality = vals[0].lower()
|
|
||||||
if quality not in ('taller', 'shorter', 'wider', 'narrower'):
|
|
||||||
log_error('Invalid quality specification: {}'.format(quality))
|
|
||||||
quality = 'wider'
|
|
||||||
increment = 1
|
|
||||||
if len(vals) == 2:
|
|
||||||
try:
|
|
||||||
increment = int(vals[1])
|
|
||||||
except Exception:
|
|
||||||
log_error('Invalid increment specification: {}'.format(vals[1]))
|
|
||||||
args = [quality, increment]
|
|
||||||
return func, args
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('move_window')
|
|
||||||
def move_window(func: str, rest: str) -> FuncArgsType:
|
|
||||||
rest = rest.lower()
|
|
||||||
rest = {'up': 'top', 'down': 'bottom'}.get(rest, rest)
|
|
||||||
prest: Union[int, str] = rest
|
|
||||||
try:
|
|
||||||
prest = int(prest)
|
|
||||||
except Exception:
|
|
||||||
if prest not in ('left', 'right', 'top', 'bottom'):
|
|
||||||
log_error('Invalid move_window specification: {}'.format(rest))
|
|
||||||
prest = 0
|
|
||||||
return func, [prest]
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('pipe')
|
|
||||||
def pipe(func: str, rest: str) -> FuncArgsType:
|
|
||||||
import shlex
|
|
||||||
r = shlex.split(rest)
|
|
||||||
if len(r) < 3:
|
|
||||||
log_error('Too few arguments to pipe function')
|
|
||||||
r = ['none', 'none', 'true']
|
|
||||||
return func, r
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('set_colors')
|
|
||||||
def set_colors(func: str, rest: str) -> FuncArgsType:
|
|
||||||
import shlex
|
|
||||||
r = shlex.split(rest)
|
|
||||||
if len(r) < 1:
|
|
||||||
log_error('Too few arguments to set_colors function')
|
|
||||||
return func, r
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('remote_control')
|
|
||||||
def remote_control(func: str, rest: str) -> FuncArgsType:
|
|
||||||
import shlex
|
|
||||||
r = shlex.split(rest)
|
|
||||||
if len(r) < 1:
|
|
||||||
log_error('Too few arguments to remote_control function')
|
|
||||||
return func, r
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('nth_window')
|
|
||||||
def nth_window(func: str, rest: str) -> FuncArgsType:
|
|
||||||
try:
|
|
||||||
num = int(rest)
|
|
||||||
except Exception:
|
|
||||||
log_error('Invalid nth_window number: {}'.format(rest))
|
|
||||||
num = 1
|
|
||||||
return func, [num]
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('disable_ligatures_in')
|
|
||||||
def disable_ligatures_in(func: str, rest: str) -> FuncArgsType:
|
|
||||||
parts = rest.split(maxsplit=1)
|
|
||||||
if len(parts) == 1:
|
|
||||||
where, strategy = 'active', parts[0]
|
|
||||||
else:
|
|
||||||
where, strategy = parts
|
|
||||||
if where not in ('active', 'all', 'tab'):
|
|
||||||
raise ValueError('{} is not a valid set of windows to disable ligatures in'.format(where))
|
|
||||||
if strategy not in ('never', 'always', 'cursor'):
|
|
||||||
raise ValueError('{} is not a valid disable ligatures strategy'.format(strategy))
|
|
||||||
return func, [where, strategy]
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('layout_action')
|
|
||||||
def layout_action(func: str, rest: str) -> FuncArgsType:
|
|
||||||
parts = rest.split(maxsplit=1)
|
|
||||||
if not parts:
|
|
||||||
raise ValueError('layout_action must have at least one argument')
|
|
||||||
return func, [parts[0], tuple(parts[1:])]
|
|
||||||
|
|
||||||
|
|
||||||
def parse_marker_spec(ftype: str, parts: Sequence[str]) -> Tuple[str, Union[str, Tuple[Tuple[int, str], ...]], int]:
|
|
||||||
flags = re.UNICODE
|
|
||||||
if ftype in ('text', 'itext', 'regex', 'iregex'):
|
|
||||||
if ftype.startswith('i'):
|
|
||||||
flags |= re.IGNORECASE
|
|
||||||
if not parts or len(parts) % 2 != 0:
|
|
||||||
raise ValueError('No color specified in marker: {}'.format(' '.join(parts)))
|
|
||||||
ans = []
|
|
||||||
for i in range(0, len(parts), 2):
|
|
||||||
try:
|
|
||||||
color = max(1, min(int(parts[i]), 3))
|
|
||||||
except Exception:
|
|
||||||
raise ValueError('color {} in marker specification is not an integer'.format(parts[i]))
|
|
||||||
sspec = parts[i + 1]
|
|
||||||
if 'regex' not in ftype:
|
|
||||||
sspec = re.escape(sspec)
|
|
||||||
ans.append((color, sspec))
|
|
||||||
ftype = 'regex'
|
|
||||||
spec: Union[str, Tuple[Tuple[int, str], ...]] = tuple(ans)
|
|
||||||
elif ftype == 'function':
|
|
||||||
spec = ' '.join(parts)
|
|
||||||
else:
|
|
||||||
raise ValueError('Unknown marker type: {}'.format(ftype))
|
|
||||||
return ftype, spec, flags
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('toggle_marker')
|
|
||||||
def toggle_marker(func: str, rest: str) -> FuncArgsType:
|
|
||||||
import shlex
|
|
||||||
parts = rest.split(maxsplit=1)
|
|
||||||
if len(parts) != 2:
|
|
||||||
raise ValueError('{} is not a valid marker specification'.format(rest))
|
|
||||||
ftype, spec = parts
|
|
||||||
parts = shlex.split(spec)
|
|
||||||
return func, list(parse_marker_spec(ftype, parts))
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('scroll_to_mark')
|
|
||||||
def scroll_to_mark(func: str, rest: str) -> FuncArgsType:
|
|
||||||
parts = rest.split()
|
|
||||||
if not parts or not rest:
|
|
||||||
return func, [True, 0]
|
|
||||||
if len(parts) == 1:
|
|
||||||
q = parts[0].lower()
|
|
||||||
if q in ('prev', 'previous', 'next'):
|
|
||||||
return func, [q != 'next', 0]
|
|
||||||
try:
|
|
||||||
return func, [True, max(0, min(int(q), 3))]
|
|
||||||
except Exception:
|
|
||||||
raise ValueError('{} is not a valid scroll_to_mark destination'.format(rest))
|
|
||||||
return func, [parts[0] != 'next', max(0, min(int(parts[1]), 3))]
|
|
||||||
|
|
||||||
|
|
||||||
def parse_key_action(action: str) -> Optional[KeyAction]:
|
|
||||||
parts = action.strip().split(maxsplit=1)
|
|
||||||
func = parts[0]
|
|
||||||
if len(parts) == 1:
|
|
||||||
return KeyAction(func, ())
|
|
||||||
rest = parts[1]
|
|
||||||
parser = args_funcs.get(func)
|
|
||||||
if parser is not None:
|
|
||||||
try:
|
|
||||||
func, args = parser(func, rest)
|
|
||||||
except Exception as err:
|
|
||||||
log_error('Ignoring invalid key action: {} with err: {}'.format(action, err))
|
|
||||||
else:
|
|
||||||
return KeyAction(func, args)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
all_key_actions: Set[str] = set()
|
|
||||||
sequence_sep = '>'
|
|
||||||
|
|
||||||
|
|
||||||
class KeyDefinition:
|
|
||||||
|
|
||||||
def __init__(self, is_sequence: bool, action: KeyAction, mods: int, is_native: bool, key: int, rest: Tuple[SingleKey, ...] = ()):
|
|
||||||
self.is_sequence = is_sequence
|
|
||||||
self.action = action
|
|
||||||
self.trigger = SingleKey(mods, is_native, key)
|
|
||||||
self.rest = rest
|
|
||||||
|
|
||||||
def resolve(self, kitty_mod: int) -> None:
|
|
||||||
|
|
||||||
def r(k: SingleKey) -> SingleKey:
|
|
||||||
mods = defines.resolve_key_mods(kitty_mod, k.mods)
|
|
||||||
key = k.key
|
|
||||||
is_native = k.is_native
|
|
||||||
return SingleKey(mods, is_native, key)
|
|
||||||
|
|
||||||
self.trigger = r(self.trigger)
|
|
||||||
self.rest = tuple(map(r, self.rest))
|
|
||||||
|
|
||||||
def resolve_kitten_aliases(self, aliases: Dict[str, Sequence[str]]) -> None:
|
|
||||||
if not self.action.args:
|
|
||||||
return
|
|
||||||
kitten = self.action.args[0]
|
|
||||||
rest = self.action.args[1] if len(self.action.args) > 1 else ''
|
|
||||||
changed = False
|
|
||||||
for key, expanded in aliases.items():
|
|
||||||
if key == kitten:
|
|
||||||
changed = True
|
|
||||||
kitten = expanded[0]
|
|
||||||
if len(expanded) > 1:
|
|
||||||
rest = expanded[1] + ' ' + rest
|
|
||||||
if changed:
|
|
||||||
self.action = self.action._replace(args=[kitten, rest.rstrip()])
|
|
||||||
|
|
||||||
|
|
||||||
def parse_key(val: str, key_definitions: List[KeyDefinition]) -> None:
|
|
||||||
parts = val.split(maxsplit=1)
|
|
||||||
if len(parts) != 2:
|
|
||||||
return
|
|
||||||
sc, action = parts
|
|
||||||
sc, action = sc.strip().strip(sequence_sep), action.strip()
|
|
||||||
if not sc or not action:
|
|
||||||
return
|
|
||||||
is_sequence = sequence_sep in sc
|
|
||||||
if is_sequence:
|
|
||||||
trigger: Optional[SingleKey] = None
|
|
||||||
restl: List[SingleKey] = []
|
|
||||||
for part in sc.split(sequence_sep):
|
|
||||||
try:
|
|
||||||
mods, is_native, key = parse_shortcut(part)
|
|
||||||
except InvalidMods:
|
|
||||||
return
|
|
||||||
if key == 0:
|
|
||||||
if mods is not None:
|
|
||||||
log_error('Shortcut: {} has unknown key, ignoring'.format(sc))
|
|
||||||
return
|
|
||||||
if trigger is None:
|
|
||||||
trigger = SingleKey(mods, is_native, key)
|
|
||||||
else:
|
|
||||||
restl.append(SingleKey(mods, is_native, key))
|
|
||||||
rest = tuple(restl)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
mods, is_native, key = parse_shortcut(sc)
|
|
||||||
except InvalidMods:
|
|
||||||
return
|
|
||||||
if key == 0:
|
|
||||||
if mods is not None:
|
|
||||||
log_error('Shortcut: {} has unknown key, ignoring'.format(sc))
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
paction = parse_key_action(action)
|
|
||||||
except Exception:
|
|
||||||
log_error('Invalid shortcut action: {}. Ignoring.'.format(
|
|
||||||
action))
|
|
||||||
else:
|
|
||||||
if paction is not None:
|
|
||||||
all_key_actions.add(paction.func)
|
|
||||||
if is_sequence:
|
|
||||||
if trigger is not None:
|
|
||||||
key_definitions.append(KeyDefinition(True, paction, trigger[0], trigger[1], trigger[2], rest))
|
|
||||||
else:
|
|
||||||
assert key is not None
|
|
||||||
key_definitions.append(KeyDefinition(False, paction, mods, is_native, key))
|
|
||||||
|
|
||||||
|
|
||||||
def parse_symbol_map(val: str) -> Dict[Tuple[int, int], str]:
|
|
||||||
parts = val.split()
|
|
||||||
symbol_map: Dict[Tuple[int, int], str] = {}
|
|
||||||
|
|
||||||
def abort() -> Dict[Tuple[int, int], str]:
|
|
||||||
log_error('Symbol map: {} is invalid, ignoring'.format(
|
|
||||||
val))
|
|
||||||
return {}
|
|
||||||
|
|
||||||
if len(parts) < 2:
|
|
||||||
return abort()
|
|
||||||
family = ' '.join(parts[1:])
|
|
||||||
|
|
||||||
def to_chr(x: str) -> int:
|
|
||||||
if not x.startswith('U+'):
|
|
||||||
raise ValueError()
|
|
||||||
return int(x[2:], 16)
|
|
||||||
|
|
||||||
for x in parts[0].split(','):
|
|
||||||
a_, b_ = x.partition('-')[::2]
|
|
||||||
b_ = b_ or a_
|
|
||||||
try:
|
|
||||||
a, b = map(to_chr, (a_, b_))
|
|
||||||
except Exception:
|
|
||||||
return abort()
|
|
||||||
if b < a or max(a, b) > sys.maxunicode or min(a, b) < 1:
|
|
||||||
return abort()
|
|
||||||
symbol_map[(a, b)] = family
|
|
||||||
return symbol_map
|
|
||||||
|
|
||||||
|
|
||||||
def parse_send_text_bytes(text: str) -> bytes:
|
|
||||||
return python_string(text).encode('utf-8')
|
|
||||||
|
|
||||||
|
|
||||||
def parse_send_text(val: str, key_definitions: List[KeyDefinition]) -> None:
|
|
||||||
parts = val.split(' ')
|
|
||||||
|
|
||||||
def abort(msg: str) -> None:
|
|
||||||
log_error('Send text: {} is invalid ({}), ignoring'.format(
|
|
||||||
val, msg))
|
|
||||||
|
|
||||||
if len(parts) < 3:
|
|
||||||
return abort('Incomplete')
|
|
||||||
mode, sc = parts[:2]
|
|
||||||
text = ' '.join(parts[2:])
|
|
||||||
key_str = '{} send_text {} {}'.format(sc, mode, text)
|
|
||||||
parse_key(key_str, key_definitions)
|
|
||||||
|
|
||||||
|
|
||||||
SpecialHandlerFunc = Callable[[str, str, Dict[str, Any]], None]
|
|
||||||
special_handlers: Dict[str, SpecialHandlerFunc] = {}
|
|
||||||
|
|
||||||
|
|
||||||
def special_handler(func: SpecialHandlerFunc) -> SpecialHandlerFunc:
|
|
||||||
special_handlers[func.__name__.partition('_')[2]] = func
|
|
||||||
return func
|
|
||||||
|
|
||||||
|
|
||||||
def deprecated_handler(*names: str) -> Callable[[SpecialHandlerFunc], SpecialHandlerFunc]:
|
|
||||||
def special_handler(func: SpecialHandlerFunc) -> SpecialHandlerFunc:
|
|
||||||
for name in names:
|
|
||||||
special_handlers[name] = func
|
|
||||||
return func
|
|
||||||
return special_handler
|
|
||||||
|
|
||||||
|
|
||||||
@special_handler
|
|
||||||
def handle_map(key: str, val: str, ans: Dict[str, Any]) -> None:
|
|
||||||
parse_key(val, ans['key_definitions'])
|
|
||||||
|
|
||||||
|
|
||||||
@special_handler
|
|
||||||
def handle_symbol_map(key: str, val: str, ans: Dict[str, Any]) -> None:
|
|
||||||
ans['symbol_map'].update(parse_symbol_map(val))
|
|
||||||
|
|
||||||
|
|
||||||
@special_handler
|
|
||||||
def handle_font_features(key: str, val: str, ans: Dict[str, Any]) -> None:
|
|
||||||
if val != 'none':
|
|
||||||
parts = val.split()
|
|
||||||
if len(parts) < 2:
|
|
||||||
log_error("Ignoring invalid font_features {}".format(val))
|
|
||||||
else:
|
|
||||||
features = []
|
|
||||||
for feat in parts[1:]:
|
|
||||||
try:
|
|
||||||
parsed = defines.parse_font_feature(feat)
|
|
||||||
except ValueError:
|
|
||||||
log_error('Ignoring invalid font feature: {}'.format(feat))
|
|
||||||
else:
|
|
||||||
features.append(FontFeature(feat, parsed))
|
|
||||||
if features:
|
|
||||||
ans['font_features'][parts[0]] = tuple(features)
|
|
||||||
|
|
||||||
|
|
||||||
@special_handler
|
|
||||||
def handle_kitten_alias(key: str, val: str, ans: Dict[str, Any]) -> None:
|
|
||||||
parts = val.split(maxsplit=2)
|
|
||||||
if len(parts) >= 2:
|
|
||||||
ans['kitten_aliases'][parts[0]] = parts[1:]
|
|
||||||
|
|
||||||
|
|
||||||
@special_handler
|
|
||||||
def handle_send_text(key: str, val: str, ans: Dict[str, Any]) -> None:
|
|
||||||
# For legacy compatibility
|
|
||||||
parse_send_text(val, ans['key_definitions'])
|
|
||||||
|
|
||||||
|
|
||||||
@special_handler
|
|
||||||
def handle_clear_all_shortcuts(key: str, val: str, ans: Dict[str, Any]) -> None:
|
|
||||||
if to_bool(val):
|
|
||||||
ans['key_definitions'] = [None]
|
|
||||||
|
|
||||||
|
|
||||||
@deprecated_handler('x11_hide_window_decorations', 'macos_hide_titlebar')
|
|
||||||
def handle_deprecated_hide_window_decorations_aliases(key: str, val: str, ans: Dict[str, Any]) -> None:
|
|
||||||
if not hasattr(handle_deprecated_hide_window_decorations_aliases, key):
|
|
||||||
setattr(handle_deprecated_hide_window_decorations_aliases, 'key', True)
|
|
||||||
log_error('The option {} is deprecated. Use hide_window_decorations instead.'.format(key))
|
|
||||||
if to_bool(val):
|
|
||||||
if is_macos and key == 'macos_hide_titlebar' or (not is_macos and key == 'x11_hide_window_decorations'):
|
|
||||||
ans['hide_window_decorations'] = True
|
|
||||||
|
|
||||||
|
|
||||||
@deprecated_handler('macos_show_window_title_in_menubar')
|
|
||||||
def handle_deprecated_macos_show_window_title_in_menubar_alias(key: str, val: str, ans: Dict[str, Any]) -> None:
|
|
||||||
if not hasattr(handle_deprecated_macos_show_window_title_in_menubar_alias, key):
|
|
||||||
setattr(handle_deprecated_macos_show_window_title_in_menubar_alias, 'key', True)
|
|
||||||
log_error('The option {} is deprecated. Use macos_show_window_title_in menubar instead.'.format(key))
|
|
||||||
macos_show_window_title_in = ans.get('macos_show_window_title_in', 'all')
|
|
||||||
if to_bool(val):
|
|
||||||
if macos_show_window_title_in == 'none':
|
|
||||||
macos_show_window_title_in = 'menubar'
|
|
||||||
elif macos_show_window_title_in == 'window':
|
|
||||||
macos_show_window_title_in = 'all'
|
|
||||||
else:
|
|
||||||
if macos_show_window_title_in == 'all':
|
|
||||||
macos_show_window_title_in = 'window'
|
|
||||||
elif macos_show_window_title_in == 'menubar':
|
|
||||||
macos_show_window_title_in = 'none'
|
|
||||||
ans['macos_show_window_title_in'] = macos_show_window_title_in
|
|
||||||
|
|
||||||
|
|
||||||
@special_handler
|
|
||||||
def handle_env(key: str, val: str, ans: Dict[str, Any]) -> None:
|
|
||||||
key, val = val.partition('=')[::2]
|
|
||||||
key, val = key.strip(), val.strip()
|
|
||||||
ans['env'][key] = expandvars(val, ans['env'])
|
|
||||||
|
|
||||||
|
|
||||||
def special_handling(key: str, val: str, ans: Dict[str, Any]) -> bool:
|
|
||||||
func = special_handlers.get(key)
|
|
||||||
if func is not None:
|
|
||||||
func(key, val, ans)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def option_names_for_completion() -> Generator[str, None, None]:
|
|
||||||
yield from defaults
|
|
||||||
yield from special_handlers
|
|
||||||
|
|
||||||
|
|
||||||
def parse_config(lines: Iterable[str], check_keys: bool = True, accumulate_bad_lines: Optional[List[BadLine]] = None) -> Dict[str, Any]:
|
|
||||||
ans: Dict[str, Any] = {
|
|
||||||
'symbol_map': {}, 'keymap': {}, 'sequence_map': {}, 'key_definitions': [],
|
|
||||||
'env': {}, 'kitten_aliases': {}, 'font_features': {}
|
|
||||||
}
|
|
||||||
defs: Optional[OptionsStub] = None
|
|
||||||
if check_keys:
|
|
||||||
defs = defaults
|
|
||||||
|
|
||||||
parse_config_base(
|
|
||||||
lines,
|
|
||||||
defs,
|
|
||||||
type_convert,
|
|
||||||
special_handling,
|
|
||||||
ans,
|
|
||||||
check_keys=check_keys,
|
|
||||||
accumulate_bad_lines=accumulate_bad_lines
|
|
||||||
)
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
def parse_defaults(lines: Iterable[str], check_keys: bool = False) -> Dict[str, Any]:
|
|
||||||
ans = parse_config(lines, check_keys)
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
xc = init_config(config_lines(all_options), parse_defaults)
|
|
||||||
Options: Type[OptionsStub] = xc[0]
|
|
||||||
defaults: OptionsStub = xc[1]
|
|
||||||
actions = frozenset(all_key_actions) | frozenset(
|
|
||||||
'run_simple_kitten combine send_text goto_tab goto_layout set_font_size new_tab_with_cwd new_window_with_cwd new_os_window_with_cwd'.
|
|
||||||
split()
|
|
||||||
)
|
|
||||||
no_op_actions = frozenset({'noop', 'no-op', 'no_op'})
|
no_op_actions = frozenset({'noop', 'no-op', 'no_op'})
|
||||||
|
|
||||||
|
|
||||||
def merge_configs(defaults: Dict, vals: Dict) -> Dict:
|
def build_ansi_color_table(opts: Optional[Options] = None) -> int:
|
||||||
ans = {}
|
if opts is None:
|
||||||
for k, v in defaults.items():
|
opts = defaults
|
||||||
if isinstance(v, dict):
|
addr, length = opts.color_table.buffer_info()
|
||||||
newvals = vals.get(k, {})
|
if length != 256 or opts.color_table.typecode != 'L':
|
||||||
ans[k] = merge_dicts(v, newvals)
|
raise TypeError(f'The color table has incorrect size length: {length} typecode: {opts.color_table.typecode}')
|
||||||
elif k == 'key_definitions':
|
return addr
|
||||||
ans['key_definitions'] = v + vals.get('key_definitions', [])
|
|
||||||
else:
|
|
||||||
ans[k] = vals.get(k, v)
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
def build_ansi_color_table(opts: OptionsStub = defaults) -> List[int]:
|
|
||||||
|
|
||||||
def as_int(x: Tuple[int, int, int]) -> int:
|
|
||||||
return (x[0] << 16) | (x[1] << 8) | x[2]
|
|
||||||
|
|
||||||
def col(i: int) -> int:
|
|
||||||
return as_int(getattr(opts, 'color{}'.format(i)))
|
|
||||||
|
|
||||||
return list(map(col, range(256)))
|
|
||||||
|
|
||||||
|
|
||||||
def atomic_save(data: bytes, path: str) -> None:
|
def atomic_save(data: bytes, path: str) -> None:
|
||||||
@@ -694,12 +75,8 @@ def cached_values_for(name: str) -> Generator[Dict, None, None]:
|
|||||||
|
|
||||||
|
|
||||||
def commented_out_default_config() -> str:
|
def commented_out_default_config() -> str:
|
||||||
ans = []
|
from .options.definition import definition
|
||||||
for line in as_conf_file(all_options.values()):
|
return '\n'.join(definition.as_conf(commented=True))
|
||||||
if line and line[0] != '#':
|
|
||||||
line = '# ' + line
|
|
||||||
ans.append(line)
|
|
||||||
return '\n'.join(ans)
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_config_file_for_editing() -> str:
|
def prepare_config_file_for_editing() -> str:
|
||||||
@@ -712,18 +89,13 @@ def prepare_config_file_for_editing() -> str:
|
|||||||
return defconf
|
return defconf
|
||||||
|
|
||||||
|
|
||||||
def finalize_keys(opts: OptionsStub) -> None:
|
def finalize_keys(opts: Options) -> None:
|
||||||
defns: List[KeyDefinition] = []
|
defns: List[KeyDefinition] = []
|
||||||
for d in getattr(opts, 'key_definitions'):
|
for d in opts.map:
|
||||||
if d is None: # clear_all_shortcuts
|
if d is None: # clear_all_shortcuts
|
||||||
defns = []
|
defns = [] # type: ignore
|
||||||
else:
|
else:
|
||||||
defns.append(d)
|
defns.append(d.resolve_and_copy(opts.kitty_mod, opts.kitten_alias))
|
||||||
kitten_aliases: List[Dict[str, Sequence[str]]] = getattr(opts, 'kitten_aliases')
|
|
||||||
for d in defns:
|
|
||||||
d.resolve(opts.kitty_mod)
|
|
||||||
if kitten_aliases and d.action.func == 'kitten':
|
|
||||||
d.resolve_kitten_aliases(kitten_aliases)
|
|
||||||
keymap: KeyMap = {}
|
keymap: KeyMap = {}
|
||||||
sequence_map: SequenceMap = {}
|
sequence_map: SequenceMap = {}
|
||||||
|
|
||||||
@@ -748,15 +120,51 @@ def finalize_keys(opts: OptionsStub) -> None:
|
|||||||
opts.sequence_map = sequence_map
|
opts.sequence_map = sequence_map
|
||||||
|
|
||||||
|
|
||||||
def load_config(*paths: str, overrides: Optional[Iterable[str]] = None, accumulate_bad_lines: Optional[List[BadLine]] = None) -> OptionsStub:
|
def finalize_mouse_mappings(opts: Options) -> None:
|
||||||
parser = parse_config
|
defns: List[MouseMapping] = []
|
||||||
if accumulate_bad_lines is not None:
|
for d in opts.mouse_map:
|
||||||
parser = partial(parse_config, accumulate_bad_lines=accumulate_bad_lines)
|
defns.append(d.resolve_and_copy(opts.kitty_mod, opts.kitten_alias))
|
||||||
opts = _load_config(Options, defaults, parser, merge_configs, *paths, overrides=overrides)
|
|
||||||
|
mousemap: MouseMap = {}
|
||||||
|
for defn in defns:
|
||||||
|
is_no_op = defn.action.func in no_op_actions
|
||||||
|
if is_no_op:
|
||||||
|
mousemap.pop(defn.trigger, None)
|
||||||
|
else:
|
||||||
|
mousemap[defn.trigger] = defn.action
|
||||||
|
opts.mousemap = mousemap
|
||||||
|
|
||||||
|
|
||||||
|
def parse_config(lines: Iterable[str], accumulate_bad_lines: Optional[List[BadLine]] = None) -> Dict[str, Any]:
|
||||||
|
from .options.parse import create_result_dict, parse_conf_item
|
||||||
|
ans: Dict[str, Any] = create_result_dict()
|
||||||
|
parse_config_base(
|
||||||
|
lines,
|
||||||
|
parse_conf_item,
|
||||||
|
ans,
|
||||||
|
accumulate_bad_lines=accumulate_bad_lines
|
||||||
|
)
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(*paths: str, overrides: Optional[Iterable[str]] = None, accumulate_bad_lines: Optional[List[BadLine]] = None) -> Options:
|
||||||
|
from .options.parse import merge_result_dicts
|
||||||
|
|
||||||
|
overrides = tuple(overrides) if overrides is not None else ()
|
||||||
|
opts_dict, paths = _load_config(defaults, partial(parse_config, accumulate_bad_lines=accumulate_bad_lines), merge_result_dicts, *paths, overrides=overrides)
|
||||||
|
opts = Options(opts_dict)
|
||||||
|
|
||||||
finalize_keys(opts)
|
finalize_keys(opts)
|
||||||
|
finalize_mouse_mappings(opts)
|
||||||
|
# delete no longer needed definitions, replacing with empty placeholders
|
||||||
|
opts.kitten_alias = {}
|
||||||
|
opts.mouse_map = []
|
||||||
|
opts.map = []
|
||||||
if opts.background_opacity < 1.0 and opts.macos_titlebar_color:
|
if opts.background_opacity < 1.0 and opts.macos_titlebar_color:
|
||||||
log_error('Cannot use both macos_titlebar_color and background_opacity')
|
log_error('Cannot use both macos_titlebar_color and background_opacity')
|
||||||
opts.macos_titlebar_color = 0
|
opts.macos_titlebar_color = 0
|
||||||
|
opts.config_paths = paths
|
||||||
|
opts.config_overrides = overrides
|
||||||
return opts
|
return opts
|
||||||
|
|
||||||
|
|
||||||
@@ -766,7 +174,7 @@ class KittyCommonOpts(TypedDict):
|
|||||||
url_prefixes: Tuple[str, ...]
|
url_prefixes: Tuple[str, ...]
|
||||||
|
|
||||||
|
|
||||||
def common_opts_as_dict(opts: Optional[OptionsStub] = None) -> KittyCommonOpts:
|
def common_opts_as_dict(opts: Optional[Options] = None) -> KittyCommonOpts:
|
||||||
if opts is None:
|
if opts is None:
|
||||||
opts = defaults
|
opts = defaults
|
||||||
return {
|
return {
|
||||||
|
|||||||
1649
kitty/config_data.py
1649
kitty/config_data.py
File diff suppressed because it is too large
Load Diff
@@ -7,11 +7,13 @@ import os
|
|||||||
import pwd
|
import pwd
|
||||||
import sys
|
import sys
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from typing import NamedTuple, Optional, Set
|
from typing import NamedTuple, Optional, Set, TYPE_CHECKING
|
||||||
|
|
||||||
from .options_stub import Options
|
|
||||||
from .types import run_once
|
from .types import run_once
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .options.types import Options
|
||||||
|
|
||||||
|
|
||||||
class Version(NamedTuple):
|
class Version(NamedTuple):
|
||||||
major: int
|
major: int
|
||||||
@@ -21,7 +23,7 @@ class Version(NamedTuple):
|
|||||||
|
|
||||||
appname: str = 'kitty'
|
appname: str = 'kitty'
|
||||||
kitty_face = '🐱'
|
kitty_face = '🐱'
|
||||||
version: Version = Version(0, 20, 3)
|
version: Version = Version(0, 21, 0)
|
||||||
str_version: str = '.'.join(map(str, version))
|
str_version: str = '.'.join(map(str, version))
|
||||||
_plat = sys.platform.lower()
|
_plat = sys.platform.lower()
|
||||||
is_macos: bool = 'darwin' in _plat
|
is_macos: bool = 'darwin' in _plat
|
||||||
@@ -149,7 +151,7 @@ def detect_if_wayland_ok() -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def is_wayland(opts: Optional[Options] = None) -> bool:
|
def is_wayland(opts: Optional['Options'] = None) -> bool:
|
||||||
if is_macos:
|
if is_macos:
|
||||||
return False
|
return False
|
||||||
if opts is None:
|
if opts is None:
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
#import <Foundation/NSString.h>
|
#import <Foundation/NSString.h>
|
||||||
#import <Foundation/NSDictionary.h>
|
#import <Foundation/NSDictionary.h>
|
||||||
|
|
||||||
|
#define debug(...) if (global_state.debug_rendering) { fprintf(stderr, __VA_ARGS__); fflush(stderr); }
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
PyObject_HEAD
|
PyObject_HEAD
|
||||||
|
|
||||||
@@ -45,7 +47,7 @@ convert_cfstring(CFStringRef src, int free_src) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
init_face(CTFace *self, CTFontRef font) {
|
init_face(CTFace *self, CTFontRef font, FONTS_DATA_HANDLE fg UNUSED) {
|
||||||
if (self->hb_font) hb_font_destroy(self->hb_font);
|
if (self->hb_font) hb_font_destroy(self->hb_font);
|
||||||
self->hb_font = NULL;
|
self->hb_font = NULL;
|
||||||
if (self->ct_font) CFRelease(self->ct_font);
|
if (self->ct_font) CFRelease(self->ct_font);
|
||||||
@@ -60,10 +62,10 @@ init_face(CTFace *self, CTFontRef font) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static inline CTFace*
|
static inline CTFace*
|
||||||
ct_face(CTFontRef font) {
|
ct_face(CTFontRef font, FONTS_DATA_HANDLE fg) {
|
||||||
CTFace *self = (CTFace *)CTFace_Type.tp_alloc(&CTFace_Type, 0);
|
CTFace *self = (CTFace *)CTFace_Type.tp_alloc(&CTFace_Type, 0);
|
||||||
if (self) {
|
if (self) {
|
||||||
init_face(self, font);
|
init_face(self, font, fg);
|
||||||
self->family_name = Py_BuildValue("s", convert_cfstring(CTFontCopyFamilyName(self->ct_font), true));
|
self->family_name = Py_BuildValue("s", convert_cfstring(CTFontCopyFamilyName(self->ct_font), true));
|
||||||
self->full_name = Py_BuildValue("s", convert_cfstring(CTFontCopyFullName(self->ct_font), true));
|
self->full_name = Py_BuildValue("s", convert_cfstring(CTFontCopyFullName(self->ct_font), true));
|
||||||
self->postscript_name = Py_BuildValue("s", convert_cfstring(CTFontCopyPostScriptName(self->ct_font), true));
|
self->postscript_name = Py_BuildValue("s", convert_cfstring(CTFontCopyPostScriptName(self->ct_font), true));
|
||||||
@@ -247,20 +249,27 @@ find_substitute_face(CFStringRef str, CTFontRef old_font, CPUCell *cpu_cell) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PyObject*
|
PyObject*
|
||||||
create_fallback_face(PyObject *base_face, CPUCell* cell, bool UNUSED bold, bool UNUSED italic, bool emoji_presentation, FONTS_DATA_HANDLE fg UNUSED) {
|
create_fallback_face(PyObject *base_face, CPUCell* cell, bool UNUSED bold, bool UNUSED italic, bool emoji_presentation, FONTS_DATA_HANDLE fg) {
|
||||||
CTFace *self = (CTFace*)base_face;
|
CTFace *self = (CTFace*)base_face;
|
||||||
CTFontRef new_font;
|
CTFontRef new_font;
|
||||||
if (emoji_presentation) new_font = CTFontCreateWithName((CFStringRef)@"AppleColorEmoji", self->scaled_point_sz, NULL);
|
#define search_for_fallback() \
|
||||||
else {
|
char text[64] = {0}; \
|
||||||
char text[64] = {0};
|
cell_as_utf8_for_fallback(cell, text); \
|
||||||
cell_as_utf8_for_fallback(cell, text);
|
CFStringRef str = CFStringCreateWithCString(NULL, text, kCFStringEncodingUTF8); \
|
||||||
CFStringRef str = CFStringCreateWithCString(NULL, text, kCFStringEncodingUTF8);
|
if (str == NULL) return PyErr_NoMemory(); \
|
||||||
if (str == NULL) return PyErr_NoMemory();
|
new_font = find_substitute_face(str, self->ct_font, cell); \
|
||||||
new_font = find_substitute_face(str, self->ct_font, cell);
|
|
||||||
CFRelease(str);
|
CFRelease(str);
|
||||||
|
|
||||||
|
if (emoji_presentation) {
|
||||||
|
new_font = CTFontCreateWithName((CFStringRef)@"AppleColorEmoji", self->scaled_point_sz, NULL);
|
||||||
|
if (!new_font || !glyph_id_for_codepoint_ctfont(new_font, cell->ch)) {
|
||||||
|
if (new_font) CFRelease(new_font);
|
||||||
|
search_for_fallback();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else { search_for_fallback(); }
|
||||||
if (new_font == NULL) return NULL;
|
if (new_font == NULL) return NULL;
|
||||||
return (PyObject*)ct_face(new_font);
|
return (PyObject*)ct_face(new_font, fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned int
|
unsigned int
|
||||||
@@ -299,7 +308,7 @@ set_size_for_face(PyObject *s, unsigned int UNUSED desired_height, bool force, F
|
|||||||
if (!force && self->scaled_point_sz == sz) return true;
|
if (!force && self->scaled_point_sz == sz) return true;
|
||||||
CTFontRef new_font = CTFontCreateCopyWithAttributes(self->ct_font, sz, NULL, NULL);
|
CTFontRef new_font = CTFontCreateCopyWithAttributes(self->ct_font, sz, NULL, NULL);
|
||||||
if (new_font == NULL) fatal("Out of memory");
|
if (new_font == NULL) fatal("Out of memory");
|
||||||
init_face(self, new_font);
|
init_face(self, new_font, fg);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,9 +341,7 @@ cell_metrics(PyObject *s, unsigned int* cell_width, unsigned int* cell_height, u
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*cell_width = MAX(1u, width);
|
*cell_width = MAX(1u, width);
|
||||||
*underline_position = (unsigned int)floor(self->ascent - self->underline_position + 0.5);
|
|
||||||
*underline_thickness = (unsigned int)ceil(MAX(0.1, self->underline_thickness));
|
*underline_thickness = (unsigned int)ceil(MAX(0.1, self->underline_thickness));
|
||||||
*baseline = (unsigned int)self->ascent;
|
|
||||||
*strikethrough_position = (unsigned int)floor(*baseline * 0.65);
|
*strikethrough_position = (unsigned int)floor(*baseline * 0.65);
|
||||||
*strikethrough_thickness = *underline_thickness;
|
*strikethrough_thickness = *underline_thickness;
|
||||||
// float line_height = MAX(1, floor(self->ascent + self->descent + MAX(0, self->leading) + 0.5));
|
// float line_height = MAX(1, floor(self->ascent + self->descent + MAX(0, self->leading) + 0.5));
|
||||||
@@ -354,8 +361,30 @@ cell_metrics(PyObject *s, unsigned int* cell_width, unsigned int* cell_height, u
|
|||||||
CTFrameGetLineOrigins(test_frame, CFRangeMake(0, 1), &origin1);
|
CTFrameGetLineOrigins(test_frame, CFRangeMake(0, 1), &origin1);
|
||||||
CTFrameGetLineOrigins(test_frame, CFRangeMake(1, 1), &origin2);
|
CTFrameGetLineOrigins(test_frame, CFRangeMake(1, 1), &origin2);
|
||||||
CGFloat line_height = origin1.y - origin2.y;
|
CGFloat line_height = origin1.y - origin2.y;
|
||||||
CFRelease(test_frame); CFRelease(path); CFRelease(framesetter);
|
CFArrayRef lines = CTFrameGetLines(test_frame);
|
||||||
|
CTLineRef line = CFArrayGetValueAtIndex(lines, 0);
|
||||||
|
CGRect bounds = CTLineGetBoundsWithOptions(line, 0);
|
||||||
|
CGRect bounds_without_leading = CTLineGetBoundsWithOptions(line, kCTLineBoundsExcludeTypographicLeading);
|
||||||
|
CGFloat typographic_ascent, typographic_descent, typographic_leading;
|
||||||
|
CTLineGetTypographicBounds(line, &typographic_ascent, &typographic_descent, &typographic_leading);
|
||||||
|
CGFloat bounds_ascent = bounds_without_leading.size.height + bounds_without_leading.origin.y;
|
||||||
|
*baseline = (unsigned int)floor(bounds_ascent + 0.5);
|
||||||
*cell_height = MAX(4u, (unsigned int)ceilf(line_height));
|
*cell_height = MAX(4u, (unsigned int)ceilf(line_height));
|
||||||
|
// Not sure if we should add this to bounds ascent and then round it or add
|
||||||
|
// it to already rounded baseline and round again.
|
||||||
|
*underline_position = (unsigned int)floor(bounds_ascent - self->underline_position + 0.5);
|
||||||
|
|
||||||
|
debug("Cell height calculation:\n");
|
||||||
|
debug("\tline height from line origins: %f\n", line_height);
|
||||||
|
debug("\tline bounds: origin-y: %f height: %f\n", bounds.origin.y, bounds.size.height);
|
||||||
|
debug("\tline bounds-no-leading: origin-y: %f height: %f\n", bounds.origin.y, bounds.size.height);
|
||||||
|
debug("\tbounds metrics: ascent: %f\n", bounds_ascent);
|
||||||
|
debug("\tline metrics: ascent: %f descent: %f leading: %f\n", typographic_ascent, typographic_descent, typographic_leading);
|
||||||
|
debug("\tfont metrics: ascent: %f descent: %f leading: %f underline_position: %f\n", self->ascent, self->descent, self->leading, self->underline_position);
|
||||||
|
debug("\tcell_height: %u baseline: %u underline_position: %u\n", *cell_height, *baseline, *underline_position);
|
||||||
|
|
||||||
|
CFRelease(test_frame); CFRelease(path); CFRelease(framesetter);
|
||||||
|
|
||||||
#undef count
|
#undef count
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,18 +395,18 @@ face_from_descriptor(PyObject *descriptor, FONTS_DATA_HANDLE fg) {
|
|||||||
CTFontRef font = CTFontCreateWithFontDescriptor(desc, scaled_point_sz(fg), NULL);
|
CTFontRef font = CTFontCreateWithFontDescriptor(desc, scaled_point_sz(fg), NULL);
|
||||||
CFRelease(desc); desc = NULL;
|
CFRelease(desc); desc = NULL;
|
||||||
if (!font) { PyErr_SetString(PyExc_ValueError, "Failed to create CTFont object"); return NULL; }
|
if (!font) { PyErr_SetString(PyExc_ValueError, "Failed to create CTFont object"); return NULL; }
|
||||||
return (PyObject*) ct_face(font);
|
return (PyObject*) ct_face(font, fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject*
|
PyObject*
|
||||||
face_from_path(const char *path, int UNUSED index, FONTS_DATA_HANDLE fg UNUSED) {
|
face_from_path(const char *path, int UNUSED index, FONTS_DATA_HANDLE fg) {
|
||||||
CFStringRef s = CFStringCreateWithCString(NULL, path, kCFStringEncodingUTF8);
|
CFStringRef s = CFStringCreateWithCString(NULL, path, kCFStringEncodingUTF8);
|
||||||
CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, s, kCFURLPOSIXPathStyle, false);
|
CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, s, kCFURLPOSIXPathStyle, false);
|
||||||
CGDataProviderRef dp = CGDataProviderCreateWithURL(url);
|
CGDataProviderRef dp = CGDataProviderCreateWithURL(url);
|
||||||
CGFontRef cg_font = CGFontCreateWithDataProvider(dp);
|
CGFontRef cg_font = CGFontCreateWithDataProvider(dp);
|
||||||
CTFontRef ct_font = CTFontCreateWithGraphicsFont(cg_font, 0.0, NULL, NULL);
|
CTFontRef ct_font = CTFontCreateWithGraphicsFont(cg_font, 0.0, NULL, NULL);
|
||||||
CFRelease(cg_font); CFRelease(dp); CFRelease(url); CFRelease(s);
|
CFRelease(cg_font); CFRelease(dp); CFRelease(url); CFRelease(s);
|
||||||
return (PyObject*) ct_face(ct_font);
|
return (PyObject*) ct_face(ct_font, fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject*
|
PyObject*
|
||||||
|
|||||||
@@ -278,6 +278,7 @@ PyInit_fast_data_types(void) {
|
|||||||
PyModule_AddIntMacro(m, CURSOR_BLOCK);
|
PyModule_AddIntMacro(m, CURSOR_BLOCK);
|
||||||
PyModule_AddIntMacro(m, CURSOR_BEAM);
|
PyModule_AddIntMacro(m, CURSOR_BEAM);
|
||||||
PyModule_AddIntMacro(m, CURSOR_UNDERLINE);
|
PyModule_AddIntMacro(m, CURSOR_UNDERLINE);
|
||||||
|
PyModule_AddIntMacro(m, NO_CURSOR_SHAPE);
|
||||||
PyModule_AddIntMacro(m, DECAWM);
|
PyModule_AddIntMacro(m, DECAWM);
|
||||||
PyModule_AddIntMacro(m, DECCOLM);
|
PyModule_AddIntMacro(m, DECCOLM);
|
||||||
PyModule_AddIntMacro(m, DECOM);
|
PyModule_AddIntMacro(m, DECOM);
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ typedef uint16_t hyperlink_id_type;
|
|||||||
typedef int key_type;
|
typedef int key_type;
|
||||||
#define HYPERLINK_MAX_NUMBER UINT16_MAX
|
#define HYPERLINK_MAX_NUMBER UINT16_MAX
|
||||||
typedef uint16_t combining_type;
|
typedef uint16_t combining_type;
|
||||||
|
typedef uint16_t glyph_index;
|
||||||
typedef uint32_t pixel;
|
typedef uint32_t pixel;
|
||||||
typedef unsigned int index_type;
|
typedef unsigned int index_type;
|
||||||
typedef uint16_t sprite_index;
|
typedef uint16_t sprite_index;
|
||||||
|
|||||||
153
kitty/debug_config.py
Normal file
153
kitty/debug_config.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
import os
|
||||||
|
from functools import partial
|
||||||
|
from typing import Callable, Dict, Generator, Iterable, Set, Tuple
|
||||||
|
from pprint import pformat
|
||||||
|
|
||||||
|
from .cli import green, title, version
|
||||||
|
from .conf.utils import KeyAction
|
||||||
|
from .constants import is_macos, is_wayland
|
||||||
|
from .options.types import Options as KittyOpts, defaults
|
||||||
|
from .options.utils import MouseMap
|
||||||
|
from .types import MouseEvent, SingleKey
|
||||||
|
from .typing import SequenceMap
|
||||||
|
|
||||||
|
ShortcutMap = Dict[Tuple[SingleKey, ...], KeyAction]
|
||||||
|
|
||||||
|
|
||||||
|
def mod_to_names(mods: int) -> Generator[str, None, None]:
|
||||||
|
from .fast_data_types import (
|
||||||
|
GLFW_MOD_ALT, GLFW_MOD_CAPS_LOCK, GLFW_MOD_CONTROL, GLFW_MOD_HYPER,
|
||||||
|
GLFW_MOD_META, GLFW_MOD_NUM_LOCK, GLFW_MOD_SHIFT, GLFW_MOD_SUPER
|
||||||
|
)
|
||||||
|
modmap = {'shift': GLFW_MOD_SHIFT, 'alt': GLFW_MOD_ALT, 'ctrl': GLFW_MOD_CONTROL, ('cmd' if is_macos else 'super'): GLFW_MOD_SUPER,
|
||||||
|
'hyper': GLFW_MOD_HYPER, 'meta': GLFW_MOD_META, 'num_lock': GLFW_MOD_NUM_LOCK, 'caps_lock': GLFW_MOD_CAPS_LOCK}
|
||||||
|
for name, val in modmap.items():
|
||||||
|
if mods & val:
|
||||||
|
yield name
|
||||||
|
|
||||||
|
|
||||||
|
def print_shortcut(key_sequence: Iterable[SingleKey], action: KeyAction, print: Callable) -> None:
|
||||||
|
from .fast_data_types import glfw_get_key_name
|
||||||
|
keys = []
|
||||||
|
for key_spec in key_sequence:
|
||||||
|
names = []
|
||||||
|
mods, is_native, key = key_spec
|
||||||
|
names = list(mod_to_names(mods))
|
||||||
|
if key:
|
||||||
|
kname = glfw_get_key_name(0, key) if is_native else glfw_get_key_name(key, 0)
|
||||||
|
names.append(kname or f'{key}')
|
||||||
|
keys.append('+'.join(names))
|
||||||
|
|
||||||
|
print('\t' + ' > '.join(keys), action)
|
||||||
|
|
||||||
|
|
||||||
|
def print_shortcut_changes(defns: ShortcutMap, text: str, changes: Set[Tuple[SingleKey, ...]], print: Callable) -> None:
|
||||||
|
if changes:
|
||||||
|
print(title(text))
|
||||||
|
|
||||||
|
for k in sorted(changes):
|
||||||
|
print_shortcut(k, defns[k], print)
|
||||||
|
|
||||||
|
|
||||||
|
def compare_keymaps(final: ShortcutMap, initial: ShortcutMap, print: Callable) -> None:
|
||||||
|
added = set(final) - set(initial)
|
||||||
|
removed = set(initial) - set(final)
|
||||||
|
changed = {k for k in set(final) & set(initial) if final[k] != initial[k]}
|
||||||
|
print_shortcut_changes(final, 'Added shortcuts:', added, print)
|
||||||
|
print_shortcut_changes(initial, 'Removed shortcuts:', removed, print)
|
||||||
|
print_shortcut_changes(final, 'Changed shortcuts:', changed, print)
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_sequence_map(m: SequenceMap) -> ShortcutMap:
|
||||||
|
ans: Dict[Tuple[SingleKey, ...], KeyAction] = {}
|
||||||
|
for key_spec, rest_map in m.items():
|
||||||
|
for r, action in rest_map.items():
|
||||||
|
ans[(key_spec,) + (r)] = action
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
def compare_mousemaps(final: MouseMap, initial: MouseMap, print: Callable) -> None:
|
||||||
|
added = set(final) - set(initial)
|
||||||
|
removed = set(initial) - set(final)
|
||||||
|
changed = {k for k in set(final) & set(initial) if final[k] != initial[k]}
|
||||||
|
|
||||||
|
def print_mouse_action(trigger: MouseEvent, action: KeyAction) -> None:
|
||||||
|
names = list(mod_to_names(trigger.mods)) + [f'b{trigger.button+1}']
|
||||||
|
when = {-1: 'repeat', 1: 'press', 2: 'doublepress', 3: 'triplepress'}.get(trigger.repeat_count, trigger.repeat_count)
|
||||||
|
grabbed = 'grabbed' if trigger.grabbed else 'ungrabbed'
|
||||||
|
print('\t', '+'.join(names), when, grabbed, action)
|
||||||
|
|
||||||
|
def print_changes(defns: MouseMap, changes: Set[MouseEvent], text: str) -> None:
|
||||||
|
if changes:
|
||||||
|
print(title(text))
|
||||||
|
for k in sorted(changes):
|
||||||
|
print_mouse_action(k, defns[k])
|
||||||
|
|
||||||
|
print_changes(final, added, 'Added mouse actions:')
|
||||||
|
print_changes(initial, removed, 'Removed mouse actions:')
|
||||||
|
print_changes(final, changed, 'Changed mouse actions:')
|
||||||
|
|
||||||
|
|
||||||
|
def compare_opts(opts: KittyOpts, print: Callable) -> None:
|
||||||
|
from .config import load_config
|
||||||
|
print()
|
||||||
|
print('Config options different from defaults:')
|
||||||
|
default_opts = load_config()
|
||||||
|
ignored = ('keymap', 'sequence_map', 'mousemap', 'map', 'mouse_map')
|
||||||
|
changed_opts = [
|
||||||
|
f for f in sorted(defaults._fields)
|
||||||
|
if f not in ignored and getattr(opts, f) != getattr(defaults, f)
|
||||||
|
]
|
||||||
|
field_len = max(map(len, changed_opts)) if changed_opts else 20
|
||||||
|
fmt = '{{:{:d}s}}'.format(field_len)
|
||||||
|
for f in changed_opts:
|
||||||
|
val = getattr(opts, f)
|
||||||
|
if isinstance(val, dict):
|
||||||
|
print(f'{title(f)}:')
|
||||||
|
if f == 'symbol_map':
|
||||||
|
for k in sorted(val):
|
||||||
|
print(f'\tU+{k[0]:04x} - U+{k[1]:04x} → {val[k]}')
|
||||||
|
else:
|
||||||
|
print(pformat(val))
|
||||||
|
else:
|
||||||
|
print(title(fmt.format(f)), str(getattr(opts, f)))
|
||||||
|
|
||||||
|
compare_mousemaps(opts.mousemap, default_opts.mousemap, print)
|
||||||
|
final_, initial_ = opts.keymap, default_opts.keymap
|
||||||
|
final: ShortcutMap = {(k,): v for k, v in final_.items()}
|
||||||
|
initial: ShortcutMap = {(k,): v for k, v in initial_.items()}
|
||||||
|
final_s, initial_s = map(flatten_sequence_map, (opts.sequence_map, default_opts.sequence_map))
|
||||||
|
final.update(final_s)
|
||||||
|
initial.update(initial_s)
|
||||||
|
compare_keymaps(final, initial, print)
|
||||||
|
|
||||||
|
|
||||||
|
def debug_config(opts: KittyOpts) -> str:
|
||||||
|
from io import StringIO
|
||||||
|
out = StringIO()
|
||||||
|
p = partial(print, file=out)
|
||||||
|
p(version(add_rev=True))
|
||||||
|
p(' '.join(os.uname()))
|
||||||
|
if is_macos:
|
||||||
|
import subprocess
|
||||||
|
p(' '.join(subprocess.check_output(['sw_vers']).decode('utf-8').splitlines()).strip())
|
||||||
|
if os.path.exists('/etc/issue'):
|
||||||
|
with open('/etc/issue', encoding='utf-8', errors='replace') as f:
|
||||||
|
p(f.read().strip())
|
||||||
|
if os.path.exists('/etc/lsb-release'):
|
||||||
|
with open('/etc/lsb-release', encoding='utf-8', errors='replace') as f:
|
||||||
|
p(f.read().strip())
|
||||||
|
if not is_macos:
|
||||||
|
p('Running under:' + green('Wayland' if is_wayland() else 'X11'))
|
||||||
|
if opts.config_paths:
|
||||||
|
p(green('Loaded config files:'))
|
||||||
|
p(' ', '\n '.join(opts.config_paths))
|
||||||
|
if opts.config_overrides:
|
||||||
|
p(green('Loaded config overrides:'))
|
||||||
|
p(' ', '\n '.join(opts.config_overrides))
|
||||||
|
compare_opts(opts, p)
|
||||||
|
return out.getvalue()
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
#include "disk-cache.h"
|
#include "disk-cache.h"
|
||||||
#include "safe-wrappers.h"
|
#include "safe-wrappers.h"
|
||||||
#include "uthash.h"
|
#include "kitty-uthash.h"
|
||||||
#include "loop-utils.h"
|
#include "loop-utils.h"
|
||||||
#include "threading.h"
|
#include "threading.h"
|
||||||
#include "cross-platform-random.h"
|
#include "cross-platform-random.h"
|
||||||
@@ -165,8 +165,11 @@ size_of_cache_file(DiskCache *self) {
|
|||||||
|
|
||||||
size_t
|
size_t
|
||||||
disk_cache_size_on_disk(PyObject *self) {
|
disk_cache_size_on_disk(PyObject *self) {
|
||||||
off_t ans = size_of_cache_file((DiskCache*)self);
|
if (((DiskCache*)self)->cache_file_fd > -1) {
|
||||||
return MAX(0, ans);
|
off_t ans = size_of_cache_file((DiskCache*)self);
|
||||||
|
return MAX(0, ans);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -362,7 +365,9 @@ write_loop(void *data) {
|
|||||||
continue;
|
continue;
|
||||||
} else if (!count) {
|
} else if (!count) {
|
||||||
mutex(lock);
|
mutex(lock);
|
||||||
if (ftruncate(self->cache_file_fd, 0) == 0) lseek(self->cache_file_fd, 0, SEEK_END);
|
if (self->cache_file_fd > -1) {
|
||||||
|
if (ftruncate(self->cache_file_fd, 0) == 0) lseek(self->cache_file_fd, 0, SEEK_END);
|
||||||
|
}
|
||||||
mutex(unlock);
|
mutex(unlock);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,17 @@ from typing import (
|
|||||||
|
|
||||||
import termios
|
import termios
|
||||||
from kitty.boss import Boss
|
from kitty.boss import Boss
|
||||||
|
from kitty.fonts import FontFeature
|
||||||
from kitty.fonts.render import FontObject
|
from kitty.fonts.render import FontObject
|
||||||
from kitty.options_stub import Options
|
from kitty.options.types import Options
|
||||||
|
|
||||||
# Constants {{{
|
# Constants {{{
|
||||||
|
MOUSE_SELECTION_LINE: int
|
||||||
|
MOUSE_SELECTION_EXTEND: int
|
||||||
|
MOUSE_SELECTION_NORMAL: int
|
||||||
|
MOUSE_SELECTION_WORD: int
|
||||||
|
MOUSE_SELECTION_RECTANGLE: int
|
||||||
|
MOUSE_SELECTION_LINE_FROM_POINT: int
|
||||||
KITTY_VCS_REV: str
|
KITTY_VCS_REV: str
|
||||||
NO_CLOSE_REQUESTED: int
|
NO_CLOSE_REQUESTED: int
|
||||||
IMPERATIVE_CLOSE_REQUESTED: int
|
IMPERATIVE_CLOSE_REQUESTED: int
|
||||||
@@ -229,6 +236,7 @@ GLFW_RELEASE: int
|
|||||||
GLFW_REPEAT: int
|
GLFW_REPEAT: int
|
||||||
CURSOR_BEAM: int
|
CURSOR_BEAM: int
|
||||||
CURSOR_BLOCK: int
|
CURSOR_BLOCK: int
|
||||||
|
NO_CURSOR_SHAPE: int
|
||||||
CURSOR_UNDERLINE: int
|
CURSOR_UNDERLINE: int
|
||||||
DECAWM: int
|
DECAWM: int
|
||||||
BGIMAGE_PROGRAM: int
|
BGIMAGE_PROGRAM: int
|
||||||
@@ -508,7 +516,7 @@ def sync_os_window_title(os_window_id: int) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def set_options(
|
def set_options(
|
||||||
opts: Options,
|
opts: Optional[Options],
|
||||||
is_wayland: bool = False,
|
is_wayland: bool = False,
|
||||||
debug_rendering: bool = False,
|
debug_rendering: bool = False,
|
||||||
debug_font_fallback: bool = False
|
debug_font_fallback: bool = False
|
||||||
@@ -516,6 +524,10 @@ def set_options(
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_options() -> Options:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def resolve_key_mods(kitty_mod: int, mods: int) -> int:
|
def resolve_key_mods(kitty_mod: int, mods: int) -> int:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -621,7 +633,7 @@ class ColorProfile:
|
|||||||
def reset_color(self, num: int) -> None:
|
def reset_color(self, num: int) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def update_ansi_color_table(self, val: List[int]) -> None:
|
def update_ansi_color_table(self, val: int) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def set_configured_colors(
|
def set_configured_colors(
|
||||||
@@ -646,7 +658,7 @@ def os_window_font_size(
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def cocoa_set_notification_activated_callback(identifier: Callable[[str], None]) -> None:
|
def cocoa_set_notification_activated_callback(identifier: Optional[Callable[[str], None]]) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -875,7 +887,7 @@ def set_font_data(
|
|||||||
descriptor_for_idx: Callable[[int], Tuple[FontObject, bool, bool]],
|
descriptor_for_idx: Callable[[int], Tuple[FontObject, bool, bool]],
|
||||||
bold: int, italic: int, bold_italic: int, num_symbol_fonts: int,
|
bold: int, italic: int, bold_italic: int, num_symbol_fonts: int,
|
||||||
symbol_maps: Tuple[Tuple[int, int, int], ...], font_sz_in_pts: float,
|
symbol_maps: Tuple[Tuple[int, int, int], ...], font_sz_in_pts: float,
|
||||||
font_feature_settings: Dict[str, Tuple[str, ...]]
|
font_feature_settings: Dict[str, Tuple[FontFeature, ...]]
|
||||||
) -> None:
|
) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -917,6 +929,8 @@ class Cursor:
|
|||||||
fg: int
|
fg: int
|
||||||
bold: bool
|
bold: bool
|
||||||
italic: bool
|
italic: bool
|
||||||
|
blink: bool
|
||||||
|
shape: int
|
||||||
|
|
||||||
|
|
||||||
class Screen:
|
class Screen:
|
||||||
@@ -928,6 +942,7 @@ class Screen:
|
|||||||
historybuf: HistoryBuf
|
historybuf: HistoryBuf
|
||||||
linebuf: LineBuf
|
linebuf: LineBuf
|
||||||
in_bracketed_paste_mode: bool
|
in_bracketed_paste_mode: bool
|
||||||
|
cursor_visible: bool
|
||||||
scrolled_by: int
|
scrolled_by: int
|
||||||
cursor: Cursor
|
cursor: Cursor
|
||||||
disable_ligatures: int
|
disable_ligatures: int
|
||||||
@@ -971,6 +986,9 @@ class Screen:
|
|||||||
def reset_callbacks(self) -> None:
|
def reset_callbacks(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def has_selection(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
def text_for_selection(self) -> Tuple[str, ...]:
|
def text_for_selection(self) -> Tuple[str, ...]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -992,6 +1010,9 @@ class Screen:
|
|||||||
def scroll_to_next_mark(self, mark: int = 0, backwards: bool = True) -> bool:
|
def scroll_to_next_mark(self, mark: int = 0, backwards: bool = True) -> bool:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def reverse_scroll(self, amt: int, fill_from_scrollback: bool = False) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
def clear_selection(self) -> None:
|
def clear_selection(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -1153,3 +1174,15 @@ def spawn(
|
|||||||
|
|
||||||
def set_window_padding(os_window_id: int, tab_id: int, window_id: int, left: int, top: int, right: int, bottom: int) -> None:
|
def set_window_padding(os_window_id: int, tab_id: int, window_id: int, left: int, top: int, right: int, bottom: int) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def click_mouse_url(os_window_id: int, tab_id: int, window_id: int) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def mouse_selection(os_window_id: int, tab_id: int, window_id: int, code: int, button: int) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def apply_options_update() -> None:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -16,6 +16,25 @@
|
|||||||
#define FC_COLOR "color"
|
#define FC_COLOR "color"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
static bool initialized = false;
|
||||||
|
|
||||||
|
static void
|
||||||
|
ensure_initialized(void) {
|
||||||
|
if (!initialized) {
|
||||||
|
if (!FcInit()) fatal("Failed to initialize fontconfig library");
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
finalize(void) {
|
||||||
|
if (initialized) {
|
||||||
|
FcFini();
|
||||||
|
initialized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static inline PyObject*
|
static inline PyObject*
|
||||||
pybool(FcBool x) { PyObject *ans = x ? Py_True: Py_False; Py_INCREF(ans); return ans; }
|
pybool(FcBool x) { PyObject *ans = x ? Py_True: Py_False; Py_INCREF(ans); return ans; }
|
||||||
|
|
||||||
@@ -109,6 +128,7 @@ font_set(FcFontSet *fs) {
|
|||||||
|
|
||||||
static PyObject*
|
static PyObject*
|
||||||
fc_list(PyObject UNUSED *self, PyObject *args) {
|
fc_list(PyObject UNUSED *self, PyObject *args) {
|
||||||
|
ensure_initialized();
|
||||||
int allow_bitmapped_fonts = 0, spacing = -1;
|
int allow_bitmapped_fonts = 0, spacing = -1;
|
||||||
PyObject *ans = NULL;
|
PyObject *ans = NULL;
|
||||||
FcObjectSet *os = NULL;
|
FcObjectSet *os = NULL;
|
||||||
@@ -198,6 +218,7 @@ end:
|
|||||||
|
|
||||||
bool
|
bool
|
||||||
information_for_font_family(const char *family, bool bold, bool italic, FontConfigFace *ans) {
|
information_for_font_family(const char *family, bool bold, bool italic, FontConfigFace *ans) {
|
||||||
|
ensure_initialized();
|
||||||
memset(ans, 0, sizeof(FontConfigFace));
|
memset(ans, 0, sizeof(FontConfigFace));
|
||||||
FcPattern *pat = FcPatternCreate();
|
FcPattern *pat = FcPatternCreate();
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
@@ -214,6 +235,7 @@ end:
|
|||||||
|
|
||||||
static PyObject*
|
static PyObject*
|
||||||
fc_match(PyObject UNUSED *self, PyObject *args) {
|
fc_match(PyObject UNUSED *self, PyObject *args) {
|
||||||
|
ensure_initialized();
|
||||||
char *family = NULL;
|
char *family = NULL;
|
||||||
int bold = 0, italic = 0, allow_bitmapped_fonts = 0, spacing = FC_MONO;
|
int bold = 0, italic = 0, allow_bitmapped_fonts = 0, spacing = FC_MONO;
|
||||||
double size_in_pts = 0, dpi = 0;
|
double size_in_pts = 0, dpi = 0;
|
||||||
@@ -248,6 +270,7 @@ end:
|
|||||||
|
|
||||||
static PyObject*
|
static PyObject*
|
||||||
fc_match_postscript_name(PyObject UNUSED *self, PyObject *args) {
|
fc_match_postscript_name(PyObject UNUSED *self, PyObject *args) {
|
||||||
|
ensure_initialized();
|
||||||
const char *postscript_name = NULL;
|
const char *postscript_name = NULL;
|
||||||
FcPattern *pat = NULL;
|
FcPattern *pat = NULL;
|
||||||
PyObject *ans = NULL;
|
PyObject *ans = NULL;
|
||||||
@@ -269,6 +292,7 @@ end:
|
|||||||
|
|
||||||
PyObject*
|
PyObject*
|
||||||
specialize_font_descriptor(PyObject *base_descriptor, FONTS_DATA_HANDLE fg) {
|
specialize_font_descriptor(PyObject *base_descriptor, FONTS_DATA_HANDLE fg) {
|
||||||
|
ensure_initialized();
|
||||||
PyObject *p = PyDict_GetItemString(base_descriptor, "path"), *ans = NULL;
|
PyObject *p = PyDict_GetItemString(base_descriptor, "path"), *ans = NULL;
|
||||||
PyObject *idx = PyDict_GetItemString(base_descriptor, "index");
|
PyObject *idx = PyDict_GetItemString(base_descriptor, "index");
|
||||||
if (p == NULL) { PyErr_SetString(PyExc_ValueError, "Base descriptor has no path"); return NULL; }
|
if (p == NULL) { PyErr_SetString(PyExc_ValueError, "Base descriptor has no path"); return NULL; }
|
||||||
@@ -292,6 +316,7 @@ end:
|
|||||||
|
|
||||||
bool
|
bool
|
||||||
fallback_font(char_type ch, const char *family, bool bold, bool italic, bool prefer_color, FontConfigFace *ans) {
|
fallback_font(char_type ch, const char *family, bool bold, bool italic, bool prefer_color, FontConfigFace *ans) {
|
||||||
|
ensure_initialized();
|
||||||
memset(ans, 0, sizeof(FontConfigFace));
|
memset(ans, 0, sizeof(FontConfigFace));
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
FcPattern *pat = FcPatternCreate();
|
FcPattern *pat = FcPatternCreate();
|
||||||
@@ -310,6 +335,7 @@ end:
|
|||||||
|
|
||||||
PyObject*
|
PyObject*
|
||||||
create_fallback_face(PyObject UNUSED *base_face, CPUCell* cell, bool bold, bool italic, bool emoji_presentation, FONTS_DATA_HANDLE fg) {
|
create_fallback_face(PyObject UNUSED *base_face, CPUCell* cell, bool bold, bool italic, bool emoji_presentation, FONTS_DATA_HANDLE fg) {
|
||||||
|
ensure_initialized();
|
||||||
PyObject *ans = NULL;
|
PyObject *ans = NULL;
|
||||||
FcPattern *pat = FcPatternCreate();
|
FcPattern *pat = FcPatternCreate();
|
||||||
if (pat == NULL) return PyErr_NoMemory();
|
if (pat == NULL) return PyErr_NoMemory();
|
||||||
@@ -336,11 +362,7 @@ static PyMethodDef module_methods[] = {
|
|||||||
|
|
||||||
bool
|
bool
|
||||||
init_fontconfig_library(PyObject *module) {
|
init_fontconfig_library(PyObject *module) {
|
||||||
if (!FcInit()) {
|
register_at_exit_cleanup_func(FONTCONFIG_CLEANUP_FUNC, finalize);
|
||||||
PyErr_SetString(PyExc_RuntimeError, "Failed to initialize the fontconfig library");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
register_at_exit_cleanup_func(FONTCONFIG_CLEANUP_FUNC, FcFini);
|
|
||||||
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
||||||
PyModule_AddIntMacro(module, FC_WEIGHT_REGULAR);
|
PyModule_AddIntMacro(module, FC_WEIGHT_REGULAR);
|
||||||
PyModule_AddIntMacro(module, FC_WEIGHT_MEDIUM);
|
PyModule_AddIntMacro(module, FC_WEIGHT_MEDIUM);
|
||||||
|
|||||||
480
kitty/fonts.c
480
kitty/fonts.c
@@ -11,10 +11,9 @@
|
|||||||
#include "emoji.h"
|
#include "emoji.h"
|
||||||
#include "unicode-data.h"
|
#include "unicode-data.h"
|
||||||
#include "charsets.h"
|
#include "charsets.h"
|
||||||
|
#include "glyph-cache.h"
|
||||||
|
|
||||||
#define MISSING_GLYPH 4
|
#define MISSING_GLYPH 4
|
||||||
#define MAX_NUM_EXTRA_GLYPHS 8u
|
|
||||||
#define CELLS_IN_CANVAS ((MAX_NUM_EXTRA_GLYPHS + 1u) * 3u)
|
|
||||||
#define MAX_NUM_EXTRA_GLYPHS_PUA 4u
|
#define MAX_NUM_EXTRA_GLYPHS_PUA 4u
|
||||||
|
|
||||||
typedef void (*send_sprite_to_gpu_func)(FONTS_DATA_HANDLE fg, unsigned int, unsigned int, unsigned int, pixel*);
|
typedef void (*send_sprite_to_gpu_func)(FONTS_DATA_HANDLE fg, unsigned int, unsigned int, unsigned int, pixel*);
|
||||||
@@ -22,38 +21,16 @@ send_sprite_to_gpu_func current_send_sprite_to_gpu = NULL;
|
|||||||
static PyObject *python_send_to_gpu_impl = NULL;
|
static PyObject *python_send_to_gpu_impl = NULL;
|
||||||
extern PyTypeObject Line_Type;
|
extern PyTypeObject Line_Type;
|
||||||
|
|
||||||
typedef struct SpecialGlyphCache SpecialGlyphCache;
|
|
||||||
enum {NO_FONT=-3, MISSING_FONT=-2, BLANK_FONT=-1, BOX_FONT=0};
|
enum {NO_FONT=-3, MISSING_FONT=-2, BLANK_FONT=-1, BOX_FONT=0};
|
||||||
typedef enum {
|
typedef enum {
|
||||||
LIGATURE_UNKNOWN, INFINITE_LIGATURE_START, INFINITE_LIGATURE_MIDDLE, INFINITE_LIGATURE_END
|
LIGATURE_UNKNOWN, INFINITE_LIGATURE_START, INFINITE_LIGATURE_MIDDLE, INFINITE_LIGATURE_END
|
||||||
} LigatureType;
|
} LigatureType;
|
||||||
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
glyph_index data[MAX_NUM_EXTRA_GLYPHS];
|
|
||||||
} ExtraGlyphs;
|
|
||||||
|
|
||||||
typedef struct SpritePosition SpritePosition;
|
|
||||||
struct SpritePosition {
|
|
||||||
SpritePosition *next;
|
|
||||||
bool filled, rendered, colored;
|
|
||||||
sprite_index x, y, z;
|
|
||||||
uint8_t ligature_index;
|
|
||||||
glyph_index glyph;
|
|
||||||
ExtraGlyphs extra_glyphs;
|
|
||||||
};
|
|
||||||
|
|
||||||
#define SPECIAL_FILLED_MASK 1
|
#define SPECIAL_FILLED_MASK 1
|
||||||
#define SPECIAL_VALUE_MASK 2
|
#define SPECIAL_VALUE_MASK 2
|
||||||
#define EMPTY_FILLED_MASK 4
|
#define EMPTY_FILLED_MASK 4
|
||||||
#define EMPTY_VALUE_MASK 8
|
#define EMPTY_VALUE_MASK 8
|
||||||
#define SPECIAL_GLYPH_CACHE_SIZE 1024
|
|
||||||
|
|
||||||
struct SpecialGlyphCache {
|
|
||||||
SpecialGlyphCache *next;
|
|
||||||
glyph_index glyph;
|
|
||||||
uint8_t data;
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
size_t max_y;
|
size_t max_y;
|
||||||
@@ -76,19 +53,24 @@ typedef struct {
|
|||||||
static SymbolMap *symbol_maps = NULL;
|
static SymbolMap *symbol_maps = NULL;
|
||||||
static size_t num_symbol_maps = 0;
|
static size_t num_symbol_maps = 0;
|
||||||
|
|
||||||
typedef enum { SPACER_STARTEGY_UNKNOWN, SPACERS_BEFORE, SPACERS_AFTER } SpacerStrategy;
|
typedef enum { SPACER_STRATEGY_UNKNOWN, SPACERS_BEFORE, SPACERS_AFTER, SPACERS_IOSEVKA } SpacerStrategy;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
PyObject *face;
|
PyObject *face;
|
||||||
// Map glyphs to sprite map co-ords
|
// Map glyphs to sprite map co-ords
|
||||||
SpritePosition sprite_map[1024];
|
SpritePosition *sprite_position_hash_table;
|
||||||
hb_feature_t* ffs_hb_features;
|
hb_feature_t* ffs_hb_features;
|
||||||
size_t num_ffs_hb_features;
|
size_t num_ffs_hb_features;
|
||||||
SpecialGlyphCache special_glyph_cache[SPECIAL_GLYPH_CACHE_SIZE];
|
GlyphProperties *glyph_properties_hash_table;
|
||||||
bool bold, italic, emoji_presentation;
|
bool bold, italic, emoji_presentation;
|
||||||
SpacerStrategy spacer_strategy;
|
SpacerStrategy spacer_strategy;
|
||||||
} Font;
|
} Font;
|
||||||
|
|
||||||
|
typedef struct Canvas {
|
||||||
|
pixel *buf;
|
||||||
|
unsigned current_cells, alloced_cells;
|
||||||
|
} Canvas;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
FONTS_DATA_HEAD
|
FONTS_DATA_HEAD
|
||||||
id_type id;
|
id_type id;
|
||||||
@@ -96,7 +78,7 @@ typedef struct {
|
|||||||
size_t fonts_capacity, fonts_count, fallback_fonts_count;
|
size_t fonts_capacity, fonts_count, fallback_fonts_count;
|
||||||
ssize_t medium_font_idx, bold_font_idx, italic_font_idx, bi_font_idx, first_symbol_font_idx, first_fallback_font_idx;
|
ssize_t medium_font_idx, bold_font_idx, italic_font_idx, bi_font_idx, first_symbol_font_idx, first_fallback_font_idx;
|
||||||
Font *fonts;
|
Font *fonts;
|
||||||
pixel *canvas;
|
Canvas canvas;
|
||||||
GPUSpriteTracker sprite_tracker;
|
GPUSpriteTracker sprite_tracker;
|
||||||
} FontGroup;
|
} FontGroup;
|
||||||
|
|
||||||
@@ -106,6 +88,19 @@ static size_t num_font_groups = 0;
|
|||||||
static id_type font_group_id_counter = 0;
|
static id_type font_group_id_counter = 0;
|
||||||
static void initialize_font_group(FontGroup *fg);
|
static void initialize_font_group(FontGroup *fg);
|
||||||
|
|
||||||
|
static void
|
||||||
|
ensure_canvas_can_fit(FontGroup *fg, unsigned cells) {
|
||||||
|
if (fg->canvas.alloced_cells < cells) {
|
||||||
|
free(fg->canvas.buf);
|
||||||
|
fg->canvas.alloced_cells = cells + 4;
|
||||||
|
fg->canvas.buf = malloc(sizeof(fg->canvas.buf[0]) * 3u * fg->canvas.alloced_cells * fg->cell_width * fg->cell_height);
|
||||||
|
if (!fg->canvas.buf) fatal("Out of memory allocating canvas");
|
||||||
|
}
|
||||||
|
fg->canvas.current_cells = cells;
|
||||||
|
if (fg->canvas.buf) memset(fg->canvas.buf, 0, sizeof(fg->canvas.buf[0]) * fg->canvas.current_cells * 3u * fg->cell_width * fg->cell_height);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
save_window_font_groups(void) {
|
save_window_font_groups(void) {
|
||||||
for (size_t o = 0; o < global_state.num_os_windows; o++) {
|
for (size_t o = 0; o < global_state.num_os_windows; o++) {
|
||||||
@@ -139,21 +134,10 @@ font_group_is_unused(FontGroup *fg) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
free_maps(Font *font) {
|
free_maps(Font *font) {
|
||||||
#define free_a_map(type, attr) {\
|
free_sprite_position_hash_table(&font->sprite_position_hash_table);
|
||||||
type *s, *t; \
|
font->sprite_position_hash_table = NULL;
|
||||||
for (size_t i = 0; i < sizeof(font->attr)/sizeof(font->attr[0]); i++) { \
|
free_glyph_properties_hash_table(&font->glyph_properties_hash_table);
|
||||||
s = font->attr[i].next; \
|
font->glyph_properties_hash_table = NULL;
|
||||||
while (s) { \
|
|
||||||
t = s; \
|
|
||||||
s = s->next; \
|
|
||||||
free(t); \
|
|
||||||
} \
|
|
||||||
}\
|
|
||||||
memset(font->attr, 0, sizeof(font->attr)); \
|
|
||||||
}
|
|
||||||
free_a_map(SpritePosition, sprite_map);
|
|
||||||
free_a_map(SpecialGlyphCache, special_glyph_cache);
|
|
||||||
#undef free_a_map
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
@@ -166,7 +150,7 @@ del_font(Font *f) {
|
|||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
del_font_group(FontGroup *fg) {
|
del_font_group(FontGroup *fg) {
|
||||||
free(fg->canvas); fg->canvas = NULL;
|
free(fg->canvas.buf); fg->canvas.buf = NULL; fg->canvas = (Canvas){0};
|
||||||
fg->sprite_map = free_sprite_map(fg->sprite_map);
|
fg->sprite_map = free_sprite_map(fg->sprite_map);
|
||||||
for (size_t i = 0; i < fg->fonts_count; i++) del_font(fg->fonts + i);
|
for (size_t i = 0; i < fg->fonts_count; i++) del_font(fg->fonts + i);
|
||||||
free(fg->fonts); fg->fonts = NULL;
|
free(fg->fonts); fg->fonts = NULL;
|
||||||
@@ -217,11 +201,6 @@ font_group_for(double font_sz_in_pts, double logical_dpi_x, double logical_dpi_y
|
|||||||
return fg;
|
return fg;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
|
||||||
clear_canvas(FontGroup *fg) {
|
|
||||||
if (fg->canvas) memset(fg->canvas, 0, sizeof(pixel) * CELLS_IN_CANVAS * fg->cell_width * fg->cell_height);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Sprites {{{
|
// Sprites {{{
|
||||||
@@ -258,64 +237,15 @@ do_increment(FontGroup *fg, int *error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static inline bool
|
|
||||||
extra_glyphs_equal(ExtraGlyphs *a, ExtraGlyphs *b) {
|
|
||||||
for (size_t i = 0; i < MAX_NUM_EXTRA_GLYPHS; i++) {
|
|
||||||
if (a->data[i] != b->data[i]) return false;
|
|
||||||
if (a->data[i] == 0) return true;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static SpritePosition*
|
static SpritePosition*
|
||||||
sprite_position_for(FontGroup *fg, Font *font, glyph_index glyph, ExtraGlyphs *extra_glyphs, uint8_t ligature_index, int *error) {
|
sprite_position_for(FontGroup *fg, Font *font, glyph_index *glyphs, unsigned glyph_count, uint8_t ligature_index, int *error) {
|
||||||
glyph_index idx = glyph & (SPECIAL_GLYPH_CACHE_SIZE - 1);
|
bool created;
|
||||||
SpritePosition *s = font->sprite_map + idx;
|
SpritePosition *s = find_or_create_sprite_position(&font->sprite_position_hash_table, glyphs, glyph_count, ligature_index, &created);
|
||||||
// Optimize for the common case of glyph under 1024 already in the cache
|
if (!s) { *error = 1; return NULL; }
|
||||||
if (LIKELY(s->glyph == glyph && s->filled && extra_glyphs_equal(&s->extra_glyphs, extra_glyphs) && s->ligature_index == ligature_index)) return s; // Cache hit
|
if (created) {
|
||||||
while(true) {
|
s->x = fg->sprite_tracker.x; s->y = fg->sprite_tracker.y; s->z = fg->sprite_tracker.z;
|
||||||
if (s->filled) {
|
do_increment(fg, error);
|
||||||
if (s->glyph == glyph && extra_glyphs_equal(&s->extra_glyphs, extra_glyphs) && s->ligature_index == ligature_index) return s; // Cache hit
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (!s->next) {
|
|
||||||
s->next = calloc(1, sizeof(SpritePosition));
|
|
||||||
if (s->next == NULL) { *error = 1; return NULL; }
|
|
||||||
}
|
|
||||||
s = s->next;
|
|
||||||
}
|
}
|
||||||
s->glyph = glyph;
|
|
||||||
memcpy(&s->extra_glyphs, extra_glyphs, sizeof(ExtraGlyphs));
|
|
||||||
s->ligature_index = ligature_index;
|
|
||||||
s->filled = true;
|
|
||||||
s->rendered = false;
|
|
||||||
s->colored = false;
|
|
||||||
s->x = fg->sprite_tracker.x; s->y = fg->sprite_tracker.y; s->z = fg->sprite_tracker.z;
|
|
||||||
do_increment(fg, error);
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline SpecialGlyphCache*
|
|
||||||
special_glyph_cache_for(Font *font, glyph_index glyph, uint8_t filled_mask) {
|
|
||||||
SpecialGlyphCache *s = font->special_glyph_cache + (glyph & 0x3ff);
|
|
||||||
// Optimize for the common case of glyph under SPECIAL_GLYPH_CACHE_SIZE already in the cache
|
|
||||||
if (LIKELY(s->glyph == glyph && s->data & filled_mask)) return s; // Cache hit
|
|
||||||
while(true) {
|
|
||||||
if (s->data & filled_mask) {
|
|
||||||
if (s->glyph == glyph) return s; // Cache hit
|
|
||||||
} else {
|
|
||||||
if (!s->glyph) break; // Empty cache slot
|
|
||||||
else if (s->glyph == glyph) return s; // Cache slot that contains other data than the data indicated by filled_mask
|
|
||||||
}
|
|
||||||
if (!s->next) {
|
|
||||||
s->next = calloc(1, sizeof(SpecialGlyphCache));
|
|
||||||
if (s->next == NULL) return NULL;
|
|
||||||
}
|
|
||||||
s = s->next;
|
|
||||||
}
|
|
||||||
s->glyph = glyph;
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,6 +318,7 @@ free_font_groups(void) {
|
|||||||
free(font_groups); font_groups = NULL;
|
free(font_groups); font_groups = NULL;
|
||||||
font_groups_capacity = 0; num_font_groups = 0;
|
font_groups_capacity = 0; num_font_groups = 0;
|
||||||
}
|
}
|
||||||
|
free_glyph_cache_global_resources();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@@ -437,9 +368,7 @@ calc_cell_metrics(FontGroup *fg) {
|
|||||||
sprite_tracker_set_layout(&fg->sprite_tracker, cell_width, cell_height);
|
sprite_tracker_set_layout(&fg->sprite_tracker, cell_width, cell_height);
|
||||||
fg->cell_width = cell_width; fg->cell_height = cell_height;
|
fg->cell_width = cell_width; fg->cell_height = cell_height;
|
||||||
fg->baseline = baseline; fg->underline_position = underline_position; fg->underline_thickness = underline_thickness, fg->strikethrough_position = strikethrough_position, fg->strikethrough_thickness = strikethrough_thickness;
|
fg->baseline = baseline; fg->underline_position = underline_position; fg->underline_thickness = underline_thickness, fg->strikethrough_position = strikethrough_position, fg->strikethrough_thickness = strikethrough_thickness;
|
||||||
free(fg->canvas);
|
ensure_canvas_can_fit(fg, 8);
|
||||||
fg->canvas = calloc((size_t)CELLS_IN_CANVAS * fg->cell_width * fg->cell_height, sizeof(pixel));
|
|
||||||
if (!fg->canvas) fatal("Out of memory allocating canvas for font group");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline bool
|
static inline bool
|
||||||
@@ -647,8 +576,7 @@ static void
|
|||||||
render_box_cell(FontGroup *fg, CPUCell *cpu_cell, GPUCell *gpu_cell) {
|
render_box_cell(FontGroup *fg, CPUCell *cpu_cell, GPUCell *gpu_cell) {
|
||||||
int error = 0;
|
int error = 0;
|
||||||
glyph_index glyph = box_glyph_id(cpu_cell->ch);
|
glyph_index glyph = box_glyph_id(cpu_cell->ch);
|
||||||
static ExtraGlyphs extra_glyphs = {{0}};
|
SpritePosition *sp = sprite_position_for(fg, &fg->fonts[BOX_FONT], &glyph, 1, false, &error);
|
||||||
SpritePosition *sp = sprite_position_for(fg, &fg->fonts[BOX_FONT], glyph, &extra_glyphs, false, &error);
|
|
||||||
if (sp == NULL) {
|
if (sp == NULL) {
|
||||||
sprite_map_set_error(error); PyErr_Print();
|
sprite_map_set_error(error); PyErr_Print();
|
||||||
set_sprite(gpu_cell, 0, 0, 0);
|
set_sprite(gpu_cell, 0, 0, 0);
|
||||||
@@ -661,10 +589,10 @@ render_box_cell(FontGroup *fg, CPUCell *cpu_cell, GPUCell *gpu_cell) {
|
|||||||
PyObject *ret = PyObject_CallFunction(box_drawing_function, "IIId", cpu_cell->ch, fg->cell_width, fg->cell_height, (fg->logical_dpi_x + fg->logical_dpi_y) / 2.0);
|
PyObject *ret = PyObject_CallFunction(box_drawing_function, "IIId", cpu_cell->ch, fg->cell_width, fg->cell_height, (fg->logical_dpi_x + fg->logical_dpi_y) / 2.0);
|
||||||
if (ret == NULL) { PyErr_Print(); return; }
|
if (ret == NULL) { PyErr_Print(); return; }
|
||||||
uint8_t *alpha_mask = PyLong_AsVoidPtr(PyTuple_GET_ITEM(ret, 0));
|
uint8_t *alpha_mask = PyLong_AsVoidPtr(PyTuple_GET_ITEM(ret, 0));
|
||||||
clear_canvas(fg);
|
ensure_canvas_can_fit(fg, 1);
|
||||||
Region r = { .right = fg->cell_width, .bottom = fg->cell_height };
|
Region r = { .right = fg->cell_width, .bottom = fg->cell_height };
|
||||||
render_alpha_mask(alpha_mask, fg->canvas, &r, &r, fg->cell_width, fg->cell_width);
|
render_alpha_mask(alpha_mask, fg->canvas.buf, &r, &r, fg->cell_width, fg->cell_width);
|
||||||
current_send_sprite_to_gpu((FONTS_DATA_HANDLE)fg, sp->x, sp->y, sp->z, fg->canvas);
|
current_send_sprite_to_gpu((FONTS_DATA_HANDLE)fg, sp->x, sp->y, sp->z, fg->canvas.buf);
|
||||||
Py_DECREF(ret);
|
Py_DECREF(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,46 +618,62 @@ load_hb_buffer(CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_
|
|||||||
|
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
set_cell_sprite(GPUCell *cell, SpritePosition *sp) {
|
set_cell_sprite(GPUCell *cell, const SpritePosition *sp) {
|
||||||
cell->sprite_x = sp->x; cell->sprite_y = sp->y; cell->sprite_z = sp->z;
|
cell->sprite_x = sp->x; cell->sprite_y = sp->y; cell->sprite_z = sp->z;
|
||||||
if (sp->colored) cell->sprite_z |= 0x4000;
|
if (sp->colored) cell->sprite_z |= 0x4000;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline pixel*
|
static inline pixel*
|
||||||
extract_cell_from_canvas(FontGroup *fg, unsigned int i, unsigned int num_cells) {
|
extract_cell_from_canvas(FontGroup *fg, unsigned int i, unsigned int num_cells) {
|
||||||
pixel *ans = fg->canvas + (fg->cell_width * fg->cell_height * (CELLS_IN_CANVAS - 1)), *dest = ans, *src = fg->canvas + (i * fg->cell_width);
|
pixel *ans = fg->canvas.buf + (fg->cell_width * fg->cell_height * (fg->canvas.current_cells - 1)), *dest = ans, *src = fg->canvas.buf + (i * fg->cell_width);
|
||||||
unsigned int stride = fg->cell_width * num_cells;
|
unsigned int stride = fg->cell_width * num_cells;
|
||||||
for (unsigned int r = 0; r < fg->cell_height; r++, dest += fg->cell_width, src += stride) memcpy(dest, src, fg->cell_width * sizeof(pixel));
|
for (unsigned int r = 0; r < fg->cell_height; r++, dest += fg->cell_width, src += stride) memcpy(dest, src, fg->cell_width * sizeof(fg->canvas.buf[0]));
|
||||||
return ans;
|
return ans;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef struct GlyphRenderScratch {
|
||||||
|
SpritePosition* *sprite_positions;
|
||||||
|
glyph_index *glyphs;
|
||||||
|
size_t sz;
|
||||||
|
} GlyphRenderScratch;
|
||||||
|
static GlyphRenderScratch global_glyph_render_scratch = {0};
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
render_group(FontGroup *fg, unsigned int num_cells, unsigned int num_glyphs, CPUCell *cpu_cells, GPUCell *gpu_cells, hb_glyph_info_t *info, hb_glyph_position_t *positions, Font *font, glyph_index glyph, ExtraGlyphs *extra_glyphs, bool center_glyph) {
|
render_group(FontGroup *fg, unsigned int num_cells, unsigned int num_glyphs, CPUCell *cpu_cells, GPUCell *gpu_cells, hb_glyph_info_t *info, hb_glyph_position_t *positions, Font *font, glyph_index *glyphs, unsigned glyph_count, bool center_glyph) {
|
||||||
static SpritePosition* sprite_position[16];
|
#define sp global_glyph_render_scratch.sprite_positions
|
||||||
int error = 0;
|
int error = 0;
|
||||||
num_cells = MIN(arraysz(sprite_position), num_cells);
|
bool all_rendered = true;
|
||||||
for (unsigned int i = 0; i < num_cells; i++) {
|
bool is_infinite_ligature = num_cells > 9 && num_glyphs == num_cells;
|
||||||
sprite_position[i] = sprite_position_for(fg, font, glyph, extra_glyphs, (uint8_t)i, &error);
|
for (unsigned i = 0, ligature_index = 0; i < num_cells; i++) {
|
||||||
|
bool is_repeat_glyph = is_infinite_ligature && i > 1 && i + 1 < num_cells && glyphs[i] == glyphs[i-1] && glyphs[i] == glyphs[i-2] && glyphs[i] == glyphs[i+1];
|
||||||
|
if (is_repeat_glyph) {
|
||||||
|
sp[i] = sp[i-1];
|
||||||
|
} else {
|
||||||
|
sp[i] = sprite_position_for(fg, font, glyphs, glyph_count, ligature_index++, &error);
|
||||||
|
}
|
||||||
if (error != 0) { sprite_map_set_error(error); PyErr_Print(); return; }
|
if (error != 0) { sprite_map_set_error(error); PyErr_Print(); return; }
|
||||||
|
if (!sp[i]->rendered) all_rendered = false;
|
||||||
}
|
}
|
||||||
if (sprite_position[0]->rendered) {
|
if (all_rendered) {
|
||||||
for (unsigned int i = 0; i < num_cells; i++) { set_cell_sprite(gpu_cells + i, sprite_position[i]); }
|
for (unsigned i = 0; i < num_cells; i++) { set_cell_sprite(gpu_cells + i, sp[i]); }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
clear_canvas(fg);
|
ensure_canvas_can_fit(fg, num_cells + 1);
|
||||||
bool was_colored = (gpu_cells->attrs & WIDTH_MASK) == 2 && is_emoji(cpu_cells->ch);
|
bool was_colored = (gpu_cells->attrs & WIDTH_MASK) == 2 && is_emoji(cpu_cells->ch);
|
||||||
render_glyphs_in_cells(font->face, font->bold, font->italic, info, positions, num_glyphs, fg->canvas, fg->cell_width, fg->cell_height, num_cells, fg->baseline, &was_colored, (FONTS_DATA_HANDLE)fg, center_glyph);
|
render_glyphs_in_cells(font->face, font->bold, font->italic, info, positions, num_glyphs, fg->canvas.buf, fg->cell_width, fg->cell_height, num_cells, fg->baseline, &was_colored, (FONTS_DATA_HANDLE)fg, center_glyph);
|
||||||
if (PyErr_Occurred()) PyErr_Print();
|
if (PyErr_Occurred()) PyErr_Print();
|
||||||
|
|
||||||
for (unsigned int i = 0; i < num_cells; i++) {
|
for (unsigned i = 0; i < num_cells; i++) {
|
||||||
sprite_position[i]->rendered = true;
|
if (!sp[i]->rendered) {
|
||||||
sprite_position[i]->colored = was_colored;
|
sp[i]->rendered = true;
|
||||||
set_cell_sprite(gpu_cells + i, sprite_position[i]);
|
sp[i]->colored = was_colored;
|
||||||
pixel *buf = num_cells == 1 ? fg->canvas : extract_cell_from_canvas(fg, i, num_cells);
|
pixel *buf = num_cells == 1 ? fg->canvas.buf : extract_cell_from_canvas(fg, i, num_cells);
|
||||||
current_send_sprite_to_gpu((FONTS_DATA_HANDLE)fg, sprite_position[i]->x, sprite_position[i]->y, sprite_position[i]->z, buf);
|
current_send_sprite_to_gpu((FONTS_DATA_HANDLE)fg, sp[i]->x, sp[i]->y, sp[i]->z, buf);
|
||||||
|
}
|
||||||
|
set_cell_sprite(gpu_cells + i, sp[i]);
|
||||||
}
|
}
|
||||||
|
#undef sp
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -807,7 +751,7 @@ static inline bool
|
|||||||
is_special_glyph(glyph_index glyph_id, Font *font, CellData* cell_data) {
|
is_special_glyph(glyph_index glyph_id, Font *font, CellData* cell_data) {
|
||||||
// A glyph is special if the codepoint it corresponds to matches a
|
// A glyph is special if the codepoint it corresponds to matches a
|
||||||
// different glyph in the font
|
// different glyph in the font
|
||||||
SpecialGlyphCache *s = special_glyph_cache_for(font, glyph_id, SPECIAL_FILLED_MASK);
|
GlyphProperties *s = find_or_create_glyph_properties(&font->glyph_properties_hash_table, glyph_id);
|
||||||
if (s == NULL) return false;
|
if (s == NULL) return false;
|
||||||
if (!(s->data & SPECIAL_FILLED_MASK)) {
|
if (!(s->data & SPECIAL_FILLED_MASK)) {
|
||||||
bool is_special = cell_data->current_codepoint ? (
|
bool is_special = cell_data->current_codepoint ? (
|
||||||
@@ -823,7 +767,7 @@ is_special_glyph(glyph_index glyph_id, Font *font, CellData* cell_data) {
|
|||||||
static inline bool
|
static inline bool
|
||||||
is_empty_glyph(glyph_index glyph_id, Font *font) {
|
is_empty_glyph(glyph_index glyph_id, Font *font) {
|
||||||
// A glyph is empty if its metrics have a width of zero
|
// A glyph is empty if its metrics have a width of zero
|
||||||
SpecialGlyphCache *s = special_glyph_cache_for(font, glyph_id, EMPTY_FILLED_MASK);
|
GlyphProperties *s = find_or_create_glyph_properties(&font->glyph_properties_hash_table, glyph_id);
|
||||||
if (s == NULL) return false;
|
if (s == NULL) return false;
|
||||||
if (!(s->data & EMPTY_FILLED_MASK)) {
|
if (!(s->data & EMPTY_FILLED_MASK)) {
|
||||||
uint8_t val = is_glyph_empty(font->face, glyph_id) ? EMPTY_VALUE_MASK : 0;
|
uint8_t val = is_glyph_empty(font->face, glyph_id) ? EMPTY_VALUE_MASK : 0;
|
||||||
@@ -863,12 +807,20 @@ check_cell_consumed(CellData *cell_data, CPUCell *last_cpu_cell) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static inline LigatureType
|
static inline LigatureType
|
||||||
ligature_type_from_glyph_name(const char *glyph_name) {
|
ligature_type_from_glyph_name(const char *glyph_name, SpacerStrategy strategy) {
|
||||||
const char *p = strrchr(glyph_name, '_');
|
const char *p, *m, *s, *e;
|
||||||
if (!p) return LIGATURE_UNKNOWN;
|
if (strategy == SPACERS_IOSEVKA) {
|
||||||
if (strcmp(p, "_start.seq") == 0) return INFINITE_LIGATURE_START;
|
p = strrchr(glyph_name, '.');
|
||||||
if (strcmp(p, "_middle.seq") == 0) return INFINITE_LIGATURE_MIDDLE;
|
m = ".join-m"; s = ".join-l"; e = ".join-r";
|
||||||
if (strcmp(p, "_end.seq") == 0) return INFINITE_LIGATURE_END;
|
} else {
|
||||||
|
p = strrchr(glyph_name, '_');
|
||||||
|
m = "_middle.seq"; s = "_start.seq"; e = "_end.seq";
|
||||||
|
}
|
||||||
|
if (p) {
|
||||||
|
if (strcmp(p, m) == 0) return INFINITE_LIGATURE_MIDDLE;
|
||||||
|
if (strcmp(p, s) == 0) return INFINITE_LIGATURE_START;
|
||||||
|
if (strcmp(p, e) == 0) return INFINITE_LIGATURE_END;
|
||||||
|
}
|
||||||
return LIGATURE_UNKNOWN;
|
return LIGATURE_UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -886,19 +838,116 @@ detect_spacer_strategy(hb_font_t *hbf, Font *font) {
|
|||||||
bool is_empty = is_special && is_empty_glyph(glyph_id, font);
|
bool is_empty = is_special && is_empty_glyph(glyph_id, font);
|
||||||
if (is_empty) font->spacer_strategy = SPACERS_AFTER;
|
if (is_empty) font->spacer_strategy = SPACERS_AFTER;
|
||||||
}
|
}
|
||||||
|
shape(cpu_cells, gpu_cells, 2, hbf, font, false);
|
||||||
|
if (G(num_glyphs)) {
|
||||||
|
char glyph_name[128]; glyph_name[arraysz(glyph_name)-1] = 0;
|
||||||
|
for (unsigned i = 0; i < G(num_glyphs); i++) {
|
||||||
|
glyph_index glyph_id = G(info)[i].codepoint;
|
||||||
|
hb_font_glyph_to_string(hbf, glyph_id, glyph_name, arraysz(glyph_name)-1);
|
||||||
|
char *dot = strrchr(glyph_name, '.');
|
||||||
|
if (dot && (!strcmp(dot, ".join-l") || !strcmp(dot, ".join-r") || !strcmp(dot, ".join-m"))) {
|
||||||
|
font->spacer_strategy = SPACERS_IOSEVKA;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
static LigatureType
|
||||||
shape_run(CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_cells, Font *font, bool disable_ligature) {
|
ligature_type_for_glyph(hb_font_t *hbf, glyph_index glyph_id, SpacerStrategy strategy) {
|
||||||
hb_font_t *hbf = harfbuzz_font_for_face(font->face);
|
static char glyph_name[128]; glyph_name[arraysz(glyph_name)-1] = 0;
|
||||||
if (font->spacer_strategy == SPACER_STARTEGY_UNKNOWN) detect_spacer_strategy(hbf, font);
|
hb_font_glyph_to_string(hbf, glyph_id, glyph_name, arraysz(glyph_name)-1);
|
||||||
shape(first_cpu_cell, first_gpu_cell, num_cells, hbf, font, disable_ligature);
|
return ligature_type_from_glyph_name(glyph_name, strategy);
|
||||||
#if 0
|
}
|
||||||
static char dbuf[1024];
|
|
||||||
// You can also generate this easily using hb-shape --show-extents --cluster-level=1 --shapers=ot /path/to/font/file text
|
#define L INFINITE_LIGATURE_START
|
||||||
hb_buffer_serialize_glyphs(harfbuzz_buffer, 0, group_state.num_glyphs, dbuf, sizeof(dbuf), NULL, harfbuzz_font_for_face(font->face), HB_BUFFER_SERIALIZE_FORMAT_TEXT, HB_BUFFER_SERIALIZE_FLAG_DEFAULT | HB_BUFFER_SERIALIZE_FLAG_GLYPH_EXTENTS);
|
#define M INFINITE_LIGATURE_MIDDLE
|
||||||
printf("\n%s\n", dbuf);
|
#define R INFINITE_LIGATURE_END
|
||||||
#endif
|
#define I LIGATURE_UNKNOWN
|
||||||
|
static bool
|
||||||
|
is_iosevka_lig_starter(LigatureType before, LigatureType current, LigatureType after) {
|
||||||
|
return (current == R || (current == I && (after == L || after == M))) \
|
||||||
|
&& \
|
||||||
|
!(before == R || before == M);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
is_iosevka_lig_ender(LigatureType before, LigatureType current, LigatureType after) {
|
||||||
|
return (current == L || (current == I && (before == R || before == M))) \
|
||||||
|
&& \
|
||||||
|
!(after == L || after == M);
|
||||||
|
}
|
||||||
|
#undef L
|
||||||
|
#undef M
|
||||||
|
#undef R
|
||||||
|
#undef I
|
||||||
|
|
||||||
|
static LigatureType *ligature_types = NULL;
|
||||||
|
static size_t ligature_types_sz = 0;
|
||||||
|
|
||||||
|
static void
|
||||||
|
group_iosevka(Font *font, hb_font_t *hbf) {
|
||||||
|
// Group as per algorithm discussed in: https://github.com/be5invis/Iosevka/issues/1007
|
||||||
|
if (ligature_types_sz <= G(num_glyphs)) {
|
||||||
|
ligature_types_sz = G(num_glyphs) + 16;
|
||||||
|
ligature_types = realloc(ligature_types, ligature_types_sz * sizeof(ligature_types[0]));
|
||||||
|
if (!ligature_types) fatal("Out of memory allocating ligature types array");
|
||||||
|
}
|
||||||
|
for (size_t i = G(glyph_idx); i < G(num_glyphs); i++) {
|
||||||
|
ligature_types[i] = ligature_type_for_glyph(hbf, G(info)[i].codepoint, font->spacer_strategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t cluster, next_cluster;
|
||||||
|
while (G(glyph_idx) < G(num_glyphs) && G(cell_idx) < G(num_cells)) {
|
||||||
|
cluster = G(info)[G(glyph_idx)].cluster;
|
||||||
|
uint32_t num_codepoints_used_by_glyph = 0;
|
||||||
|
bool is_last_glyph = G(glyph_idx) == G(num_glyphs) - 1;
|
||||||
|
Group *current_group = G(groups) + G(group_idx);
|
||||||
|
if (is_last_glyph) {
|
||||||
|
num_codepoints_used_by_glyph = UINT32_MAX;
|
||||||
|
next_cluster = 0;
|
||||||
|
} else {
|
||||||
|
next_cluster = G(info)[G(glyph_idx) + 1].cluster;
|
||||||
|
// RTL languages like Arabic have decreasing cluster numbers
|
||||||
|
if (next_cluster != cluster) num_codepoints_used_by_glyph = cluster > next_cluster ? cluster - next_cluster : next_cluster - cluster;
|
||||||
|
}
|
||||||
|
const LigatureType before = G(glyph_idx) ? ligature_types[G(glyph_idx - 1)] : LIGATURE_UNKNOWN;
|
||||||
|
const LigatureType current = ligature_types[G(glyph_idx)];
|
||||||
|
const LigatureType after = is_last_glyph ? LIGATURE_UNKNOWN : ligature_types[G(glyph_idx + 1)];
|
||||||
|
bool end_current_group = false;
|
||||||
|
if (current_group->num_glyphs && is_iosevka_lig_ender(before, current, after)) {
|
||||||
|
end_current_group = true;
|
||||||
|
}
|
||||||
|
if (!current_group->num_glyphs++) {
|
||||||
|
if (is_iosevka_lig_starter(before, current, after)) current_group->has_special_glyph = true;
|
||||||
|
else end_current_group = true;
|
||||||
|
current_group->first_glyph_idx = G(glyph_idx);
|
||||||
|
current_group->first_cell_idx = G(cell_idx);
|
||||||
|
}
|
||||||
|
if (is_last_glyph) {
|
||||||
|
// soak up all remaining cells
|
||||||
|
if (G(cell_idx) < G(num_cells)) {
|
||||||
|
unsigned int num_left = G(num_cells) - G(cell_idx);
|
||||||
|
current_group->num_cells += num_left;
|
||||||
|
G(cell_idx) += num_left;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unsigned int num_cells_consumed = 0;
|
||||||
|
while (num_codepoints_used_by_glyph && G(cell_idx) < G(num_cells)) {
|
||||||
|
unsigned int w = check_cell_consumed(&G(current_cell_data), G(last_cpu_cell));
|
||||||
|
G(cell_idx) += w;
|
||||||
|
num_cells_consumed += w;
|
||||||
|
num_codepoints_used_by_glyph--;
|
||||||
|
}
|
||||||
|
current_group->num_cells += num_cells_consumed;
|
||||||
|
}
|
||||||
|
if (end_current_group) G(group_idx)++;
|
||||||
|
G(glyph_idx)++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
group_normal(Font *font, hb_font_t *hbf) {
|
||||||
/* Now distribute the glyphs into groups of cells
|
/* Now distribute the glyphs into groups of cells
|
||||||
* Considerations to keep in mind:
|
* Considerations to keep in mind:
|
||||||
* Group sizes should be as small as possible for best performance
|
* Group sizes should be as small as possible for best performance
|
||||||
@@ -919,13 +968,10 @@ shape_run(CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_cells
|
|||||||
*/
|
*/
|
||||||
uint32_t cluster, next_cluster;
|
uint32_t cluster, next_cluster;
|
||||||
bool add_to_current_group;
|
bool add_to_current_group;
|
||||||
char glyph_name[128]; glyph_name[arraysz(glyph_name)-1] = 0;
|
|
||||||
bool prev_glyph_was_inifinte_ligature_end = false;
|
bool prev_glyph_was_inifinte_ligature_end = false;
|
||||||
#define MAX_GLYPHS_IN_GROUP (MAX_NUM_EXTRA_GLYPHS + 1u)
|
|
||||||
while (G(glyph_idx) < G(num_glyphs) && G(cell_idx) < G(num_cells)) {
|
while (G(glyph_idx) < G(num_glyphs) && G(cell_idx) < G(num_cells)) {
|
||||||
glyph_index glyph_id = G(info)[G(glyph_idx)].codepoint;
|
glyph_index glyph_id = G(info)[G(glyph_idx)].codepoint;
|
||||||
hb_font_glyph_to_string(hbf, glyph_id, glyph_name, arraysz(glyph_name)-1);
|
LigatureType ligature_type = ligature_type_for_glyph(hbf, glyph_id, font->spacer_strategy);
|
||||||
LigatureType ligature_type = ligature_type_from_glyph_name(glyph_name);
|
|
||||||
cluster = G(info)[G(glyph_idx)].cluster;
|
cluster = G(info)[G(glyph_idx)].cluster;
|
||||||
bool is_special = is_special_glyph(glyph_id, font, &G(current_cell_data));
|
bool is_special = is_special_glyph(glyph_id, font, &G(current_cell_data));
|
||||||
bool is_empty = is_special && is_empty_glyph(glyph_id, font);
|
bool is_empty = is_special && is_empty_glyph(glyph_id, font);
|
||||||
@@ -955,16 +1001,14 @@ shape_run(CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_cells
|
|||||||
add_to_current_group = !G(prev_was_special) || !current_group->num_cells;
|
add_to_current_group = !G(prev_was_special) || !current_group->num_cells;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
static const bool debug_grouping = false;
|
#if 0
|
||||||
if (debug_grouping) {
|
char ch[8] = {0};
|
||||||
char ch[8] = {0};
|
encode_utf8(G(current_cell_data).current_codepoint, ch);
|
||||||
encode_utf8(G(current_cell_data).current_codepoint, ch);
|
printf("\x1b[32m→ %s\x1b[m glyph_idx: %zu glyph_id: %u (%s) group_idx: %zu cluster: %u -> %u is_special: %d\n"
|
||||||
printf("\x1b[32m→ %s\x1b[m glyph_idx: %zu glyph_id: %u (%s) group_idx: %zu cluster: %u -> %u is_special: %d\n"
|
" num_codepoints_used_by_glyph: %u current_group: (%u cells, %u glyphs) add_to_current_group: %d\n",
|
||||||
" num_codepoints_used_by_glyph: %u current_group: (%u cells, %u glyphs) add_to_current_group: %d\n",
|
ch, G(glyph_idx), glyph_id, glyph_name, G(group_idx), cluster, next_cluster, is_special,
|
||||||
ch, G(glyph_idx), glyph_id, glyph_name, G(group_idx), cluster, next_cluster, is_special,
|
num_codepoints_used_by_glyph, current_group->num_cells, current_group->num_glyphs, add_to_current_group);
|
||||||
num_codepoints_used_by_glyph, current_group->num_cells, current_group->num_glyphs, add_to_current_group);
|
#endif
|
||||||
}
|
|
||||||
if (current_group->num_glyphs >= MAX_GLYPHS_IN_GROUP || current_group->num_cells >= MAX_GLYPHS_IN_GROUP) add_to_current_group = false;
|
|
||||||
|
|
||||||
if (!add_to_current_group) { current_group = G(groups) + ++G(group_idx); }
|
if (!add_to_current_group) { current_group = G(groups) + ++G(group_idx); }
|
||||||
if (!current_group->num_glyphs++) {
|
if (!current_group->num_glyphs++) {
|
||||||
@@ -984,13 +1028,11 @@ shape_run(CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_cells
|
|||||||
// soak up all remaining cells
|
// soak up all remaining cells
|
||||||
if (G(cell_idx) < G(num_cells)) {
|
if (G(cell_idx) < G(num_cells)) {
|
||||||
unsigned int num_left = G(num_cells) - G(cell_idx);
|
unsigned int num_left = G(num_cells) - G(cell_idx);
|
||||||
if (current_group->num_cells + num_left > MAX_GLYPHS_IN_GROUP) MOVE_GLYPH_TO_NEXT_GROUP(G(cell_idx));
|
|
||||||
current_group->num_cells += num_left;
|
current_group->num_cells += num_left;
|
||||||
if (current_group->num_cells > MAX_GLYPHS_IN_GROUP) current_group->num_cells = MAX_GLYPHS_IN_GROUP; // leave any trailing cells empty
|
|
||||||
G(cell_idx) += num_left;
|
G(cell_idx) += num_left;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
unsigned int num_cells_consumed = 0, start_cell_idx = G(cell_idx);
|
unsigned int num_cells_consumed = 0;
|
||||||
while (num_codepoints_used_by_glyph && G(cell_idx) < G(num_cells)) {
|
while (num_codepoints_used_by_glyph && G(cell_idx) < G(num_cells)) {
|
||||||
unsigned int w = check_cell_consumed(&G(current_cell_data), G(last_cpu_cell));
|
unsigned int w = check_cell_consumed(&G(current_cell_data), G(last_cpu_cell));
|
||||||
G(cell_idx) += w;
|
G(cell_idx) += w;
|
||||||
@@ -998,24 +1040,9 @@ shape_run(CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_cells
|
|||||||
num_codepoints_used_by_glyph--;
|
num_codepoints_used_by_glyph--;
|
||||||
}
|
}
|
||||||
if (num_cells_consumed) {
|
if (num_cells_consumed) {
|
||||||
if (num_cells_consumed > MAX_GLYPHS_IN_GROUP) {
|
current_group->num_cells += num_cells_consumed;
|
||||||
// Nasty, a single glyph used more than MAX_GLYPHS_IN_GROUP cells, we cannot render this case correctly
|
if (!is_special) { // not a ligature, end the group
|
||||||
log_error("The glyph: %u needs more than %u cells, cannot render it", glyph_id, MAX_GLYPHS_IN_GROUP);
|
G(group_idx)++; current_group = G(groups) + G(group_idx);
|
||||||
current_group->num_glyphs--;
|
|
||||||
while (num_cells_consumed) {
|
|
||||||
G(group_idx)++; current_group = G(groups) + G(group_idx);
|
|
||||||
current_group->num_glyphs = 1; current_group->first_glyph_idx = G(glyph_idx);
|
|
||||||
current_group->num_cells = MIN(num_cells_consumed, MAX_GLYPHS_IN_GROUP);
|
|
||||||
current_group->first_cell_idx = start_cell_idx;
|
|
||||||
start_cell_idx += current_group->num_cells;
|
|
||||||
num_cells_consumed -= current_group->num_cells;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (num_cells_consumed + current_group->num_cells > MAX_GLYPHS_IN_GROUP) MOVE_GLYPH_TO_NEXT_GROUP(start_cell_idx);
|
|
||||||
current_group->num_cells += num_cells_consumed;
|
|
||||||
if (!is_special) { // not a ligature, end the group
|
|
||||||
G(group_idx)++; current_group = G(groups) + G(group_idx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1026,8 +1053,22 @@ shape_run(CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_cells
|
|||||||
prev_glyph_was_inifinte_ligature_end = ligature_type == INFINITE_LIGATURE_END;
|
prev_glyph_was_inifinte_ligature_end = ligature_type == INFINITE_LIGATURE_END;
|
||||||
G(glyph_idx)++;
|
G(glyph_idx)++;
|
||||||
}
|
}
|
||||||
#undef MOVE_GLYPH_TO_NEXT_GROUP
|
}
|
||||||
#undef MAX_GLYPHS_IN_GROUP
|
|
||||||
|
|
||||||
|
static void
|
||||||
|
shape_run(CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_cells, Font *font, bool disable_ligature) {
|
||||||
|
hb_font_t *hbf = harfbuzz_font_for_face(font->face);
|
||||||
|
if (font->spacer_strategy == SPACER_STRATEGY_UNKNOWN) detect_spacer_strategy(hbf, font);
|
||||||
|
shape(first_cpu_cell, first_gpu_cell, num_cells, hbf, font, disable_ligature);
|
||||||
|
if (font->spacer_strategy == SPACERS_IOSEVKA) group_iosevka(font, hbf);
|
||||||
|
else group_normal(font, hbf);
|
||||||
|
#if 0
|
||||||
|
static char dbuf[1024];
|
||||||
|
// You can also generate this easily using hb-shape --show-extents --cluster-level=1 --shapers=ot /path/to/font/file text
|
||||||
|
hb_buffer_serialize_glyphs(harfbuzz_buffer, 0, group_state.num_glyphs, dbuf, sizeof(dbuf), NULL, harfbuzz_font_for_face(font->face), HB_BUFFER_SERIALIZE_FORMAT_TEXT, HB_BUFFER_SERIALIZE_FLAG_DEFAULT | HB_BUFFER_SERIALIZE_FLAG_GLYPH_EXTENTS);
|
||||||
|
printf("\n%s\n", dbuf);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
@@ -1036,12 +1077,13 @@ merge_groups_for_pua_space_ligature(void) {
|
|||||||
Group *g = G(groups), *g1 = G(groups) + 1;
|
Group *g = G(groups), *g1 = G(groups) + 1;
|
||||||
g->num_cells += g1->num_cells;
|
g->num_cells += g1->num_cells;
|
||||||
g->num_glyphs += g1->num_glyphs;
|
g->num_glyphs += g1->num_glyphs;
|
||||||
g->num_glyphs = MIN(g->num_glyphs, MAX_NUM_EXTRA_GLYPHS + 1);
|
|
||||||
G(group_idx)--;
|
G(group_idx)--;
|
||||||
}
|
}
|
||||||
G(groups)->is_space_ligature = true;
|
G(groups)->is_space_ligature = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#undef MOVE_GLYPH_TO_NEXT_GROUP
|
||||||
|
|
||||||
static inline bool
|
static inline bool
|
||||||
is_group_calt_ligature(const Group *group) {
|
is_group_calt_ligature(const Group *group) {
|
||||||
GPUCell *first_cell = G(first_gpu_cell) + group->first_cell_idx;
|
GPUCell *first_cell = G(first_gpu_cell) + group->first_cell_idx;
|
||||||
@@ -1068,22 +1110,26 @@ split_run_at_offset(index_type cursor_offset, index_type *left, index_type *righ
|
|||||||
static inline void
|
static inline void
|
||||||
render_groups(FontGroup *fg, Font *font, bool center_glyph) {
|
render_groups(FontGroup *fg, Font *font, bool center_glyph) {
|
||||||
unsigned idx = 0;
|
unsigned idx = 0;
|
||||||
ExtraGlyphs ed;
|
|
||||||
while (idx <= G(group_idx)) {
|
while (idx <= G(group_idx)) {
|
||||||
Group *group = G(groups) + idx;
|
Group *group = G(groups) + idx;
|
||||||
if (!group->num_cells) break;
|
if (!group->num_cells) break;
|
||||||
/* printf("Group: idx: %u num_cells: %u num_glyphs: %u first_glyph_idx: %u first_cell_idx: %u total_num_glyphs: %zu\n", */
|
/* printf("Group: idx: %u num_cells: %u num_glyphs: %u first_glyph_idx: %u first_cell_idx: %u total_num_glyphs: %zu\n", */
|
||||||
/* idx, group->num_cells, group->num_glyphs, group->first_glyph_idx, group->first_cell_idx, group_state.num_glyphs); */
|
/* idx, group->num_cells, group->num_glyphs, group->first_glyph_idx, group->first_cell_idx, group_state.num_glyphs); */
|
||||||
glyph_index primary = group->num_glyphs ? G(info)[group->first_glyph_idx].codepoint : 0;
|
if (group->num_glyphs) {
|
||||||
unsigned int i;
|
size_t sz = MAX(group->num_glyphs, group->num_cells) + 16;
|
||||||
int last = -1;
|
if (global_glyph_render_scratch.sz < sz) {
|
||||||
for (i = 1; i < MIN(arraysz(ed.data) + 1, group->num_glyphs); i++) { last = i - 1; ed.data[last] = G(info)[group->first_glyph_idx + i].codepoint; }
|
#define a(what) free(global_glyph_render_scratch.what); global_glyph_render_scratch.what = malloc(sz * sizeof(global_glyph_render_scratch.what[0])); if (!global_glyph_render_scratch.what) fatal("Out of memory");
|
||||||
if ((size_t)(last + 1) < arraysz(ed.data)) ed.data[last + 1] = 0;
|
a(glyphs); a(sprite_positions);
|
||||||
// We dont want to render the spaces in a space ligature because
|
#undef a
|
||||||
// there exist stupid fonts like Powerline that have no space glyph,
|
global_glyph_render_scratch.sz = sz;
|
||||||
// so special case it: https://github.com/kovidgoyal/kitty/issues/1225
|
}
|
||||||
unsigned int num_glyphs = group->is_space_ligature ? 1 : group->num_glyphs;
|
for (unsigned i = 0; i < group->num_glyphs; i++) global_glyph_render_scratch.glyphs[i] = G(info)[group->first_glyph_idx + i].codepoint;
|
||||||
render_group(fg, group->num_cells, num_glyphs, G(first_cpu_cell) + group->first_cell_idx, G(first_gpu_cell) + group->first_cell_idx, G(info) + group->first_glyph_idx, G(positions) + group->first_glyph_idx, font, primary, &ed, center_glyph);
|
// We dont want to render the spaces in a space ligature because
|
||||||
|
// there exist stupid fonts like Powerline that have no space glyph,
|
||||||
|
// so special case it: https://github.com/kovidgoyal/kitty/issues/1225
|
||||||
|
unsigned int num_glyphs = group->is_space_ligature ? 1 : group->num_glyphs;
|
||||||
|
render_group(fg, group->num_cells, num_glyphs, G(first_cpu_cell) + group->first_cell_idx, G(first_gpu_cell) + group->first_cell_idx, G(info) + group->first_glyph_idx, G(positions) + group->first_glyph_idx, font, global_glyph_render_scratch.glyphs, num_glyphs, center_glyph);
|
||||||
|
}
|
||||||
idx++;
|
idx++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1118,8 +1164,8 @@ test_shape(PyObject UNUSED *self, PyObject *args) {
|
|||||||
if (!group->num_cells) break;
|
if (!group->num_cells) break;
|
||||||
first_glyph = group->num_glyphs ? G(info)[group->first_glyph_idx].codepoint : 0;
|
first_glyph = group->num_glyphs ? G(info)[group->first_glyph_idx].codepoint : 0;
|
||||||
|
|
||||||
PyObject *eg = PyTuple_New(MAX_NUM_EXTRA_GLYPHS);
|
PyObject *eg = PyTuple_New(group->num_glyphs);
|
||||||
for (size_t g = 0; g < MAX_NUM_EXTRA_GLYPHS; g++) PyTuple_SET_ITEM(eg, g, Py_BuildValue("H", g + 1 < group->num_glyphs ? G(info)[group->first_glyph_idx + g].codepoint : 0));
|
for (size_t g = 0; g < group->num_glyphs; g++) PyTuple_SET_ITEM(eg, g, Py_BuildValue("H", G(info)[group->first_glyph_idx + g].codepoint));
|
||||||
PyList_Append(ans, Py_BuildValue("IIHN", group->num_cells, group->num_glyphs, first_glyph, eg));
|
PyList_Append(ans, Py_BuildValue("IIHN", group->num_cells, group->num_glyphs, first_glyph, eg));
|
||||||
idx++;
|
idx++;
|
||||||
}
|
}
|
||||||
@@ -1267,7 +1313,7 @@ set_font_data(PyObject UNUSED *m, PyObject *args) {
|
|||||||
if (!PyArg_ParseTuple(args, "OOOIIIIO!dO",
|
if (!PyArg_ParseTuple(args, "OOOIIIIO!dO",
|
||||||
&box_drawing_function, &prerender_function, &descriptor_for_idx,
|
&box_drawing_function, &prerender_function, &descriptor_for_idx,
|
||||||
&descriptor_indices.bold, &descriptor_indices.italic, &descriptor_indices.bi, &descriptor_indices.num_symbol_fonts,
|
&descriptor_indices.bold, &descriptor_indices.italic, &descriptor_indices.bi, &descriptor_indices.num_symbol_fonts,
|
||||||
&PyTuple_Type, &sm, &global_state.font_sz_in_pts, &font_feature_settings)) return NULL;
|
&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);
|
Py_INCREF(box_drawing_function); Py_INCREF(prerender_function); Py_INCREF(descriptor_for_idx); Py_INCREF(font_feature_settings);
|
||||||
free_font_groups();
|
free_font_groups();
|
||||||
clear_symbol_maps();
|
clear_symbol_maps();
|
||||||
@@ -1288,8 +1334,8 @@ send_prerendered_sprites(FontGroup *fg) {
|
|||||||
int error = 0;
|
int error = 0;
|
||||||
sprite_index x = 0, y = 0, z = 0;
|
sprite_index x = 0, y = 0, z = 0;
|
||||||
// blank cell
|
// blank cell
|
||||||
clear_canvas(fg);
|
ensure_canvas_can_fit(fg, 1);
|
||||||
current_send_sprite_to_gpu((FONTS_DATA_HANDLE)fg, x, y, z, fg->canvas);
|
current_send_sprite_to_gpu((FONTS_DATA_HANDLE)fg, x, y, z, fg->canvas.buf);
|
||||||
do_increment(fg, &error);
|
do_increment(fg, &error);
|
||||||
if (error != 0) { sprite_map_set_error(error); PyErr_Print(); fatal("Failed"); }
|
if (error != 0) { sprite_map_set_error(error); PyErr_Print(); fatal("Failed"); }
|
||||||
PyObject *args = PyObject_CallFunction(prerender_function, "IIIIIIIffdd", fg->cell_width, fg->cell_height, fg->baseline, fg->underline_position, fg->underline_thickness, fg->strikethrough_position, fg->strikethrough_thickness, OPT(cursor_beam_thickness), OPT(cursor_underline_thickness), fg->logical_dpi_x, fg->logical_dpi_y);
|
PyObject *args = PyObject_CallFunction(prerender_function, "IIIIIIIffdd", fg->cell_width, fg->cell_height, fg->baseline, fg->underline_position, fg->underline_thickness, fg->strikethrough_position, fg->strikethrough_thickness, OPT(cursor_beam_thickness), OPT(cursor_underline_thickness), fg->logical_dpi_x, fg->logical_dpi_y);
|
||||||
@@ -1300,10 +1346,10 @@ send_prerendered_sprites(FontGroup *fg) {
|
|||||||
do_increment(fg, &error);
|
do_increment(fg, &error);
|
||||||
if (error != 0) { sprite_map_set_error(error); PyErr_Print(); fatal("Failed"); }
|
if (error != 0) { sprite_map_set_error(error); PyErr_Print(); fatal("Failed"); }
|
||||||
uint8_t *alpha_mask = PyLong_AsVoidPtr(PyTuple_GET_ITEM(args, i));
|
uint8_t *alpha_mask = PyLong_AsVoidPtr(PyTuple_GET_ITEM(args, i));
|
||||||
clear_canvas(fg);
|
ensure_canvas_can_fit(fg, 1); // clear canvas
|
||||||
Region r = { .right = fg->cell_width, .bottom = fg->cell_height };
|
Region r = { .right = fg->cell_width, .bottom = fg->cell_height };
|
||||||
render_alpha_mask(alpha_mask, fg->canvas, &r, &r, fg->cell_width, fg->cell_width);
|
render_alpha_mask(alpha_mask, fg->canvas.buf, &r, &r, fg->cell_width, fg->cell_width);
|
||||||
current_send_sprite_to_gpu((FONTS_DATA_HANDLE)fg, x, y, z, fg->canvas);
|
current_send_sprite_to_gpu((FONTS_DATA_HANDLE)fg, x, y, z, fg->canvas.buf);
|
||||||
}
|
}
|
||||||
Py_CLEAR(args);
|
Py_CLEAR(args);
|
||||||
}
|
}
|
||||||
@@ -1372,8 +1418,12 @@ finalize(void) {
|
|||||||
Py_CLEAR(descriptor_for_idx);
|
Py_CLEAR(descriptor_for_idx);
|
||||||
Py_CLEAR(font_feature_settings);
|
Py_CLEAR(font_feature_settings);
|
||||||
free_font_groups();
|
free_font_groups();
|
||||||
|
free(ligature_types);
|
||||||
if (harfbuzz_buffer) { hb_buffer_destroy(harfbuzz_buffer); harfbuzz_buffer = NULL; }
|
if (harfbuzz_buffer) { hb_buffer_destroy(harfbuzz_buffer); harfbuzz_buffer = NULL; }
|
||||||
free(group_state.groups); group_state.groups = NULL; group_state.groups_capacity = 0;
|
free(group_state.groups); group_state.groups = NULL; group_state.groups_capacity = 0;
|
||||||
|
free(global_glyph_render_scratch.glyphs);
|
||||||
|
free(global_glyph_render_scratch.sprite_positions);
|
||||||
|
global_glyph_render_scratch = (GlyphRenderScratch){0};
|
||||||
}
|
}
|
||||||
|
|
||||||
static PyObject*
|
static PyObject*
|
||||||
@@ -1387,13 +1437,19 @@ sprite_map_set_layout(PyObject UNUSED *self, PyObject *args) {
|
|||||||
|
|
||||||
static PyObject*
|
static PyObject*
|
||||||
test_sprite_position_for(PyObject UNUSED *self, PyObject *args) {
|
test_sprite_position_for(PyObject UNUSED *self, PyObject *args) {
|
||||||
glyph_index glyph;
|
|
||||||
ExtraGlyphs extra_glyphs = {{0}};
|
|
||||||
if (!PyArg_ParseTuple(args, "H|H", &glyph, &extra_glyphs.data)) return NULL;
|
|
||||||
int error;
|
int error;
|
||||||
|
FREE_AFTER_FUNCTION glyph_index *glyphs = calloc(PyTuple_GET_SIZE(args), sizeof(glyph_index));
|
||||||
|
for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(args); i++) {
|
||||||
|
if (!PyLong_Check(PyTuple_GET_ITEM(args, i))) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "glyph indices must be integers");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
glyphs[i] = (glyph_index)PyLong_AsUnsignedLong(PyTuple_GET_ITEM(args, i));
|
||||||
|
if (PyErr_Occurred()) return NULL;
|
||||||
|
}
|
||||||
FontGroup *fg = font_groups;
|
FontGroup *fg = font_groups;
|
||||||
if (!num_font_groups) { PyErr_SetString(PyExc_RuntimeError, "must create font group first"); return NULL; }
|
if (!num_font_groups) { PyErr_SetString(PyExc_RuntimeError, "must create font group first"); return NULL; }
|
||||||
SpritePosition *pos = sprite_position_for(fg, &fg->fonts[fg->medium_font_idx], glyph, &extra_glyphs, 0, &error);
|
SpritePosition *pos = sprite_position_for(fg, &fg->fonts[fg->medium_font_idx], glyphs, PyTuple_GET_SIZE(args), 0, &error);
|
||||||
if (pos == NULL) { sprite_map_set_error(error); return NULL; }
|
if (pos == NULL) { sprite_map_set_error(error); return NULL; }
|
||||||
return Py_BuildValue("HHH", pos->x, pos->y, pos->z);
|
return Py_BuildValue("HHH", pos->x, pos->y, pos->z);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ typedef struct {
|
|||||||
} StringCanvas;
|
} StringCanvas;
|
||||||
|
|
||||||
// API that font backends need to implement
|
// API that font backends need to implement
|
||||||
typedef uint16_t glyph_index;
|
|
||||||
unsigned int glyph_id_for_codepoint(PyObject *, char_type);
|
unsigned int glyph_id_for_codepoint(PyObject *, char_type);
|
||||||
int get_glyph_width(PyObject *, glyph_index);
|
int get_glyph_width(PyObject *, glyph_index);
|
||||||
bool is_glyph_empty(PyObject *, glyph_index);
|
bool is_glyph_empty(PyObject *, glyph_index);
|
||||||
|
|||||||
@@ -11,9 +11,13 @@ class ListedFont(TypedDict):
|
|||||||
is_monospace: bool
|
is_monospace: bool
|
||||||
|
|
||||||
|
|
||||||
class FontFeature(str):
|
class FontFeature:
|
||||||
|
|
||||||
def __new__(cls, name: str, parsed: bytes) -> 'FontFeature':
|
__slots__ = 'name', 'parsed'
|
||||||
ans: FontFeature = str.__new__(cls, name)
|
|
||||||
ans.parsed = parsed # type: ignore
|
def __init__(self, name: str, parsed: bytes):
|
||||||
return ans
|
self.name = name
|
||||||
|
self.parsed = parsed
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return repr(self.name)
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import re
|
|||||||
from typing import Dict, Generator, Iterable, List, Optional, Tuple
|
from typing import Dict, Generator, Iterable, List, Optional, Tuple
|
||||||
|
|
||||||
from kitty.fast_data_types import coretext_all_fonts
|
from kitty.fast_data_types import coretext_all_fonts
|
||||||
from kitty.options_stub import Options
|
from kitty.fonts import FontFeature
|
||||||
|
from kitty.options.types import Options
|
||||||
from kitty.typing import CoreTextFont
|
from kitty.typing import CoreTextFont
|
||||||
from kitty.utils import log_error
|
from kitty.utils import log_error
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ def list_fonts() -> Generator[ListedFont, None, None]:
|
|||||||
yield {'family': f, 'full_name': fn, 'postscript_name': fd['postscript_name'] or '', 'is_monospace': is_mono}
|
yield {'family': f, 'full_name': fn, 'postscript_name': fd['postscript_name'] or '', 'is_monospace': is_mono}
|
||||||
|
|
||||||
|
|
||||||
def find_font_features(postscript_name: str) -> Tuple[str, ...]:
|
def find_font_features(postscript_name: str) -> Tuple[FontFeature, ...]:
|
||||||
"""Not Implemented"""
|
"""Not Implemented"""
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from kitty.fast_data_types import (
|
|||||||
FC_WEIGHT_REGULAR, FC_WIDTH_NORMAL, fc_list, fc_match as fc_match_impl,
|
FC_WEIGHT_REGULAR, FC_WIDTH_NORMAL, fc_list, fc_match as fc_match_impl,
|
||||||
fc_match_postscript_name, parse_font_feature
|
fc_match_postscript_name, parse_font_feature
|
||||||
)
|
)
|
||||||
from kitty.options_stub import Options
|
from kitty.options.types import Options
|
||||||
from kitty.typing import FontConfigPattern
|
from kitty.typing import FontConfigPattern
|
||||||
from kitty.utils import log_error
|
from kitty.utils import log_error
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ def fc_match(family: str, bold: bool, italic: bool, spacing: int = FC_MONO) -> F
|
|||||||
return fc_match_impl(family, bold, italic, spacing)
|
return fc_match_impl(family, bold, italic, spacing)
|
||||||
|
|
||||||
|
|
||||||
def find_font_features(postscript_name: str) -> Tuple[str, ...]:
|
def find_font_features(postscript_name: str) -> Tuple[FontFeature, ...]:
|
||||||
pat = fc_match_postscript_name(postscript_name)
|
pat = fc_match_postscript_name(postscript_name)
|
||||||
|
|
||||||
if pat.get('postscript_name') != postscript_name or 'fontfeatures' not in pat:
|
if pat.get('postscript_name') != postscript_name or 'fontfeatures' not in pat:
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from typing import (
|
|||||||
Any, Callable, Dict, Generator, List, Optional, Tuple, Union, cast
|
Any, Callable, Dict, Generator, List, Optional, Tuple, Union, cast
|
||||||
)
|
)
|
||||||
|
|
||||||
from kitty.config import defaults
|
|
||||||
from kitty.constants import is_macos
|
from kitty.constants import is_macos
|
||||||
from kitty.fast_data_types import (
|
from kitty.fast_data_types import (
|
||||||
Screen, create_test_font_group, get_fallback_font, set_font_data,
|
Screen, create_test_font_group, get_fallback_font, set_font_data,
|
||||||
@@ -20,7 +19,7 @@ from kitty.fast_data_types import (
|
|||||||
from kitty.fonts.box_drawing import (
|
from kitty.fonts.box_drawing import (
|
||||||
BufType, render_box_char, render_missing_glyph
|
BufType, render_box_char, render_missing_glyph
|
||||||
)
|
)
|
||||||
from kitty.options_stub import Options as OptionsStub
|
from kitty.options.types import Options, defaults
|
||||||
from kitty.typing import CoreTextFont, FontConfigPattern
|
from kitty.typing import CoreTextFont, FontConfigPattern
|
||||||
from kitty.utils import log_error
|
from kitty.utils import log_error
|
||||||
|
|
||||||
@@ -33,7 +32,7 @@ FontObject = Union[CoreTextFont, FontConfigPattern]
|
|||||||
current_faces: List[Tuple[FontObject, bool, bool]] = []
|
current_faces: List[Tuple[FontObject, bool, bool]] = []
|
||||||
|
|
||||||
|
|
||||||
def get_font_files(opts: OptionsStub) -> Dict[str, Any]:
|
def get_font_files(opts: Options) -> Dict[str, Any]:
|
||||||
if is_macos:
|
if is_macos:
|
||||||
return get_font_files_coretext(opts)
|
return get_font_files_coretext(opts)
|
||||||
return get_font_files_fontconfig(opts)
|
return get_font_files_fontconfig(opts)
|
||||||
@@ -136,7 +135,7 @@ def coalesce_symbol_maps(maps: Dict[Tuple[int, int], str]) -> Dict[Tuple[int, in
|
|||||||
return dict(ans)
|
return dict(ans)
|
||||||
|
|
||||||
|
|
||||||
def create_symbol_map(opts: OptionsStub) -> Tuple[Tuple[int, int, int], ...]:
|
def create_symbol_map(opts: Options) -> Tuple[Tuple[int, int, int], ...]:
|
||||||
val = coalesce_symbol_maps(opts.symbol_map)
|
val = coalesce_symbol_maps(opts.symbol_map)
|
||||||
family_map: Dict[str, int] = {}
|
family_map: Dict[str, int] = {}
|
||||||
count = 0
|
count = 0
|
||||||
@@ -174,7 +173,7 @@ def dump_faces(ftypes: List[str], indices: Dict[str, int]) -> None:
|
|||||||
log_error(face_str(face))
|
log_error(face_str(face))
|
||||||
|
|
||||||
|
|
||||||
def set_font_family(opts: Optional[OptionsStub] = None, override_font_size: Optional[float] = None, debug_font_matching: bool = False) -> None:
|
def set_font_family(opts: Optional[Options] = None, override_font_size: Optional[float] = None, debug_font_matching: bool = False) -> None:
|
||||||
global current_faces
|
global current_faces
|
||||||
opts = opts or defaults
|
opts = opts or defaults
|
||||||
sz = override_font_size or opts.font_size
|
sz = override_font_size or opts.font_size
|
||||||
@@ -236,7 +235,15 @@ def add_dline(buf: ctypes.Array, cell_width: int, position: int, thickness: int,
|
|||||||
def add_curl(buf: ctypes.Array, cell_width: int, position: int, thickness: int, cell_height: int) -> None:
|
def add_curl(buf: ctypes.Array, cell_width: int, position: int, thickness: int, cell_height: int) -> None:
|
||||||
max_x, max_y = cell_width - 1, cell_height - 1
|
max_x, max_y = cell_width - 1, cell_height - 1
|
||||||
xfactor = 2.0 * pi / max_x
|
xfactor = 2.0 * pi / max_x
|
||||||
half_height = max(thickness // 2, 1)
|
thickness = max(1, thickness)
|
||||||
|
if thickness < 3:
|
||||||
|
half_height = thickness
|
||||||
|
thickness -= 1
|
||||||
|
elif thickness == 3:
|
||||||
|
half_height = thickness = 2
|
||||||
|
else:
|
||||||
|
half_height = thickness // 2
|
||||||
|
thickness -= 2
|
||||||
|
|
||||||
def add_intensity(x: int, y: int, val: int) -> None:
|
def add_intensity(x: int, y: int, val: int) -> None:
|
||||||
y += position
|
y += position
|
||||||
@@ -244,20 +251,22 @@ def add_curl(buf: ctypes.Array, cell_width: int, position: int, thickness: int,
|
|||||||
idx = cell_width * y + x
|
idx = cell_width * y + x
|
||||||
buf[idx] = min(255, buf[idx] + val)
|
buf[idx] = min(255, buf[idx] + val)
|
||||||
|
|
||||||
# Ensure all space at bottom of cell is used
|
# Ensure curve doesn't exceed cell boundary at the bottom
|
||||||
if position + half_height < max_y:
|
position += half_height * 2
|
||||||
position += max_y - (position + half_height)
|
|
||||||
if position + half_height > max_y:
|
if position + half_height > max_y:
|
||||||
position -= position + half_height - max_y
|
position = max_y - half_height
|
||||||
|
|
||||||
# Use the Wu antialias algorithm to draw the curve
|
# Use the Wu antialias algorithm to draw the curve
|
||||||
# cosine waves always have slope <= 1 so are never steep
|
# cosine waves always have slope <= 1 so are never steep
|
||||||
for x in range(cell_width):
|
for x in range(cell_width):
|
||||||
y = half_height * cos(x * xfactor)
|
y = half_height * cos(x * xfactor)
|
||||||
y1, y2 = floor(y), ceil(y)
|
y1, y2 = floor(y - thickness), ceil(y)
|
||||||
i1 = int(255 * abs(y - y1))
|
i1 = int(255 * abs(y - floor(y)))
|
||||||
add_intensity(x, y1, 255 - i1)
|
add_intensity(x, y1, 255 - i1) # upper bound
|
||||||
add_intensity(x, y2, i1)
|
add_intensity(x, y2, i1) # lower bound
|
||||||
|
# fill between upper and lower bound
|
||||||
|
for t in range(1, thickness + 1):
|
||||||
|
add_intensity(x, y1 + t, 255)
|
||||||
|
|
||||||
|
|
||||||
def render_special(
|
def render_special(
|
||||||
@@ -269,7 +278,9 @@ def render_special(
|
|||||||
underline_position: int = 0,
|
underline_position: int = 0,
|
||||||
underline_thickness: int = 0,
|
underline_thickness: int = 0,
|
||||||
strikethrough_position: int = 0,
|
strikethrough_position: int = 0,
|
||||||
strikethrough_thickness: int = 0
|
strikethrough_thickness: int = 0,
|
||||||
|
dpi_x: float = 96.,
|
||||||
|
dpi_y: float = 96.,
|
||||||
) -> ctypes.Array:
|
) -> ctypes.Array:
|
||||||
underline_position = min(underline_position, cell_height - underline_thickness)
|
underline_position = min(underline_position, cell_height - underline_thickness)
|
||||||
CharTexture = ctypes.c_ubyte * (cell_width * cell_height)
|
CharTexture = ctypes.c_ubyte * (cell_width * cell_height)
|
||||||
@@ -356,7 +367,9 @@ def prerender_function(
|
|||||||
f = partial(
|
f = partial(
|
||||||
render_special, cell_width=cell_width, cell_height=cell_height, baseline=baseline,
|
render_special, cell_width=cell_width, cell_height=cell_height, baseline=baseline,
|
||||||
underline_position=underline_position, underline_thickness=underline_thickness,
|
underline_position=underline_position, underline_thickness=underline_thickness,
|
||||||
strikethrough_position=strikethrough_position, strikethrough_thickness=strikethrough_thickness)
|
strikethrough_position=strikethrough_position, strikethrough_thickness=strikethrough_thickness,
|
||||||
|
dpi_x=dpi_x, dpi_y=dpi_y
|
||||||
|
)
|
||||||
c = partial(
|
c = partial(
|
||||||
render_cursor, cursor_beam_thickness=cursor_beam_thickness,
|
render_cursor, cursor_beam_thickness=cursor_beam_thickness,
|
||||||
cursor_underline_thickness=cursor_underline_thickness, cell_width=cell_width,
|
cursor_underline_thickness=cursor_underline_thickness, cell_width=cell_width,
|
||||||
|
|||||||
@@ -576,7 +576,7 @@ place_bitmap_in_canvas(pixel *cell, ProcessedBitmap *bm, size_t cell_width, size
|
|||||||
dest.top = baseline - yoff;
|
dest.top = baseline - yoff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* printf("x_offset: %d bearing_x: %f y_offset: %d bearing_y: %f src_start_row: %u src_start_column: %u dest_start_row: %u dest_start_column: %u bm_width: %lu bitmap_rows: %lu\n", xoff, bearing_x, yoff, bearing_y, src.top, src.left, dest.top, dest.left, bm->width, bm->rows); */
|
/* printf("x_offset: %d y_offset: %d src_start_row: %u src_start_column: %u dest_start_row: %u dest_start_column: %u bm_width: %lu bitmap_rows: %lu\n", xoff, yoff, src.top, src.left, dest.top, dest.left, bm->width, bm->rows); */
|
||||||
|
|
||||||
if (bm->pixel_mode == FT_PIXEL_MODE_BGRA) {
|
if (bm->pixel_mode == FT_PIXEL_MODE_BGRA) {
|
||||||
copy_color_bitmap(bm->buf, cell, &src, &dest, bm->stride, cell_width);
|
copy_color_bitmap(bm->buf, cell, &src, &dest, bm->stride, cell_width);
|
||||||
|
|||||||
26
kitty/glfw-wrapper.h
generated
26
kitty/glfw-wrapper.h
generated
@@ -261,18 +261,20 @@ typedef enum {
|
|||||||
*
|
*
|
||||||
* @ingroup input
|
* @ingroup input
|
||||||
* @{ */
|
* @{ */
|
||||||
#define GLFW_MOUSE_BUTTON_1 0
|
typedef enum GLFWMouseButton {
|
||||||
#define GLFW_MOUSE_BUTTON_2 1
|
GLFW_MOUSE_BUTTON_1 = 0,
|
||||||
#define GLFW_MOUSE_BUTTON_3 2
|
GLFW_MOUSE_BUTTON_LEFT = 0,
|
||||||
#define GLFW_MOUSE_BUTTON_4 3
|
GLFW_MOUSE_BUTTON_2 = 1,
|
||||||
#define GLFW_MOUSE_BUTTON_5 4
|
GLFW_MOUSE_BUTTON_RIGHT = 1,
|
||||||
#define GLFW_MOUSE_BUTTON_6 5
|
GLFW_MOUSE_BUTTON_3 = 2,
|
||||||
#define GLFW_MOUSE_BUTTON_7 6
|
GLFW_MOUSE_BUTTON_MIDDLE = 2,
|
||||||
#define GLFW_MOUSE_BUTTON_8 7
|
GLFW_MOUSE_BUTTON_4 = 3,
|
||||||
#define GLFW_MOUSE_BUTTON_LAST GLFW_MOUSE_BUTTON_8
|
GLFW_MOUSE_BUTTON_5 = 4,
|
||||||
#define GLFW_MOUSE_BUTTON_LEFT GLFW_MOUSE_BUTTON_1
|
GLFW_MOUSE_BUTTON_6 = 5,
|
||||||
#define GLFW_MOUSE_BUTTON_RIGHT GLFW_MOUSE_BUTTON_2
|
GLFW_MOUSE_BUTTON_7 = 6,
|
||||||
#define GLFW_MOUSE_BUTTON_MIDDLE GLFW_MOUSE_BUTTON_3
|
GLFW_MOUSE_BUTTON_8 = 7,
|
||||||
|
GLFW_MOUSE_BUTTON_LAST = 7
|
||||||
|
} GLFWMouseButton;
|
||||||
/*! @} */
|
/*! @} */
|
||||||
|
|
||||||
/*! @defgroup joysticks Joysticks
|
/*! @defgroup joysticks Joysticks
|
||||||
|
|||||||
38
kitty/glfw.c
38
kitty/glfw.c
@@ -32,6 +32,18 @@ static GLFWcursor *standard_cursor = NULL, *click_cursor = NULL, *arrow_cursor =
|
|||||||
static void set_os_window_dpi(OSWindow *w);
|
static void set_os_window_dpi(OSWindow *w);
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
get_platform_dependent_config_values(void *glfw_window) {
|
||||||
|
if (OPT(click_interval) < 0) OPT(click_interval) = glfwGetDoubleClickInterval(glfw_window);
|
||||||
|
if (OPT(cursor_blink_interval) < 0) {
|
||||||
|
OPT(cursor_blink_interval) = ms_to_monotonic_t(500ll);
|
||||||
|
#ifdef __APPLE__
|
||||||
|
monotonic_t cbi = cocoa_cursor_blink_interval();
|
||||||
|
if (cbi >= 0) OPT(cursor_blink_interval) = cbi / 2;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
request_tick_callback(void) {
|
request_tick_callback(void) {
|
||||||
glfwPostEmptyEvent();
|
glfwPostEmptyEvent();
|
||||||
@@ -653,7 +665,7 @@ create_os_window(PyObject UNUSED *self, PyObject *args) {
|
|||||||
float xscale, yscale;
|
float xscale, yscale;
|
||||||
double xdpi, ydpi;
|
double xdpi, ydpi;
|
||||||
get_window_content_scale(temp_window, &xscale, &yscale, &xdpi, &ydpi);
|
get_window_content_scale(temp_window, &xscale, &yscale, &xdpi, &ydpi);
|
||||||
FONTS_DATA_HANDLE fonts_data = load_fonts_data(global_state.font_sz_in_pts, xdpi, ydpi);
|
FONTS_DATA_HANDLE fonts_data = load_fonts_data(OPT(font_size), xdpi, ydpi);
|
||||||
PyObject *ret = PyObject_CallFunction(get_window_size, "IIddff", fonts_data->cell_width, fonts_data->cell_height, fonts_data->logical_dpi_x, fonts_data->logical_dpi_y, xscale, yscale);
|
PyObject *ret = PyObject_CallFunction(get_window_size, "IIddff", fonts_data->cell_width, fonts_data->cell_height, fonts_data->logical_dpi_x, fonts_data->logical_dpi_y, xscale, yscale);
|
||||||
if (ret == NULL) return NULL;
|
if (ret == NULL) return NULL;
|
||||||
int width = PyLong_AsLong(PyTuple_GET_ITEM(ret, 0)), height = PyLong_AsLong(PyTuple_GET_ITEM(ret, 1));
|
int width = PyLong_AsLong(PyTuple_GET_ITEM(ret, 0)), height = PyLong_AsLong(PyTuple_GET_ITEM(ret, 1));
|
||||||
@@ -693,7 +705,7 @@ create_os_window(PyObject UNUSED *self, PyObject *args) {
|
|||||||
if (n_xdpi != xdpi || n_ydpi != ydpi) {
|
if (n_xdpi != xdpi || n_ydpi != ydpi) {
|
||||||
// this can happen if the window is moved by the OS to a different monitor when shown
|
// this can happen if the window is moved by the OS to a different monitor when shown
|
||||||
xdpi = n_xdpi; ydpi = n_ydpi;
|
xdpi = n_xdpi; ydpi = n_ydpi;
|
||||||
fonts_data = load_fonts_data(global_state.font_sz_in_pts, xdpi, ydpi);
|
fonts_data = load_fonts_data(OPT(font_size), xdpi, ydpi);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -706,16 +718,9 @@ create_os_window(PyObject UNUSED *self, PyObject *args) {
|
|||||||
dest##_cursor = glfwCreateStandardCursor(GLFW_##shape##_CURSOR); \
|
dest##_cursor = glfwCreateStandardCursor(GLFW_##shape##_CURSOR); \
|
||||||
if (dest##_cursor == NULL) { log_error("Failed to create the %s mouse cursor, using default cursor.", #shape); } \
|
if (dest##_cursor == NULL) { log_error("Failed to create the %s mouse cursor, using default cursor.", #shape); } \
|
||||||
}}
|
}}
|
||||||
CC(standard, IBEAM); CC(click, HAND); CC(arrow, ARROW);
|
CC(standard, IBEAM); CC(click, HAND); CC(arrow, ARROW);
|
||||||
#undef CC
|
#undef CC
|
||||||
if (OPT(click_interval) < 0) OPT(click_interval) = glfwGetDoubleClickInterval(glfw_window);
|
get_platform_dependent_config_values(glfw_window);
|
||||||
if (OPT(cursor_blink_interval) < 0) {
|
|
||||||
OPT(cursor_blink_interval) = ms_to_monotonic_t(500ll);
|
|
||||||
#ifdef __APPLE__
|
|
||||||
monotonic_t cbi = cocoa_cursor_blink_interval();
|
|
||||||
if (cbi >= 0) OPT(cursor_blink_interval) = cbi / 2;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
is_first_window = false;
|
is_first_window = false;
|
||||||
}
|
}
|
||||||
OSWindow *w = add_os_window();
|
OSWindow *w = add_os_window();
|
||||||
@@ -730,7 +735,7 @@ create_os_window(PyObject UNUSED *self, PyObject *args) {
|
|||||||
w->fonts_data = fonts_data;
|
w->fonts_data = fonts_data;
|
||||||
w->shown_once = true;
|
w->shown_once = true;
|
||||||
w->last_focused_counter = ++focus_counter;
|
w->last_focused_counter = ++focus_counter;
|
||||||
if (OPT(resize_in_steps)) os_window_update_size_increments(w);
|
os_window_update_size_increments(w);
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
if (OPT(macos_option_as_alt)) glfwSetCocoaTextInputFilter(glfw_window, filter_option);
|
if (OPT(macos_option_as_alt)) glfwSetCocoaTextInputFilter(glfw_window, filter_option);
|
||||||
glfwSetCocoaToggleFullscreenIntercept(glfw_window, intercept_cocoa_fullscreen);
|
glfwSetCocoaToggleFullscreenIntercept(glfw_window, intercept_cocoa_fullscreen);
|
||||||
@@ -783,8 +788,13 @@ create_os_window(PyObject UNUSED *self, PyObject *args) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
os_window_update_size_increments(OSWindow *window) {
|
os_window_update_size_increments(OSWindow *window) {
|
||||||
if (window->handle && window->fonts_data) glfwSetWindowSizeIncrements(
|
if (OPT(resize_in_steps)) {
|
||||||
window->handle, window->fonts_data->cell_width, window->fonts_data->cell_height);
|
if (window->handle && window->fonts_data) glfwSetWindowSizeIncrements(
|
||||||
|
window->handle, window->fonts_data->cell_width, window->fonts_data->cell_height);
|
||||||
|
} else {
|
||||||
|
if (window->handle) glfwSetWindowSizeIncrements(
|
||||||
|
window->handle, GLFW_DONT_CARE, GLFW_DONT_CARE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
|
|||||||
91
kitty/glyph-cache.c
Normal file
91
kitty/glyph-cache.c
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* glyph-cache.c
|
||||||
|
* Copyright (C) 2021 Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GPL3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "glyph-cache.h"
|
||||||
|
#include "kitty-uthash.h"
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct SpritePosItem {
|
||||||
|
SpritePositionHead
|
||||||
|
UT_hash_handle hh;
|
||||||
|
glyph_index key[];
|
||||||
|
} SpritePosItem;
|
||||||
|
|
||||||
|
static glyph_index *scratch = NULL;
|
||||||
|
static unsigned scratch_sz = 0;
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
free_glyph_cache_global_resources(void) {
|
||||||
|
free(scratch);
|
||||||
|
scratch = NULL; scratch_sz = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline unsigned
|
||||||
|
key_size_for_glyph_count(unsigned count) { return count + 2; }
|
||||||
|
|
||||||
|
|
||||||
|
SpritePosition*
|
||||||
|
find_or_create_sprite_position(SpritePosition **head_, glyph_index *glyphs, glyph_index count, glyph_index ligature_index, bool *created) {
|
||||||
|
SpritePosItem **head = (SpritePosItem**)head_, *p;
|
||||||
|
const unsigned key_sz = key_size_for_glyph_count(count);
|
||||||
|
if (key_sz > scratch_sz) {
|
||||||
|
scratch = realloc(scratch, sizeof(glyph_index) * (key_sz + 16));
|
||||||
|
if (!scratch) return NULL;
|
||||||
|
scratch_sz = key_sz + 16;
|
||||||
|
}
|
||||||
|
const unsigned key_sz_bytes = key_sz * sizeof(glyph_index);
|
||||||
|
scratch[0] = count; scratch[1] = ligature_index;
|
||||||
|
memcpy(scratch + 2, glyphs, count * sizeof(glyph_index));
|
||||||
|
HASH_FIND(hh, *head, scratch, key_sz_bytes, p);
|
||||||
|
if (p) { *created = false; return (SpritePosition*)p; }
|
||||||
|
|
||||||
|
p = calloc(1, sizeof(SpritePosItem) + key_sz_bytes);
|
||||||
|
if (!p) return NULL;
|
||||||
|
memcpy(p->key, scratch, key_sz_bytes);
|
||||||
|
HASH_ADD(hh, *head, key, key_sz_bytes, p);
|
||||||
|
*created = true;
|
||||||
|
return (SpritePosition*)p;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
free_sprite_position_hash_table(SpritePosition **head_) {
|
||||||
|
SpritePosItem **head = (SpritePosItem**)head_, *s, *tmp;
|
||||||
|
HASH_ITER(hh, *head, s, tmp) {
|
||||||
|
HASH_DEL(*head, s);
|
||||||
|
free(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef struct GlyphPropertiesItem {
|
||||||
|
GlyphPropertiesHead
|
||||||
|
UT_hash_handle hh;
|
||||||
|
unsigned key;
|
||||||
|
} GlyphPropertiesItem;
|
||||||
|
|
||||||
|
|
||||||
|
GlyphProperties*
|
||||||
|
find_or_create_glyph_properties(GlyphProperties **head_, unsigned glyph) {
|
||||||
|
GlyphPropertiesItem **head = (GlyphPropertiesItem**)head_, *p;
|
||||||
|
HASH_FIND_INT(*head, &glyph, p);
|
||||||
|
if (p) return (GlyphProperties*)p;
|
||||||
|
p = calloc(1, sizeof(GlyphPropertiesItem));
|
||||||
|
if (!p) return NULL;
|
||||||
|
p->key = glyph;
|
||||||
|
HASH_ADD_INT(*head, key, p);
|
||||||
|
return (GlyphProperties*)p;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
free_glyph_properties_hash_table(GlyphProperties **head_) {
|
||||||
|
GlyphPropertiesItem **head = (GlyphPropertiesItem**)head_, *s, *tmp;
|
||||||
|
HASH_ITER(hh, *head, s, tmp) {
|
||||||
|
HASH_DEL(*head, s);
|
||||||
|
free(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
kitty/glyph-cache.h
Normal file
35
kitty/glyph-cache.h
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GPL3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "data-types.h"
|
||||||
|
|
||||||
|
void free_glyph_cache_global_resources(void);
|
||||||
|
|
||||||
|
#define SpritePositionHead \
|
||||||
|
bool rendered, colored; \
|
||||||
|
sprite_index x, y, z; \
|
||||||
|
|
||||||
|
typedef struct SpritePosition {
|
||||||
|
SpritePositionHead
|
||||||
|
} SpritePosition;
|
||||||
|
|
||||||
|
|
||||||
|
void free_sprite_position_hash_table(SpritePosition **head);
|
||||||
|
SpritePosition*
|
||||||
|
find_or_create_sprite_position(SpritePosition **head, glyph_index *glyphs, glyph_index count, glyph_index ligature_index, bool *created);
|
||||||
|
|
||||||
|
#define GlyphPropertiesHead \
|
||||||
|
uint8_t data;
|
||||||
|
|
||||||
|
typedef struct GlyphProperties {
|
||||||
|
GlyphPropertiesHead
|
||||||
|
} GlyphProperties;
|
||||||
|
|
||||||
|
void free_glyph_properties_hash_table(GlyphProperties **head);
|
||||||
|
GlyphProperties*
|
||||||
|
find_or_create_glyph_properties(GlyphProperties **head, unsigned glyph);
|
||||||
@@ -306,7 +306,7 @@ png_path_to_bitmap(const char* path, uint8_t** data, unsigned int* width, unsign
|
|||||||
unsigned char *buf = malloc(capacity);
|
unsigned char *buf = malloc(capacity);
|
||||||
if (!buf) { log_error("Out of memory reading PNG file at: %s", path); fclose(fp); return false; }
|
if (!buf) { log_error("Out of memory reading PNG file at: %s", path); fclose(fp); return false; }
|
||||||
while (!feof(fp)) {
|
while (!feof(fp)) {
|
||||||
if (pos - capacity < 1024) {
|
if (capacity - pos < 1024) {
|
||||||
capacity *= 2;
|
capacity *= 2;
|
||||||
unsigned char *new_buf = realloc(buf, capacity);
|
unsigned char *new_buf = realloc(buf, capacity);
|
||||||
if (!new_buf) {
|
if (!new_buf) {
|
||||||
|
|||||||
@@ -536,7 +536,8 @@ HistoryBuf *alloc_historybuf(unsigned int lines, unsigned int columns, unsigned
|
|||||||
|
|
||||||
#include "rewrap.h"
|
#include "rewrap.h"
|
||||||
|
|
||||||
void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other, ANSIBuf *as_ansi_buf) {
|
void
|
||||||
|
historybuf_rewrap(HistoryBuf *self, HistoryBuf *other, ANSIBuf *as_ansi_buf) {
|
||||||
while(other->num_segments < self->num_segments) add_segment(other);
|
while(other->num_segments < self->num_segments) add_segment(other);
|
||||||
if (other->xnum == self->xnum && other->ynum == self->ynum) {
|
if (other->xnum == self->xnum && other->ynum == self->ynum) {
|
||||||
// Fast path
|
// Fast path
|
||||||
@@ -551,9 +552,8 @@ void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other, ANSIBuf *as_ansi_buf
|
|||||||
if (other->pagerhist && other->xnum != self->xnum && ringbuf_bytes_used(other->pagerhist->ringbuf))
|
if (other->pagerhist && other->xnum != self->xnum && ringbuf_bytes_used(other->pagerhist->ringbuf))
|
||||||
other->pagerhist->rewrap_needed = true;
|
other->pagerhist->rewrap_needed = true;
|
||||||
other->count = 0; other->start_of_data = 0;
|
other->count = 0; other->start_of_data = 0;
|
||||||
index_type x = 0, y = 0;
|
|
||||||
if (self->count > 0) {
|
if (self->count > 0) {
|
||||||
rewrap_inner(self, other, self->count, NULL, &x, &y, as_ansi_buf);
|
rewrap_inner(self, other, self->count, NULL, NULL, as_ansi_buf);
|
||||||
for (index_type i = 0; i < other->count; i++) *attrptr(other, (other->start_of_data + i) % other->ynum) |= TEXT_DIRTY_MASK;
|
for (index_type i = 0; i < other->count; i++) *attrptr(other, (other->start_of_data + i) % other->ynum) |= TEXT_DIRTY_MASK;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "hyperlink.h"
|
#include "hyperlink.h"
|
||||||
#include "uthash.h"
|
#include "kitty-uthash.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#define MAX_KEY_LEN 2048
|
#define MAX_KEY_LEN 2048
|
||||||
#define MAX_ID_LEN 256
|
#define MAX_ID_LEN 256
|
||||||
#define MAX_ADDS_BEFORE_GC 256
|
#define MAX_ADDS_BEFORE_GC 256
|
||||||
#undef uthash_fatal
|
|
||||||
#define uthash_fatal(msg) fatal(msg)
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const char *key;
|
const char *key;
|
||||||
|
|||||||
@@ -176,15 +176,20 @@ encode_function_key(const KeyEvent *ev, char *output) {
|
|||||||
case GLFW_FKEY_KP_BEGIN: SIMPLE("\x1bOE");
|
case GLFW_FKEY_KP_BEGIN: SIMPLE("\x1bOE");
|
||||||
case GLFW_FKEY_END: SIMPLE("\x1bOF");
|
case GLFW_FKEY_END: SIMPLE("\x1bOF");
|
||||||
case GLFW_FKEY_HOME: SIMPLE("\x1bOH");
|
case GLFW_FKEY_HOME: SIMPLE("\x1bOH");
|
||||||
case GLFW_FKEY_F1: SIMPLE("\x1bOP");
|
|
||||||
case GLFW_FKEY_F2: SIMPLE("\x1bOQ");
|
|
||||||
case GLFW_FKEY_F3: SIMPLE("\x1bOR");
|
|
||||||
case GLFW_FKEY_F4: SIMPLE("\x1bOS");
|
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!ev->mods.value) {
|
if (!ev->mods.value) {
|
||||||
if (!ev->disambiguate && !ev->report_text && key_number == GLFW_FKEY_ESCAPE) SIMPLE("\x1b");
|
if (!ev->disambiguate && !ev->report_text && key_number == GLFW_FKEY_ESCAPE) SIMPLE("\x1b");
|
||||||
|
if (legacy_mode) {
|
||||||
|
switch(key_number) {
|
||||||
|
case GLFW_FKEY_F1: SIMPLE("\x1bOP");
|
||||||
|
case GLFW_FKEY_F2: SIMPLE("\x1bOQ");
|
||||||
|
case GLFW_FKEY_F3: SIMPLE("\x1bOR");
|
||||||
|
case GLFW_FKEY_F4: SIMPLE("\x1bOS");
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!ev->report_text) {
|
if (!ev->report_text) {
|
||||||
switch(key_number) {
|
switch(key_number) {
|
||||||
case GLFW_FKEY_ENTER: SIMPLE("\r");
|
case GLFW_FKEY_ENTER: SIMPLE("\r");
|
||||||
|
|||||||
10
kitty/key_encoding.py
generated
10
kitty/key_encoding.py
generated
@@ -216,7 +216,7 @@ class KeyEvent(NamedTuple):
|
|||||||
num_lock: bool = False
|
num_lock: bool = False
|
||||||
|
|
||||||
def matches(self, spec: Union[str, ParsedShortcut], types: int = EventType.PRESS | EventType.REPEAT) -> bool:
|
def matches(self, spec: Union[str, ParsedShortcut], types: int = EventType.PRESS | EventType.REPEAT) -> bool:
|
||||||
mods = self.mods & ~(NUM_LOCK | CAPS_LOCK)
|
mods = self.mods_without_locks
|
||||||
if not self.type & types:
|
if not self.type & types:
|
||||||
return False
|
return False
|
||||||
if isinstance(spec, str):
|
if isinstance(spec, str):
|
||||||
@@ -228,6 +228,14 @@ class KeyEvent(NamedTuple):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mods_without_locks(self) -> int:
|
||||||
|
return self.mods & ~(NUM_LOCK | CAPS_LOCK)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_mods(self) -> bool:
|
||||||
|
return bool(self.mods_without_locks)
|
||||||
|
|
||||||
def as_window_system_event(self) -> WindowSystemKeyEvent:
|
def as_window_system_event(self) -> WindowSystemKeyEvent:
|
||||||
action = defines.GLFW_PRESS
|
action = defines.GLFW_PRESS
|
||||||
if self.type is EventType.REPEAT:
|
if self.type is EventType.REPEAT:
|
||||||
|
|||||||
@@ -4,11 +4,10 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Dict, Optional
|
||||||
|
|
||||||
from .constants import is_macos
|
from .constants import is_macos
|
||||||
|
|
||||||
|
|
||||||
functional_key_name_aliases = {
|
functional_key_name_aliases = {
|
||||||
'ESC': 'ESCAPE',
|
'ESC': 'ESCAPE',
|
||||||
'PGUP': 'PAGE_UP',
|
'PGUP': 'PAGE_UP',
|
||||||
@@ -26,7 +25,7 @@ functional_key_name_aliases = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
character_key_name_aliases = {
|
character_key_name_aliases: Dict[str, str] = {
|
||||||
'SPC': ' ',
|
'SPC': ' ',
|
||||||
'SPACE': ' ',
|
'SPACE': ' ',
|
||||||
'STAR': '*',
|
'STAR': '*',
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ update_ime_position(OSWindow *os_window, Window* w, Screen *screen) {
|
|||||||
glfwUpdateIMEState(global_state.callback_os_window->handle, &ev);
|
glfwUpdateIMEState(global_state.callback_os_window->handle, &ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline const char*
|
const char*
|
||||||
format_mods(unsigned mods) {
|
format_mods(unsigned mods) {
|
||||||
static char buf[128];
|
static char buf[128];
|
||||||
char *p = buf, *s;
|
char *p = buf, *s;
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
|
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
from .config import KeyAction, KeyMap, SequenceMap, SubSequenceMap
|
from .conf.utils import KeyAction
|
||||||
from .fast_data_types import (
|
from .fast_data_types import (
|
||||||
GLFW_MOD_ALT, GLFW_MOD_CAPS_LOCK, GLFW_MOD_CONTROL, GLFW_MOD_HYPER,
|
GLFW_MOD_ALT, GLFW_MOD_CAPS_LOCK, GLFW_MOD_CONTROL, GLFW_MOD_HYPER,
|
||||||
GLFW_MOD_META, GLFW_MOD_NUM_LOCK, GLFW_MOD_SHIFT, GLFW_MOD_SUPER, KeyEvent
|
GLFW_MOD_META, GLFW_MOD_NUM_LOCK, GLFW_MOD_SHIFT, GLFW_MOD_SUPER, KeyEvent
|
||||||
)
|
)
|
||||||
|
from .options.utils import KeyMap, SequenceMap, SubSequenceMap
|
||||||
from .types import SingleKey
|
from .types import SingleKey
|
||||||
from .typing import ScreenType
|
from .typing import ScreenType
|
||||||
|
|
||||||
|
|
||||||
mod_mask = GLFW_MOD_ALT | GLFW_MOD_CONTROL | GLFW_MOD_SHIFT | GLFW_MOD_SUPER | GLFW_MOD_META | GLFW_MOD_HYPER
|
mod_mask = GLFW_MOD_ALT | GLFW_MOD_CONTROL | GLFW_MOD_SHIFT | GLFW_MOD_SUPER | GLFW_MOD_META | GLFW_MOD_HYPER
|
||||||
lock_mask = GLFW_MOD_NUM_LOCK | GLFW_MOD_CAPS_LOCK
|
lock_mask = GLFW_MOD_NUM_LOCK | GLFW_MOD_CAPS_LOCK
|
||||||
|
|
||||||
|
|||||||
11
kitty/kitty-uthash.h
Normal file
11
kitty/kitty-uthash.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GPL3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "data-types.h"
|
||||||
|
#define uthash_fatal(msg) fatal(msg)
|
||||||
|
#include "uthash.h"
|
||||||
@@ -10,7 +10,9 @@ from .child import Child
|
|||||||
from .cli import WATCHER_DEFINITION, parse_args
|
from .cli import WATCHER_DEFINITION, parse_args
|
||||||
from .cli_stub import LaunchCLIOptions
|
from .cli_stub import LaunchCLIOptions
|
||||||
from .constants import resolve_custom_file
|
from .constants import resolve_custom_file
|
||||||
from .fast_data_types import patch_color_profiles, set_clipboard_string
|
from .fast_data_types import (
|
||||||
|
get_options, patch_color_profiles, set_clipboard_string
|
||||||
|
)
|
||||||
from .tabs import Tab
|
from .tabs import Tab
|
||||||
from .types import run_once
|
from .types import run_once
|
||||||
from .utils import find_exe, read_shell_environment, set_primary_selection
|
from .utils import find_exe, read_shell_environment, set_primary_selection
|
||||||
@@ -219,7 +221,7 @@ def load_watch_modules(watchers: Sequence[str]) -> Optional[Watchers]:
|
|||||||
ans = Watchers()
|
ans = Watchers()
|
||||||
for path in watchers:
|
for path in watchers:
|
||||||
path = resolve_custom_file(path)
|
path = resolve_custom_file(path)
|
||||||
m = runpy.run_path(path, run_name='__kitty_watcher__') # type: ignore
|
m = runpy.run_path(path, run_name='__kitty_watcher__')
|
||||||
w = m.get('on_close')
|
w = m.get('on_close')
|
||||||
if callable(w):
|
if callable(w):
|
||||||
ans.on_close.append(w)
|
ans.on_close.append(w)
|
||||||
@@ -285,6 +287,20 @@ def launch(
|
|||||||
kw['location'] = opts.location
|
kw['location'] = opts.location
|
||||||
if opts.copy_colors and active:
|
if opts.copy_colors and active:
|
||||||
kw['copy_colors_from'] = active
|
kw['copy_colors_from'] = active
|
||||||
|
pipe_data: Dict[str, Any] = {}
|
||||||
|
if opts.stdin_source != 'none':
|
||||||
|
q = str(opts.stdin_source)
|
||||||
|
if opts.stdin_add_formatting:
|
||||||
|
if q in ('@screen', '@screen_scrollback', '@alternate', '@alternate_scrollback'):
|
||||||
|
q = '@ansi_' + q[1:]
|
||||||
|
if opts.stdin_add_line_wrap_markers:
|
||||||
|
q += '_wrap'
|
||||||
|
penv, stdin = boss.process_stdin_source(window=active, stdin=q, copy_pipe_data=pipe_data)
|
||||||
|
if stdin:
|
||||||
|
kw['stdin'] = stdin
|
||||||
|
if penv:
|
||||||
|
env.update(penv)
|
||||||
|
|
||||||
cmd = args or None
|
cmd = args or None
|
||||||
if opts.copy_cmdline and active_child:
|
if opts.copy_cmdline and active_child:
|
||||||
cmd = active_child.foreground_cmdline
|
cmd = active_child.foreground_cmdline
|
||||||
@@ -298,10 +314,25 @@ def launch(
|
|||||||
x = s
|
x = s
|
||||||
elif x == '@active-kitty-window-id':
|
elif x == '@active-kitty-window-id':
|
||||||
x = str(active.id)
|
x = str(active.id)
|
||||||
|
elif x == '@input-line-number':
|
||||||
|
if 'input_line_number' in pipe_data:
|
||||||
|
x = str(pipe_data['input_line_number'])
|
||||||
|
elif x == '@line-count':
|
||||||
|
if 'lines' in pipe_data:
|
||||||
|
x = str(pipe_data['lines'])
|
||||||
|
elif x in ('@cursor-x', '@cursor-y', '@scrolled-by'):
|
||||||
|
if active is not None:
|
||||||
|
screen = active.screen
|
||||||
|
if x == '@scrolled-by':
|
||||||
|
x = str(screen.scrolled_by)
|
||||||
|
elif x == '@cursor-x':
|
||||||
|
x = str(screen.cursor.x + 1)
|
||||||
|
elif x == '@cursor-y':
|
||||||
|
x = str(screen.cursor.y + 1)
|
||||||
final_cmd.append(x)
|
final_cmd.append(x)
|
||||||
exe = find_exe(final_cmd[0])
|
exe = find_exe(final_cmd[0])
|
||||||
if not exe:
|
if not exe:
|
||||||
env = read_shell_environment(boss.opts)
|
env = read_shell_environment(get_options())
|
||||||
if 'PATH' in env:
|
if 'PATH' in env:
|
||||||
import shutil
|
import shutil
|
||||||
exe = shutil.which(final_cmd[0], path=env['PATH'])
|
exe = shutil.which(final_cmd[0], path=env['PATH'])
|
||||||
@@ -310,19 +341,6 @@ def launch(
|
|||||||
kw['cmd'] = final_cmd
|
kw['cmd'] = final_cmd
|
||||||
if opts.type == 'overlay' and active:
|
if opts.type == 'overlay' and active:
|
||||||
kw['overlay_for'] = active.id
|
kw['overlay_for'] = active.id
|
||||||
if opts.stdin_source != 'none':
|
|
||||||
q = str(opts.stdin_source)
|
|
||||||
if opts.stdin_add_formatting:
|
|
||||||
if q in ('@screen', '@screen_scrollback', '@alternate', '@alternate_scrollback'):
|
|
||||||
q = '@ansi_' + q[1:]
|
|
||||||
if opts.stdin_add_line_wrap_markers:
|
|
||||||
q += '_wrap'
|
|
||||||
penv, stdin = boss.process_stdin_source(window=active, stdin=q)
|
|
||||||
if stdin:
|
|
||||||
kw['stdin'] = stdin
|
|
||||||
if penv:
|
|
||||||
env.update(penv)
|
|
||||||
|
|
||||||
if opts.type == 'background':
|
if opts.type == 'background':
|
||||||
cmd = kw['cmd']
|
cmd = kw['cmd']
|
||||||
if not cmd:
|
if not cmd:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from kitty.borders import BorderColor
|
|||||||
from kitty.fast_data_types import (
|
from kitty.fast_data_types import (
|
||||||
Region, set_active_window, viewport_for_window
|
Region, set_active_window, viewport_for_window
|
||||||
)
|
)
|
||||||
from kitty.options_stub import Options
|
from kitty.options.types import Options
|
||||||
from kitty.types import Edges, WindowGeometry
|
from kitty.types import Edges, WindowGeometry
|
||||||
from kitty.typing import TypedDict, WindowType
|
from kitty.typing import TypedDict, WindowType
|
||||||
from kitty.window_list import WindowGroup, WindowList
|
from kitty.window_list import WindowGroup, WindowList
|
||||||
@@ -374,6 +374,10 @@ class Layout:
|
|||||||
def compute_needs_borders_map(self, all_windows: WindowList) -> Dict[int, bool]:
|
def compute_needs_borders_map(self, all_windows: WindowList) -> Dict[int, bool]:
|
||||||
return all_windows.compute_needs_borders_map(lgd.draw_active_borders)
|
return all_windows.compute_needs_borders_map(lgd.draw_active_borders)
|
||||||
|
|
||||||
|
def get_minimal_borders(self, windows: WindowList) -> Generator[BorderLine, None, None]:
|
||||||
|
self._set_dimensions()
|
||||||
|
yield from self.minimal_borders(windows)
|
||||||
|
|
||||||
def minimal_borders(self, windows: WindowList) -> Generator[BorderLine, None, None]:
|
def minimal_borders(self, windows: WindowList) -> Generator[BorderLine, None, None]:
|
||||||
return
|
return
|
||||||
yield BorderLine() # type: ignore
|
yield BorderLine() # type: ignore
|
||||||
|
|||||||
@@ -544,7 +544,7 @@ copy_old(LineBuf *self, PyObject *y) {
|
|||||||
#include "rewrap.h"
|
#include "rewrap.h"
|
||||||
|
|
||||||
void
|
void
|
||||||
linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *num_content_lines_before, index_type *num_content_lines_after, HistoryBuf *historybuf, index_type *track_x, index_type *track_y, ANSIBuf *as_ansi_buf) {
|
linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *num_content_lines_before, index_type *num_content_lines_after, HistoryBuf *historybuf, index_type *track_x, index_type *track_y, index_type *track_x2, index_type *track_y2, ANSIBuf *as_ansi_buf) {
|
||||||
index_type first, i;
|
index_type first, i;
|
||||||
bool is_empty = true;
|
bool is_empty = true;
|
||||||
|
|
||||||
@@ -574,7 +574,10 @@ linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *num_content_lines_befo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
rewrap_inner(self, other, first + 1, historybuf, track_x, track_y, as_ansi_buf);
|
TrackCursor tcarr[3] = {{.x = *track_x, .y = *track_y }, {.x = *track_x2, .y = *track_y2}, {.is_sentinel = true}};
|
||||||
|
rewrap_inner(self, other, first + 1, historybuf, (TrackCursor*)tcarr, as_ansi_buf);
|
||||||
|
*track_x = tcarr[0].x; *track_y = tcarr[0].y;
|
||||||
|
*track_x2 = tcarr[1].x; *track_y2 = tcarr[1].y;
|
||||||
*num_content_lines_after = other->line->ynum + 1;
|
*num_content_lines_after = other->line->ynum + 1;
|
||||||
for (i = 0; i < *num_content_lines_after; i++) other->line_attrs[i] |= TEXT_DIRTY_MASK;
|
for (i = 0; i < *num_content_lines_after; i++) other->line_attrs[i] |= TEXT_DIRTY_MASK;
|
||||||
*num_content_lines_before = first + 1;
|
*num_content_lines_before = first + 1;
|
||||||
@@ -587,9 +590,9 @@ rewrap(LineBuf *self, PyObject *args) {
|
|||||||
unsigned int nclb, ncla;
|
unsigned int nclb, ncla;
|
||||||
|
|
||||||
if (!PyArg_ParseTuple(args, "O!O!", &LineBuf_Type, &other, &HistoryBuf_Type, &historybuf)) return NULL;
|
if (!PyArg_ParseTuple(args, "O!O!", &LineBuf_Type, &other, &HistoryBuf_Type, &historybuf)) return NULL;
|
||||||
index_type x = 0, y = 0;
|
index_type x = 0, y = 0, x2 = 0, y2 = 0;
|
||||||
ANSIBuf as_ansi_buf = {0};
|
ANSIBuf as_ansi_buf = {0};
|
||||||
linebuf_rewrap(self, other, &nclb, &ncla, historybuf, &x, &y, &as_ansi_buf);
|
linebuf_rewrap(self, other, &nclb, &ncla, historybuf, &x, &y, &x2, &y2, &as_ansi_buf);
|
||||||
free(as_ansi_buf.buf);
|
free(as_ansi_buf.buf);
|
||||||
|
|
||||||
return Py_BuildValue("II", nclb, ncla);
|
return Py_BuildValue("II", nclb, ncla);
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ cell_text(CPUCell *cell) {
|
|||||||
static inline index_type
|
static inline index_type
|
||||||
find_colon_slash(Line *self, index_type x, index_type limit) {
|
find_colon_slash(Line *self, index_type x, index_type limit) {
|
||||||
// Find :// at or before x
|
// Find :// at or before x
|
||||||
index_type pos = x;
|
index_type pos = MIN(x, self->xnum - 1);
|
||||||
enum URL_PARSER_STATES {ANY, FIRST_SLASH, SECOND_SLASH};
|
enum URL_PARSER_STATES {ANY, FIRST_SLASH, SECOND_SLASH};
|
||||||
enum URL_PARSER_STATES state = ANY;
|
enum URL_PARSER_STATES state = ANY;
|
||||||
limit = MAX(2u, limit);
|
limit = MAX(2u, limit);
|
||||||
@@ -108,7 +108,6 @@ has_url_prefix_at(Line *self, index_type at, index_type min_prefix_len, index_ty
|
|||||||
|
|
||||||
static inline bool
|
static inline bool
|
||||||
has_url_beyond(Line *self, index_type x) {
|
has_url_beyond(Line *self, index_type x) {
|
||||||
if (self->xnum <= x + MIN_URL_LEN + 3) return false;
|
|
||||||
for (index_type i = x; i < MIN(x + MIN_URL_LEN + 3, self->xnum); i++) {
|
for (index_type i = x; i < MIN(x + MIN_URL_LEN + 3, self->xnum); i++) {
|
||||||
if (!is_url_char(self->cpu_cells[i].ch)) return false;
|
if (!is_url_char(self->cpu_cells[i].ch)) return false;
|
||||||
}
|
}
|
||||||
@@ -122,7 +121,7 @@ line_url_start_at(Line *self, index_type x) {
|
|||||||
if (x >= self->xnum || self->xnum <= MIN_URL_LEN + 3) return self->xnum;
|
if (x >= self->xnum || self->xnum <= MIN_URL_LEN + 3) return self->xnum;
|
||||||
index_type ds_pos = 0, t;
|
index_type ds_pos = 0, t;
|
||||||
// First look for :// ahead of x
|
// First look for :// ahead of x
|
||||||
if (self->xnum - x > OPT(url_prefixes).max_prefix_len + 3) ds_pos = find_colon_slash(self, x + OPT(url_prefixes).max_prefix_len + 3, x < 2 ? 0 : x - 2);
|
ds_pos = find_colon_slash(self, x + OPT(url_prefixes).max_prefix_len + 3, x < 2 ? 0 : x - 2);
|
||||||
if (ds_pos != 0 && has_url_beyond(self, ds_pos)) {
|
if (ds_pos != 0 && has_url_beyond(self, ds_pos)) {
|
||||||
if (has_url_prefix_at(self, ds_pos, ds_pos > x ? ds_pos - x: 0, &t)) return t;
|
if (has_url_prefix_at(self, ds_pos, ds_pos > x ? ds_pos - x: 0, &t)) return t;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ void linebuf_insert_lines(LineBuf *self, unsigned int num, unsigned int y, unsig
|
|||||||
void linebuf_delete_lines(LineBuf *self, index_type num, index_type y, index_type bottom);
|
void linebuf_delete_lines(LineBuf *self, index_type num, index_type y, index_type bottom);
|
||||||
void linebuf_copy_line_to(LineBuf *, Line *, index_type);
|
void linebuf_copy_line_to(LineBuf *, Line *, index_type);
|
||||||
void linebuf_set_attribute(LineBuf *, unsigned int , unsigned int );
|
void linebuf_set_attribute(LineBuf *, unsigned int , unsigned int );
|
||||||
void linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *, index_type *, HistoryBuf *, index_type *, index_type *, ANSIBuf*);
|
void linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *, index_type *, HistoryBuf *, index_type *, index_type *, index_type *, index_type *, ANSIBuf*);
|
||||||
void linebuf_mark_line_dirty(LineBuf *self, index_type y);
|
void linebuf_mark_line_dirty(LineBuf *self, index_type y);
|
||||||
void linebuf_mark_line_clean(LineBuf *self, index_type y);
|
void linebuf_mark_line_clean(LineBuf *self, index_type y);
|
||||||
void linebuf_mark_line_as_not_continued(LineBuf *self, index_type y);
|
void linebuf_mark_line_as_not_continued(LineBuf *self, index_type y);
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ handle_signal(int sig_num) {
|
|||||||
#define SIGNAL_SET \
|
#define SIGNAL_SET \
|
||||||
sigset_t signals = {0}; \
|
sigset_t signals = {0}; \
|
||||||
sigemptyset(&signals); \
|
sigemptyset(&signals); \
|
||||||
sigaddset(&signals, SIGINT); sigaddset(&signals, SIGTERM); sigaddset(&signals, SIGCHLD); \
|
sigaddset(&signals, SIGINT); sigaddset(&signals, SIGTERM); sigaddset(&signals, SIGCHLD); sigaddset(&signals, SIGUSR1); \
|
||||||
|
|
||||||
void
|
void
|
||||||
free_loop_data(LoopData *ld) {
|
free_loop_data(LoopData *ld) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
from contextlib import contextmanager, suppress
|
from contextlib import contextmanager, suppress
|
||||||
from typing import Dict, Generator, List, Optional, Sequence
|
from typing import Dict, Generator, List, Optional, Sequence, Tuple
|
||||||
|
|
||||||
from .borders import load_borders_program
|
from .borders import load_borders_program
|
||||||
from .boss import Boss
|
from .boss import Boss
|
||||||
@@ -27,7 +27,7 @@ from .fast_data_types import (
|
|||||||
)
|
)
|
||||||
from .fonts.box_drawing import set_scale
|
from .fonts.box_drawing import set_scale
|
||||||
from .fonts.render import set_font_family
|
from .fonts.render import set_font_family
|
||||||
from .options_stub import Options as OptionsStub
|
from .options.types import Options
|
||||||
from .os_window_size import initial_window_size_func
|
from .os_window_size import initial_window_size_func
|
||||||
from .session import get_os_window_sizing_data
|
from .session import get_os_window_sizing_data
|
||||||
from .types import SingleKey
|
from .types import SingleKey
|
||||||
@@ -98,17 +98,17 @@ def init_glfw_module(glfw_module: str, debug_keyboard: bool = False, debug_rende
|
|||||||
raise SystemExit('GLFW initialization failed')
|
raise SystemExit('GLFW initialization failed')
|
||||||
|
|
||||||
|
|
||||||
def init_glfw(opts: OptionsStub, debug_keyboard: bool = False, debug_rendering: bool = False) -> str:
|
def init_glfw(opts: Options, debug_keyboard: bool = False, debug_rendering: bool = False) -> str:
|
||||||
glfw_module = 'cocoa' if is_macos else ('wayland' if is_wayland(opts) else 'x11')
|
glfw_module = 'cocoa' if is_macos else ('wayland' if is_wayland(opts) else 'x11')
|
||||||
init_glfw_module(glfw_module, debug_keyboard, debug_rendering)
|
init_glfw_module(glfw_module, debug_keyboard, debug_rendering)
|
||||||
return glfw_module
|
return glfw_module
|
||||||
|
|
||||||
|
|
||||||
def get_macos_shortcut_for(opts: OptionsStub, function: str = 'new_os_window') -> Optional[SingleKey]:
|
def get_macos_shortcut_for(opts: Options, function: str = 'new_os_window', args: Tuple = (), lookup_name: str = '') -> Optional[SingleKey]:
|
||||||
ans = None
|
ans = None
|
||||||
candidates = []
|
candidates = []
|
||||||
for k, v in opts.keymap.items():
|
for k, v in opts.keymap.items():
|
||||||
if v.func == function:
|
if v.func == function and v.args == args:
|
||||||
candidates.append(k)
|
candidates.append(k)
|
||||||
if candidates:
|
if candidates:
|
||||||
from .fast_data_types import cocoa_set_global_shortcut
|
from .fast_data_types import cocoa_set_global_shortcut
|
||||||
@@ -120,7 +120,7 @@ def get_macos_shortcut_for(opts: OptionsStub, function: str = 'new_os_window') -
|
|||||||
# presumably because Apple reserves them for IME, see
|
# presumably because Apple reserves them for IME, see
|
||||||
# https://github.com/kovidgoyal/kitty/issues/3515
|
# https://github.com/kovidgoyal/kitty/issues/3515
|
||||||
continue
|
continue
|
||||||
if cocoa_set_global_shortcut(function, candidate[0], candidate[2]):
|
if cocoa_set_global_shortcut(lookup_name or function, candidate[0], candidate[2]):
|
||||||
ans = candidate
|
ans = candidate
|
||||||
break
|
break
|
||||||
return ans
|
return ans
|
||||||
@@ -132,13 +132,17 @@ def set_x11_window_icon() -> None:
|
|||||||
set_default_window_icon(path + '-128' + ext)
|
set_default_window_icon(path + '-128' + ext)
|
||||||
|
|
||||||
|
|
||||||
def _run_app(opts: OptionsStub, args: CLIOptions, bad_lines: Sequence[BadLine] = ()) -> None:
|
def _run_app(opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = ()) -> None:
|
||||||
global_shortcuts: Dict[str, SingleKey] = {}
|
global_shortcuts: Dict[str, SingleKey] = {}
|
||||||
if is_macos:
|
if is_macos:
|
||||||
for ac in ('new_os_window', 'close_os_window', 'close_tab', 'edit_config_file', 'previous_tab', 'next_tab', 'new_tab'):
|
for ac in ('new_os_window', 'close_os_window', 'close_tab', 'edit_config_file', 'previous_tab',
|
||||||
|
'next_tab', 'new_tab', 'new_window', 'close_window'):
|
||||||
val = get_macos_shortcut_for(opts, ac)
|
val = get_macos_shortcut_for(opts, ac)
|
||||||
if val is not None:
|
if val is not None:
|
||||||
global_shortcuts[ac] = val
|
global_shortcuts[ac] = val
|
||||||
|
val = get_macos_shortcut_for(opts, 'clear_terminal', args=('reset', True), lookup_name='reset_terminal')
|
||||||
|
if val is not None:
|
||||||
|
global_shortcuts['reset_terminal'] = val
|
||||||
if is_macos and opts.macos_custom_beam_cursor:
|
if is_macos and opts.macos_custom_beam_cursor:
|
||||||
set_custom_ibeam_cursor()
|
set_custom_ibeam_cursor()
|
||||||
if not is_wayland() and not is_macos: # no window icons on wayland
|
if not is_wayland() and not is_macos: # no window icons on wayland
|
||||||
@@ -168,14 +172,18 @@ class AppRunner:
|
|||||||
self.first_window_callback = lambda window_handle: None
|
self.first_window_callback = lambda window_handle: None
|
||||||
self.initial_window_size_func = initial_window_size_func
|
self.initial_window_size_func = initial_window_size_func
|
||||||
|
|
||||||
def __call__(self, opts: OptionsStub, args: CLIOptions, bad_lines: Sequence[BadLine] = ()) -> None:
|
def __call__(self, opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = ()) -> None:
|
||||||
set_scale(opts.box_drawing_scale)
|
set_scale(opts.box_drawing_scale)
|
||||||
set_options(opts, is_wayland(), args.debug_rendering, args.debug_font_fallback)
|
set_options(opts, is_wayland(), args.debug_rendering, args.debug_font_fallback)
|
||||||
set_font_family(opts, debug_font_matching=args.debug_font_fallback)
|
|
||||||
try:
|
try:
|
||||||
|
set_font_family(opts, debug_font_matching=args.debug_font_fallback)
|
||||||
_run_app(opts, args, bad_lines)
|
_run_app(opts, args, bad_lines)
|
||||||
finally:
|
finally:
|
||||||
|
set_options(None)
|
||||||
free_font_data() # must free font data before glfw/freetype/fontconfig/opengl etc are finalized
|
free_font_data() # must free font data before glfw/freetype/fontconfig/opengl etc are finalized
|
||||||
|
if is_macos:
|
||||||
|
from kitty.fast_data_types import cocoa_set_notification_activated_callback
|
||||||
|
cocoa_set_notification_activated_callback(None)
|
||||||
|
|
||||||
|
|
||||||
run_app = AppRunner()
|
run_app = AppRunner()
|
||||||
@@ -246,7 +254,7 @@ def expand_listen_on(listen_on: str, from_config_file: bool) -> str:
|
|||||||
return listen_on
|
return listen_on
|
||||||
|
|
||||||
|
|
||||||
def setup_environment(opts: OptionsStub, cli_opts: CLIOptions) -> None:
|
def setup_environment(opts: Options, cli_opts: CLIOptions) -> None:
|
||||||
from_config_file = False
|
from_config_file = False
|
||||||
if not cli_opts.listen_on and opts.listen_on.startswith('unix:'):
|
if not cli_opts.listen_on and opts.listen_on.startswith('unix:'):
|
||||||
cli_opts.listen_on = opts.listen_on
|
cli_opts.listen_on = opts.listen_on
|
||||||
@@ -302,9 +310,6 @@ def _main() -> None:
|
|||||||
os.chdir(os.path.expanduser('~'))
|
os.chdir(os.path.expanduser('~'))
|
||||||
cli_opts, rest = parse_args(args=args, result_class=CLIOptions)
|
cli_opts, rest = parse_args(args=args, result_class=CLIOptions)
|
||||||
cli_opts.args = rest
|
cli_opts.args = rest
|
||||||
if cli_opts.debug_config:
|
|
||||||
create_opts(cli_opts, debug_config=True)
|
|
||||||
return
|
|
||||||
if cli_opts.detach:
|
if cli_opts.detach:
|
||||||
if cli_opts.session == '-':
|
if cli_opts.session == '-':
|
||||||
from .session import PreReadSession
|
from .session import PreReadSession
|
||||||
|
|||||||
@@ -91,5 +91,5 @@ def marker_from_spec(ftype: str, spec: Union[str, Sequence[Tuple[int, str]]], fl
|
|||||||
path = spec
|
path = spec
|
||||||
if not os.path.isabs(path):
|
if not os.path.isabs(path):
|
||||||
path = os.path.join(config_dir, path)
|
path = os.path.join(config_dir, path)
|
||||||
return marker_from_function(runpy.run_path(path, run_name='__marker__')["marker"]) # type: ignore
|
return marker_from_function(runpy.run_path(path, run_name='__marker__')["marker"])
|
||||||
raise ValueError('Unknown marker type: {}'.format(ftype))
|
raise ValueError('Unknown marker type: {}'.format(ftype))
|
||||||
|
|||||||
411
kitty/mouse.c
411
kitty/mouse.c
@@ -19,6 +19,7 @@ extern PyTypeObject Screen_Type;
|
|||||||
|
|
||||||
static MouseShape mouse_cursor_shape = BEAM;
|
static MouseShape mouse_cursor_shape = BEAM;
|
||||||
typedef enum MouseActions { PRESS, RELEASE, DRAG, MOVE } MouseAction;
|
typedef enum MouseActions { PRESS, RELEASE, DRAG, MOVE } MouseAction;
|
||||||
|
#define debug(...) if (OPT(debug_keyboard)) printf(__VA_ARGS__);
|
||||||
|
|
||||||
// Encoding of mouse events {{{
|
// Encoding of mouse events {{{
|
||||||
#define SHIFT_INDICATOR (1 << 2)
|
#define SHIFT_INDICATOR (1 << 2)
|
||||||
@@ -122,6 +123,44 @@ encode_mouse_scroll(Window *w, bool upwards, int mods) {
|
|||||||
|
|
||||||
// }}}
|
// }}}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (OPT(debug_keyboard)) {
|
||||||
|
const char *evname = "move";
|
||||||
|
switch(count) {
|
||||||
|
case -3: evname = "doubleclick"; break;
|
||||||
|
case -2: evname = "click"; break;
|
||||||
|
case -1: evname = "release"; break;
|
||||||
|
case 1: evname = "press"; break;
|
||||||
|
case 2: evname = "doublepress"; break;
|
||||||
|
case 3: evname = "triplepress"; break;
|
||||||
|
}
|
||||||
|
const char *bname = "unknown";
|
||||||
|
switch(button) {
|
||||||
|
case GLFW_MOUSE_BUTTON_LEFT: bname = "left"; break;
|
||||||
|
case GLFW_MOUSE_BUTTON_MIDDLE: bname = "middle"; break;
|
||||||
|
case GLFW_MOUSE_BUTTON_RIGHT: bname = "right"; break;
|
||||||
|
case GLFW_MOUSE_BUTTON_4: bname = "b4"; break;
|
||||||
|
case GLFW_MOUSE_BUTTON_5: bname = "b5"; break;
|
||||||
|
case GLFW_MOUSE_BUTTON_6: bname = "b6"; break;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return handled;
|
||||||
|
}
|
||||||
|
|
||||||
static inline unsigned int
|
static inline unsigned int
|
||||||
window_left(Window *w) {
|
window_left(Window *w) {
|
||||||
return w->geometry.left - w->padding.left;
|
return w->geometry.left - w->padding.left;
|
||||||
@@ -204,21 +243,9 @@ set_mouse_cursor_when_dragging(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
update_drag(bool from_button, Window *w, bool is_release, int modifiers) {
|
update_drag(Window *w) {
|
||||||
Screen *screen = w->render_data.screen;
|
Screen *screen = w->render_data.screen;
|
||||||
if (from_button) {
|
if (screen && screen->selections.in_progress) {
|
||||||
if (is_release) {
|
|
||||||
global_state.active_drag_in_window = 0;
|
|
||||||
w->last_drag_scroll_at = 0;
|
|
||||||
if (screen->selections.in_progress) {
|
|
||||||
screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, true, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
global_state.active_drag_in_window = w->id;
|
|
||||||
screen_start_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, modifiers == (int)OPT(rectangle_select_modifiers) || modifiers == ((int)OPT(rectangle_select_modifiers) | (int)OPT(terminal_select_modifiers)), EXTEND_CELL);
|
|
||||||
}
|
|
||||||
} else if (screen->selections.in_progress) {
|
|
||||||
screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, false, false);
|
screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, false, false);
|
||||||
}
|
}
|
||||||
set_mouse_cursor_when_dragging();
|
set_mouse_cursor_when_dragging();
|
||||||
@@ -229,7 +256,7 @@ do_drag_scroll(Window *w, bool upwards) {
|
|||||||
Screen *screen = w->render_data.screen;
|
Screen *screen = w->render_data.screen;
|
||||||
if (screen->linebuf == screen->main_linebuf) {
|
if (screen->linebuf == screen->main_linebuf) {
|
||||||
screen_history_scroll(screen, SCROLL_LINE, upwards);
|
screen_history_scroll(screen, SCROLL_LINE, upwards);
|
||||||
update_drag(false, w, false, 0);
|
update_drag(w);
|
||||||
if (mouse_cursor_shape != ARROW) {
|
if (mouse_cursor_shape != ARROW) {
|
||||||
mouse_cursor_shape = ARROW;
|
mouse_cursor_shape = ARROW;
|
||||||
set_mouse_cursor(mouse_cursor_shape);
|
set_mouse_cursor(mouse_cursor_shape);
|
||||||
@@ -259,48 +286,6 @@ extend_selection(Window *w, bool ended) {
|
|||||||
if (screen_has_selection(screen)) {
|
if (screen_has_selection(screen)) {
|
||||||
screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, ended, false);
|
screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, ended, false);
|
||||||
}
|
}
|
||||||
set_mouse_cursor_when_dragging();
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
|
||||||
extend_url(Screen *screen, Line *line, index_type *x, index_type *y, char_type sentinel) {
|
|
||||||
unsigned int count = 0;
|
|
||||||
while(count++ < 10) {
|
|
||||||
if (*x != line->xnum - 1) break;
|
|
||||||
bool next_line_starts_with_url_chars = false;
|
|
||||||
line = screen_visual_line(screen, *y + 2);
|
|
||||||
if (line) next_line_starts_with_url_chars = line_startswith_url_chars(line);
|
|
||||||
line = screen_visual_line(screen, *y + 1);
|
|
||||||
if (!line) break;
|
|
||||||
// we deliberately allow non-continued lines as some programs, like
|
|
||||||
// mutt split URLs with newlines at line boundaries
|
|
||||||
index_type new_x = line_url_end_at(line, 0, false, sentinel, next_line_starts_with_url_chars);
|
|
||||||
if (!new_x && !line_startswith_url_chars(line)) break;
|
|
||||||
*y += 1; *x = new_x;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline char_type
|
|
||||||
get_url_sentinel(Line *line, index_type url_start) {
|
|
||||||
char_type before = 0, sentinel;
|
|
||||||
if (url_start > 0 && url_start < line->xnum) before = line->cpu_cells[url_start - 1].ch;
|
|
||||||
switch(before) {
|
|
||||||
case '"':
|
|
||||||
case '\'':
|
|
||||||
case '*':
|
|
||||||
sentinel = before; break;
|
|
||||||
case '(':
|
|
||||||
sentinel = ')'; break;
|
|
||||||
case '[':
|
|
||||||
sentinel = ']'; break;
|
|
||||||
case '{':
|
|
||||||
sentinel = '}'; break;
|
|
||||||
case '<':
|
|
||||||
sentinel = '>'; break;
|
|
||||||
default:
|
|
||||||
sentinel = 0; break;
|
|
||||||
}
|
|
||||||
return sentinel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
@@ -308,55 +293,25 @@ set_mouse_cursor_for_screen(Screen *screen) {
|
|||||||
mouse_cursor_shape = screen->modes.mouse_tracking_mode == NO_TRACKING ? OPT(default_pointer_shape): OPT(pointer_shape_when_grabbed);
|
mouse_cursor_shape = screen->modes.mouse_tracking_mode == NO_TRACKING ? OPT(default_pointer_shape): OPT(pointer_shape_when_grabbed);
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
|
||||||
detect_url(Screen *screen, unsigned int x, unsigned int y) {
|
|
||||||
bool has_url = false;
|
|
||||||
index_type url_start, url_end = 0;
|
|
||||||
Line *line = screen_visual_line(screen, y);
|
|
||||||
if (line->cpu_cells[x].hyperlink_id) {
|
|
||||||
mouse_cursor_shape = HAND;
|
|
||||||
screen_mark_hyperlink(screen, x, y);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
char_type sentinel;
|
|
||||||
if (line) {
|
|
||||||
url_start = line_url_start_at(line, x);
|
|
||||||
sentinel = get_url_sentinel(line, url_start);
|
|
||||||
if (url_start < line->xnum) {
|
|
||||||
bool next_line_starts_with_url_chars = false;
|
|
||||||
if (y < screen->lines - 1) {
|
|
||||||
line = screen_visual_line(screen, y+1);
|
|
||||||
next_line_starts_with_url_chars = line_startswith_url_chars(line);
|
|
||||||
line = screen_visual_line(screen, y);
|
|
||||||
}
|
|
||||||
url_end = line_url_end_at(line, x, true, sentinel, next_line_starts_with_url_chars);
|
|
||||||
}
|
|
||||||
has_url = url_end > url_start;
|
|
||||||
}
|
|
||||||
if (has_url) {
|
|
||||||
mouse_cursor_shape = HAND;
|
|
||||||
index_type y_extended = y;
|
|
||||||
extend_url(screen, line, &url_end, &y_extended, sentinel);
|
|
||||||
screen_mark_url(screen, url_start, y, url_end, y_extended);
|
|
||||||
} else {
|
|
||||||
set_mouse_cursor_for_screen(screen);
|
|
||||||
screen_mark_url(screen, 0, 0, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
handle_mouse_movement_in_kitty(Window *w, int button, bool mouse_cell_changed) {
|
handle_mouse_movement_in_kitty(Window *w, int button, bool mouse_cell_changed) {
|
||||||
Screen *screen = w->render_data.screen;
|
Screen *screen = w->render_data.screen;
|
||||||
if (screen->selections.in_progress && (button == GLFW_MOUSE_BUTTON_LEFT || button == GLFW_MOUSE_BUTTON_RIGHT)) {
|
if (screen->selections.in_progress && (button == global_state.active_drag_button)) {
|
||||||
monotonic_t now = monotonic();
|
monotonic_t now = monotonic();
|
||||||
if ((now - w->last_drag_scroll_at) >= ms_to_monotonic_t(20ll) || mouse_cell_changed) {
|
if ((now - w->last_drag_scroll_at) >= ms_to_monotonic_t(20ll) || mouse_cell_changed) {
|
||||||
update_drag(false, w, false, 0);
|
update_drag(w);
|
||||||
w->last_drag_scroll_at = now;
|
w->last_drag_scroll_at = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
detect_url(Screen *screen, unsigned int x, unsigned int y) {
|
||||||
|
if (screen_detect_url(screen, x, y)) mouse_cursor_shape = HAND;
|
||||||
|
else set_mouse_cursor_for_screen(screen);
|
||||||
|
}
|
||||||
|
|
||||||
HANDLER(handle_move_event) {
|
HANDLER(handle_move_event) {
|
||||||
modifiers &= ~GLFW_LOCK_MASK;
|
modifiers &= ~GLFW_LOCK_MASK;
|
||||||
unsigned int x = 0, y = 0;
|
unsigned int x = 0, y = 0;
|
||||||
@@ -377,8 +332,7 @@ HANDLER(handle_move_event) {
|
|||||||
bool in_tracking_mode = (
|
bool in_tracking_mode = (
|
||||||
screen->modes.mouse_tracking_mode == ANY_MODE ||
|
screen->modes.mouse_tracking_mode == ANY_MODE ||
|
||||||
(screen->modes.mouse_tracking_mode == MOTION_MODE && button >= 0));
|
(screen->modes.mouse_tracking_mode == MOTION_MODE && button >= 0));
|
||||||
bool has_terminal_select_modifiers = modifiers == (int)OPT(terminal_select_modifiers) || modifiers == ((int)OPT(rectangle_select_modifiers) | (int)OPT(terminal_select_modifiers));
|
bool handle_in_kitty = !in_tracking_mode || global_state.active_drag_in_window == w->id;
|
||||||
bool handle_in_kitty = !in_tracking_mode || has_terminal_select_modifiers;
|
|
||||||
if (handle_in_kitty) {
|
if (handle_in_kitty) {
|
||||||
handle_mouse_movement_in_kitty(w, button, mouse_cell_changed | cell_half_changed);
|
handle_mouse_movement_in_kitty(w, button, mouse_cell_changed | cell_half_changed);
|
||||||
} else {
|
} else {
|
||||||
@@ -388,93 +342,108 @@ HANDLER(handle_move_event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
|
||||||
multi_click(Window *w, unsigned int count) {
|
|
||||||
Screen *screen = w->render_data.screen;
|
|
||||||
index_type start, end;
|
|
||||||
SelectionExtendMode mode = EXTEND_CELL;
|
|
||||||
unsigned int y1, y2;
|
|
||||||
switch(count) {
|
|
||||||
case 2:
|
|
||||||
if (screen_selection_range_for_word(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, &y1, &y2, &start, &end, true)) mode = EXTEND_WORD;
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
if (screen_selection_range_for_line(screen, w->mouse_pos.cell_y, &start, &end)) mode = EXTEND_LINE;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (mode != EXTEND_CELL) {
|
|
||||||
screen_start_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, false, mode);
|
|
||||||
screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, false, true);
|
|
||||||
}
|
|
||||||
set_mouse_cursor_when_dragging();
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline double
|
static inline double
|
||||||
distance(double x1, double y1, double x2, double y2) {
|
distance(double x1, double y1, double x2, double y2) {
|
||||||
return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
|
return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
clear_click_queue(Window *w) {
|
clear_click_queue(Window *w, int button) {
|
||||||
w->click_queue.length = 0;
|
if (0 <= button && button <= (ssize_t)arraysz(w->click_queues)) w->click_queues[button].length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
HANDLER(add_click) {
|
|
||||||
modifiers &= ~GLFW_LOCK_MASK;
|
|
||||||
ClickQueue *q = &w->click_queue;
|
|
||||||
if (q->length == CLICK_QUEUE_SZ) { memmove(q->clicks, q->clicks + 1, sizeof(Click) * (CLICK_QUEUE_SZ - 1)); q->length--; }
|
|
||||||
monotonic_t now = monotonic();
|
|
||||||
#define N(n) (q->clicks[q->length - n])
|
#define N(n) (q->clicks[q->length - n])
|
||||||
N(0).at = now; N(0).button = button; N(0).modifiers = modifiers; N(0).x = w->mouse_pos.x; N(0).y = w->mouse_pos.y;
|
|
||||||
q->length++;
|
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);
|
||||||
|
monotonic_t now = monotonic();
|
||||||
|
return (q->length > 0 && distance(N(1).x, N(1).y, w->mouse_pos.x, w->mouse_pos.y) <= click_allowed_radius && 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);
|
double multi_click_allowed_radius = 1.2 * (global_state.callback_os_window ? global_state.callback_os_window->fonts_data->cell_height : 20);
|
||||||
// Now dispatch the multi-click if any
|
|
||||||
if (q->length > 2) {
|
if (q->length > 2) {
|
||||||
// possible triple-click
|
// possible triple-click
|
||||||
if (
|
if (
|
||||||
N(1).at - N(3).at <= 2 * OPT(click_interval) &&
|
N(1).at - N(3).at <= 2 * OPT(click_interval) &&
|
||||||
distance(N(1).x, N(1).y, N(3).x, N(3).y) <= multi_click_allowed_radius
|
distance(N(1).x, N(1).y, N(3).x, N(3).y) <= multi_click_allowed_radius
|
||||||
) {
|
) return 3;
|
||||||
multi_click(w, 3);
|
|
||||||
q->length = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (q->length > 1) {
|
if (q->length > 1) {
|
||||||
// possible double-click
|
// possible double-click
|
||||||
if (
|
if (
|
||||||
N(1).at - N(2).at <= OPT(click_interval) &&
|
N(1).at - N(2).at <= OPT(click_interval) &&
|
||||||
distance(N(1).x, N(1).y, N(2).x, N(2).y) <= multi_click_allowed_radius
|
distance(N(1).x, N(1).y, N(2).x, N(2).y) <= multi_click_allowed_radius
|
||||||
) {
|
) return 2;
|
||||||
multi_click(w, 2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#undef N
|
return q->length ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
|
||||||
open_url(Window *w) {
|
static void
|
||||||
|
add_press(Window *w, int button, int modifiers) {
|
||||||
|
if (button < 0 || button > (ssize_t)arraysz(w->click_queues)) return;
|
||||||
|
modifiers &= ~GLFW_LOCK_MASK;
|
||||||
|
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 = w->mouse_pos.x; N(0).y = w->mouse_pos.y;
|
||||||
|
q->length++;
|
||||||
|
Screen *screen = w->render_data.screen;
|
||||||
|
int count = multi_click_count(w, button);
|
||||||
|
if (count > 1) {
|
||||||
|
if (screen) dispatch_mouse_event(w, button, count, modifiers, screen->modes.mouse_tracking_mode != 0);
|
||||||
|
if (count > 2) q->length = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#undef N
|
||||||
|
|
||||||
|
void
|
||||||
|
mouse_open_url(Window *w) {
|
||||||
Screen *screen = w->render_data.screen;
|
Screen *screen = w->render_data.screen;
|
||||||
detect_url(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y);
|
detect_url(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y);
|
||||||
screen_open_url(screen);
|
screen_open_url(screen);
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
typedef struct PendingClick {
|
||||||
handle_button_event_in_kitty(Window *w, int button, int modifiers, bool is_release) {
|
id_type window_id;
|
||||||
switch(button) {
|
int button, count, modifiers;
|
||||||
case GLFW_MOUSE_BUTTON_LEFT:
|
bool grabbed;
|
||||||
update_drag(true, w, is_release, modifiers);
|
monotonic_t at;
|
||||||
if (is_release) {
|
} PendingClick;
|
||||||
if (modifiers == (int)OPT(open_url_modifiers)) open_url(w);
|
|
||||||
} else add_click(w, button, modifiers, 0);
|
static void
|
||||||
break;
|
free_pending_click(id_type timer_id UNUSED, void *pc) { free(pc); }
|
||||||
case GLFW_MOUSE_BUTTON_MIDDLE:
|
|
||||||
if (is_release) { call_boss(paste_from_selection, NULL); return; }
|
void
|
||||||
break;
|
send_pending_click_to_window(Window *w, void *data) {
|
||||||
case GLFW_MOUSE_BUTTON_RIGHT:
|
PendingClick *pc = (PendingClick*)data;
|
||||||
extend_selection(w, is_release);
|
ClickQueue *q = &w->click_queues[pc->button];
|
||||||
break;
|
// 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) {
|
||||||
|
dispatch_mouse_event(w, pc->button, pc->count, pc->modifiers, pc->grabbed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
dispatch_possible_click(Window *w, int button, int modifiers) {
|
||||||
|
Screen *screen = w->render_data.screen;
|
||||||
|
int count = multi_click_count(w, button);
|
||||||
|
if (release_is_click(w, button)) {
|
||||||
|
PendingClick *pc = calloc(sizeof(PendingClick), 1);
|
||||||
|
if (pc) {
|
||||||
|
pc->window_id = w->id;
|
||||||
|
pc->at = monotonic();
|
||||||
|
pc->button = button;
|
||||||
|
pc->count = count == 2 ? -3 : -2;
|
||||||
|
pc->modifiers = modifiers;
|
||||||
|
pc->grabbed = screen->modes.mouse_tracking_mode != 0;
|
||||||
|
add_main_loop_timer(OPT(click_interval), false, send_pending_click_to_window_id, pc, free_pending_click);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,22 +456,19 @@ HANDLER(handle_button_event) {
|
|||||||
}
|
}
|
||||||
Screen *screen = w->render_data.screen;
|
Screen *screen = w->render_data.screen;
|
||||||
if (!screen) return;
|
if (!screen) return;
|
||||||
const int ts1 = OPT(terminal_select_modifiers), ts2 = OPT(terminal_select_modifiers) | OPT(rectangle_select_modifiers);
|
if (!dispatch_mouse_event(w, button, is_release ? -1 : 1, modifiers, screen->modes.mouse_tracking_mode != 0)) {
|
||||||
bool handle_in_kitty = (
|
if (screen->modes.mouse_tracking_mode != 0) {
|
||||||
modifiers == ts1 || modifiers == ts2 ||
|
int sz = encode_mouse_button(w, button, is_release ? RELEASE : PRESS, modifiers);
|
||||||
screen->modes.mouse_tracking_mode == 0 ||
|
if (sz > 0) { mouse_event_buf[sz] = 0; write_escape_code_to_child(screen, CSI, mouse_event_buf); }
|
||||||
(modifiers == (int)OPT(open_url_modifiers) && button == GLFW_MOUSE_BUTTON_LEFT)
|
}
|
||||||
);
|
|
||||||
if (handle_in_kitty) handle_button_event_in_kitty(w, button, modifiers, is_release);
|
|
||||||
else {
|
|
||||||
int sz = encode_mouse_button(w, button, is_release ? RELEASE : PRESS, modifiers);
|
|
||||||
if (sz > 0) { mouse_event_buf[sz] = 0; write_escape_code_to_child(screen, CSI, mouse_event_buf); }
|
|
||||||
}
|
}
|
||||||
|
if (is_release) dispatch_possible_click(w, button, modifiers);
|
||||||
|
else add_press(w, button, modifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline int
|
static inline int
|
||||||
currently_pressed_button(void) {
|
currently_pressed_button(void) {
|
||||||
for (int i = 0; i <= GLFW_MOUSE_BUTTON_8; i++) {
|
for (int i = 0; i <= GLFW_MOUSE_BUTTON_LAST; i++) {
|
||||||
if (global_state.callback_os_window->mouse_button_pressed[i]) return i;
|
if (global_state.callback_os_window->mouse_button_pressed[i]) return i;
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
@@ -550,7 +516,7 @@ static inline Window*
|
|||||||
window_for_event(unsigned int *window_idx, bool *in_tab_bar) {
|
window_for_event(unsigned int *window_idx, bool *in_tab_bar) {
|
||||||
Region central, tab_bar;
|
Region central, tab_bar;
|
||||||
os_window_regions(global_state.callback_os_window, ¢ral, &tab_bar);
|
os_window_regions(global_state.callback_os_window, ¢ral, &tab_bar);
|
||||||
*in_tab_bar = mouse_in_region(&tab_bar);
|
*in_tab_bar = !mouse_in_region(¢ral);
|
||||||
if (!*in_tab_bar && global_state.callback_os_window->num_tabs > 0) {
|
if (!*in_tab_bar && global_state.callback_os_window->num_tabs > 0) {
|
||||||
Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab;
|
Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab;
|
||||||
for (unsigned int i = 0; i < t->num_windows; i++) {
|
for (unsigned int i = 0; i < t->num_windows; i++) {
|
||||||
@@ -605,31 +571,93 @@ enter_event() {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
end_drag(Window *w) {
|
||||||
|
Screen *screen = w->render_data.screen;
|
||||||
|
global_state.active_drag_in_window = 0;
|
||||||
|
global_state.active_drag_button = -1;
|
||||||
|
w->last_drag_scroll_at = 0;
|
||||||
|
if (screen->selections.in_progress) {
|
||||||
|
screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, true, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef enum MouseSelectionType {
|
||||||
|
MOUSE_SELECTION_NORMAL,
|
||||||
|
MOUSE_SELECTION_EXTEND,
|
||||||
|
MOUSE_SELECTION_RECTANGLE,
|
||||||
|
MOUSE_SELECTION_WORD,
|
||||||
|
MOUSE_SELECTION_LINE,
|
||||||
|
MOUSE_SELECTION_LINE_FROM_POINT,
|
||||||
|
} MouseSelectionType;
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
mouse_selection(Window *w, int code, int button) {
|
||||||
|
global_state.active_drag_in_window = w->id;
|
||||||
|
global_state.active_drag_button = button;
|
||||||
|
Screen *screen = w->render_data.screen;
|
||||||
|
index_type start, end;
|
||||||
|
unsigned int y1, y2;
|
||||||
|
#define S(mode) {\
|
||||||
|
screen_start_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, false, mode); \
|
||||||
|
screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, false, true); }
|
||||||
|
|
||||||
|
switch((MouseSelectionType)code) {
|
||||||
|
case MOUSE_SELECTION_NORMAL:
|
||||||
|
screen_start_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, false, EXTEND_CELL);
|
||||||
|
break;
|
||||||
|
case MOUSE_SELECTION_RECTANGLE:
|
||||||
|
screen_start_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, true, EXTEND_CELL);
|
||||||
|
break;
|
||||||
|
case MOUSE_SELECTION_WORD:
|
||||||
|
if (screen_selection_range_for_word(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, &y1, &y2, &start, &end, true)) S(EXTEND_WORD);
|
||||||
|
break;
|
||||||
|
case MOUSE_SELECTION_LINE:
|
||||||
|
if (screen_selection_range_for_line(screen, w->mouse_pos.cell_y, &start, &end)) S(EXTEND_LINE);
|
||||||
|
break;
|
||||||
|
case MOUSE_SELECTION_LINE_FROM_POINT:
|
||||||
|
if (screen_selection_range_for_line(screen, w->mouse_pos.cell_y, &start, &end) && end > w->mouse_pos.cell_x) S(EXTEND_LINE_FROM_POINT);
|
||||||
|
break;
|
||||||
|
case MOUSE_SELECTION_EXTEND:
|
||||||
|
extend_selection(w, false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
set_mouse_cursor_when_dragging();
|
||||||
|
#undef S
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
mouse_event(int button, int modifiers, int action) {
|
mouse_event(int button, int modifiers, int action) {
|
||||||
MouseShape old_cursor = mouse_cursor_shape;
|
MouseShape old_cursor = mouse_cursor_shape;
|
||||||
bool in_tab_bar;
|
bool in_tab_bar;
|
||||||
unsigned int window_idx = 0;
|
unsigned int window_idx = 0;
|
||||||
Window *w = NULL;
|
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 (global_state.active_drag_in_window) {
|
if (global_state.active_drag_in_window) {
|
||||||
if (button == -1) { // drag move
|
if (button == -1) { // drag move
|
||||||
w = window_for_id(global_state.active_drag_in_window);
|
w = window_for_id(global_state.active_drag_in_window);
|
||||||
if (w) {
|
if (w) {
|
||||||
button = currently_pressed_button();
|
button = currently_pressed_button();
|
||||||
if (button == GLFW_MOUSE_BUTTON_LEFT) {
|
if (button == global_state.active_drag_button) {
|
||||||
clamp_to_window = true;
|
clamp_to_window = true;
|
||||||
Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab;
|
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++);
|
for (window_idx = 0; window_idx < t->num_windows && t->windows[window_idx].id != w->id; window_idx++);
|
||||||
handle_move_event(w, button, modifiers, window_idx);
|
handle_move_event(w, button, modifiers, window_idx);
|
||||||
clamp_to_window = false;
|
clamp_to_window = false;
|
||||||
|
debug("handled as drag move\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (action == GLFW_RELEASE && button == GLFW_MOUSE_BUTTON_LEFT) {
|
else if (action == GLFW_RELEASE && button == global_state.active_drag_button) {
|
||||||
w = window_for_id(global_state.active_drag_in_window);
|
w = window_for_id(global_state.active_drag_in_window);
|
||||||
if (w) {
|
if (w) {
|
||||||
update_drag(true, w, true, modifiers);
|
end_drag(w);
|
||||||
|
debug("handled as drag end\n");
|
||||||
|
dispatch_possible_click(w, button, modifiers);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -637,17 +665,20 @@ mouse_event(int button, int modifiers, int action) {
|
|||||||
if (in_tab_bar) {
|
if (in_tab_bar) {
|
||||||
mouse_cursor_shape = HAND;
|
mouse_cursor_shape = HAND;
|
||||||
handle_tab_bar_mouse(button, modifiers);
|
handle_tab_bar_mouse(button, modifiers);
|
||||||
|
debug("handled by tab bar\n");
|
||||||
} else if (w) {
|
} else if (w) {
|
||||||
|
debug("grabbed: %d\n", w->render_data.screen->modes.mouse_tracking_mode != 0);
|
||||||
handle_event(w, button, modifiers, window_idx);
|
handle_event(w, button, modifiers, window_idx);
|
||||||
} else if (button == GLFW_MOUSE_BUTTON_LEFT && global_state.callback_os_window->mouse_button_pressed[button]) {
|
} else if (button == GLFW_MOUSE_BUTTON_LEFT && global_state.callback_os_window->mouse_button_pressed[button]) {
|
||||||
// initial click, clamp it to the closest window
|
// initial click, clamp it to the closest window
|
||||||
w = closest_window_for_event(&window_idx);
|
w = closest_window_for_event(&window_idx);
|
||||||
if (w) {
|
if (w) {
|
||||||
clamp_to_window = true;
|
clamp_to_window = true;
|
||||||
|
debug("grabbed: %d\n", w->render_data.screen->modes.mouse_tracking_mode != 0);
|
||||||
handle_event(w, button, modifiers, window_idx);
|
handle_event(w, button, modifiers, window_idx);
|
||||||
clamp_to_window = false;
|
clamp_to_window = false;
|
||||||
}
|
} else debug("no window for event\n");
|
||||||
}
|
} else debug("\n");
|
||||||
if (mouse_cursor_shape != old_cursor) {
|
if (mouse_cursor_shape != old_cursor) {
|
||||||
set_mouse_cursor(mouse_cursor_shape);
|
set_mouse_cursor(mouse_cursor_shape);
|
||||||
}
|
}
|
||||||
@@ -768,6 +799,17 @@ test_encode_mouse(PyObject *self UNUSED, PyObject *args) {
|
|||||||
return PyUnicode_FromStringAndSize(mouse_event_buf, sz);
|
return PyUnicode_FromStringAndSize(mouse_event_buf, sz);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PyObject*
|
||||||
|
mock_mouse_selection(PyObject *self UNUSED, PyObject *args) {
|
||||||
|
PyObject *capsule;
|
||||||
|
int button, code;
|
||||||
|
if (!PyArg_ParseTuple(args, "O!ii", &PyCapsule_Type, &capsule, &button, &code)) return NULL;
|
||||||
|
Window *w = PyCapsule_GetPointer(capsule, "Window");
|
||||||
|
if (!w) return NULL;
|
||||||
|
mouse_selection(w, code, button);
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
static PyObject*
|
static PyObject*
|
||||||
send_mock_mouse_event_to_window(PyObject *self UNUSED, PyObject *args) {
|
send_mock_mouse_event_to_window(PyObject *self UNUSED, PyObject *args) {
|
||||||
PyObject *capsule;
|
PyObject *capsule;
|
||||||
@@ -776,18 +818,26 @@ send_mock_mouse_event_to_window(PyObject *self UNUSED, PyObject *args) {
|
|||||||
if (!PyArg_ParseTuple(args, "O!iipIIpp", &PyCapsule_Type, &capsule, &button, &modifiers, &is_release, &x, &y, &clear_clicks, &in_left_half_of_cell)) return NULL;
|
if (!PyArg_ParseTuple(args, "O!iipIIpp", &PyCapsule_Type, &capsule, &button, &modifiers, &is_release, &x, &y, &clear_clicks, &in_left_half_of_cell)) return NULL;
|
||||||
Window *w = PyCapsule_GetPointer(capsule, "Window");
|
Window *w = PyCapsule_GetPointer(capsule, "Window");
|
||||||
if (!w) return NULL;
|
if (!w) return NULL;
|
||||||
if (clear_clicks) clear_click_queue(w);
|
if (clear_clicks) clear_click_queue(w, button);
|
||||||
bool mouse_cell_changed = x != w->mouse_pos.cell_x || y != w->mouse_pos.cell_y || w->mouse_pos.in_left_half_of_cell != in_left_half_of_cell;
|
bool mouse_cell_changed = x != w->mouse_pos.cell_x || y != w->mouse_pos.cell_y || w->mouse_pos.in_left_half_of_cell != in_left_half_of_cell;
|
||||||
w->mouse_pos.x = 10 * x; w->mouse_pos.y = 20 * y;
|
w->mouse_pos.x = 10 * x; w->mouse_pos.y = 20 * y;
|
||||||
w->mouse_pos.cell_x = x; w->mouse_pos.cell_y = y;
|
w->mouse_pos.cell_x = x; w->mouse_pos.cell_y = y;
|
||||||
w->mouse_pos.in_left_half_of_cell = in_left_half_of_cell;
|
w->mouse_pos.in_left_half_of_cell = in_left_half_of_cell;
|
||||||
|
static int last_button_pressed = GLFW_MOUSE_BUTTON_LEFT;
|
||||||
if (button < 0) {
|
if (button < 0) {
|
||||||
if (button == -2) do_drag_scroll(w, true);
|
if (button == -2) do_drag_scroll(w, true);
|
||||||
else if (button == -3) do_drag_scroll(w, false);
|
else if (button == -3) do_drag_scroll(w, false);
|
||||||
else handle_mouse_movement_in_kitty(w, GLFW_MOUSE_BUTTON_LEFT, mouse_cell_changed);
|
else handle_mouse_movement_in_kitty(w, last_button_pressed, mouse_cell_changed);
|
||||||
}
|
} else {
|
||||||
else {
|
if (global_state.active_drag_in_window && is_release && button == global_state.active_drag_button) {
|
||||||
handle_button_event_in_kitty(w, button, modifiers, (bool)is_release);
|
end_drag(w);
|
||||||
|
} else {
|
||||||
|
dispatch_mouse_event(w, button, is_release ? -1 : 1, modifiers, false);
|
||||||
|
if (!is_release) {
|
||||||
|
last_button_pressed = button;
|
||||||
|
add_press(w, button, modifiers);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
@@ -796,6 +846,7 @@ static PyMethodDef module_methods[] = {
|
|||||||
METHODB(send_mouse_event, METH_VARARGS),
|
METHODB(send_mouse_event, METH_VARARGS),
|
||||||
METHODB(test_encode_mouse, METH_VARARGS),
|
METHODB(test_encode_mouse, METH_VARARGS),
|
||||||
METHODB(send_mock_mouse_event_to_window, METH_VARARGS),
|
METHODB(send_mock_mouse_event_to_window, METH_VARARGS),
|
||||||
|
METHODB(mock_mouse_selection, METH_VARARGS),
|
||||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -805,6 +856,12 @@ init_mouse(PyObject *module) {
|
|||||||
PyModule_AddIntMacro(module, RELEASE);
|
PyModule_AddIntMacro(module, RELEASE);
|
||||||
PyModule_AddIntMacro(module, DRAG);
|
PyModule_AddIntMacro(module, DRAG);
|
||||||
PyModule_AddIntMacro(module, MOVE);
|
PyModule_AddIntMacro(module, MOVE);
|
||||||
|
PyModule_AddIntMacro(module, MOUSE_SELECTION_NORMAL);
|
||||||
|
PyModule_AddIntMacro(module, MOUSE_SELECTION_EXTEND);
|
||||||
|
PyModule_AddIntMacro(module, MOUSE_SELECTION_RECTANGLE);
|
||||||
|
PyModule_AddIntMacro(module, MOUSE_SELECTION_WORD);
|
||||||
|
PyModule_AddIntMacro(module, MOUSE_SELECTION_LINE);
|
||||||
|
PyModule_AddIntMacro(module, MOUSE_SELECTION_LINE_FROM_POINT);
|
||||||
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ from typing import (
|
|||||||
)
|
)
|
||||||
from urllib.parse import ParseResult, unquote, urlparse
|
from urllib.parse import ParseResult, unquote, urlparse
|
||||||
|
|
||||||
from .conf.utils import to_cmdline
|
from .conf.utils import KeyAction, to_cmdline_implementation
|
||||||
from .config import KeyAction, parse_key_action
|
|
||||||
from .constants import config_dir
|
from .constants import config_dir
|
||||||
from .guess_mime_type import guess_type
|
from .guess_mime_type import guess_type
|
||||||
|
from .options.utils import parse_key_action
|
||||||
from .types import run_once
|
from .types import run_once
|
||||||
from .typing import MatchType
|
from .typing import MatchType
|
||||||
from .utils import expandvars, log_error
|
from .utils import expandvars, log_error
|
||||||
@@ -50,7 +50,7 @@ def parse(lines: Iterable[str]) -> Generator[OpenAction, None, None]:
|
|||||||
key, rest = parts
|
key, rest = parts
|
||||||
key = key.lower()
|
key = key.lower()
|
||||||
if key == 'action':
|
if key == 'action':
|
||||||
with to_cmdline.filter_env_vars('URL', 'FILE_PATH', 'FILE', 'FRAGMENT'):
|
with to_cmdline_implementation.filter_env_vars('URL', 'FILE_PATH', 'FILE', 'FRAGMENT'):
|
||||||
x = parse_key_action(rest)
|
x = parse_key_action(rest)
|
||||||
if x is not None:
|
if x is not None:
|
||||||
actions.append(x)
|
actions.append(x)
|
||||||
|
|||||||
0
kitty/options/__init__.py
Normal file
0
kitty/options/__init__.py
Normal file
3367
kitty/options/definition.py
Normal file
3367
kitty/options/definition.py
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user