Compare commits

..

No commits in common. "bold_is_bright" and "v0.26.5" have entirely different histories.

552 changed files with 92517 additions and 108546 deletions

View File

@ -1,12 +1,12 @@
root = true root = true
[*] [*]
indent_style = space indent_style = spaces
indent_size = 4 indent_size = 4
end_of_line = lf end_of_line = lf
trim_trailing_whitespace = true trim_trailing_whitespace = true
[{Makefile,*.terminfo,*.go}] [{Makefile,*.terminfo}]
indent_style = tab indent_style = tab
# Autogenerated files with tabs below this line. # Autogenerated files with tabs below this line.

5
.gitattributes vendored
View File

@ -3,9 +3,7 @@ kitty/emoji.h linguist-generated=true
kitty/charsets.c linguist-generated=true kitty/charsets.c linguist-generated=true
kitty/key_encoding.py linguist-generated=true kitty/key_encoding.py linguist-generated=true
kitty/unicode-data.c linguist-generated=true kitty/unicode-data.c linguist-generated=true
kitty/rowcolumn-diacritics.c linguist-generated=true
kitty/rgb.py linguist-generated=true kitty/rgb.py linguist-generated=true
kitty/srgb_gamma.c 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
@ -17,9 +15,6 @@ 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
tools/wcswidth/std.go linguist-generated=true
tools/unicode_names/names.txt linguist-generated=true
*.py text diff=python *.py text diff=python
*.m text diff=objc *.m text diff=objc
*.go text diff=go

4
.github/FUNDING.yml vendored
View File

@ -1,2 +1,4 @@
custom: https://my.fsf.org/donate github: kovidgoyal
patreon: kovidgoyal
liberapay: kovidgoyal
custom: https://sw.kovidgoyal.net/kitty/support.html custom: https://sw.kovidgoyal.net/kitty/support.html

View File

@ -30,9 +30,8 @@ def install_deps():
print('Installing kitty dependencies...') print('Installing kitty dependencies...')
sys.stdout.flush() sys.stdout.flush()
if is_macos: if is_macos:
items = [x.split()[1].strip('"') for x in open('Brewfile').readlines() if x.strip().startswith('brew ')] items = (x.split()[1].strip('"') for x in open('Brewfile').readlines() if x.strip().startswith('brew '))
openssl = 'openssl' openssl = 'openssl'
items.remove('go') # already installed by ci.yml
import ssl import ssl
if ssl.OPENSSL_VERSION_INFO[0] == 1: if ssl.OPENSSL_VERSION_INFO[0] == 1:
openssl += '@1.1' openssl += '@1.1'
@ -130,12 +129,6 @@ def main():
package_kitty() package_kitty()
elif action == 'test': elif action == 'test':
test_kitty() test_kitty()
elif action == 'gofmt':
q = subprocess.check_output('gofmt -s -l tools'.split())
if q.strip():
q = '\n'.join(filter(lambda x: not x.rstrip().endswith('_generated.go'), q.decode().strip().splitlines())).strip()
if q:
raise SystemExit(q)
else: else:
raise SystemExit(f'Unknown action: {action}') raise SystemExit(f'Unknown action: {action}')

View File

@ -48,17 +48,11 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Set up Python ${{ matrix.pyver }} - name: Set up Python ${{ matrix.pyver }}
uses: actions/setup-python@v4 uses: actions/setup-python@v3
with: with:
python-version: ${{ matrix.pyver }} python-version: ${{ matrix.pyver }}
- name: Install Go
uses: actions/setup-go@v3
with:
go-version-file: go.mod
- name: Build kitty - name: Build kitty
run: python .github/workflows/ci.py build run: python .github/workflows/ci.py build
@ -77,26 +71,18 @@ jobs:
fetch-depth: 0 # needed for :commit: docs role fetch-depth: 0 # needed for :commit: docs role
- name: Test for trailing whitespace - name: Test for trailing whitespace
run: if grep -Inr '\s$' kitty kitty_tests kittens docs *.py *.asciidoc *.rst *.go .gitattributes .gitignore; then echo Trailing whitespace found, aborting.; exit 1; fi run: if grep -Inr '\s$' kitty kitty_tests kittens docs *.py *.asciidoc *.rst .gitattributes .gitignore; then echo Trailing whitespace found, aborting.; exit 1; fi
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v3
with: with:
python-version: "3.10" python-version: "3.10"
- name: Install Go
uses: actions/setup-go@v3
with:
go-version-file: go.mod
- name: Install build-only deps - name: Install build-only deps
run: python -m pip install -r docs/requirements.txt ruff mypy types-requests types-docutils run: pip install -r docs/requirements.txt flake8 mypy types-requests types-docutils
- name: Run ruff - name: Run pyflakes
run: ruff . run: python -m flake8 --count .
- name: Run gofmt
run: go version && python .github/workflows/ci.py gofmt
- name: Build kitty package - name: Build kitty package
run: python .github/workflows/ci.py package run: python .github/workflows/ci.py package
@ -104,14 +90,8 @@ jobs:
- name: Build kitty - name: Build kitty
run: python setup.py build --debug run: python setup.py build --debug
- name: Build static kitten
run: python setup.py build-static-binaries
- name: Run mypy - name: Run mypy
run: which python && python -m mypy --version && ./test.py mypy run: ./test.py mypy
- name: Run go vet
run: go version && go vet ./...
- name: Build man page - name: Build man page
run: make FAIL_WARN=1 man run: make FAIL_WARN=1 man
@ -129,15 +109,10 @@ jobs:
KITTY_BUNDLE: 1 KITTY_BUNDLE: 1
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v3 uses: actions/checkout@master
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install Go
uses: actions/setup-go@v3
with:
go-version-file: go.mod
- name: Build kitty - name: Build kitty
run: which python3 && python3 .github/workflows/ci.py build run: which python3 && python3 .github/workflows/ci.py build
@ -149,20 +124,15 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v3 uses: actions/checkout@master
with: with:
fetch-depth: 0 # needed for :commit: docs role fetch-depth: 0 # needed for :commit: docs role
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v3
with: with:
python-version: "3.10" python-version: "3.10"
- name: Install Go
uses: actions/setup-go@v3
with:
go-version-file: go.mod
- name: Build kitty - name: Build kitty
run: python3 .github/workflows/ci.py build run: python3 .github/workflows/ci.py build

View File

@ -22,7 +22,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
@ -30,14 +29,9 @@ jobs:
# a pull request then we can checkout the head. # a pull request then we can checkout the head.
fetch-depth: 2 fetch-depth: 2
- name: Install Go
uses: actions/setup-go@v3
with:
go-version-file: go.mod
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v1
with: with:
languages: python, c languages: python, c
setup-python-dependencies: false setup-python-dependencies: false
@ -46,4 +40,4 @@ jobs:
run: python3 .github/workflows/ci.py build run: python3 .github/workflows/ci.py build
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v1

7
.gitignore vendored
View File

@ -1,7 +1,6 @@
*.so *.so
*.pyc *.pyc
*.pyo *.pyo
*.bin
*_stub.pyi *_stub.pyi
*_generated.go *_generated.go
*_generated.h *_generated.h
@ -10,16 +9,16 @@
/build/ /build/
/linux-package/ /linux-package/
/kitty.app/ /kitty.app/
/compile_commands.json
/link_commands.json
/glad/out/ /glad/out/
/kitty/launcher/kitt* /kitty/launcher/kitty*
/*.dSYM/ /*.dSYM/
__pycache__/ __pycache__/
/glfw/wayland-*-client-protocol.[ch] /glfw/wayland-*-client-protocol.[ch]
/docs/_build/ /docs/_build/
/docs/generated/ /docs/generated/
/.mypy_cache /.mypy_cache
/.ruff_cache
.DS_Store .DS_Store
.cache
bypy/b bypy/b
bypy/virtual-machines.conf bypy/virtual-machines.conf

View File

@ -5,4 +5,3 @@ brew "python"
brew "imagemagick" brew "imagemagick"
brew "harfbuzz" brew "harfbuzz"
brew "sphinx-doc" brew "sphinx-doc"
brew "go"

View File

@ -7,11 +7,6 @@ When reporting a bug, provide full details of your environment, that means, at
a minimum, kitty version, OS and OS version, kitty config (ideally a minimal a minimum, kitty version, OS and OS version, kitty config (ideally a minimal
config to reproduce the issue with). config to reproduce the issue with).
Note that bugs and feature requests are often closed quickly as they are either
fixed or deemed wontfix/invalid. In my experience, this is the only scaleable way to
manage a bug tracker. Feel free to continue to post to a closed bug report
if you would like to discuss the issue further. Being closed does not mean you
will not get any more responses.
### Contributing code ### Contributing code

View File

@ -11,8 +11,9 @@ import sys
import tempfile import tempfile
from contextlib import suppress from contextlib import suppress
from bypy.constants import LIBDIR, PREFIX, PYTHON, ismacos, worker_env from bypy.constants import (
from bypy.constants import SRC as KITTY_DIR LIBDIR, PREFIX, PYTHON, SRC as KITTY_DIR, ismacos, worker_env
)
from bypy.utils import run_shell, walk from bypy.utils import run_shell, walk
@ -62,31 +63,20 @@ def build_frozen_launcher(extra_include_dirs):
def run_tests(kitty_exe): def run_tests(kitty_exe):
with tempfile.TemporaryDirectory() as tdir: with tempfile.TemporaryDirectory() as tdir:
uenv = { env = {
'KITTY_CONFIG_DIRECTORY': os.path.join(tdir, 'conf'), 'KITTY_CONFIG_DIRECTORY': os.path.join(tdir, 'conf'),
'KITTY_CACHE_DIRECTORY': os.path.join(tdir, 'cache') 'KITTY_CACHE_DIRECTORY': os.path.join(tdir, 'cache')
} }
[os.mkdir(x) for x in uenv.values()] [os.mkdir(x) for x in env.values()]
env = os.environ.copy() cmd = [kitty_exe, '+runpy', 'from kitty_tests.main import run_tests; run_tests()']
env.update(uenv)
cmd = [kitty_exe, '+runpy', 'from kitty_tests.main import run_tests; run_tests(report_env=True)']
print(*map(shlex.quote, cmd), flush=True) print(*map(shlex.quote, cmd), flush=True)
if subprocess.call(cmd, env=env, cwd=build_frozen_launcher.writeable_src_dir) != 0: if subprocess.call(cmd, env=env) != 0:
print('Checking of kitty build failed, in directory:', build_frozen_launcher.writeable_src_dir, file=sys.stderr) print('Checking of kitty build failed', file=sys.stderr)
os.chdir(os.path.dirname(kitty_exe)) os.chdir(os.path.dirname(kitty_exe))
run_shell() run_shell()
raise SystemExit('Checking of kitty build failed') raise SystemExit('Checking of kitty build failed')
def build_frozen_tools(kitty_exe):
cmd = SETUP_CMD + ['--prefix', os.path.dirname(kitty_exe)] + ['build-frozen-tools']
if run(*cmd, cwd=build_frozen_launcher.writeable_src_dir) != 0:
print('Building of frozen kitten failed', file=sys.stderr)
os.chdir(KITTY_DIR)
run_shell()
raise SystemExit('Building of kitten launcher failed')
def sanitize_source_folder(path: str) -> None: def sanitize_source_folder(path: str) -> None:
for q in walk(path): for q in walk(path):
if os.path.splitext(q)[1] not in ('.py', '.glsl', '.ttf', '.otf'): if os.path.splitext(q)[1] not in ('.py', '.glsl', '.ttf', '.otf'):
@ -106,8 +96,6 @@ def build_c_extensions(ext_dir, args):
cmd = SETUP_CMD + ['macos-freeze' if ismacos else 'linux-freeze'] cmd = SETUP_CMD + ['macos-freeze' if ismacos else 'linux-freeze']
if args.dont_strip: if args.dont_strip:
cmd.append('--debug') cmd.append('--debug')
if args.extra_program_data:
cmd.append(f'--vcs-rev={args.extra_program_data}')
dest = kitty_constants['appname'] + ('.app' if ismacos else '') dest = kitty_constants['appname'] + ('.app' if ismacos else '')
dest = build_frozen_launcher.prefix = os.path.join(ext_dir, dest) dest = build_frozen_launcher.prefix = os.path.join(ext_dir, dest)
cmd += ['--prefix', dest, '--full'] cmd += ['--prefix', dest, '--full']

View File

@ -1,3 +1,3 @@
image 'https://cloud-images.ubuntu.com/releases/bionic/release/ubuntu-18.04-server-cloudimg-{}.img' image 'https://cloud-images.ubuntu.com/releases/bionic/release/ubuntu-18.04-server-cloudimg-{}.img'
deps 'bison flex libxcursor-dev libxrandr-dev libxi-dev libxinerama-dev libgl1-mesa-dev libx11-xcb-dev libxcb-xkb-dev libfontconfig1-dev libdbus-1-dev' deps 'bison flex libxcursor-dev libxrandr-dev libxi-dev libxinerama-dev libgl1-mesa-dev libxcb-xkb-dev libfontconfig1-dev libdbus-1-dev'

View File

@ -10,8 +10,12 @@ import subprocess
import tarfile import tarfile
import time import time
from bypy.constants import OUTPUT_DIR, PREFIX, is64bit, python_major_minor_version from bypy.constants import (
from bypy.freeze import extract_extension_modules, freeze_python, path_to_freeze_dir OUTPUT_DIR, PREFIX, is64bit, python_major_minor_version
)
from bypy.freeze import (
extract_extension_modules, freeze_python, path_to_freeze_dir
)
from bypy.utils import get_dll_path, mkdtemp, py_compile, walk from bypy.utils import get_dll_path, mkdtemp, py_compile, walk
j = os.path.join j = os.path.join
@ -234,12 +238,10 @@ def main():
files = find_binaries(env) files = find_binaries(env)
fix_permissions(files) fix_permissions(files)
add_ca_certs(env) add_ca_certs(env)
kitty_exe = os.path.join(env.base, 'bin', 'kitty')
iv['build_frozen_tools'](kitty_exe)
if not args.dont_strip: if not args.dont_strip:
strip_binaries(files) strip_binaries(files)
if not args.skip_tests: if not args.skip_tests:
iv['run_tests'](kitty_exe) iv['run_tests'](os.path.join(env.base, 'bin', 'kitty'))
create_tarfile(env, args.compression_level) create_tarfile(env, args.compression_level)

View File

@ -1,4 +1,4 @@
# Requires installation of XCode 10.3 and go 1.19 and Python 3 and # Requires installation of XCode 10.3 and Python 3 and
# python3 -m pip install certifi # python3 -m pip install certifi
vm_name 'macos-kitty' vm_name 'macos-kitty'

View File

@ -13,9 +13,16 @@ import tempfile
import zipfile import zipfile
from bypy.constants import PREFIX, PYTHON, SW, python_major_minor_version from bypy.constants import PREFIX, PYTHON, SW, python_major_minor_version
from bypy.freeze import extract_extension_modules, freeze_python, path_to_freeze_dir from bypy.freeze import (
from bypy.macos_sign import codesign, create_entitlements_file, make_certificate_useable, notarize_app, verify_signature extract_extension_modules, freeze_python, path_to_freeze_dir
from bypy.utils import current_dir, mkdtemp, py_compile, run_shell, timeit, walk )
from bypy.macos_sign import (
codesign, create_entitlements_file, make_certificate_useable, notarize_app,
verify_signature
)
from bypy.utils import (
current_dir, mkdtemp, py_compile, run_shell, timeit, walk
)
iv = globals()['init_env'] iv = globals()['init_env']
kitty_constants = iv['kitty_constants'] kitty_constants = iv['kitty_constants']
@ -106,9 +113,6 @@ def do_sign(app_dir):
codesign(fw) codesign(fw)
items = set(os.listdir('.')) - fw items = set(os.listdir('.')) - fw
codesign(expand_dirs(items)) codesign(expand_dirs(items))
# Sign kitten
with current_dir('MacOS'):
codesign('kitten')
# Now sign the main app # Now sign the main app
codesign(app_dir) codesign(app_dir)
@ -167,7 +171,6 @@ class Freeze(object):
self.add_misc_libraries() self.add_misc_libraries()
self.freeze_python() self.freeze_python()
self.add_ca_certs() self.add_ca_certs()
self.build_frozen_tools()
if not self.dont_strip: if not self.dont_strip:
self.strip_files() self.strip_files()
if not self.skip_tests: if not self.skip_tests:
@ -375,10 +378,6 @@ class Freeze(object):
if f.endswith('.so') or f.endswith('.dylib'): if f.endswith('.so') or f.endswith('.dylib'):
self.fix_dependencies_in_lib(f) self.fix_dependencies_in_lib(f)
@flush
def build_frozen_tools(self):
iv['build_frozen_tools'](join(self.contents_dir, 'MacOS', 'kitty'))
@flush @flush
def add_site_packages(self): def add_site_packages(self):
print('\nAdding site-packages') print('\nAdding site-packages')

View File

@ -1 +1 @@
to_vm_excludes '/build /dist /kitty/launcher/kitty* /.build-cache /tags __pycache__ /*_commands.json *.so *.pyd *.pyc *_generated.go' to_vm_excludes '/build /dist /kitty/launcher/kitty /.build-cache /tags __pycache__ /*_commands.json *.so *.pyd *.pyc'

View File

@ -2,8 +2,8 @@
{ {
"name": "zlib", "name": "zlib",
"unix": { "unix": {
"filename": "zlib-1.2.13.tar.xz", "filename": "zlib-1.2.11.tar.xz",
"hash": "sha256:d14c38e313afc35a9a8760dadf26042f51ea0f5d154b0630a31da0540107fb98", "hash": "sha256:4ff941449631ace0d4d203e3483be9dbc9da454084111f97ea0a2114e19bf066",
"urls": ["https://zlib.net/{filename}"] "urls": ["https://zlib.net/{filename}"]
} }
}, },
@ -163,6 +163,15 @@
} }
}, },
{
"name": "pygments",
"unix": {
"filename": "Pygments-2.11.2.tar.gz",
"hash": "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a",
"urls": ["pypi"]
}
},
{ {
"name": "libpng", "name": "libpng",
"unix": { "unix": {

View File

@ -2,20 +2,29 @@
import subprocess import subprocess
ignored = [] files_to_exclude = '''\
for line in subprocess.check_output(['git', 'status', '--ignored', '--porcelain']).decode().splitlines(): kitty/wcwidth-std.h
if line.startswith('!! '): kitty/charsets.c
ignored.append(line[3:]) kitty/unicode-data.c
files_to_exclude = '\n'.join(ignored) kitty/key_encoding.py
kitty/rgb.py
cp = subprocess.run(['git', 'check-attr', 'linguist-generated', '--stdin'], kitty/gl.h
check=True, stdout=subprocess.PIPE, input=subprocess.check_output([ 'git', 'ls-files'])) kitty/gl-wrapper.h
for line in cp.stdout.decode().splitlines(): kitty/gl-wrapper.c
if line.endswith(' true'): kitty/glfw-wrapper.h
files_to_exclude += '\n' + line.split(':')[0] kitty/glfw-wrapper.c
kitty/emoji.h
kittens/unicode_input/names.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([
'cloc', '--exclude-list-file', '/dev/stdin', 'kitty', 'kittens', 'tools', 'kitty_tests', 'docs', 'cloc', '--exclude-list-file', '/dev/stdin', 'kitty', 'kittens'
], stdin=subprocess.PIPE) ], stdin=subprocess.PIPE)
p.communicate(files_to_exclude.encode('utf-8')) p.communicate(files_to_exclude.encode('utf-8'))
raise SystemExit(p.wait()) raise SystemExit(p.wait())

View File

@ -27,11 +27,6 @@ Browse scrollback in less :sc:`show_scrollback`
Browse last cmd output :sc:`show_last_command_output` (see :ref:`shell_integration`) Browse last cmd output :sc:`show_last_command_output` (see :ref:`shell_integration`)
========================= ======================= ========================= =======================
The scroll actions only take effect when the terminal is in the main screen.
When the alternate screen is active (for example when using a full screen
program like an editor) the key events are instead passed to program running in the
terminal.
Tabs Tabs
~~~~~~~~~~~ ~~~~~~~~~~~
@ -58,7 +53,6 @@ Action Shortcut
New window :sc:`new_window` (also :kbd:`⌘+↩` on macOS) New window :sc:`new_window` (also :kbd:`⌘+↩` on macOS)
New OS window :sc:`new_os_window` (also :kbd:`⌘+n` on macOS) New OS window :sc:`new_os_window` (also :kbd:`⌘+n` on macOS)
Close window :sc:`close_window` (also :kbd:`⇧+⌘+d` on macOS) Close window :sc:`close_window` (also :kbd:`⇧+⌘+d` on macOS)
Resize window :sc:`start_resizing_window` (also :kbd:`⌘+r` on macOS)
Next window :sc:`next_window` Next window :sc:`next_window`
Previous window :sc:`previous_window` Previous window :sc:`previous_window`
Move window forward :sc:`move_window_forward` Move window forward :sc:`move_window_forward`

View File

@ -22,8 +22,7 @@ simply re-run the command.
.. warning:: .. warning::
**Do not** copy the kitty binary out of the installation folder. If you want **Do not** copy the kitty binary out of the installation folder. If you want
to add it to your :envvar:`PATH`, create a symlink in :file:`~/.local/bin` or to add it to your :envvar:`PATH`, create a symlink in :file:`~/.local/bin` or
:file:`/usr/bin` or wherever. You should create a symlink for the :file:`kitten` :file:`/usr/bin` or wherever.
binary as well.
Manually installing Manually installing
@ -31,7 +30,7 @@ Manually installing
If something goes wrong or you simply do not want to run the installer, you can If something goes wrong or you simply do not want to run the installer, you can
manually download and install |kitty| from the `GitHub releases page manually download and install |kitty| from the `GitHub releases page
<https://gitea.rexy712.xyz/KittyPatch/kitty/releases>`__. If you are on macOS, download <https://github.com/kovidgoyal/kitty/releases>`__. If you are on macOS, download
the :file:`.dmg` and install as normal. If you are on Linux, download the the :file:`.dmg` and install as normal. If you are on Linux, download the
tarball and extract it into a directory. The |kitty| executable will be in the tarball and extract it into a directory. The |kitty| executable will be in the
:file:`bin` sub-directory. :file:`bin` sub-directory.
@ -47,9 +46,9 @@ particular desktop, but it should work for most major desktop environments.
.. code-block:: sh .. code-block:: sh
# Create symbolic links to add kitty and kitten to PATH (assuming ~/.local/bin is in # Create a symbolic link to add kitty to PATH (assuming ~/.local/bin is in
# your system-wide PATH) # your system-wide PATH)
ln -sf ~/.local/kitty.app/bin/kitty ~/.local/kitty.app/bin/kitten ~/.local/bin/ ln -s ~/.local/kitty.app/bin/kitty ~/.local/bin/
# Place the kitty.desktop file somewhere it can be found by the OS # Place the kitty.desktop file somewhere it can be found by the OS
cp ~/.local/kitty.app/share/applications/kitty.desktop ~/.local/share/applications/ cp ~/.local/kitty.app/share/applications/kitty.desktop ~/.local/share/applications/
# If you want to open text files and images in kitty via your file manager also add the kitty-open.desktop file # If you want to open text files and images in kitty via your file manager also add the kitty-open.desktop file

View File

@ -1,9 +1,9 @@
Build from source Build from source
================== ==================
.. image:: https://gitea.rexy712.xyz/KittyPatch/kitty/workflows/CI/badge.svg .. image:: https://github.com/kovidgoyal/kitty/workflows/CI/badge.svg
:alt: Build status :alt: Build status
:target: https://gitea.rexy712.xyz/KittyPatch/kitty/actions?query=workflow%3ACI :target: https://github.com/kovidgoyal/kitty/actions?query=workflow%3ACI
.. highlight:: sh .. highlight:: sh
@ -39,13 +39,13 @@ Run-time dependencies:
* ``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 display uncommon image formats in the terminal) * ``ImageMagick`` (optional, needed to use the ``kitty +kitten icat`` tool to display images in the terminal)
* ``pygments`` (optional, needed for syntax highlighting in ``kitty +kitten diff``)
Build-time dependencies: Build-time dependencies:
* ``gcc`` or ``clang`` * ``gcc`` or ``clang``
* ``go`` >= _build_go_version (see :file:`go.mod` for go packages used during building)
* ``pkg-config`` * ``pkg-config``
* For building on Linux in addition to the above dependencies you might also * For building on Linux in addition to the above dependencies you might also
need to install the following packages, if they are not already installed by need to install the following packages, if they are not already installed by
@ -61,7 +61,6 @@ Build-time dependencies:
- ``libfontconfig-dev`` - ``libfontconfig-dev``
- ``libx11-xcb-dev`` - ``libx11-xcb-dev``
- ``liblcms2-dev`` - ``liblcms2-dev``
- ``libssl-dev``
- ``libpython3-dev`` - ``libpython3-dev``
- ``librsync-dev`` - ``librsync-dev``
@ -71,7 +70,7 @@ Install and run from source
.. code-block:: sh .. code-block:: sh
git clone https://gitea.rexy712.xyz/KittyPatch/kitty && cd kitty git clone https://github.com/kovidgoyal/kitty && cd kitty
Now build the native code parts of |kitty| with the following command:: Now build the native code parts of |kitty| with the following command::
@ -106,7 +105,7 @@ dependencies 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
`bypy framework <https://gitea.rexy712.xyz/KittyPatch/bypy>`__ however, that is `bypy framework <https://github.com/kovidgoyal/bypy>`__ however, that is
designed to run on Linux and is not for the faint of heart. designed to run on Linux and is not for the faint of heart.
.. note:: .. note::
@ -155,7 +154,7 @@ Notes for Linux/macOS packagers
---------------------------------- ----------------------------------
The released |kitty| source code is available as a `tarball`_ from The released |kitty| source code is available as a `tarball`_ from
`the GitHub releases page <https://gitea.rexy712.xyz/KittyPatch/kitty/releases>`__. `the GitHub releases page <https://github.com/kovidgoyal/kitty/releases>`__.
While |kitty| does use Python, it is not a traditional Python package, so please While |kitty| does use Python, it is not a traditional Python package, so please
do not install it in site-packages. do not install it in site-packages.

View File

@ -35,189 +35,6 @@ mouse anywhere in the current command to move the cursor there. See
Detailed list of changes Detailed list of changes
------------------------------------- -------------------------------------
0.28.2 [future]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- A new escape code ``<ESC>[22J`` that moves the current contents of the screen into the scrollback before clearing it
- unicode_input kitten: Fix a regression in 0.28.0 that caused the order of recent and favorites entries to not be respected (:iss:`6214`)
- unicode_input kitten: Fix a regression in 0.28.0 that caused editing of favorites to sometimes hang
- clipboard kitten: Fix a bug causing the last MIME type available on the clipboard not being recognized when pasting
- Fix regression in 0.28.0 causing color fringing when rendering in transparent windows on light backgrounds (:iss:`6209`)
- show_key kitten: In kitty mode show the actual bytes sent by the terminal rather than a re-encoding of the parsed key event
- hints kitten: Fix a regression in 0.28.0 that broke using sub-groups in regexp captures (:iss:`6228`)
- hints kitten: Fix a regression in 0.28.0 that broke using lookahead/lookbehind in regexp captures (:iss:`6265`)
- diff kitten: Fix a regression in 0.28.0 that broke using relative paths as arguments to the kitten (:iss:`6325`)
- Fix re-using the image id of an animated image for a still image causing a crash (:iss:`6244`)
- kitty +open: Ask for permission before executing script files that are not marked as executable. This prevents accidental execution
of script files via MIME type association from programs that unconditionally "open" attachments/downloaded files
- edit-in-kitty: Fix running edit-in-kitty with elevated privileges to edit a restricted file not working (:disc:`6245`)
- ssh kitten: Fix a regression in 0.28.0 that caused interrupt during setup to not be handled gracefully (:iss:`6254`)
0.28.1 [2023-04-21]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Fix a regression in the previous release that broke the remote file kitten (:iss:`6186`)
- Fix a regression in the previous release that broke handling of some keyboard shortcuts in some kittens on some keyboard layouts (:iss:`6189`)
- Fix a regression in the previous release that broke usage of custom themes (:iss:`6191`)
0.28.0 [2023-04-15]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- **Text rendering change**: Use sRGB correct linear gamma blending for nicer font
rendering and better color accuracy with transparent windows.
See the option :opt:`text_composition_strategy` for details.
The obsolete :opt:`macos_thicken_font` will make the font too thick and needs to be removed manually
if it is configured. (:pull:`5969`)
- icat kitten: Support display of images inside tmux >= 3.3 (:pull:`5664`)
- Graphics protocol: Add support for displaying images inside programs that do not support the protocol such as vim and tmux (:pull:`5664`)
- diff kitten: Add support for selecting multi-line text with the mouse
- Fix a regression in 0.27.0 that broke ``kitty @ set-font-size 0`` (:iss:`5992`)
- launch: When using ``--cwd=current`` for a remote system support running non shell commands as well (:disc:`5987`)
- When changing the cursor color via escape codes or remote control to a fixed color, do not reset cursor_text_color (:iss:`5994`)
- Input Method Extensions: Fix incorrect rendering of IME in-progress and committed text in some situations (:pull:`6049`, :pull:`6087`)
- Linux: Reduce minimum required OpenGL version from 3.3 to 3.1 + extensions (:iss:`2790`)
- Fix a regression that broke drawing of images below cell backgrounds (:iss:`6061`)
- macOS: Fix the window buttons not being hidden after exiting the traditional full screen (:iss:`6009`)
- When reloading configuration, also reload custom MIME types from :file:`mime.types` config file (:pull:`6012`)
- launch: Allow specifying the state (full screen/maximized/minimized) for newly created OS Windows (:iss:`6026`)
- Sessions: Allow specifying the OS window state via the ``os_window_state`` directive (:iss:`5863`)
- macOS: Display the newly created OS window in specified state to avoid or reduce the window transition animations (:pull:`6035`)
- macOS: Fix the maximized window not taking up full space when the title bar is hidden or when :opt:`resize_in_steps` is configured (:iss:`6021`)
- Linux: A new option :opt:`linux_bell_theme` to control which sound theme is used for the bell sound (:pull:`4858`)
- ssh kitten: Change the syntax of glob patterns slightly to match common usage
elsewhere. Now the syntax is the same as "extendedglob" in most shells.
- hints kitten: Allow copying matches to named buffers (:disc:`6073`)
- Fix overlay windows not inheriting the per-window padding and margin settings
of their parents (:iss:`6063`)
- Wayland KDE: Fix selecting in un-focused OS window not working correctly (:iss:`6095`)
- Linux X11: Fix a crash if the X server requests clipboard data after we have relinquished the clipboard (:iss:`5650`)
- Allow stopping of URL detection at newlines via :opt:`url_excluded_characters` (:iss:`6122`)
- Linux Wayland: Fix animated images not being animated continuously (:iss:`6126`)
- Keyboard input: Fix text not being reported as unicode codepoints for multi-byte characters in the kitty keyboard protocol (:iss:`6167`)
0.27.1 [2023-02-07]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Fix :opt:`modify_font` not working for strikethrough position (:iss:`5946`)
- Fix a regression causing the ``edit-in-kitty`` command not working if :file:`kitten` is not added
to PATH (:iss:`5956`)
- icat kitten: Fix a regression that broke display of animated GIFs over SSH (:iss:`5958`)
- Wayland GNOME: Fix for ibus not working when using XWayland (:iss:`5967`)
- Fix regression in previous release that caused incorrect entries in terminfo for modifier+F3 key combinations (:pull:`5970`)
- Bring back the deprecated and removed ``kitty +complete`` and delegate it to :program:`kitten` for backward compatibility (:pull:`5977`)
- Bump the version of Go needed to build kitty to ``1.20`` so we can use the Go stdlib ecdh package for crypto.
0.27.0 [2023-01-31]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- A new statically compiled, standalone executable, ``kitten`` (written in Go)
that can be used on all UNIX-like servers for remote control (``kitten @``),
viewing images (``kitten icat``), manipulating the clipboard (``kitten clipboard``), etc.
- :doc:`clipboard kitten </kittens/clipboard>`: Allow copying arbitrary data types to/from the clipboard, not just plain text
- Speed up the ``kitty @`` executable by ~10x reducing the time for typical
remote control commands from ~50ms to ~5ms
- icat kitten: Speed up by using POSIX shared memory when possible to transfer
image data to the terminal. Also support common image formats
GIF/PNG/JPEG/WEBP/TIFF/BMP out of the box without needing ImageMagick.
- Option :opt:`show_hyperlink_targets` to show the target of terminal hyperlinks when hovering over them with the mouse (:pull:`5830`)
- Keyboard protocol: Remove ``CSI R`` from the allowed encodings of the :kbd:`F3` key as it conflicts with the *Cursor Position Report* escape code (:disc:`5813`)
- Allow using the cwd of the original process for :option:`launch --cwd` (:iss:`5672`)
- Session files: Expand environment variables (:disc:`5917`)
- Pass key events mapped to scroll actions to the program running in the terminal when the terminal is in alternate screen mode (:iss:`5839`)
- Implement :ref:`edit-in-kitty <edit_file>` using the new ``kitten`` static executable (:iss:`5546`, :iss:`5630`)
- Add an option :opt:`background_tint_gaps` to control background image tinting for window gaps (:iss:`5596`)
- A new option :opt:`undercurl_style` to control the rendering of undercurls (:pull:`5883`)
- Bash integration: Fix ``clone-in-kitty`` not working on bash >= 5.2 if environment variable values contain newlines or other special characters (:iss:`5629`)
- A new :ac:`sleep` action useful in combine based mappings to make kitty sleep before executing the next action
- Wayland GNOME: Workaround for latest mutter release breaking full screen for semi-transparent kitty windows (:iss:`5677`)
- A new option :opt:`tab_title_max_length` to limit the length of tab (:iss:`5718`)
- When drawing the tab bar have the default left and right margins drawn in a color matching the neighboring tab (:iss:`5719`)
- When using the :code:`include` directive in :file:`kitty.conf` make the environment variable :envvar:`KITTY_OS` available for OS specific config
- Wayland: Fix signal handling not working with some GPU drivers (:iss:`4636`)
- Remote control: When matching windows allow using negative id numbers to match recently created windows (:iss:`5753`)
- ZSH Integration: Bind :kbd:`alt+left` and :kbd:`alt+right` to move by word if not already bound. This mimics the default bindings in Terminal.app (:iss:`5793`)
- macOS: Allow to customize :sc:`Hide <hide_macos_app>`, :sc:`Hide Others <hide_macos_other_apps>`, :sc:`Minimize <minimize_macos_window>`, and :sc:`Quit <quit>` global menu shortcuts. Note that :opt:`clear_all_shortcuts` will remove these shortcuts now (:iss:`948`)
- When a multi-key sequence does not match any action, send all key events to the child program (:pull:`5841`)
- broadcast kitten: Allow pressing a key to stop echoing of input into the broadcast window itself (:disc:`5868`)
- When reporting unused activity in a window, ignore activity that occurs soon after a window resize (:iss:`5881`)
- Fix using :opt:`cursor` = ``none`` not working on text that has reverse video (:iss:`5897`)
- Fix ssh kitten not working on FreeBSD (:iss:`5928`)
- macOS: Export kitty selected text to the system for use with services that accept it (patch by Sertaç Ö. Yıldız)
0.26.5 [2022-11-07] 0.26.5 [2022-11-07]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -239,7 +56,6 @@ Detailed list of changes
- Remote control: When matching window by `state:focused` and no window currently has keyboard focus, match the window belonging to the OS window that was last focused (:iss:`5602`) - Remote control: When matching window by `state:focused` and no window currently has keyboard focus, match the window belonging to the OS window that was last focused (:iss:`5602`)
0.26.4 [2022-10-17] 0.26.4 [2022-10-17]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -312,7 +128,6 @@ Detailed list of changes
code execution if the user clicked on a notification popup from a malicious code execution if the user clicked on a notification popup from a malicious
source. Thanks to Carter Sande for discovering this vulnerability. source. Thanks to Carter Sande for discovering this vulnerability.
0.26.1 [2022-08-30] 0.26.1 [2022-08-30]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -322,7 +137,6 @@ Detailed list of changes
- Allow specifying a title when using the :ac:`set_tab_title` action (:iss:`5441`) - Allow specifying a title when using the :ac:`set_tab_title` action (:iss:`5441`)
0.26.0 [2022-08-29] 0.26.0 [2022-08-29]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -1809,7 +1623,6 @@ Detailed list of changes
- Fix :option:`--title` not being applied at window creation time (:iss:`2570`) - Fix :option:`--title` not being applied at window creation time (:iss:`2570`)
0.17.2 [2020-03-29] 0.17.2 [2020-03-29]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -1994,7 +1807,6 @@ Detailed list of changes
- When windows are semi-transparent and all contain graphics, correctly render - When windows are semi-transparent and all contain graphics, correctly render
them. (:iss:`2310`) them. (:iss:`2310`)
0.15.1 [2019-12-21] 0.15.1 [2019-12-21]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -2103,7 +1915,6 @@ Detailed list of changes
- Use selection foreground color for underlines as well (:iss:`1982`) - Use selection foreground color for underlines as well (:iss:`1982`)
0.14.4 [2019-08-31] 0.14.4 [2019-08-31]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -2410,7 +2221,6 @@ Detailed list of changes
- Mouse selection: When extending by word, fix extending selection to non-word - Mouse selection: When extending by word, fix extending selection to non-word
characters not working well (:iss:`1616`) characters not working well (:iss:`1616`)
0.13.3 [2019-01-19] 0.13.3 [2019-01-19]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -2504,7 +2314,6 @@ Detailed list of changes
- Fix resizing window smaller and then restoring causing some wrapped lines to not - Fix resizing window smaller and then restoring causing some wrapped lines to not
be properly unwrapped (:iss:`1206`) be properly unwrapped (:iss:`1206`)
0.13.0 [2018-12-05] 0.13.0 [2018-12-05]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -2602,7 +2411,6 @@ Detailed list of changes
- Fix hover detection of URLs not working when hovering over the first colon - Fix hover detection of URLs not working when hovering over the first colon
and slash characters in short URLs (:iss:`1201`) and slash characters in short URLs (:iss:`1201`)
0.12.3 [2018-09-29] 0.12.3 [2018-09-29]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -2679,7 +2487,6 @@ Detailed list of changes
- Fix using :opt:`focus_follows_mouse` causing text selection with the - Fix using :opt:`focus_follows_mouse` causing text selection with the
mouse to malfunction when using multiple kitty windows (:iss:`1002`) mouse to malfunction when using multiple kitty windows (:iss:`1002`)
0.12.1 [2018-09-08] 0.12.1 [2018-09-08]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -2723,7 +2530,6 @@ Detailed list of changes
- macOS: Diff kitten: Fix syntax highlighting not working because of - macOS: Diff kitten: Fix syntax highlighting not working because of
a bug in the 0.12.0 macOS package a bug in the 0.12.0 macOS package
0.12.0 [2018-09-01] 0.12.0 [2018-09-01]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -3234,7 +3040,6 @@ Detailed list of changes
- Fix a crash when getting the contents of the scrollback buffer as text - Fix a crash when getting the contents of the scrollback buffer as text
0.8.1 [2018-03-09] 0.8.1 [2018-03-09]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -3312,7 +3117,6 @@ Detailed list of changes
- Browsing the scrollback buffer now happens in an overlay window instead of a - Browsing the scrollback buffer now happens in an overlay window instead of a
new window/tab. new window/tab.
0.7.1 [2018-01-31] 0.7.1 [2018-01-31]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -1,160 +0,0 @@
Copying all data types to the clipboard
==============================================
There already exists an escape code to allow terminal programs to
read/write plain text data from the system clipboard, *OSC 52*.
kitty introduces a more advanced protocol that supports:
* Copy arbitrary data including images, rich text documents, etc.
* Allow terminals to ask the user for permission to access the clipboard and
report permission denied
The escape code is *OSC 5522*, an extension of *OSC 52*. The basic format
of the escape code is::
<OSC>5522;metadata;payload<ST>
Here, *metadata* is a colon separated list of key-value pairs and payload is
base64 encoded data. :code:`OSC` is :code:`<ESC>[`.
:code:`ST` is the string terminator, :code:`<ESC>\\`.
Reading data from the system clipboard
----------------------------------------
To read data from the system clipboard, the escape code is::
<OSC>5522;type=read;<base 64 encoded space separated list of mime types to read><ST>
For example, to read plain text and PNG data, the payload would be::
text/plain image/png
encoded as base64. To read from the primary selection instead of the
clipboard, add the key ``loc=primary`` to the metadata section.
To get the list of MIME types available on the clipboard the payload must be
just a period (``.``), encoded as base64.
The terminal emulator will reply with a sequence of escape codes of the form::
<OSC>5522;type=read:status=OK<ST>
<OSC>5522;type=read:status=DATA:mime=<base 64 encoded mime type>;<base64 encoded data><ST>
<OSC>5522;type=read:status=DATA:mime=<base 64 encoded mime type>;<base64 encoded data><ST>
.
.
.
<OSC>5522;type=read:status=DONE<ST>
Here, the ``status=DATA`` packets deliver the data (as base64 encoded bytes)
associated with each MIME type. The terminal emulator should chunk up the data
for an individual type. A recommended size for each chunk is 4096 bytes. All
the chunks for a given type must be transmitted sequentially and only once they
are done the chunks for the next type, if any, should be sent. The end of data
is indicated by a ``status=DONE`` packet.
If an error occurs, instead of the opening ``status=OK`` packet the terminal
must send a ``status=ERRORCODE`` packet. The error code must be one of:
``status=ENOSYS``
Sent if the requested clipboard type is not available. For example, primary
selection is not available on all systems and ``loc=primary`` was used.
``status=EPERM``
Sent if permission to read from the clipboard was denied by the system or
the user.
``status=EBUSY``
Sent if there is some temporary problem, such as multiple clients in a
multiplexer trying to access the clipboard simultaneously.
Terminals should ask the user for permission before allowing a read request.
However, if a read request only wishes to list the available data types on the
clipboard, it should be allowed without a permission prompt. This is so that
the user is not presented with a double permission prompt for reading the
available MIME types and then reading the actual data.
Writing data to the system clipboard
----------------------------------------
To write data to the system clipboard, the terminal programs sends the
following sequence of packets::
<OSC>5522;type=write<ST>
<OSC>5522;type=wdata:mime=<base64 encoded mime type>;<base 64 encoded chunk of data for this type><ST>
<OSC>5522;type=wdata:mime=<base64 encoded mime type>;<base 64 encoded chunk of data for this type><ST>
.
.
.
<OSC>5522;type=wdata<ST>
The final packet with no mime and no data indicates end of transmission. The
data for every MIME type should be split into chunks of no more than 4096
bytes. All the chunks for a given MIME type must be sent sequentially, before
sending chunks for the next MIME type. After the transmission is complete, the
terminal replies with a single packet indicating success::
<OSC>5522;type=write:status=DONE<ST>
If an error occurs the terminal can, at any time, send an error packet of the
form::
<OSC>5522;type=write:status=ERRORCODE<ST>
Here ``ERRORCODE`` must be one of:
``status=EIO``
An I/O error occurred while processing the data
``status=EINVAL``
One of the packets was invalid, usually because of invalid base64 encoding.
``status=ENOSYS``
The client asked to write to the primary selection with (``loc=primary``) and that is not
available on the system
``status=EPERM``
Sent if permission to write to the clipboard was denied by the system or
the user.
``status=EBUSY``
Sent if there is some temporary problem, such as multiple clients in a
multiplexer trying to access the clipboard simultaneously.
Once an error occurs, the terminal must ignore all further OSC 5522 write related packets until it
sees the start of a new write with a ``type=write`` packet.
The client can send to the primary selection instead of the clipboard by adding
``loc=primary`` to the initial ``type=write`` packet.
Finally, clients have the ability to *alias* MIME types when sending data to
the clipboard. To do that, the client must send a ``type=walias`` packet of the
form::
<OSC>5522;type=walias;mime=<base64 encoded target MIME type>;<base64 encoded, space separated list of aliases><ST>
The effect of an alias is that the system clipboard will make available all the
aliased MIME types, with the same data as was transmitted for the target MIME
type. This saves bandwidth, allowing the client to only transmit one copy of
the data, but create multiple references to it in the system clipboard. Alias
packets can be sent anytime after the initial write packet and before the end
of data packet.
Support for terminal multiplexers
------------------------------------
Since this protocol involves two way communication between the terminal
emulator and the client program, multiplexers need a way to know which window
to send responses from the terminal to. In order to make this possible, the
metadata portion of this escape code includes an optional ``id`` field. If
present the terminal emulator must send it back unchanged with every response.
Valid ids must include only characters from the set: ``[a-zA-Z0-9-_+.]``. Any
other characters must be stripped out from the id by the terminal emulator
before retransmitting it.
Note that when using a terminal multiplexer it is possible for two different
programs to tread on each others clipboard requests. This is fundamentally
unavoidable since the system clipboard is a single global shared resource.
However, there is an additional complication where responses form this protocol
could get lost if, for instance, multiple write requests are received
simultaneously. It is up to well designed multiplexers to ensure that only a
single request is in flight at a time. The multiplexer can abort requests by
sending back the ``EBUSY`` error code indicating some other window is trying
to access the clipboard.

View File

@ -18,7 +18,9 @@ from typing import Any, Callable, Dict, Iterable, List, Tuple
from docutils import nodes from docutils import nodes
from docutils.parsers.rst.roles import set_classes from docutils.parsers.rst.roles import set_classes
from pygments.lexer import RegexLexer, bygroups # type: ignore from pygments.lexer import RegexLexer, bygroups # type: ignore
from pygments.token import Comment, Keyword, Literal, Name, Number, String, Whitespace # type: ignore from pygments.token import ( # type: ignore
Comment, Keyword, Literal, Name, Number, String, Whitespace
)
from sphinx import addnodes, version_info from sphinx import addnodes, version_info
from sphinx.util.logging import getLogger from sphinx.util.logging import getLogger
@ -33,8 +35,8 @@ from kitty.constants import str_version, website_url # noqa
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
project = 'kitty' project = 'kitty'
copyright = time.strftime('%Y, Kovid Goyal, KittyPatch') copyright = time.strftime('%Y, Kovid Goyal')
author = 'Kovid Goyal, KittyPatch' author = 'Kovid Goyal'
building_man_pages = 'man' in sys.argv building_man_pages = 'man' in sys.argv
# The short X.Y version # The short X.Y version
@ -65,10 +67,6 @@ extensions = [
# URL for OpenGraph tags # URL for OpenGraph tags
ogp_site_url = website_url() ogp_site_url = website_url()
# OGP needs a PNG image because of: https://github.com/wpilibsuite/sphinxext-opengraph/issues/96
ogp_social_cards = {
'image': '../logo/kitty.png'
}
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ['_templates']
@ -100,23 +98,14 @@ exclude_patterns = [
rst_prolog = ''' rst_prolog = '''
.. |kitty| replace:: *kitty* .. |kitty| replace:: *kitty*
.. |version| replace:: VERSION .. |version| replace:: VERSION
.. _tarball: https://gitea.rexy712.xyz/KittyPatch/kitty/releases/download/vVERSION/kitty-VERSION.tar.xz .. _tarball: https://github.com/kovidgoyal/kitty/releases/download/vVERSION/kitty-VERSION.tar.xz
.. role:: italic .. role:: italic
'''.replace('VERSION', str_version) '''.replace('VERSION', str_version)
smartquotes_action = 'qe' # educate quotes and ellipses but not dashes smartquotes_action = 'qe' # educate quotes and ellipses but not dashes
def go_version(go_mod_path: str) -> str: # {{{
with open(go_mod_path) as f:
for line in f:
if line.startswith('go '):
return line.strip().split()[1]
raise SystemExit(f'No Go version in {go_mod_path}')
# }}}
string_replacements = { string_replacements = {
'_kitty_install_cmd': 'curl -L https://sw.kovidgoyal.net/kitty/installer.sh | sh /dev/stdin', '_kitty_install_cmd': 'curl -L https://sw.kovidgoyal.net/kitty/installer.sh | sh /dev/stdin',
'_build_go_version': go_version('../go.mod'),
} }
@ -215,7 +204,7 @@ def commit_role(
f'GitHub commit id "{text}" not recognized.', line=lineno) f'GitHub commit id "{text}" not recognized.', line=lineno)
prb = inliner.problematic(rawtext, rawtext, msg) prb = inliner.problematic(rawtext, rawtext, msg)
return [prb], [msg] return [prb], [msg]
url = f'https://gitea.rexy712.xyz/KittyPatch/kitty/commit/{commit_id}' url = f'https://github.com/kovidgoyal/kitty/commit/{commit_id}'
set_classes(options) set_classes(options)
short_id = subprocess.check_output( short_id = subprocess.check_output(
f'git rev-list --max-count=1 --abbrev-commit --skip=# {commit_id}'.split()).decode('utf-8').strip() f'git rev-list --max-count=1 --abbrev-commit --skip=# {commit_id}'.split()).decode('utf-8').strip()
@ -226,16 +215,15 @@ def commit_role(
# 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 kittens.ssh.main import copy_message, option_text
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
from kittens.ssh.copy import option_text
from kittens.ssh.options.definition import copy_message
with open('generated/ssh-copy.rst', 'w') as f: with open('generated/ssh-copy.rst', 'w') as f:
f.write(option_spec_as_rst( f.write(option_spec_as_rst(
appname='copy', ospec=option_text, heading_char='^', appname='copy', ospec=option_text, heading_char='^',
usage='file-or-dir-to-copy ...', message=copy_message usage='file-or-dir-to-copy ...', message=copy_message
)) ))
del sys.modules['kittens.ssh.main']
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='_',
@ -267,7 +255,6 @@ if you specify a program-to-run you can use the special placeholder
p('.. program::', 'kitty @', func.name) p('.. program::', 'kitty @', func.name)
p('\n\n' + as_rst(*cli_params_for(func))) p('\n\n' + as_rst(*cli_params_for(func)))
from kittens.runner import get_kitten_cli_docs from kittens.runner import get_kitten_cli_docs
for kitten in all_kitten_names: for kitten in all_kitten_names:
data = get_kitten_cli_docs(kitten) data = get_kitten_cli_docs(kitten)
if data: if data:
@ -276,8 +263,7 @@ if you specify a program-to-run you can use the special placeholder
p('.. program::', 'kitty +kitten', kitten) p('.. program::', 'kitty +kitten', kitten)
p('\nSource code for', kitten) p('\nSource code for', kitten)
p('-' * 72) p('-' * 72)
scurl = f'https://github.com/kovidgoyal/kitty/tree/master/kittens/{kitten}' p(f'\nThe source code for this kitten is `available on GitHub <https://github.com/kovidgoyal/kitty/tree/master/kittens/{kitten}>`_.')
p(f'\nThe source code for this kitten is `available on GitHub <{scurl}>`_.')
p('\nCommand Line Interface') p('\nCommand Line Interface')
p('-' * 72) p('-' * 72)
p('\n\n' + option_spec_as_rst( p('\n\n' + option_spec_as_rst(
@ -288,7 +274,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 RemoteCommand, all_command_names, command_for_name from kitty.rc.base import (
RemoteCommand, all_command_names, command_for_name
)
field_pat = re.compile(r'\s*([^:]+?)\s*:\s*(.+)') field_pat = re.compile(r'\s*([^:]+?)\s*:\s*(.+)')
def format_cmd(p: Callable[..., None], name: str, cmd: RemoteCommand) -> None: def format_cmd(p: Callable[..., None], name: str, cmd: RemoteCommand) -> None:
@ -514,7 +502,7 @@ def write_conf_docs(app: Any, all_kitten_names: Iterable[str]) -> None:
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(definition.as_conf(commented=True)) text = '\n'.join(definition.as_conf())
print(text, file=f) print(text, file=f)
from kitty.options.definition import definition from kitty.options.definition import definition
@ -522,9 +510,9 @@ def write_conf_docs(app: Any, all_kitten_names: Iterable[str]) -> None:
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:
defn = get_kitten_conf_docs(kitten) definition = get_kitten_conf_docs(kitten)
if defn is not None: if definition:
generate_default_config(defn, f'kitten-{kitten}') generate_default_config(definition, f'kitten-{kitten}')
from kitty.actions import as_rst from kitty.actions import as_rst
with open('generated/actions.rst', 'w', encoding='utf-8') as f: with open('generated/actions.rst', 'w', encoding='utf-8') as f:

View File

@ -31,9 +31,7 @@ You can include secondary config files via the :code:`include` directive. If
you use a relative path for :code:`include`, it is resolved with respect to the you use a relative path for :code:`include`, it is resolved with respect to the
location of the current config file. Note that environment variables are location of the current config file. Note that environment variables are
expanded, so :code:`${USER}.conf` becomes :file:`name.conf` if expanded, so :code:`${USER}.conf` becomes :file:`name.conf` if
:code:`USER=name`. A special environment variable :envvar:`KITTY_OS` is available, :code:`USER=name`. Also, you can use :code:`globinclude` to include files
to detect the operating system. It is ``linux``, ``macos`` or ``bsd``.
Also, you can use :code:`globinclude` to include files
matching a shell glob pattern and :code:`envinclude` to include configuration matching a shell glob pattern and :code:`envinclude` to include configuration
from environment variables. For example:: from environment variables. For example::
@ -68,11 +66,6 @@ Sample kitty.conf
pre-existing :file:`kitty.conf`, then that will be used instead, delete it to pre-existing :file:`kitty.conf`, then that will be used instead, delete it to
see the sample file. see the sample file.
A default configuration file can also be generated by running::
kitty +runpy 'from kitty.config import *; print(commented_out_default_config())'
This will print the commented out default config file to :file:`STDOUT`.
All mappable actions All mappable actions
------------------------ ------------------------

View File

@ -50,8 +50,7 @@ and the terminal emulator should hold off displaying it. A value of ``1`` means
the notification is done, and should be displayed. You can specify the title or the notification is done, and should be displayed. You can specify the title or
body multiple times and the terminal emulator will concatenate them, thereby body multiple times and the terminal emulator will concatenate them, thereby
allowing arbitrarily long text (terminal emulators are free to impose a sensible allowing arbitrarily long text (terminal emulators are free to impose a sensible
limit to avoid Denial-of-Service attacks). The size of the payload must be no limit to avoid Denial-of-Service attacks).
longer than ``2048`` bytes, *before being encoded*.
Both the ``title`` and ``body`` payloads must be either UTF-8 encoded plain Both the ``title`` and ``body`` payloads must be either UTF-8 encoded plain
text with no embedded escape codes, or UTF-8 text that is Base64 encoded, in text with no embedded escape codes, or UTF-8 text that is Base64 encoded, in

View File

@ -27,145 +27,96 @@ turned off for specific symbols using :opt:`narrow_symbols`.
Using a color theme with a background color does not work well in vim? Using a color theme with a background color does not work well in vim?
----------------------------------------------------------------------- -----------------------------------------------------------------------
Sadly, vim has very poor out-of-the-box detection for modern terminal features. First make sure you have not changed the :envvar:`TERM` environment variable, it
Furthermore, it `recently broke detection even more <https://github.com/vim/vim/issues/11729>`__. should be ``xterm-kitty``. vim uses *background color erase* even if the
It kind of, but not really, supports terminfo, except it overrides it with its own hard-coded terminfo file does not contain the ``bce`` capability. This is a bug in vim. You
values when it feels like it. Worst of all, it has no ability to detect modern can work around it by adding the following to your vimrc::
features not present in terminfo, at all, even security sensitive ones like
bracketed paste.
Thankfully, probably as a consequence of this lack of detection, vim allows users to
configure these low level details. So, to make vim work well with any modern
terminal, including kitty, add the following to your :file:`~/.vimrc`.
.. code-block:: vim
" Mouse support
set mouse=a
set ttymouse=sgr
set balloonevalterm
" Styled and colored underline support
let &t_AU = "\e[58:5:%dm"
let &t_8u = "\e[58:2:%lu:%lu:%lum"
let &t_Us = "\e[4:2m"
let &t_Cs = "\e[4:3m"
let &t_ds = "\e[4:4m"
let &t_Ds = "\e[4:5m"
let &t_Ce = "\e[4:0m"
" Strikethrough
let &t_Ts = "\e[9m"
let &t_Te = "\e[29m"
" Truecolor support
let &t_8f = "\e[38:2:%lu:%lu:%lum"
let &t_8b = "\e[48:2:%lu:%lu:%lum"
let &t_RF = "\e]10;?\e\\"
let &t_RB = "\e]11;?\e\\"
" Bracketed paste
let &t_BE = "\e[?2004h"
let &t_BD = "\e[?2004l"
let &t_PS = "\e[200~"
let &t_PE = "\e[201~"
" Cursor control
let &t_RC = "\e[?12$p"
let &t_SH = "\e[%d q"
let &t_RS = "\eP$q q\e\\"
let &t_SI = "\e[5 q"
let &t_SR = "\e[3 q"
let &t_EI = "\e[1 q"
let &t_VS = "\e[?12l"
" Focus tracking
let &t_fe = "\e[?1004h"
let &t_fd = "\e[?1004l"
execute "set <FocusGained>=\<Esc>[I"
execute "set <FocusLost>=\<Esc>[O"
" Window title
let &t_ST = "\e[22;2t"
let &t_RT = "\e[23;2t"
" vim hardcodes background color erase even if the terminfo file does
" not contain bce. This causes incorrect background rendering when
" using a color theme with a background color in terminals such as
" kitty that do not support background color erase.
let &t_ut='' let &t_ut=''
These settings must be placed **before** setting the ``colorscheme``. It is See :doc:`here <deccara>` for why |kitty| does not support background color
also important that the value of the vim ``term`` variable is not changed erase.
after these settings.
I get errors about the terminal being unknown or opening the terminal failing or functional keys like arrow keys don't work?
-------------------------------------------------------------------------------------------------------------------------------
These issues all have the same root cause: the kitty terminfo files not being I get errors about the terminal being unknown or opening the terminal failing when SSHing into a different computer?
available. The most common way this happens is SSHing into a computer that does -----------------------------------------------------------------------------------------------------------------------
not have the kitty terminfo files. The simplest fix for that is running::
This happens because the |kitty| terminfo files are not available on the server.
You can ssh in using the following command which will automatically copy the
terminfo files to the server::
kitty +kitten ssh myserver kitty +kitten ssh myserver
It will automatically copy over the terminfo files and also magically enable
:doc:`shell integration </shell-integration>` on the remote machine.
This :doc:`ssh kitten <kittens/ssh>` takes all the same command line arguments This :doc:`ssh kitten <kittens/ssh>` takes all the same command line arguments
as :program:`ssh`, you can alias it to something small in your shell's rc files as :program:`ssh`, you can alias it to something small in your shell's rc files
to avoid having to type it each time:: to avoid having to type it each time::
alias s="kitty +kitten ssh" alias s="kitty +kitten ssh"
If this does not work, see :ref:`manual_terminfo_copy` for alternative ways to If the ssh kitten fails, use the following one-liner instead (it is slower as it
get the kitty terminfo files onto a remote computer. needs to ssh into the server twice, but will work with most servers)::
The next most common reason for this is if you are running commands as root infocmp -a xterm-kitty | ssh myserver tic -x -o \~/.terminfo /dev/stdin
using :program:`sudo` or :program:`su`. These programs often filter the
:envvar:`TERMINFO` environment variable which is what points to the kitty
terminfo files.
First, make sure the :envvar:`TERM` is set to ``xterm-kitty`` in the sudo If you are behind a proxy (like Balabit) that prevents this, or :program:`tic`
environment. By default, it should be automatically copied over. comes with macOS that does not support reading from STDIN, you must redirect the
first command to a file, copy that to the server and run :program:`tic`
manually. If you connect to a server, embedded or Android system that doesn't
have :program:`tic`, copy over your local file terminfo to the other system as
:file:`~/.terminfo/x/xterm-kitty`.
If you are using a well maintained Linux distribution, it will have a Really, the correct solution for this is to convince the OpenSSH maintainers to
``kitty-terminfo`` package that you can simply install to make the kitty have :program:`ssh` do this automatically, if possible, when connecting to a
terminfo files available system-wide. Then the problem will no longer occur. server, so that all terminals work transparently.
Alternately, you can configure :program:`sudo` to preserve :envvar:`TERMINFO` If the server is running FreeBSD, or another system that relies on termcap
by running ``sudo visudo`` and adding the following line:: rather than terminfo, you will need to convert the terminfo file on your local
machine by running (on local machine with |kitty|)::
infocmp -CrT0 xterm-kitty
The output of this command is the termcap description, which should be appended
to :file:`/usr/share/misc/termcap` on the remote server. Then run the following
command to apply your change (on the server)::
cap_mkdb /usr/share/misc/termcap
Keys such as arrow keys, backspace, delete, home/end, etc. do not work when using su or sudo?
-------------------------------------------------------------------------------------------------
Make sure the :envvar:`TERM` environment variable, is ``xterm-kitty``. And
either the :envvar:`TERMINFO` environment variable points to a directory
containing :file:`x/xterm-kitty` or that file is under :file:`~/.terminfo/x/`.
For macOS, you may also need to put that file under :file:`~/.terminfo/78/`::
mkdir -p ~/.terminfo/{78,x}
ln -snf ../x/xterm-kitty ~/.terminfo/78/xterm-kitty
tic -x -o ~/.terminfo "$KITTY_INSTALLATION_DIR/terminfo/kitty.terminfo"
Note that :program:`sudo` might remove :envvar:`TERMINFO`. Then setting it at
the shell prompt can be too late, because command line editing may not be
reinitialized. In that case you can either ask :program:`sudo` to set it or if
that is not supported, insert an :program:`env` command before starting the
shell, or, if not possible, after sudo start another shell providing the right
terminfo path::
sudo … TERMINFO=$HOME/.terminfo bash -i
sudo … env TERMINFO=$HOME/.terminfo bash -i
TERMINFO=/home/ORIGINALUSER/.terminfo exec bash -i
You can configure :program:`sudo` to preserve :envvar:`TERMINFO` by running
``sudo visudo`` and adding the following line::
Defaults env_keep += "TERM TERMINFO" Defaults env_keep += "TERM TERMINFO"
If none of these are suitable for you, you can run sudo as follows::
sudo TERMINFO="$TERMINFO" -s -H
This will start a new root shell with the correct :envvar:`TERMINFO` value from your
current environment copied over.
If you have double width characters in your prompt, you may also need to If you have double width characters in your prompt, you may also need to
explicitly set a UTF-8 locale, like:: explicitly set a UTF-8 locale, like::
export LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 export LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
I cannot use the key combination X in program Y?
-------------------------------------------------------
First, run::
kitty +kitten show_key -m kitty
Press the key combination X. If the kitten reports the key press
that means kitty is correctly sending the key press to terminal programs.
You need to report the issue to the developer of the terminal program. Most
likely they have not added support for :doc:`/keyboard-protocol`.
If the kitten does not report it, it means that the key is bound to some action
in kitty. You can unbind it in :file:`kitty.conf` with:
.. code-block:: conf
map X no_op
Here X is the keys you press on the keyboard. So for example
:kbd:`ctrl+shift+1`.
How do I change the colors in a running kitty instance? How do I change the colors in a running kitty instance?
------------------------------------------------------------ ------------------------------------------------------------
@ -260,9 +211,9 @@ fonts to be freely resizable, so it does not support bitmapped fonts.
symbols from it automatically, and you can tell it to do so explicitly in symbols from it automatically, and you can tell it to do so explicitly in
case it doesn't with the :opt:`symbol_map` directive:: case it doesn't with the :opt:`symbol_map` directive::
# Nerd Fonts v2.3.3 # Nerd Fonts v2.2.2
symbol_map U+23FB-U+23FE,U+2665,U+26A1,U+2B58,U+E000-U+E00A,U+E0A0-U+E0A3,U+E0B0-U+E0D4,U+E200-U+E2A9,U+E300-U+E3E3,U+E5FA-U+E6AA,U+E700-U+E7C5,U+EA60-U+EBEB,U+F000-U+F2E0,U+F300-U+F32F,U+F400-U+F4A9,U+F500-U+F8FF,U+F0001-U+F1AF0 Symbols Nerd Font Mono symbol_map U+23FB-U+23FE,U+2665,U+26A1,U+2B58,U+E000-U+E00A,U+E0A0-U+E0A3,U+E0B0-U+E0C8,U+E0CA,U+E0CC-U+E0D2,U+E0D4,U+E200-U+E2A9,U+E300-U+E3E3,U+E5FA-U+E634,U+E700-U+E7C5,U+EA60-U+EBEB,U+F000-U+F2E0,U+F300-U+F32F,U+F400-U+F4A9,U+F500-U+F8FF Symbols Nerd Font Mono
Those Unicode symbols beyond the ``E000-F8FF`` Unicode private use area are Those Unicode symbols beyond the ``E000-F8FF`` Unicode private use area are
not included. not included.
@ -314,7 +265,7 @@ I do not like the kitty icon!
There are many alternate icons available, click on an icon to visit its There are many alternate icons available, click on an icon to visit its
homepage: homepage:
.. image:: https://github.com/k0nserv/kitty-icon/raw/main/kitty.iconset/icon_256x256.png .. image:: https://github.com/k0nserv/kitty-icon/raw/main/icon_512x512.png
:target: https://github.com/k0nserv/kitty-icon :target: https://github.com/k0nserv/kitty-icon
:width: 256 :width: 256
@ -338,14 +289,6 @@ homepage:
:target: https://github.com/samholmes/whiskers :target: https://github.com/samholmes/whiskers
:width: 256 :width: 256
.. image:: https://github.com/eccentric-j/eccentric-icons/raw/main/icons/kitty-terminal/2d/kitty-preview.png
:target: https://github.com/eccentric-j/eccentric-icons
:width: 256
.. image:: https://github.com/eccentric-j/eccentric-icons/raw/main/icons/kitty-terminal/3d/kitty-preview.png
:target: https://github.com/eccentric-j/eccentric-icons
:width: 256
On macOS you can put :file:`kitty.app.icns` or :file:`kitty.app.png` in the On macOS you can put :file:`kitty.app.icns` or :file:`kitty.app.png` in the
:ref:`kitty configuration directory <confloc>`, and this icon will be applied :ref:`kitty configuration directory <confloc>`, and this icon will be applied
automatically at startup. Unfortunately, Apple's Dock does not change its automatically at startup. Unfortunately, Apple's Dock does not change its

View File

@ -45,12 +45,6 @@ Glossary
hyperlink, based on the type of link and its URL. See also `Hyperlinks in terminal hyperlink, based on the type of link and its URL. See also `Hyperlinks in terminal
emulators <https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda>`__. emulators <https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda>`__.
kittens
Small, independent statically compiled command line programs that are designed to run
inside kitty windows and provide it with lots of powerful and flexible
features such as viewing images, connecting conveniently to remote
computers, transferring files, inputting unicode characters, etc.
.. _env_vars: .. _env_vars:
Environment variables Environment variables
@ -218,8 +212,3 @@ Variables that kitty sets when running child programs
Set when enabling :ref:`shell_integration` with :program:`bash`, allowing Set when enabling :ref:`shell_integration` with :program:`bash`, allowing
:program:`bash` to automatically load the integration script. :program:`bash` to automatically load the integration script.
.. envvar:: KITTY_OS
Set when using the include directive in kitty.conf. Can take values:
``linux``, ``macos``, ``bsd``.

View File

@ -28,14 +28,15 @@ alpha-blending and text over graphics.
Some programs and libraries that use the kitty graphics protocol: Some programs and libraries that use the kitty graphics protocol:
* `termpdf.py <https://github.com/dsanson/termpdf.py>`_ - a terminal PDF/DJVU/CBR viewer * `termpdf.py <https://github.com/dsanson/termpdf.py>`_ - a terminal PDF/DJVU/CBR viewer
* `ranger <https://github.com/ranger/ranger>`_ - a terminal file manager, with image previews * `ranger <https://github.com/ranger/ranger>`_ - a terminal file manager, with
image previews, see this `PR <https://github.com/ranger/ranger/pull/1077>`_
* :doc:`kitty-diff <kittens/diff>` - a side-by-side terminal diff program with support for images * :doc:`kitty-diff <kittens/diff>` - a side-by-side terminal diff program with support for images
* `tpix <https://github.com/jesvedberg/tpix>`_ - a statically compiled binary that can be used to display images and easily installed on remote servers without root access * `tpix <https://github.com/jesvedberg/tpix>`_ - a statically compiled binary that can be used to display images and easily installed on remote servers without root access
* `mpv <https://github.com/mpv-player/mpv/commit/874e28f4a41a916bb567a882063dd2589e9234e1>`_ - A video player that can play videos in the terminal
* `pixcat <https://github.com/mirukana/pixcat>`_ - a third party CLI and python library that wraps the graphics protocol * `pixcat <https://github.com/mirukana/pixcat>`_ - a third party CLI and python library that wraps the graphics protocol
* `neofetch <https://github.com/dylanaraps/neofetch>`_ - A command line system * `neofetch <https://github.com/dylanaraps/neofetch>`_ - A command line system
information tool information tool
* `viu <https://github.com/atanunq/viu>`_ - a terminal image viewer * `viu <https://github.com/atanunq/viu>`_ - a terminal image viewer
* `glkitty <https://github.com/michaeljclark/glkitty>`_ - C library to draw OpenGL shaders in the terminal with a glgears demo
* `ctx.graphics <https://ctx.graphics/>`_ - Library for drawing graphics * `ctx.graphics <https://ctx.graphics/>`_ - Library for drawing graphics
* `timg <https://github.com/hzeller/timg>`_ - a terminal image and video viewer * `timg <https://github.com/hzeller/timg>`_ - a terminal image and video viewer
* `notcurses <https://github.com/dankamongmen/notcurses>`_ - C library for terminal graphics with bindings for C++, Rust and Python * `notcurses <https://github.com/dankamongmen/notcurses>`_ - C library for terminal graphics with bindings for C++, Rust and Python
@ -43,8 +44,6 @@ Some programs and libraries that use the kitty graphics protocol:
* `chafa <https://github.com/hpjansson/chafa>`_ - a terminal image viewer * `chafa <https://github.com/hpjansson/chafa>`_ - a terminal image viewer
* `hologram.nvim <https://github.com/edluffy/hologram.nvim>`_ - view images inside nvim * `hologram.nvim <https://github.com/edluffy/hologram.nvim>`_ - view images inside nvim
* `term-image <https://github.com/AnonymouX47/term-image>`_ - A Python library, CLI and TUI to display and browse images in the terminal * `term-image <https://github.com/AnonymouX47/term-image>`_ - A Python library, CLI and TUI to display and browse images in the terminal
* `glkitty <https://github.com/michaeljclark/glkitty>`_ - C library to draw OpenGL shaders in the terminal with a glgears demo
* `twitch-tui <https://github.com/Xithrius/twitch-tui>`_ - Twitch chat in the terminal
Other terminals that have implemented the graphics protocol: Other terminals that have implemented the graphics protocol:
@ -58,8 +57,7 @@ Getting the window size
In order to know what size of images to display and how to position them, the In order to know what size of images to display and how to position them, the
client must be able to get the window size in pixels and the number of cells client must be able to get the window size in pixels and the number of cells
per row and column. The cell width is then simply the window size divided by the per row and column. This can be done by using the ``TIOCGWINSZ`` ioctl. Some
number of rows. This can be done by using the ``TIOCGWINSZ`` ioctl. Some
code to demonstrate its use code to demonstrate its use
.. tab:: C .. tab:: C
@ -90,31 +88,6 @@ code to demonstrate its use
'number of rows: {} number of columns: {}' 'number of rows: {} number of columns: {}'
'screen width: {} screen height: {}').format(*buf)) 'screen width: {} screen height: {}').format(*buf))
.. tab:: Go
.. code-block:: go
import "golang.org/x/sys/unix"
fd, err := unix.Open(fd, unix.O_NOCTTY|unix.O_CLOEXEC|unix.O_NDELAY|unix.O_RDWR, 0666)
sz, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ)
fmt.Println("rows: %v columns: %v width: %v height %v", sz.Row, sz.Col, sz.Xpixel, sz.Ypixel)
.. tab:: Bash
.. code-block:: sh
#!/bin/bash
# This uses the kitten standalone binary from kitty to get the pixel sizes
# since we cant do IOCTLs directly. Fortunately, kitten is a static exe
# pre-built for every Unix like OS under the sun.
builtin read -r rows cols < <(command stty size)
IFS=x builtin read -r width height < <(command kitten icat --print-window-size); builtin unset IFS
builtin echo "number of rows: $rows number of columns: $cols screen width: $width screen height: $height"
Note that some terminals return ``0`` for the width and height values. Such Note that some terminals return ``0`` for the width and height values. Such
terminals should be modified to return the correct values. Examples of terminals should be modified to return the correct values. Examples of
terminals that return correct values: ``kitty, xterm`` terminals that return correct values: ``kitty, xterm``
@ -128,40 +101,15 @@ kitty.
A minimal example A minimal example
------------------ ------------------
Some minimal code to display PNG images in kitty, using the most basic Some minimal python code to display PNG images in kitty, using the most basic
features of the graphics protocol: features of the graphics protocol:
.. tab:: Bash
.. code-block:: sh
#!/bin/bash
transmit_png() {
data=$(base64 "$1")
data="${data//[[:space:]]}"
builtin local pos=0
builtin local chunk_size=4096
while [ $pos -lt ${#data} ]; do
builtin printf "\e_G"
[ $pos = "0" ] && printf "a=T,f=100,"
builtin local chunk="${data:$pos:$chunk_size}"
pos=$(($pos+$chunk_size))
[ $pos -lt ${#data} ] && builtin printf "m=1"
[ ${#chunk} -gt 0 ] && builtin printf ";%s" "${chunk}"
builtin printf "\e\\"
done
}
transmit_png "$1"
.. tab:: Python
.. code-block:: python .. code-block:: python
#!/usr/bin/python
import sys import sys
from base64 import standard_b64encode from base64 import standard_b64encode
def serialize_gr_command(**cmd): def serialize_gr_command(**cmd):
payload = cmd.pop('payload', None) payload = cmd.pop('payload', None)
cmd = ','.join(f'{k}={v}' for k, v in cmd.items()) cmd = ','.join(f'{k}={v}' for k, v in cmd.items())
@ -174,6 +122,7 @@ features of the graphics protocol:
w(b'\033\\') w(b'\033\\')
return b''.join(ans) return b''.join(ans)
def write_chunked(**cmd): def write_chunked(**cmd):
data = standard_b64encode(cmd.pop('data')) data = standard_b64encode(cmd.pop('data'))
while data: while data:
@ -184,15 +133,15 @@ features of the graphics protocol:
sys.stdout.flush() sys.stdout.flush()
cmd.clear() cmd.clear()
with open(sys.argv[-1], 'rb') as f: with open(sys.argv[-1], 'rb') as f:
write_chunked(a='T', f=100, data=f.read()) write_chunked(a='T', f=100, data=f.read())
Save this script as :file:`send-png`, then you can use it to display any PNG Save this script as :file:`png.py`, then you can use it to display any PNG
file in kitty as:: file in kitty as::
chmod +x send-png python png.py file.png
./send-png file.png
The graphics escape code The graphics escape code
@ -346,13 +295,12 @@ sequence of escape codes to the terminal emulator::
<ESC>_Gm=0;<encoded pixel data last chunk><ESC>\ <ESC>_Gm=0;<encoded pixel data last chunk><ESC>\
Note that only the first escape code needs to have the full set of control Note that only the first escape code needs to have the full set of control
codes such as width, height, format, etc. Subsequent chunks **must** have only codes such as width, height, format etc. Subsequent chunks **must** have
the ``m`` and optionally ``q`` keys. When sending animation frame data, subsequent only the ``m`` key. The client **must** finish sending all chunks for a single image
chunks **must** also specify the ``a=f`` key. The client **must** finish sending before sending any other graphics related escape codes. Note that the cursor
all chunks for a single image before sending any other graphics related escape position used to display the image **must** be the position when the final chunk is
codes. Note that the cursor position used to display the image **must** be the received. Finally, terminals must not display anything, until the entire sequence is
position when the final chunk is received. Finally, terminals must not display received and validated.
anything, until the entire sequence is received and validated.
Querying support and available transmission mediums Querying support and available transmission mediums
@ -488,132 +436,6 @@ z-index and the same id, then the behavior is undefined.
Support for the C=1 cursor movement policy Support for the C=1 cursor movement policy
.. _graphics_unicode_placeholders:
Unicode placeholders
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionadded:: 0.28.0
Support for image display via Unicode placeholders
You can also use a special Unicode character ``U+10EEEE`` as a placeholder for
an image. This approach is less flexible, but it allows using images inside
any host application that supports Unicode and foreground colors (tmux, vim, weechat, etc.)
and has a way to pass escape codes through to the underlying terminal.
The central idea is that we use a single *Private Use* Unicode character as a
*placeholder* to indicate to the terminal that an image is supposed to be
displayed at that cell. Since this character is just normal text, Unicode aware
application will move it around as needed when they redraw their screens,
thereby automatically moving the displayed image as well, even though they know
nothing about the graphics protocol. So an image is first created using the
normal graphics protocol escape codes (albeit in quiet mode (``q=2``) so that there are
no responses from the terminal that could confuse the host application). Then,
the actual image is displayed by getting the host application to emit normal
text consisting of ``U+10EEEE`` and various diacritics (Unicode combining
characters) and colors.
To use it, first create an image as you would normally with the graphics
protocol with (``q=2``), but do not create a placement for it, that is, do not
display it. Then, create a *virtual image placement* by specifying ``U=1`` and
the desired number of lines and columns::
<ESC>_Ga=p,U=1,i=<image_id>,c=<columns>,r=<rows><ESC>\
The creation of the placement need not be a separate escape code, it can be
combined with ``a=T`` to both transmit and create the virtual placement with a
single code.
The image will eventually be fit to the specified rectangle, its aspect ratio
preserved. Finally, the image can be actually displayed by using the
placeholder character, encoding the image ID in its foreground color. The row
and column values are specified with diacritics listed in
:download:`rowcolumn-diacritics.txt <../rowcolumn-diacritics.txt>`. For
example, here is how you can print a ``2x2`` placeholder for image ID ``42``:
.. code-block:: sh
printf "\e[38;5;42m\U10EEEE\U0305\U0305\U10EEEE\U0305\U030D\e[39m\n"
printf "\e[38;5;42m\U10EEEE\U030D\U0305\U10EEEE\U030D\U030D\e[39m\n"
Here, ``U+305`` is the diacritic corresponding to the number ``0``
and ``U+30D`` corresponds to ``1``. So these two commands create the following
``2x2`` placeholder:
========== ==========
(0, 0) (1, 0)
(1, 0) (1, 1)
========== ==========
This will cause the image with ID ``42`` to be displayed in a ``2x2`` grid.
Ideally, you would print out as many cells as the number of rows and columns
specified when creating the virtual placement, but in case of a mismatch only
part of the image will be displayed.
By using only the foreground color for image ID you are limited to either 8-bit IDs in 256 color
mode or 24-bit IDs in true color mode. Since IDs are in a global namespace
there can easily be collisions. If you need more bits for the image
ID, you can specify the most significant byte via a third diacritic. For
example, this is the placeholder for the image ID ``33554474 = 42 + (2 << 24)``:
.. code-block:: sh
printf "\e[38;5;42m\U10EEEE\U0305\U0305\U030E\U10EEEE\U0305\U030D\U030E\n"
printf "\e[38;5;42m\U10EEEE\U030D\U0305\U030E\U10EEEE\U030D\U030D\U030E\n"
Here, ``U+30E`` is the diacritic corresponding to the number ``2``.
You can also specify a placement ID using the underline color (if it's omitted
or zero, the terminal may choose any virtual placement of the given image). The
background color is interpreted as the background color, visible if the image is
transparent. Other text attributes are reserved for future use.
Row, column and most significant byte diacritics may also be omitted, in which
case the placeholder cell will inherit the missing values from the placeholder
cell to the left, following the algorithm:
- If no diacritics are present, and the previous placeholder cell has the same
foreground and underline colors, then the row of the current cell will be the
row of the cell to the left, the column will be the column of the cell to the
left plus one, and the most significant image ID byte will be the most
significant image ID byte of the cell to the left.
- If only the row diacritic is present, and the previous placeholder cell has
the same row and the same foreground and underline colors, then the column of
the current cell will be the column of the cell to the left plus one, and the
most significant image ID byte will be the most significant image ID byte of
the cell to the left.
- If only the row and column diacritics are present, and the previous
placeholder cell has the same row, the same foreground and underline colors,
and its column is one less than the current column, then the most significant
image ID byte of the current cell will be the most significant image ID byte
of the cell to the left.
These rules are applied left-to-right, which allows specifying only row
diacritics of the first column, i.e. here is a 2 rows by 3 columns placeholder:
.. code-block:: sh
printf "\e[38;5;42m\U10EEEE\U0305\U10EEEE\U10EEEE\n"
printf "\e[38;5;42m\U10EEEE\U030D\U10EEEE\U10EEEE\n"
This will not work for horizontal scrolling and overlapping images since the two
given rules will fail to guess the missing information. In such cases, the
terminal may apply other heuristics (but it doesn't have to).
It is important to distinguish between virtual image placements and real images
displayed on top of Unicode placeholders. Virtual placements are invisible and only play
the role of prototypes for real images. Virtual placements can be deleted by a
deletion command only when the `d` key is equal to ``i``, ``I``, ``n`` or ``N``.
The key values ``a``, ``c``, ``p``, ``q``, ``x``, ``y``, ``z`` and their capital
variants never affect virtual placements because they do not have a physical
location on the screen.
Real images displayed on top of Unicode placeholders are not considered
placements from the protocol perspective. They cannot be manipulated using
graphics commands, instead they should be moved, deleted, or modified by
manipulating the underlying Unicode placeholder as normal text.
Deleting images Deleting images
--------------------- ---------------------
@ -932,8 +754,6 @@ Key Value Default Description
``r`` Positive integer ``0`` The number of rows to display the image over ``r`` Positive integer ``0`` The number of rows to display the image over
``C`` Positive integer ``0`` Cursor movement policy. ``0`` is the default, to move the cursor to after the image. ``C`` Positive integer ``0`` Cursor movement policy. ``0`` is the default, to move the cursor to after the image.
``1`` is to not move the cursor at all when placing the image. ``1`` is to not move the cursor at all when placing the image.
``U`` Positive integer ``0`` Set to ``1`` to create a virtual placement for a Unicode placeholder.
``1`` is to not move the cursor at all when placing the image.
``z`` 32-bit integer ``0`` The *z-index* vertical stacking order of the image ``z`` 32-bit integer ``0`` The *z-index* vertical stacking order of the image
**Keys for animation frame loading** **Keys for animation frame loading**

View File

@ -46,7 +46,7 @@ detect_os() {
'Linux') 'Linux')
OS="linux" OS="linux"
case "$(command uname -m)" in case "$(command uname -m)" in
amd64|x86_64) arch="x86_64";; x86_64) arch="x86_64";;
aarch64*) arch="arm64";; aarch64*) arch="arm64";;
armv8*) arch="arm64";; armv8*) arch="arm64";;
i386) arch="i686";; i386) arch="i686";;
@ -114,40 +114,38 @@ get_download_url() {
esac esac
} }
download_installer() {
tdir=$(command mktemp -d "/tmp/kitty-install-XXXXXXXXXXXX")
[ "$installer_is_file" != "y" ] && {
printf '%s\n\n' "Downloading from: $url"
if [ "$OS" = "macos" ]; then
installer="$tdir/kitty.dmg"
else
installer="$tdir/kitty.txz"
fi
fetch "$url" > "$installer" || die "Failed to download: $url"
installer_is_file="y"
}
}
linux_install() { linux_install() {
command mkdir "$tdir/mp" if [ "$installer_is_file" = "y" ]; then
command tar -C "$tdir/mp" "-xJof" "$installer" || die "Failed to extract kitty tarball" command tar -C "$dest" "-xJof" "$installer"
printf "%s\n" "Installing to $dest" else
command rm -rf "$dest" || die "Failed to delete $dest" printf '%s\n\n' "Downloading from: $url"
command mv "$tdir/mp" "$dest" || die "Failed to move kitty.app to $dest" fetch "$url" | command tar -C "$dest" "-xJof" "-"
fi
} }
macos_install() { macos_install() {
tdir=$(command mktemp -d "/tmp/kitty-install-XXXXXXXXXXXX")
[ "$installer_is_file" != "y" ] && {
installer="$tdir/kitty.dmg"
printf '%s\n\n' "Downloading from: $url"
fetch "$url" > "$installer" || die "Failed to download: $url"
}
command mkdir "$tdir/mp" command mkdir "$tdir/mp"
command hdiutil attach "$installer" "-mountpoint" "$tdir/mp" || die "Failed to mount kitty.dmg" command hdiutil attach "$installer" "-mountpoint" "$tdir/mp" || die "Failed to mount kitty.dmg"
printf "%s\n" "Installing to $dest"
command rm -rf "$dest"
command mkdir -p "$dest" || die "Failed to create the directory: $dest"
command ditto -v "$tdir/mp/kitty.app" "$dest" command ditto -v "$tdir/mp/kitty.app" "$dest"
rc="$?" rc="$?"
command hdiutil detach "$tdir/mp" command hdiutil detach "$tdir/mp"
command rm -rf "$tdir"
tdir=''
[ "$rc" != "0" ] && die "Failed to copy kitty.app from mounted dmg" [ "$rc" != "0" ] && die "Failed to copy kitty.app from mounted dmg"
} }
prepare_install_dest() {
printf "%s\n" "Installing to $dest"
command rm -rf "$dest"
command mkdir -p "$dest" || die "Failed to create the directory: $dest"
}
exec_kitty() { exec_kitty() {
if [ "$OS" = "macos" ]; then if [ "$OS" = "macos" ]; then
exec "open" "$dest" exec "open" "$dest"
@ -162,13 +160,12 @@ main() {
parse_args "$@" parse_args "$@"
detect_network_tool detect_network_tool
get_download_url get_download_url
download_installer prepare_install_dest
if [ "$OS" = "macos" ]; then if [ "$OS" = "macos" ]; then
macos_install macos_install
else else
linux_install linux_install
fi fi
cleanup
[ "$launch" = "y" ] && exec_kitty [ "$launch" = "y" ] && exec_kitty
exit 0 exit 0
} }

View File

@ -80,17 +80,6 @@ base application that uses kitty's graphics protocol for images.
A text mode WWW browser that supports kitty's graphics protocol to display A text mode WWW browser that supports kitty's graphics protocol to display
images. images.
`awrit <https://github.com/chase/awrit>`__
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A full Chromium based web browser running in the terminal using kitty's
graphics protocol.
.. _tool_mpv:
`mpv <https://github.com/mpv-player/mpv/commit/874e28f4a41a916bb567a882063dd2589e9234e1>`_
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A video player that can play videos in the terminal.
.. _tool_timg: .. _tool_timg:
`timg <https://github.com/hzeller/timg>`_ `timg <https://github.com/hzeller/timg>`_
@ -225,8 +214,7 @@ Allows easily running tests in a terminal window
`hologram.nvim <https://github.com/edluffy/hologram.nvim>`_ `hologram.nvim <https://github.com/edluffy/hologram.nvim>`_
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Terminal image viewer for Neovim. For a bit of fun, you can even have `cats Terminal image viewer for Neovim
running around inside nvim <https://github.com/giusgad/pets.nvim>`__.
Scrollback manipulation Scrollback manipulation

View File

@ -40,13 +40,9 @@ In addition to kitty, this protocol is also implemented in:
<https://github.com/dankamongmen/notcurses/issues/2131>`__ <https://github.com/dankamongmen/notcurses/issues/2131>`__
* The `crossterm library * The `crossterm library
<https://github.com/crossterm-rs/crossterm/pull/688>`__ <https://github.com/crossterm-rs/crossterm/pull/688>`__
* The `Vim text editor <https://github.com/vim/vim/commit/63a2e360cca2c70ab0a85d14771d3259d4b3aafa>`__ * The `neovim text editor <https://github.com/neovim/neovim/pull/18181>`__
* The `Emacs text editor via the kkp package <https://github.com/benjaminor/kkp>`__
* The `Neovim text editor <https://github.com/neovim/neovim/pull/18181>`__
* The `kakoune text editor <https://github.com/mawww/kakoune/issues/4103>`__ * The `kakoune text editor <https://github.com/mawww/kakoune/issues/4103>`__
* The `dte text editor <https://gitlab.com/craigbarnes/dte/-/issues/138>`__ * The `dte text editor <https://gitlab.com/craigbarnes/dte/-/issues/138>`__
* The `Helix text editor <https://github.com/helix-editor/helix/pull/4939>`__
* The `far2l file manager <https://github.com/elfmz/far2l/commit/e1f2ee0ef2b8332e5fa3ad7f2e4afefe7c96fc3b>`__
.. versionadded:: 0.20.0 .. versionadded:: 0.20.0
@ -64,14 +60,14 @@ without too many changes, do the following:
that are easy to parse unambiguously. that are easy to parse unambiguously.
#. Emit the escape sequence ``CSI < u`` at application exit if using the main #. Emit the escape sequence ``CSI < u`` at application exit if using the main
screen or just before leaving alternate screen mode if using the alternate screen, screen or just before leaving alternate screen mode if using the alternate screen,
to restore whatever the keyboard mode was before step 1. to restore the previously used keyboard mode.
Key events will all be delivered to your application either as plain UTF-8 Key events will all be delivered to your application either as plain UTF-8
text, or using the following escape codes, for those keys that do not produce text, or using the following escape codes, for those keys that do not produce
text (``CSI`` is the bytes ``0x1b 0x5b``):: text (``CSI`` is the bytes ``0x1b 0x5b``)::
CSI number ; modifiers [u~] CSI number ; modifiers [u~]
CSI 1; modifiers [ABCDEFHPQS] CSI 1; modifiers [ABCDEFHPQRS]
0x0d - for the Enter key 0x0d - for the Enter key
0x7f or 0x08 - for Backspace 0x7f or 0x08 - for Backspace
0x09 - for Tab 0x09 - for Tab
@ -86,7 +82,7 @@ The second form is used for a few functional keys, such as the :kbd:`Home`,
:kbd:`End`, :kbd:`Arrow` keys and :kbd:`F1` ... :kbd:`F4`, they are enumerated in :kbd:`End`, :kbd:`Arrow` keys and :kbd:`F1` ... :kbd:`F4`, they are enumerated in
the :ref:`functional` table below. Note that if no modifiers are present the the :ref:`functional` table below. Note that if no modifiers are present the
parameters are omitted entirely giving an escape code of the form ``CSI parameters are omitted entirely giving an escape code of the form ``CSI
[ABCDEFHPQS]``. [ABCDEFHPQRS]``.
If you want support for more advanced features such as repeat and release If you want support for more advanced features such as repeat and release
events, alternate keys for shortcut matching et cetera, these can be turned on events, alternate keys for shortcut matching et cetera, these can be turned on
@ -317,7 +313,7 @@ With this flag turned on, all key events that do not generate text are
represented in one of the following two forms:: represented in one of the following two forms::
CSI number; modifier u CSI number; modifier u
CSI 1; modifier [~ABCDEFHPQS] CSI 1; modifier [~ABCDEFHPQRS]
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
@ -408,7 +404,7 @@ Legacy functional keys
These keys are encoded using three schemes:: These keys are encoded using three schemes::
CSI number ; modifier ~ CSI number ; modifier ~
CSI 1 ; modifier {ABCDEFHPQS} CSI 1 ; modifier {ABCDEFHPQRS}
SS3 {ABCDEFHPQRS} SS3 {ABCDEFHPQRS}
In the above, if there are no modifiers, the modifier parameter is omitted. In the above, if there are no modifiers, the modifier parameter is omitted.
@ -536,7 +532,7 @@ compatibility reasons.
"NUM_LOCK", "``57360 u``", "PRINT_SCREEN", "``57361 u``" "NUM_LOCK", "``57360 u``", "PRINT_SCREEN", "``57361 u``"
"PAUSE", "``57362 u``", "MENU", "``57363 u``" "PAUSE", "``57362 u``", "MENU", "``57363 u``"
"F1", "``1 P or 11 ~``", "F2", "``1 Q or 12 ~``" "F1", "``1 P or 11 ~``", "F2", "``1 Q or 12 ~``"
"F3", "``13 ~``", "F4", "``1 S or 14 ~``" "F3", "``1 R or 13 ~``", "F4", "``1 S or 14 ~``"
"F5", "``15 ~``", "F6", "``17 ~``" "F5", "``15 ~``", "F6", "``17 ~``"
"F7", "``18 ~``", "F8", "``19 ~``" "F7", "``18 ~``", "F8", "``19 ~``"
"F9", "``20 ~``", "F10", "``21 ~``" "F9", "``20 ~``", "F10", "``21 ~``"
@ -585,15 +581,9 @@ compatibility reasons.
.. end functional key table .. end functional key table
.. }}} .. }}}
.. note:: Note that the escape codes above of the form ``CSI 1 letter`` will omit the
The escape codes above of the form ``CSI 1 letter`` will omit the
``1`` if there are no modifiers, since ``1`` is the default value. ``1`` if there are no modifiers, since ``1`` is the default value.
.. note::
The original version of this specification allowed F3 to be encoded as both
CSI R and CSI ~. However, CSI R conflicts with the Cursor Position Report,
so it was removed.
.. _ctrl_mapping: .. _ctrl_mapping:
Legacy :kbd:`ctrl` mapping of ASCII keys Legacy :kbd:`ctrl` mapping of ASCII keys
@ -685,31 +675,3 @@ specification.
* Handwaves that :kbd:`ctrl` *tends to* mask with ``0x1f``. In actual fact it * Handwaves that :kbd:`ctrl` *tends to* mask with ``0x1f``. In actual fact it
does this only for some keys. The action of :kbd:`ctrl` is not specified and does this only for some keys. The action of :kbd:`ctrl` is not specified and
varies between terminals, historically because of different keyboard layouts. varies between terminals, historically because of different keyboard layouts.
Why xterm's modifyOtherKeys should not be used
---------------------------------------------------
* Does not support release events
* Does not fix the issue of :kbd:`Esc` key presses not being distinguishable from
escape codes.
* Does not fix the issue of some keypresses generating identical bytes and thus
being indistinguishable
* There is no robust way to query it or manage its state from a program running
in the terminal.
* No support for shifted keys.
* No support for alternate keyboard layouts.
* No support for modifiers beyond the basic four.
* No support for lock keys like Num lock and Caps lock.
* Is completely unspecified. The most discussion of it available anywhere is
`here <https://invisible-island.net/xterm/modified-keys.html>`__
And it contains no specification of what numbers to assign to what function
keys beyond running a Perl script on an X11 system!!

View File

@ -11,46 +11,14 @@ from the shell. It even works over SSH. Using it is as simple as::
echo hooray | kitty +kitten clipboard echo hooray | kitty +kitten clipboard
All text received on :file:`STDIN` is copied to the clipboard. All text received on :file:`stdin` is copied to the clipboard.
To get text from the clipboard:: To get text from the clipboard you have to enable reading of the clipboard
in :opt:`clipboard_control` in :file:`kitty.conf`. Once you do that, you can
use::
kitty +kitten clipboard --get-clipboard kitty +kitten clipboard --get-clipboard
The text will be written to :file:`STDOUT`. Note that by default kitty asks for
permission when a program attempts to read the clipboard. This can be
controlled via :opt:`clipboard_control`.
.. versionadded:: 0.27.0
Support for copying arbitrary data types
The clipboard kitten can be used to send/receive
more than just plain text from the system clipboard. You can transfer arbitrary
data types. Best illustrated with some examples::
# Copy an image to the clipboard:
kitty +kitten clipboard picture.png
# Copy an image and some text to the clipboard:
kitty +kitten clipboard picture.jpg text.txt
# Copy text from STDIN and an image to the clipboard:
echo hello | kitty +kitten clipboard picture.png /dev/stdin
# Copy any raster image available on the clipboard to a PNG file:
kitty +kitten clipboard -g picture.png
# Copy an image to a file and text to STDOUT:
kitty +kitten clipboard -g picture.png /dev/stdout
# List the formats available on the system clipboard
kitty +kitten clipboard -g -m . /dev/stdout
Normally, the kitten guesses MIME types based on the file names. To control the
MIME types precisely, use the :option:`--mime <kitty +kitten clipboard --mime>` option.
This kitten uses a new protocol developed by kitty to function, for details,
see :doc:`/clipboard`.
.. program:: kitty +kitten clipboard .. program:: kitty +kitten clipboard

View File

@ -31,7 +31,11 @@ Major Features
Installation Installation
--------------- ---------------
Simply :ref:`install kitty <quickstart>`. Simply :ref:`install kitty <quickstart>`. You also need to have either the `git
<https://git-scm.com/>`__ program or the :program:`diff` program installed.
Additionally, for syntax highlighting to work, `pygments
<https://pygments.org/>`__ must be installed (note that pygments is included in
the official kitty binary builds).
Usage Usage
@ -61,10 +65,10 @@ directory contents.
Keyboard controls Keyboard controls
---------------------- ----------------------
=========================== =========================== ========================= ===========================
Action Shortcut Action Shortcut
=========================== =========================== ========================= ===========================
Quit :kbd:`Q`, :kbd:`Esc` Quit :kbd:`Q`, :kbd:`Ctrl+C`, :kbd:`Esc`
Scroll line up :kbd:`K`, :kbd:`Up` Scroll line up :kbd:`K`, :kbd:`Up`
Scroll line down :kbd:`J`, :kbd:`Down` Scroll line down :kbd:`J`, :kbd:`Down`
Scroll page up :kbd:`PgUp` Scroll page up :kbd:`PgUp`
@ -84,9 +88,7 @@ Search backwards :kbd:`?`
Clear search :kbd:`Esc` Clear search :kbd:`Esc`
Scroll to next match :kbd:`>`, :kbd:`.` Scroll to next match :kbd:`>`, :kbd:`.`
Scroll to previous match :kbd:`<`, :kbd:`,` Scroll to previous match :kbd:`<`, :kbd:`,`
Copy selection to clipboard :kbd:`y` ========================= ===========================
Copy selection or exit :kbd:`Ctrl+C`
=========================== ===========================
Integrating with git Integrating with git
@ -122,7 +124,7 @@ The diff kitten makes use of various features that are :doc:`kitty only
</graphics-protocol>`, the :doc:`extended keyboard protocol </graphics-protocol>`, the :doc:`extended keyboard protocol
</keyboard-protocol>`, etc. It also leverages terminal program infrastructure </keyboard-protocol>`, etc. It also leverages terminal program infrastructure
I created for all of kitty's other kittens to reduce the amount of code needed I created for all of kitty's other kittens to reduce the amount of code needed
(the entire implementation is under 3000 lines of code). (the entire implementation is under 2000 lines of code).
And fundamentally, it's kitty only because I wrote it for myself, and I am And fundamentally, it's kitty only because I wrote it for myself, and I am
highly unlikely to use any other terminals :) highly unlikely to use any other terminals :)

View File

@ -72,8 +72,7 @@ the :ref:`kitty config directory <confloc>` with the following contents:
start, end = m.span() start, end = m.span()
mark_text = text[start:end].replace('\n', '').replace('\0', '') mark_text = text[start:end].replace('\n', '').replace('\0', '')
# The empty dictionary below will be available as groupdicts # The empty dictionary below will be available as groupdicts
# in handle_result() and can contain string keys and arbitrary JSON # in handle_result() and can contain arbitrary data.
# serializable values.
yield Mark(idx, start, end, mark_text, {}) yield Mark(idx, start, end, mark_text, {})

View File

@ -43,18 +43,46 @@ You can now run searches with::
hg some-search-term hg some-search-term
If you want to enable completion, for the kitten, you can delegate completion
to :program:`rg`. How to do that varies based on the shell:
.. tab:: zsh
Instead of using an alias, create a simple wrapper script named
:program:`hg` somewhere in your :envvar:`PATH`:
.. code-block:: sh
#!/bin/sh
exec kitty +kitten hyperlinked_grep "$@"
Then, add the following to :file:`.zshrc`::
compdef _rg hg
.. tab:: fish
You can combine both the aliasing/wrapping and pointing fish to ripgrep's
autocompletion with a fish wrapper function in your :file:`config.fish`
or :file:`~/.config/fish/functions/hg.fish`:
.. code-block:: fish
function hg --wraps rg; kitty +kitten hyperlinked_grep $argv; end
To learn more about kitty's powerful framework for customizing URL click To learn more about kitty's powerful framework for customizing URL click
actions, see :doc:`here </open_actions>`. actions, see :doc:`here </open_actions>`.
By default, this kitten adds hyperlinks for several parts of ripgrep output: By default, this kitten adds hyperlinks for several parts of ripgrep output:
the per-file header, match context lines, and match lines. You can control the per-file header, match context lines, and match lines. You can control
which items are linked with a :code:`--kitten hyperlink` flag. For example, which items are linked with a :command:`--kitten hyperlink` flag. For example,
:code:`--kitten hyperlink=matching_lines` will only add hyperlinks to the :command:`--kitten hyperlink=matching_lines` will only add hyperlinks to the
match lines. :code:`--kitten hyperlink=file_headers,context_lines` will link match lines. :command:`--kitten hyperlink=file_headers,context_lines` will link
file headers and context lines but not match lines. :code:`--kitten file headers and context lines but not match lines. :command:`--kitten
hyperlink=none` will cause the command line to be passed to directly to hyperlink=none` will cause the command line to be passed to directly to
:command:`rg` so no hyperlinking will be performed. :code:`--kitten hyperlink` :command:`rg` so no hyperlinking will be performed. :command:`--kitten
may be specified multiple times. hyperlink` may be specified multiple times.
Hopefully, someday this functionality will make it into some `upstream grep Hopefully, someday this functionality will make it into some `upstream grep
<https://github.com/BurntSushi/ripgrep/issues/665>`__ program directly removing <https://github.com/BurntSushi/ripgrep/issues/665>`__ program directly removing
@ -65,9 +93,3 @@ the need for this kitten.
While you can pass any of ripgrep's comand line options to the kitten and While you can pass any of ripgrep's comand line options to the kitten and
they will be forwarded to :program:`rg`, do not use options that change the they will be forwarded to :program:`rg`, do not use options that change the
output formatting as the kitten works by parsing the output from ripgrep. output formatting as the kitten works by parsing the output from ripgrep.
The unsupported options are: :code:`--context-separator`,
:code:`--field-context-separator`, :code:`--field-match-separator`,
:code:`--json`, :code:`-I --no-filename`, :code:`-0 --null`,
:code:`--null-data`, :code:`--path-separator`. If you specify options via
configuration file, then any changes to the default output format will not be
supported, not just the ones listed.

View File

@ -7,7 +7,6 @@ The ``icat`` kitten can be used to display arbitrary images in the |kitty|
terminal. Using it is as simple as:: terminal. Using it is as simple as::
kitty +kitten icat image.jpeg kitty +kitten icat image.jpeg
kitten icat image.jpeg
It supports all image types supported by `ImageMagick It supports all image types supported by `ImageMagick
<https://www.imagemagick.org>`__. It even works over SSH. For details, see the <https://www.imagemagick.org>`__. It even works over SSH. For details, see the
@ -21,9 +20,8 @@ Then you can simply use ``icat image.png`` to view images.
.. note:: .. note::
`ImageMagick <https://www.imagemagick.org>`__ must be installed for the `ImageMagick <https://www.imagemagick.org>`__ must be installed for icat
full range of image types. Without it only PNG/JPG/GIF/BMP/TIFF/WEBP are kitten to work.
supported.
.. note:: .. note::
@ -37,15 +35,16 @@ Then you can simply use ``icat image.png`` to view images.
The ``icat`` kitten has various command line arguments to allow it to be used The ``icat`` kitten has various command line arguments to allow it to be used
from inside other programs to display images. In particular, :option:`--place`, from inside other programs to display images. In particular, :option:`--place`,
:option:`--detect-support` and :option:`--print-window-size`. :option:`--detect-support`, :option:`--silent` and
:option:`--print-window-size`.
If you are trying to integrate icat into a complex program like a file manager If you are trying to integrate icat into a complex program like a file manager
or editor, there are a few things to keep in mind. icat works by communicating or editor, there are a few things to keep in mind. icat works by communicating
over the TTY device, it both writes to and reads from the TTY. So it is over the TTY device, it both writes to and reads from the TTY. So it is
imperative that while it is running the host program does not do any TTY I/O. imperative that while it is running the host program does not do any TTY I/O.
Any key presses or other input from the user on the TTY device will be Any key presses or other input from the user on the TTY device will be
discarded. At a minimum, you should use the :option:`--transfer-mode` discarded. At a minimum, you should use the :option:`--silent` and
command line arguments. To be really robust you should :option:`--transfer-mode` command line arguments. To be really robust you should
consider writing proper support for the :doc:`kitty graphics protocol consider writing proper support for the :doc:`kitty graphics protocol
</graphics-protocol>` in the program instead. Nowadays there are many libraries </graphics-protocol>` in the program instead. Nowadays there are many libraries
that have support for it. that have support for it.

View File

@ -162,42 +162,3 @@ The copy command
-------------------- --------------------
.. include:: /generated/ssh-copy.rst .. include:: /generated/ssh-copy.rst
.. _manual_terminfo_copy:
Copying terminfo files manually
-------------------------------------
Sometimes, the ssh kitten can fail, or maybe you dont like to use it. In such
cases, the terminfo files can be copied over manually to a server with the
following one liner::
infocmp -a xterm-kitty | ssh myserver tic -x -o \~/.terminfo /dev/stdin
If you are behind a proxy (like Balabit) that prevents this, or you are SSHing
into macOS where the :program:`tic` does not support reading from :file:`STDIN`,
you must redirect the first command to a file, copy that to the server and run :program:`tic`
manually. If you connect to a server, embedded, or Android system that doesn't
have :program:`tic`, copy over your local file terminfo to the other system as
:file:`~/.terminfo/x/xterm-kitty`.
If the server is running a relatively modern Linux distribution and you have
root access to it, you could simply install the ``kitty-terminfo`` package on
the server to make the terminfo files available.
Really, the correct solution for this is to convince the OpenSSH maintainers to
have :program:`ssh` do this automatically, if possible, when connecting to a
server, so that all terminals work transparently.
If the server is running FreeBSD, or another system that relies on termcap
rather than terminfo, you will need to convert the terminfo file on your local
machine by running (on local machine with |kitty|)::
infocmp -CrT0 xterm-kitty
The output of this command is the termcap description, which should be appended
to :file:`/usr/share/misc/termcap` on the remote server. Then run the following
command to apply your change (on the server)::
cap_mkdb /usr/share/misc/termcap

View File

@ -44,8 +44,7 @@ You can also create your own themes as :file:`.conf` files. Put them in the
usually, :file:`~/.config/kitty/themes`. The kitten will automatically add them usually, :file:`~/.config/kitty/themes`. The kitten will automatically add them
to the list of themes. You can use this to modify the builtin themes, by giving to the list of themes. You can use this to modify the builtin themes, by giving
the conf file the name :file:`Some theme name.conf` to override the builtin the conf file the name :file:`Some theme name.conf` to override the builtin
theme of that name. Here, ``Some theme name`` is the actual builtin theme name, not theme of that name. Note that after doing so you have to run the kitten and
its file name. Note that after doing so you have to run the kitten and
choose that theme once for your changes to be applied. choose that theme once for your changes to be applied.

295
docs/kitty_at_template.py Normal file
View File

@ -0,0 +1,295 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
import argparse
import base64
import hashlib
import json
import os
import sys
import termios
import time
import tty
from contextlib import contextmanager
from ctypes import (
CDLL, POINTER, byref, c_char_p, c_int, c_size_t, c_void_p,
create_string_buffer
)
from ctypes.util import find_library
_plat = sys.platform.lower()
is_macos: bool = 'darwin' in _plat
def build_crypto_tools(): # {{{
class EVP_PKEY_POINTER(c_void_p):
algorithm = 0
def __del__(self):
EVP_PKEY_free(self)
@property
def public(self):
sz = c_size_t(0)
EVP_PKEY_get_raw_public_key(self, None, byref(sz))
buf = create_string_buffer(sz.value)
EVP_PKEY_get_raw_public_key(self, buf, byref(sz))
return buf.raw
def derive_secret(self, pubkey):
pubkey = EVP_PKEY_new_raw_public_key(self.algorithm, None, pubkey, len(pubkey))
ctx = EVP_PKEY_CTX_new(self, None)
EVP_PKEY_derive_init(ctx)
EVP_PKEY_derive_set_peer(ctx, pubkey)
sz = c_size_t(0)
EVP_PKEY_derive(ctx, None, byref(sz))
buf = create_string_buffer(sz.value)
EVP_PKEY_derive(ctx, buf, byref(sz))
return hashlib.sha256(buf.raw).digest()
class EVP_PKEY_CTX_POINTER(c_void_p):
def __del__(self):
EVP_PKEY_CTX_free(self)
class EVP_CIPHER_CTX_POINTER(c_void_p):
def __del__(self):
EVP_CIPHER_CTX_free(self)
class EVP_CIPHER_POINTER(c_void_p):
pass
cl = find_library('crypto')
if not cl:
raise SystemExit('Failed to find libcrypto on your system, make sure OpenSSL is installed')
crypto = CDLL(cl)
libc = CDLL(None)
def create_crypto_func(name, *argtypes, restype=c_int, int_return_ok=lambda x: x == 1):
impl = getattr(crypto, name)
impl.restype = restype
impl.argtypes = argtypes
def func(*a):
res = impl(*a)
if restype is c_int:
if not int_return_ok(res):
print('Call to', name, 'failed with return code:', res, file=sys.stderr)
abort_on_openssl_error()
elif restype is not None and issubclass(restype, c_void_p):
if res.value is None:
print('Call to', name, 'failed with NULL return', file=sys.stderr)
abort_on_openssl_error()
return res
return func
OBJ_txt2nid = create_crypto_func('OBJ_txt2nid', c_char_p, int_return_ok=bool)
EVP_PKEY_CTX_new_id = create_crypto_func('EVP_PKEY_CTX_new_id', c_int, c_void_p, restype=EVP_PKEY_CTX_POINTER)
EVP_PKEY_CTX_new = create_crypto_func('EVP_PKEY_CTX_new', EVP_PKEY_POINTER, c_void_p, restype=EVP_PKEY_CTX_POINTER)
EVP_PKEY_keygen_init = create_crypto_func('EVP_PKEY_keygen_init', EVP_PKEY_CTX_POINTER)
EVP_PKEY_keygen = create_crypto_func('EVP_PKEY_keygen', EVP_PKEY_CTX_POINTER, POINTER(EVP_PKEY_POINTER))
ERR_print_errors_fp = create_crypto_func('ERR_print_errors_fp', c_void_p, restype=None)
EVP_PKEY_free = create_crypto_func('EVP_PKEY_free', EVP_PKEY_POINTER, restype=None)
EVP_PKEY_CTX_free = create_crypto_func('EVP_PKEY_CTX_free', EVP_PKEY_CTX_POINTER, restype=None)
EVP_PKEY_get_raw_public_key = create_crypto_func('EVP_PKEY_get_raw_public_key', EVP_PKEY_POINTER, c_char_p, POINTER(c_size_t))
EVP_PKEY_new_raw_public_key = create_crypto_func('EVP_PKEY_new_raw_public_key', c_int, c_void_p, c_char_p, c_size_t, restype=EVP_PKEY_POINTER)
EVP_PKEY_derive_init = create_crypto_func('EVP_PKEY_derive_init', EVP_PKEY_CTX_POINTER)
EVP_PKEY_derive_set_peer = create_crypto_func('EVP_PKEY_derive_set_peer', EVP_PKEY_CTX_POINTER, EVP_PKEY_POINTER)
EVP_PKEY_derive = create_crypto_func('EVP_PKEY_derive', EVP_PKEY_CTX_POINTER, c_char_p, POINTER(c_size_t))
EVP_CIPHER_CTX_free = create_crypto_func('EVP_CIPHER_CTX_free', EVP_CIPHER_CTX_POINTER, restype=None)
EVP_get_cipherbyname = create_crypto_func('EVP_get_cipherbyname', c_char_p, restype=EVP_CIPHER_POINTER)
EVP_CIPHER_key_length = create_crypto_func('EVP_CIPHER_key_length', EVP_CIPHER_POINTER, int_return_ok=bool)
EVP_CIPHER_iv_length = create_crypto_func('EVP_CIPHER_iv_length', EVP_CIPHER_POINTER, int_return_ok=bool)
EVP_CIPHER_CTX_block_size = create_crypto_func('EVP_CIPHER_CTX_block_size', EVP_CIPHER_CTX_POINTER, int_return_ok=bool)
EVP_CIPHER_CTX_new = create_crypto_func('EVP_CIPHER_CTX_new', restype=EVP_CIPHER_CTX_POINTER)
EVP_EncryptInit_ex = create_crypto_func('EVP_EncryptInit_ex', EVP_CIPHER_CTX_POINTER, EVP_CIPHER_POINTER, c_void_p, c_char_p, c_char_p)
EVP_EncryptUpdate = create_crypto_func('EVP_EncryptUpdate', EVP_CIPHER_CTX_POINTER, c_char_p, POINTER(c_int), c_char_p, c_int)
EVP_EncryptFinal_ex = create_crypto_func('EVP_EncryptFinal_ex', EVP_CIPHER_CTX_POINTER, c_char_p, POINTER(c_int))
EVP_CIPHER_CTX_ctrl = create_crypto_func('EVP_CIPHER_CTX_ctrl', EVP_CIPHER_CTX_POINTER, c_int, c_int, c_char_p)
try:
EVP_CIPHER_CTX_tag_length = create_crypto_func('EVP_CIPHER_CTX_tag_length', EVP_CIPHER_CTX_POINTER, int_return_ok=bool)
except AttributeError: # need openssl >= 3
def EVP_CIPHER_CTX_tag_length(cipher):
return 16
EVP_CTRL_AEAD_GET_TAG, EVP_CTRL_AEAD_SET_TAG = 0x10, 0x11 # these are defines in the header dont know how to get them programmatically
EVP_CTRL_AEAD_SET_TAG
def abort_on_openssl_error():
stderr = c_void_p.in_dll(libc, 'stderr')
ERR_print_errors_fp(stderr)
raise SystemExit(1)
def elliptic_curve_keypair(algorithm='X25519'):
nid = OBJ_txt2nid(algorithm.encode())
pctx = EVP_PKEY_CTX_new_id(nid, None)
EVP_PKEY_keygen_init(pctx)
key = EVP_PKEY_POINTER()
EVP_PKEY_keygen(pctx, byref(key))
key.algorithm = nid
return key
def encrypt(plaintext, symmetric_key, algorithm='aes-256-gcm'):
cipher = EVP_get_cipherbyname(algorithm.encode())
if len(symmetric_key) != EVP_CIPHER_key_length(cipher):
raise KeyError(f'The symmetric key has length {len(symmetric_key)} != {EVP_CIPHER_key_length(cipher)} needed for {algorithm}')
ctx = EVP_CIPHER_CTX_new()
iv = os.urandom(EVP_CIPHER_iv_length(cipher))
EVP_EncryptInit_ex(ctx, cipher, None, symmetric_key, iv)
bs = EVP_CIPHER_CTX_block_size(ctx)
ciphertext = create_string_buffer(len(plaintext) + 2 * bs)
outlen = c_int(len(ciphertext))
EVP_EncryptUpdate(ctx, ciphertext, byref(outlen), plaintext, len(plaintext))
ans = ciphertext[:outlen.value]
outlen = c_int(len(ciphertext))
EVP_EncryptFinal_ex(ctx, ciphertext, byref(outlen))
if outlen.value:
ans += ciphertext[:outlen.value]
tag = create_string_buffer(EVP_CIPHER_CTX_tag_length(cipher))
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, len(tag), tag)
return iv, ans, tag.raw
return elliptic_curve_keypair, encrypt
# }}}
# utils {{{
def encrypt_cmd(cmd, password, pubkey=None):
elliptic_curve_keypair, encrypt = build_crypto_tools()
if pubkey is None:
pubkey = os.environ['KITTY_PUBLIC_KEY']
v, d = pubkey.split(':', 1)
if v != '1':
raise SystemExit(f'Unsupported encryption protocol: {v}')
pubkey = base64.b85decode(d)
k = elliptic_curve_keypair()
sk = k.derive_secret(pubkey)
cmd['timestamp'] = time.time_ns()
cmd['password'] = password
data = json.dumps(cmd).encode()
iv, encrypted, tag = encrypt(data, sk)
def e(x):
return base64.b85encode(x).decode('ascii')
return {
'encrypted': e(encrypted), 'iv': e(iv), 'tag': e(tag), 'pubkey': e(k.public), 'version': cmd['version']
}
@contextmanager
def raw_mode(fd):
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
yield
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
def config_dir():
if 'KITTY_CONFIG_DIRECTORY' in os.environ:
return os.path.abspath(os.path.expanduser(os.environ['KITTY_CONFIG_DIRECTORY']))
locations = []
if 'XDG_CONFIG_HOME' in os.environ:
locations.append(os.path.abspath(os.path.expanduser(os.environ['XDG_CONFIG_HOME'])))
locations.append(os.path.expanduser('~/.config'))
if is_macos:
locations.append(os.path.expanduser('~/Library/Preferences'))
for loc in filter(None, os.environ.get('XDG_CONFIG_DIRS', '').split(os.pathsep)):
locations.append(os.path.abspath(os.path.expanduser(loc)))
for loc in locations:
if loc:
q = os.path.join(loc, 'kitty')
if os.access(q, os.W_OK) and os.path.exists(os.path.join(q, 'kitty.conf')):
return q
for loc in locations:
if loc:
q = os.path.join(loc, 'kitty')
if os.path.isdir(q) and os.access(q, os.W_OK):
return q
return ''
def resolve_custom_file(path):
path = os.path.expanduser(path)
path = os.path.expandvars(path)
if not os.path.isabs(path):
cdir = config_dir()
if cdir:
path = os.path.join(cdir, path)
return path
def get_password(opts):
if opts.use_password == 'never':
return ''
ans = ''
if opts.password:
ans = opts.password
if not ans and opts.password_file:
if opts.password_file == '-':
if sys.stdin.isatty():
from getpass import getpass
ans = getpass()
else:
ans = sys.stdin.read().rstrip()
try:
tty_fd = os.open(os.ctermid(), os.O_RDONLY | os.O_CLOEXEC)
except OSError:
pass
else:
with open(tty_fd, closefd=True):
os.dup2(tty_fd, sys.stdin.fileno())
else:
try:
with open(resolve_custom_file(opts.password_file)) as f:
ans = f.read().rstrip()
except OSError:
pass
if not ans and opts.password_env:
ans = os.environ.get(opts.password_env, '')
if not ans and opts.use_password == 'always':
raise SystemExit('No password was found')
if ans and len(ans) > 1024:
raise SystemExit('Specified password is too long')
return ans
# }}}
arg_parser = argparse.ArgumentParser(prog='kitty@', description='Control kitty remotely.')
arg_parser.add_argument('--password', default='', help='''\
A password to use when contacting kitty. This will cause kitty to ask the user
for permission to perform the specified action, unless the password has been
accepted before or is pre-configured in kitty.conf''')
arg_parser.add_argument('--password-file', default='rc-pass', help='''\
A file from which to read the password. Trailing whitespace is ignored. Relative
paths are resolved from the kitty configuration directory. Use - to read from STDIN.
Used if no --password is supplied. Defaults to checking for the
rc-pass file in the kitty configuration directory.''')
arg_parser.add_argument('--password-env', default='KITTY_RC_PASSWORD', help='''\
The name of an environment variable to read the password from.
Used if no --password-file is supplied. Defaults to checking the KITTY_RC_PASSWORD.''')
arg_parser.add_argument('--use-password', default='if-available', choices=('if-available', 'always', 'never'), help='''\
If no password is available, kitty will usually just send the remote control command
without a password. This option can be used to force it to always or never use
the supplied password.''')
args = arg_parser.parse_args()
def populate_cmd(cmd):
raise NotImplementedError()
password = get_password(args)
cmd = {'version': [0, 20, 0]} # use a random version that's fairly old
populate_cmd(cmd)
if password:
encrypt_cmd(cmd, password)
# cmd = {'version': [0, 14, 2], 'cmd': 'ls'}
# cmd = encrypt_cmd(cmd, 'test')
# with open(os.open(os.ctermid(), os.O_RDWR | os.O_CLOEXEC), 'w') as tty_file, raw_mode(tty_file.fileno()):
# print(end=f'\x1bP@kitty-cmd{json.dumps(cmd)}\x1b\\', flush=True, file=tty_file)
# os.read(tty_file.fileno(), 4096)

View File

@ -68,13 +68,6 @@ some special variables, documented below:
The path, query and fragment portions of the URL, without any The path, query and fragment portions of the URL, without any
unquoting. unquoting.
``EDITOR``
The terminal based text editor. The configured :opt:`editor` in
:file:`kitty.conf` is preferred.
``SHELL``
The path to the shell. The configured :opt:`shell` in :file:`kitty.conf` is
preferred, without arguments.
.. note:: .. note::
You can use the :opt:`action_alias` option just as in :file:`kitty.conf` to You can use the :opt:`action_alias` option just as in :file:`kitty.conf` to
@ -110,8 +103,7 @@ lines. The various available criteria are:
Useful if your system MIME database does not have definitions you need. This Useful if your system MIME database does not have definitions you need. This
file is in the standard format of one definition per line, like: file is in the standard format of one definition per line, like:
``text/plain rst md``. Note that the MIME type for directories is ``text/plain rst md``. Note that the MIME type for directories is
``inode/directory``. MIME types are detected based on file extension, not ``inode/directory``.
file contents.
``ext`` ``ext``
A comma separated list of file extensions, for example: ``jpeg, tar.gz`` A comma separated list of file extensions, for example: ``jpeg, tar.gz``

View File

@ -10,9 +10,8 @@ configuration is a simple, human editable, single file for easy reproducibility
(I like to store configuration in source control). (I like to store configuration in source control).
The code in |kitty| is designed to be simple, modular and hackable. It is The code in |kitty| is designed to be simple, modular and hackable. It is
written in a mix of C (for performance sensitive parts), Python (for easy written in a mix of C (for performance sensitive parts) and Python (for easy
extensibility and flexibility of the UI) and Go (for the command line hackability of the UI). It does not depend on any large and complex UI toolkit,
:term:`kittens`). It does not depend on any large and complex UI toolkit,
using only OpenGL for rendering everything. using only OpenGL for rendering everything.
Finally, |kitty| is designed from the ground up to support all modern terminal Finally, |kitty| is designed from the ground up to support all modern terminal
@ -86,7 +85,7 @@ Extending kitty
------------------ ------------------
kitty has a powerful framework for scripting. You can create small terminal kitty has a powerful framework for scripting. You can create small terminal
programs called :doc:`kittens <kittens_intro>`. These can be used to add features programs called :doc:`kittens <kittens_intro>`. These can used to add features
to kitty, for example, :doc:`editing remote files <kittens/remote_file>` or to kitty, for example, :doc:`editing remote files <kittens/remote_file>` or
:doc:`inputting Unicode characters <kittens/unicode_input>`. They can also be :doc:`inputting Unicode characters <kittens/unicode_input>`. They can also be
used to create programs that leverage kitty's powerful features, for example, used to create programs that leverage kitty's powerful features, for example,
@ -128,7 +127,7 @@ Startup Sessions
You can control the :term:`tabs <tab>`, :term:`kitty window <window>` layout, You can control the :term:`tabs <tab>`, :term:`kitty window <window>` layout,
working directory, startup programs, etc. by creating a *session* file and using working directory, startup programs, etc. by creating a *session* file and using
the :option:`kitty --session` command line flag or the :opt:`startup_session` the :option:`kitty --session` command line flag or the :opt:`startup_session`
option in :file:`kitty.conf`. An example, showing all available commands: option in :file:`kitty.conf`. For example:
.. code-block:: session .. code-block:: session
@ -155,14 +154,12 @@ option in :file:`kitty.conf`. An example, showing all available commands:
launch zsh launch zsh
# Create a new OS window # Create a new OS window
# Any definitions specified before the first new_os_window will apply to first OS window. # Any definitions specifed before the first new_os_window will apply to first OS window.
new_os_window new_os_window
# Set new window size to 80x24 cells # Set new window size to 80x24 cells
os_window_size 80c 24c os_window_size 80c 24c
# Set the --class for the new OS window # Set the --class for the new OS window
os_window_class mywindow os_window_class mywindow
# Change the OS window state to normal, fullscreen, maximized or minimized
os_window_state normal
launch sh launch sh
# Resize the current window (see the resize_window action for details) # Resize the current window (see the resize_window action for details)
resize_window wider 2 resize_window wider 2
@ -176,11 +173,6 @@ option in :file:`kitty.conf`. An example, showing all available commands:
The :doc:`launch <launch>` command when used in a session file cannot create The :doc:`launch <launch>` command when used in a session file cannot create
new OS windows, or tabs. new OS windows, or tabs.
.. note::
Environment variables of the for :code:`${NAME}` or :code:`$NAME` are
expanded in the session file, except in the *arguments* (not options) to the
launch command.
Creating tabs/windows Creating tabs/windows
------------------------------- -------------------------------
@ -235,10 +227,9 @@ Font control
|kitty| has extremely flexible and powerful font selection features. You can |kitty| has extremely flexible and powerful font selection features. You can
specify individual families for the regular, bold, italic and bold+italic fonts. specify individual families for the regular, bold, italic and bold+italic fonts.
You can even specify specific font families for specific ranges of Unicode You can even specify specific font families for specific ranges of Unicode
characters. This allows precise control over text rendering. It can come in characters. This allows precise control over text rendering. It can comein handy
handy for applications like powerline, without the need to use patched fonts. for applications like powerline, without the need to use patched fonts. See the
See the various font related configuration directives in various font related configuration directives in :ref:`conf-kitty-fonts`.
:ref:`conf-kitty-fonts`.
.. _scrollback: .. _scrollback:

View File

@ -32,4 +32,3 @@ please do so by opening issues in the `GitHub bug tracker
unscroll unscroll
color-stack color-stack
deccara deccara
clipboard

View File

@ -15,7 +15,6 @@ Where ``<ESC>`` is the byte ``0x1b``. The JSON object has the form:
"cmd": "command name", "cmd": "command name",
"version": "<kitty version>", "version": "<kitty version>",
"no_response": "<Optional Boolean>", "no_response": "<Optional Boolean>",
"kitty_window_id": "<Optional value of the KITTY_WINDOW_ID env var>",
"payload": "<Optional JSON object>" "payload": "<Optional JSON object>"
} }
@ -41,12 +40,6 @@ with the following command line::
echo -en '\eP@kitty-cmd{"cmd":"ls","version":[0,14,2]}\e\\' | socat - unix:/tmp/test | awk '{ print substr($0, 13, length($0) - 14) }' | jq -c '.data | fromjson' | jq . echo -en '\eP@kitty-cmd{"cmd":"ls","version":[0,14,2]}\e\\' | socat - unix:/tmp/test | awk '{ print substr($0, 13, length($0) - 14) }' | jq -c '.data | fromjson' | jq .
There is also the statically compiled stand-alone executable ``kitten``
that can be used for this, available from the `kitty releases
<https://github.com/kovidgoyal/kitty/releases>`__ page::
kitten @ --help
.. _rc_crypto: .. _rc_crypto:
Encrypted communication Encrypted communication
@ -83,25 +76,5 @@ is created and transmitted that contains the fields:
"encrypted": "The original command encrypted and base85 encoded" "encrypted": "The original command encrypted and base85 encoded"
} }
Async and streaming requests
---------------------------------
Some remote control commands require asynchronous communication, that is, the
response from the terminal can happen after an arbitrary amount of time. For
example, the :code:`select-window` command requires the user to select a window
before a response can be sent. Such command must set the field :code:`async`
in the JSON block above to a random string that serves as a unique id. The
client can cancel an async request in flight by adding the :code:`cancel_async`
field to the JSON block. A async response remains in flight until the terminal
sends a response to the request. Note that cancellation requests dont need to
be encrypted as users must not be prompted for these and the worst a malicious
cancellation request can do is prevent another sync request from getting a
response.
Similar to async requests are *streaming* requests. In these the client has to
send a large amount of data to the terminal and so the request is split into
chunks. In every chunk the JSON block must contain the field ``stream`` set to
``true`` and ``stream_id`` set to a random long string, that should be the same for
all chunks in a request. End of data is indicated by sending a chunk with no data.
.. include:: generated/rc.rst .. include:: generated/rc.rst

View File

@ -304,13 +304,7 @@ The remote control protocol
----------------------------------------------- -----------------------------------------------
If you wish to develop your own client to talk to |kitty|, you can use the If you wish to develop your own client to talk to |kitty|, you can use the
:doc:`remote control protocol specification <rc_protocol>`. Note that there :doc:`remote control protocol specification <rc_protocol>`.
is a statically compiled, standalone executable, ``kitten`` available that
can be used as a remote control client on any UNIX like computer. This can be
downloaded and used directly from the `kitty releases
<https://github.com/kovidgoyal/kitty/releases>`__ page::
kitten @ --help
.. _search_syntax: .. _search_syntax:

View File

@ -1,21 +1,5 @@
A message from us at KittyPatch
===============================
KittyPatch was created as a home for useful features that are unavailable
in the kitty main branch. To this end, we do not accept donations directly.
If you wish to support KittyPatch: share your ideas and spread the word.
If you still wish to donate, please `support the Free Software Foundation
<https://my.fsf.org/donate>`.
A message from the maintainer of Kitty
======================================
Support kitty development ❤️ Support kitty development ❤️
============================== ==============================
>>>>>>> upstream/master
My goal with |kitty| is to move the stagnant terminal ecosystem forward. To that My goal with |kitty| is to move the stagnant terminal ecosystem forward. To that
end kitty has many foundational features, such as: :doc:`image support end kitty has many foundational features, such as: :doc:`image support

View File

@ -274,7 +274,6 @@ def graphics_parser() -> None:
'Y': ('cell_y_offset', 'uint'), 'Y': ('cell_y_offset', 'uint'),
'z': ('z_index', 'int'), 'z': ('z_index', 'int'),
'C': ('cursor_movement', 'uint'), 'C': ('cursor_movement', 'uint'),
'U': ('unicode_placement', 'uint'),
} }
text = generate('parse_graphics_code', 'screen_handle_graphics_command', 'graphics_command', keymap, 'GraphicsCommand') text = generate('parse_graphics_code', 'screen_handle_graphics_command', 'graphics_command', keymap, 'GraphicsCommand')
write_header(text, 'kitty/parse-graphics-command.h') write_header(text, 'kitty/parse-graphics-command.h')

View File

@ -3,7 +3,6 @@
import re import re
import subprocess
from typing import List from typing import List
from kitty.conf.generate import write_output from kitty.conf.generate import write_output
@ -12,25 +11,14 @@ from kitty.conf.generate import write_output
def patch_color_list(path: str, colors: List[str], name: str, spc: str = ' ') -> None: def patch_color_list(path: str, colors: List[str], name: str, spc: str = ' ') -> None:
with open(path, 'r+') as f: with open(path, 'r+') as f:
raw = f.read() raw = f.read()
colors = sorted(colors)
if path.endswith('.go'):
spc = '\t'
nraw = re.sub(
fr'(// {name}_COLORS_START).+?(\s+// {name}_COLORS_END)',
r'\1' + f'\n{spc}' + f'\n{spc}'.join(map(lambda x: f'"{x}":true,', colors)) + r'\2',
raw, flags=re.DOTALL | re.MULTILINE)
else:
nraw = re.sub( nraw = re.sub(
fr'(# {name}_COLORS_START).+?(\s+# {name}_COLORS_END)', fr'(# {name}_COLORS_START).+?(\s+# {name}_COLORS_END)',
r'\1' + f'\n{spc}' + f'\n{spc}'.join(map(lambda x: f'{x!r},', colors)) + r'\2', r'\1' + f'\n{spc}' + f'\n{spc}'.join(map(lambda x: f'{x!r},', sorted(colors))) + r'\2',
raw, flags=re.DOTALL | re.MULTILINE) raw, flags=re.DOTALL | re.MULTILINE)
if nraw != raw: if nraw != raw:
f.seek(0) f.seek(0)
f.truncate() f.truncate()
f.write(nraw) f.write(nraw)
f.flush()
if path.endswith('.go'):
subprocess.check_call(['gofmt', '-w', path])
def main() -> None: def main() -> None:
@ -46,8 +34,12 @@ def main() -> None:
elif opt.parser_func.__name__ in ('to_color', 'titlebar_color', 'macos_titlebar_color'): elif opt.parser_func.__name__ in ('to_color', 'titlebar_color', 'macos_titlebar_color'):
all_colors.append(opt.name) all_colors.append(opt.name)
patch_color_list('kitty/rc/set_colors.py', nullable_colors, 'NULLABLE') patch_color_list('kitty/rc/set_colors.py', nullable_colors, 'NULLABLE')
patch_color_list('tools/cmd/at/set_colors.go', nullable_colors, 'NULLABLE') patch_color_list('kittens/themes/collection.py', all_colors, 'ALL', ' ' * 8)
patch_color_list('tools/themes/collection.go', all_colors, 'ALL')
from kittens.diff.options.definition import definition as kd
write_output('kittens.diff', kd)
from kittens.ssh.options.definition import definition as sd
write_output('kittens.ssh', sd)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,767 +0,0 @@
#!./kitty/launcher/kitty +launch
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
import bz2
import io
import json
import os
import re
import struct
import subprocess
import sys
import tarfile
from contextlib import contextmanager, suppress
from functools import lru_cache
from itertools import chain
from typing import (
Any,
BinaryIO,
Dict,
Iterator,
List,
Optional,
Sequence,
Set,
TextIO,
Tuple,
Union,
)
import kitty.constants as kc
from kittens.tui.operations import Mode
from kittens.tui.spinners import spinners
from kitty.cli import (
CompletionSpec,
GoOption,
go_options_for_seq,
parse_option_spec,
serialize_as_go_string,
)
from kitty.conf.generate import gen_go_code
from kitty.conf.types import Definition
from kitty.guess_mime_type import known_extensions, text_mimes
from kitty.key_encoding import config_mod_map
from kitty.key_names import character_key_name_aliases, functional_key_name_aliases
from kitty.options.types import Options
from kitty.rc.base import RemoteCommand, all_command_names, command_for_name
from kitty.remote_control import global_options_spec
from kitty.rgb import color_names
changed: List[str] = []
def newer(dest: str, *sources: str) -> bool:
try:
dtime = os.path.getmtime(dest)
except OSError:
return True
for s in chain(sources, (__file__,)):
with suppress(FileNotFoundError):
if os.path.getmtime(s) >= dtime:
return True
return False
# Utils {{{
def serialize_go_dict(x: Union[Dict[str, int], Dict[int, str], Dict[int, int], Dict[str, str]]) -> str:
ans = []
def s(x: Union[int, str]) -> str:
if isinstance(x, int):
return str(x)
return f'"{serialize_as_go_string(x)}"'
for k, v in x.items():
ans.append(f'{s(k)}: {s(v)}')
return '{' + ', '.join(ans) + '}'
def replace(template: str, **kw: str) -> str:
for k, v in kw.items():
template = template.replace(k, v)
return template
# }}}
# Completions {{{
@lru_cache
def kitten_cli_docs(kitten: str) -> Any:
from kittens.runner import get_kitten_cli_docs
return get_kitten_cli_docs(kitten)
@lru_cache
def go_options_for_kitten(kitten: str) -> Tuple[Sequence[GoOption], Optional[CompletionSpec]]:
kcd = kitten_cli_docs(kitten)
if kcd:
ospec = kcd['options']
return (tuple(go_options_for_seq(parse_option_spec(ospec())[0])), kcd.get('args_completion'))
return (), None
def generate_kittens_completion() -> None:
from kittens.runner import all_kitten_names, get_kitten_wrapper_of
for kitten in sorted(all_kitten_names()):
kn = 'kitten_' + kitten
print(f'{kn} := plus_kitten.AddSubCommand(&cli.Command{{Name:"{kitten}", Group: "Kittens"}})')
wof = get_kitten_wrapper_of(kitten)
if wof:
print(f'{kn}.ArgCompleter = cli.CompletionForWrapper("{serialize_as_go_string(wof)}")')
print(f'{kn}.OnlyArgsAllowed = true')
continue
gopts, ac = go_options_for_kitten(kitten)
if gopts or ac:
for opt in gopts:
print(opt.as_option(kn))
if ac is not None:
print(''.join(ac.as_go_code(kn + '.ArgCompleter', ' = ')))
else:
print(f'{kn}.HelpText = ""')
@lru_cache
def clone_safe_launch_opts() -> Sequence[GoOption]:
from kitty.launch import clone_safe_opts, options_spec
ans = []
allowed = clone_safe_opts()
for o in go_options_for_seq(parse_option_spec(options_spec())[0]):
if o.obj_dict['name'] in allowed:
ans.append(o)
return tuple(ans)
def completion_for_launch_wrappers(*names: str) -> None:
for o in clone_safe_launch_opts():
for name in names:
print(o.as_option(name))
def generate_completions_for_kitty() -> None:
from kitty.config import option_names_for_completion
print('package completion\n')
print('import "kitty/tools/cli"')
print('import "kitty/tools/cmd/tool"')
print('import "kitty/tools/cmd/at"')
conf_names = ', '.join((f'"{serialize_as_go_string(x)}"' for x in option_names_for_completion()))
print('var kitty_option_names_for_completion = []string{' + conf_names + '}')
print('func kitty(root *cli.Command) {')
# The kitty exe
print('k := root.AddSubCommand(&cli.Command{'
'Name:"kitty", SubCommandIsOptional: true, ArgCompleter: cli.CompleteExecutableFirstArg, SubCommandMustBeFirst: true })')
print('kt := root.AddSubCommand(&cli.Command{Name:"kitten", SubCommandMustBeFirst: true })')
print('tool.KittyToolEntryPoints(kt)')
for opt in go_options_for_seq(parse_option_spec()[0]):
print(opt.as_option('k'))
# kitty +
print('plus := k.AddSubCommand(&cli.Command{Name:"+", Group:"Entry points", ShortDescription: "Various special purpose tools and kittens"})')
# kitty +launch
print('plus_launch := plus.AddSubCommand(&cli.Command{'
'Name:"launch", Group:"Entry points", ShortDescription: "Launch Python scripts", ArgCompleter: complete_plus_launch})')
print('k.AddClone("", plus_launch).Name = "+launch"')
# kitty +list-fonts
print('plus_list_fonts := plus.AddSubCommand(&cli.Command{'
'Name:"list-fonts", Group:"Entry points", ShortDescription: "List all available monospaced fonts"})')
print('k.AddClone("", plus_list_fonts).Name = "+list-fonts"')
# kitty +runpy
print('plus_runpy := plus.AddSubCommand(&cli.Command{'
'Name: "runpy", Group:"Entry points", ArgCompleter: complete_plus_runpy, ShortDescription: "Run Python code"})')
print('k.AddClone("", plus_runpy).Name = "+runpy"')
# kitty +open
print('plus_open := plus.AddSubCommand(&cli.Command{'
'Name:"open", Group:"Entry points", ArgCompleter: complete_plus_open, ShortDescription: "Open files and URLs"})')
print('for _, og := range k.OptionGroups { plus_open.OptionGroups = append(plus_open.OptionGroups, og.Clone(plus_open)) }')
print('k.AddClone("", plus_open).Name = "+open"')
# kitty +kitten
print('plus_kitten := plus.AddSubCommand(&cli.Command{Name:"kitten", Group:"Kittens", SubCommandMustBeFirst: true})')
generate_kittens_completion()
print('k.AddClone("", plus_kitten).Name = "+kitten"')
# @
print('at.EntryPoint(k)')
# clone-in-kitty, edit-in-kitty
print('cik := root.AddSubCommand(&cli.Command{Name:"clone-in-kitty"})')
completion_for_launch_wrappers('cik')
print('}')
print('func init() {')
print('cli.RegisterExeForCompletion(kitty)')
print('}')
# }}}
# rc command wrappers {{{
json_field_types: Dict[str, str] = {
'bool': 'bool', 'str': 'escaped_string', 'list.str': '[]escaped_string', 'dict.str': 'map[escaped_string]escaped_string', 'float': 'float64', 'int': 'int',
'scroll_amount': 'any', 'spacing': 'any', 'colors': 'any',
}
def go_field_type(json_field_type: str) -> str:
q = json_field_types.get(json_field_type)
if q:
return q
if json_field_type.startswith('choices.'):
return 'string'
if '.' in json_field_type:
p, r = json_field_type.split('.', 1)
p = {'list': '[]', 'dict': 'map[string]'}[p]
return p + go_field_type(r)
raise TypeError(f'Unknown JSON field type: {json_field_type}')
class JSONField:
def __init__(self, line: str) -> None:
field_def = line.split(':', 1)[0]
self.required = False
self.field, self.field_type = field_def.split('/', 1)
if self.field.endswith('+'):
self.required = True
self.field = self.field[:-1]
self.struct_field_name = self.field[0].upper() + self.field[1:]
def go_declaration(self) -> str:
return self.struct_field_name + ' ' + go_field_type(self.field_type) + f'`json:"{self.field},omitempty"`'
def go_code_for_remote_command(name: str, cmd: RemoteCommand, template: str) -> str:
template = '\n' + template[len('//go:build exclude'):]
NO_RESPONSE_BASE = 'false'
af: List[str] = []
a = af.append
af.extend(cmd.args.as_go_completion_code('ans'))
od: List[str] = []
option_map: Dict[str, GoOption] = {}
for o in rc_command_options(name):
option_map[o.go_var_name] = o
a(o.as_option('ans'))
if o.go_var_name in ('NoResponse', 'ResponseTimeout'):
continue
od.append(o.struct_declaration())
jd: List[str] = []
json_fields = []
field_types: Dict[str, str] = {}
for line in cmd.protocol_spec.splitlines():
line = line.strip()
if ':' not in line:
continue
f = JSONField(line)
json_fields.append(f)
field_types[f.field] = f.field_type
jd.append(f.go_declaration())
jc: List[str] = []
handled_fields: Set[str] = set()
jc.extend(cmd.args.as_go_code(name, field_types, handled_fields))
unhandled = {}
used_options = set()
for field in json_fields:
oq = (cmd.field_to_option_map or {}).get(field.field, field.field)
oq = ''.join(x.capitalize() for x in oq.split('_'))
if oq in option_map:
o = option_map[oq]
used_options.add(oq)
if field.field_type == 'str':
jc.append(f'payload.{field.struct_field_name} = escaped_string(options_{name}.{o.go_var_name})')
elif field.field_type == 'list.str':
jc.append(f'payload.{field.struct_field_name} = escape_list_of_strings(options_{name}.{o.go_var_name})')
elif field.field_type == 'dict.str':
jc.append(f'payload.{field.struct_field_name} = escape_dict_of_strings(options_{name}.{o.go_var_name})')
else:
jc.append(f'payload.{field.struct_field_name} = options_{name}.{o.go_var_name}')
elif field.field in handled_fields:
pass
else:
unhandled[field.field] = field
for x in tuple(unhandled):
if x == 'match_window' and 'Match' in option_map and 'Match' not in used_options:
used_options.add('Match')
o = option_map['Match']
field = unhandled[x]
if field.field_type == 'str':
jc.append(f'payload.{field.struct_field_name} = escaped_string(options_{name}.{o.go_var_name})')
else:
jc.append(f'payload.{field.struct_field_name} = options_{name}.{o.go_var_name}')
del unhandled[x]
if unhandled:
raise SystemExit(f'Cant map fields: {", ".join(unhandled)} for cmd: {name}')
if name != 'send_text':
unused_options = set(option_map) - used_options - {'NoResponse', 'ResponseTimeout'}
if unused_options:
raise SystemExit(f'Unused options: {", ".join(unused_options)} for command: {name}')
argspec = cmd.args.spec
if argspec:
argspec = ' ' + argspec
ans = replace(
template,
CMD_NAME=name, __FILE__=__file__, CLI_NAME=name.replace('_', '-'),
SHORT_DESC=serialize_as_go_string(cmd.short_desc),
LONG_DESC=serialize_as_go_string(cmd.desc.strip()),
IS_ASYNC='true' if cmd.is_asynchronous else 'false',
NO_RESPONSE_BASE=NO_RESPONSE_BASE, ADD_FLAGS_CODE='\n'.join(af),
WAIT_TIMEOUT=str(cmd.response_timeout),
OPTIONS_DECLARATION_CODE='\n'.join(od),
JSON_DECLARATION_CODE='\n'.join(jd),
JSON_INIT_CODE='\n'.join(jc), ARGSPEC=argspec,
STRING_RESPONSE_IS_ERROR='true' if cmd.string_return_is_error else 'false',
STREAM_WANTED='true' if cmd.reads_streaming_data else 'false',
)
return ans
# }}}
# kittens {{{
@lru_cache
def wrapped_kittens() -> Sequence[str]:
with open('shell-integration/ssh/kitty') as f:
for line in f:
if line.startswith(' wrapped_kittens="'):
val = line.strip().partition('"')[2][:-1]
return tuple(sorted(filter(None, val.split())))
raise Exception('Failed to read wrapped kittens from kitty wrapper script')
def generate_conf_parser(kitten: str, defn: Definition) -> None:
with replace_if_needed(f'kittens/{kitten}/conf_generated.go'):
print(f'package {kitten}')
print(gen_go_code(defn))
def generate_extra_cli_parser(name: str, spec: str) -> None:
print('import "kitty/tools/cli"')
go_opts = tuple(go_options_for_seq(parse_option_spec(spec)[0]))
print(f'type {name}_options struct ''{')
for opt in go_opts:
print(opt.struct_declaration())
print('}')
print(f'func parse_{name}_args(args []string) (*{name}_options, []string, error) ''{')
print(f'root := cli.Command{{Name: `{name}` }}')
for opt in go_opts:
print(opt.as_option('root'))
print('cmd, err := root.ParseArgs(args)')
print('if err != nil { return nil, nil, err }')
print(f'var opts {name}_options')
print('err = cmd.GetOptionValues(&opts)')
print('if err != nil { return nil, nil, err }')
print('return &opts, cmd.Args, nil')
print('}')
def kitten_clis() -> None:
from kittens.runner import get_kitten_conf_docs, get_kitten_extra_cli_parsers
for kitten in wrapped_kittens():
defn = get_kitten_conf_docs(kitten)
if defn is not None:
generate_conf_parser(kitten, defn)
ecp = get_kitten_extra_cli_parsers(kitten)
if ecp:
for name, spec in ecp.items():
with replace_if_needed(f'kittens/{kitten}/{name}_cli_generated.go'):
print(f'package {kitten}')
generate_extra_cli_parser(name, spec)
with replace_if_needed(f'kittens/{kitten}/cli_generated.go'):
od = []
kcd = kitten_cli_docs(kitten)
has_underscore = '_' in kitten
print(f'package {kitten}')
print('import "kitty/tools/cli"')
print('func create_cmd(root *cli.Command, run_func func(*cli.Command, *Options, []string)(int, error)) {')
print('ans := root.AddSubCommand(&cli.Command{')
print(f'Name: "{kitten}",')
if kcd:
print(f'ShortDescription: "{serialize_as_go_string(kcd["short_desc"])}",')
if kcd['usage']:
print(f'Usage: "[options] {serialize_as_go_string(kcd["usage"])}",')
print(f'HelpText: "{serialize_as_go_string(kcd["help_text"])}",')
print('Run: func(cmd *cli.Command, args []string) (int, error) {')
print('opts := Options{}')
print('err := cmd.GetOptionValues(&opts)')
print('if err != nil { return 1, err }')
print('return run_func(cmd, &opts, args)},')
if has_underscore:
print('Hidden: true,')
print('})')
gopts, ac = go_options_for_kitten(kitten)
for opt in gopts:
print(opt.as_option('ans'))
od.append(opt.struct_declaration())
if ac is not None:
print(''.join(ac.as_go_code('ans.ArgCompleter', ' = ')))
if has_underscore:
print("clone := root.AddClone(ans.Group, ans)")
print('clone.Hidden = false')
print(f'clone.Name = "{serialize_as_go_string(kitten.replace("_", "-"))}"')
if not kcd:
print('specialize_command(ans)')
print('}')
print('type Options struct {')
print('\n'.join(od))
print('}')
# }}}
# Constants {{{
def generate_spinners() -> str:
ans = ['package tui', 'import "time"', 'func NewSpinner(name string) *Spinner {', 'var ans *Spinner', 'switch name {']
a = ans.append
for name, spinner in spinners.items():
a(f'case "{serialize_as_go_string(name)}":')
a('ans = &Spinner{')
a(f'Name: "{serialize_as_go_string(name)}",')
a(f'interval: {spinner["interval"]},')
frames = ', '.join(f'"{serialize_as_go_string(x)}"' for x in spinner['frames'])
a(f'frames: []string{{{frames}}},')
a('}')
a('}')
a('if ans != nil {')
a('ans.interval *= time.Millisecond')
a('ans.current_frame = -1')
a('ans.last_change_at = time.Now().Add(-ans.interval)')
a('}')
a('return ans}')
return '\n'.join(ans)
def generate_color_names() -> str:
selfg = "" if Options.selection_foreground is None else Options.selection_foreground.as_sharp
selbg = "" if Options.selection_background is None else Options.selection_background.as_sharp
cursor = "" if Options.cursor is None else Options.cursor.as_sharp
return 'package style\n\nvar ColorNames = map[string]RGBA{' + '\n'.join(
f'\t"{name}": RGBA{{ Red:{val.red}, Green:{val.green}, Blue:{val.blue} }},'
for name, val in color_names.items()
) + '\n}' + '\n\nvar ColorTable = [256]uint32{' + ', '.join(
f'{x}' for x in Options.color_table) + '}\n' + f'''
var DefaultColors = struct {{
Foreground, Background, Cursor, SelectionFg, SelectionBg string
}}{{
Foreground: "{Options.foreground.as_sharp}",
Background: "{Options.background.as_sharp}",
Cursor: "{cursor}",
SelectionFg: "{selfg}",
SelectionBg: "{selbg}",
}}
'''
def load_ref_map() -> Dict[str, Dict[str, str]]:
with open('kitty/docs_ref_map_generated.h') as f:
raw = f.read()
raw = raw.split('{', 1)[1].split('}', 1)[0]
data = json.loads(bytes(bytearray(json.loads(f'[{raw}]'))))
return data # type: ignore
def generate_constants() -> str:
from kittens.hints.main import DEFAULT_REGEX
from kitty.options.types import Options
from kitty.options.utils import allowed_shell_integration_values
del sys.modules['kittens.hints.main']
ref_map = load_ref_map()
with open('kitty/data-types.h') as dt:
m = re.search(r'^#define IMAGE_PLACEHOLDER_CHAR (\S+)', dt.read(), flags=re.M)
assert m is not None
placeholder_char = int(m.group(1), 16)
dp = ", ".join(map(lambda x: f'"{serialize_as_go_string(x)}"', kc.default_pager_for_help))
url_prefixes = ','.join(f'"{x}"' for x in Options.url_prefixes)
return f'''\
package kitty
type VersionType struct {{
Major, Minor, Patch int
}}
const VersionString string = "{kc.str_version}"
const WebsiteBaseURL string = "{kc.website_base_url}"
const ImagePlaceholderChar rune = {placeholder_char}
const VCSRevision string = ""
const SSHControlMasterTemplate = "{kc.ssh_control_master_template}"
const RC_ENCRYPTION_PROTOCOL_VERSION string = "{kc.RC_ENCRYPTION_PROTOCOL_VERSION}"
const IsFrozenBuild bool = false
const IsStandaloneBuild bool = false
const HandleTermiosSignals = {Mode.HANDLE_TERMIOS_SIGNALS.value[0]}
const HintsDefaultRegex = `{DEFAULT_REGEX}`
var Version VersionType = VersionType{{Major: {kc.version.major}, Minor: {kc.version.minor}, Patch: {kc.version.patch},}}
var DefaultPager []string = []string{{ {dp} }}
var FunctionalKeyNameAliases = map[string]string{serialize_go_dict(functional_key_name_aliases)}
var CharacterKeyNameAliases = map[string]string{serialize_go_dict(character_key_name_aliases)}
var ConfigModMap = map[string]uint16{serialize_go_dict(config_mod_map)}
var RefMap = map[string]string{serialize_go_dict(ref_map['ref'])}
var DocTitleMap = map[string]string{serialize_go_dict(ref_map['doc'])}
var AllowedShellIntegrationValues = []string{{ {str(sorted(allowed_shell_integration_values))[1:-1].replace("'", '"')} }}
var KittyConfigDefaults = struct {{
Term, Shell_integration, Select_by_word_characters string
Wheel_scroll_multiplier int
Url_prefixes []string
}}{{
Term: "{Options.term}", Shell_integration: "{' '.join(Options.shell_integration)}", Url_prefixes: []string{{ {url_prefixes} }},
Select_by_word_characters: `{Options.select_by_word_characters}`, Wheel_scroll_multiplier: {Options.wheel_scroll_multiplier},
}}
''' # }}}
# Boilerplate {{{
@contextmanager
def replace_if_needed(path: str, show_diff: bool = False) -> Iterator[io.StringIO]:
buf = io.StringIO()
origb = sys.stdout
sys.stdout = buf
try:
yield buf
finally:
sys.stdout = origb
orig = ''
with suppress(FileNotFoundError), open(path, 'r') as f:
orig = f.read()
new = buf.getvalue()
new = f'// Code generated by {os.path.basename(__file__)}; DO NOT EDIT.\n\n' + new
if orig != new:
changed.append(path)
if show_diff:
with open(path + '.new', 'w') as f:
f.write(new)
subprocess.run(['diff', '-Naurp', path, f.name], stdout=open('/dev/tty', 'w'))
os.remove(f.name)
with open(path, 'w') as f:
f.write(new)
@lru_cache(maxsize=256)
def rc_command_options(name: str) -> Tuple[GoOption, ...]:
cmd = command_for_name(name)
return tuple(go_options_for_seq(parse_option_spec(cmd.options_spec or '\n\n')[0]))
def update_at_commands() -> None:
with open('tools/cmd/at/template.go') as f:
template = f.read()
for name in all_command_names():
cmd = command_for_name(name)
code = go_code_for_remote_command(name, cmd, template)
dest = f'tools/cmd/at/cmd_{name}_generated.go'
with replace_if_needed(dest) as f:
f.write(code)
struct_def = []
opt_def = []
for o in go_options_for_seq(parse_option_spec(global_options_spec())[0]):
struct_def.append(o.struct_declaration())
opt_def.append(o.as_option(depth=1, group="Global options"))
sdef = '\n'.join(struct_def)
odef = '\n'.join(opt_def)
code = f'''
package at
import "kitty/tools/cli"
type rc_global_options struct {{
{sdef}
}}
var rc_global_opts rc_global_options
func add_rc_global_opts(cmd *cli.Command) {{
{odef}
}}
'''
with replace_if_needed('tools/cmd/at/global_opts_generated.go') as f:
f.write(code)
def update_completion() -> None:
with replace_if_needed('tools/cmd/completion/kitty_generated.go'):
generate_completions_for_kitty()
with replace_if_needed('tools/cmd/edit_in_kitty/launch_generated.go'):
print('package edit_in_kitty')
print('import "kitty/tools/cli"')
print('func AddCloneSafeOpts(cmd *cli.Command) {')
completion_for_launch_wrappers('cmd')
print(''.join(CompletionSpec.from_string('type:file mime:text/* group:"Text files"').as_go_code('cmd.ArgCompleter', ' = ')))
print('}')
def define_enum(package_name: str, type_name: str, items: str, underlying_type: str = 'uint') -> str:
actions = []
for x in items.splitlines():
x = x.strip()
if x:
actions.append(x)
ans = [f'package {package_name}', 'import "strconv"', f'type {type_name} {underlying_type}', 'const (']
stringer = [f'func (ac {type_name}) String() string ''{', 'switch(ac) {']
for i, ac in enumerate(actions):
stringer.append(f'case {ac}: return "{ac}"')
if i == 0:
ac = ac + f' {type_name} = iota'
ans.append(ac)
ans.append(')')
stringer.append('}\nreturn strconv.Itoa(int(ac)) }')
return '\n'.join(ans + stringer)
def generate_readline_actions() -> str:
return define_enum('readline', 'Action', '''\
ActionNil
ActionBackspace
ActionDelete
ActionMoveToStartOfLine
ActionMoveToEndOfLine
ActionMoveToStartOfDocument
ActionMoveToEndOfDocument
ActionMoveToEndOfWord
ActionMoveToStartOfWord
ActionCursorLeft
ActionCursorRight
ActionEndInput
ActionAcceptInput
ActionCursorUp
ActionHistoryPreviousOrCursorUp
ActionCursorDown
ActionHistoryNextOrCursorDown
ActionHistoryNext
ActionHistoryPrevious
ActionHistoryFirst
ActionHistoryLast
ActionHistoryIncrementalSearchBackwards
ActionHistoryIncrementalSearchForwards
ActionTerminateHistorySearchAndApply
ActionTerminateHistorySearchAndRestore
ActionClearScreen
ActionAddText
ActionAbortCurrentLine
ActionStartKillActions
ActionKillToEndOfLine
ActionKillToStartOfLine
ActionKillNextWord
ActionKillPreviousWord
ActionKillPreviousSpaceDelimitedWord
ActionEndKillActions
ActionYank
ActionPopYank
ActionNumericArgumentDigit0
ActionNumericArgumentDigit1
ActionNumericArgumentDigit2
ActionNumericArgumentDigit3
ActionNumericArgumentDigit4
ActionNumericArgumentDigit5
ActionNumericArgumentDigit6
ActionNumericArgumentDigit7
ActionNumericArgumentDigit8
ActionNumericArgumentDigit9
ActionNumericArgumentDigitMinus
ActionCompleteForward
ActionCompleteBackward
''')
def generate_mimetypes() -> str:
import mimetypes
if not mimetypes.inited:
mimetypes.init()
ans = ['package utils', 'import "sync"', 'var only_once sync.Once', 'var builtin_types_map map[string]string',
'func set_builtins() {', 'builtin_types_map = map[string]string{',]
for k, v in mimetypes.types_map.items():
ans.append(f' "{serialize_as_go_string(k)}": "{serialize_as_go_string(v)}",')
ans.append('}}')
return '\n'.join(ans)
def generate_textual_mimetypes() -> str:
ans = ['package utils', 'var KnownTextualMimes = map[string]bool{',]
for k in text_mimes:
ans.append(f' "{serialize_as_go_string(k)}": true,')
ans.append('}')
ans.append('var KnownExtensions = map[string]string{')
for k, v in known_extensions.items():
ans.append(f' ".{serialize_as_go_string(k)}": "{serialize_as_go_string(v)}",')
ans.append('}')
return '\n'.join(ans)
def write_compressed_data(data: bytes, d: BinaryIO) -> None:
d.write(struct.pack('<I', len(data)))
d.write(bz2.compress(data))
def generate_unicode_names(src: TextIO, dest: BinaryIO) -> None:
num_names, num_of_words = map(int, next(src).split())
gob = io.BytesIO()
gob.write(struct.pack('<II', num_names, num_of_words))
for line in src:
line = line.strip()
if line:
a, aliases = line.partition('\t')[::2]
cp, name = a.partition(' ')[::2]
ename = name.encode()
record = struct.pack('<IH', int(cp), len(ename)) + ename
if aliases:
record += aliases.encode()
gob.write(struct.pack('<H', len(record)) + record)
write_compressed_data(gob.getvalue(), dest)
def generate_ssh_kitten_data() -> None:
files = {
'terminfo/kitty.terminfo', 'terminfo/x/xterm-kitty',
}
for dirpath, dirnames, filenames in os.walk('shell-integration'):
for f in filenames:
path = os.path.join(dirpath, f)
files.add(path.replace(os.sep, '/'))
dest = 'kittens/ssh/data_generated.bin'
def normalize(t: tarfile.TarInfo) -> tarfile.TarInfo:
t.uid = t.gid = 0
t.uname = t.gname = ''
return t
if newer(dest, *files):
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode='w') as tf:
for f in sorted(files):
tf.add(f, filter=normalize)
with open(dest, 'wb') as d:
write_compressed_data(buf.getvalue(), d)
def main() -> None:
with replace_if_needed('constants_generated.go') as f:
f.write(generate_constants())
with replace_if_needed('tools/utils/style/color-names_generated.go') as f:
f.write(generate_color_names())
with replace_if_needed('tools/tui/readline/actions_generated.go') as f:
f.write(generate_readline_actions())
with replace_if_needed('tools/tui/spinners_generated.go') as f:
f.write(generate_spinners())
with replace_if_needed('tools/utils/mimetypes_generated.go') as f:
f.write(generate_mimetypes())
with replace_if_needed('tools/utils/mimetypes_textual_generated.go') as f:
f.write(generate_textual_mimetypes())
if newer('tools/unicode_names/data_generated.bin', 'tools/unicode_names/names.txt'):
with open('tools/unicode_names/data_generated.bin', 'wb') as dest, open('tools/unicode_names/names.txt') as src:
generate_unicode_names(src, dest)
generate_ssh_kitten_data()
update_completion()
update_at_commands()
kitten_clis()
print(json.dumps(changed, indent=2))
if __name__ == '__main__':
main() # }}}

View File

@ -2,8 +2,8 @@
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net> # License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
import string import string
from typing import Dict, List, Any
from pprint import pformat from pprint import pformat
from typing import Any, Dict, List, Union
functional_key_defs = '''# {{{ functional_key_defs = '''# {{{
# kitty XKB macVK macU # kitty XKB macVK macU
@ -130,7 +130,7 @@ functional_encoding_overrides = {
} }
different_trailer_functionals = { different_trailer_functionals = {
'up': 'A', 'down': 'B', 'right': 'C', 'left': 'D', 'kp_begin': 'E', 'end': 'F', 'home': 'H', 'up': 'A', 'down': 'B', 'right': 'C', 'left': 'D', 'kp_begin': 'E', 'end': 'F', 'home': 'H',
'f1': 'P', 'f2': 'Q', 'f3': '~', 'f4': 'S', 'enter': 'u', 'tab': 'u', 'f1': 'P', 'f2': 'Q', 'f3': 'R', 'f4': 'S', 'enter': 'u', 'tab': 'u',
'backspace': 'u', 'escape': 'u' 'backspace': 'u', 'escape': 'u'
} }
@ -248,19 +248,6 @@ def serialize_dict(x: Dict[Any, Any]) -> str:
return pformat(x, indent=4).replace('{', '{\n ', 1) return pformat(x, indent=4).replace('{', '{\n ', 1)
def serialize_go_dict(x: Union[Dict[str, int], Dict[int, str], Dict[int, int]]) -> str:
ans = []
def s(x: Union[int, str]) -> str:
if isinstance(x, int):
return str(x)
return f'"{x}"'
for k, v in x.items():
ans.append(f'{s(k)}: {s(v)}')
return '{' + ', '.join(ans) + '}'
def generate_glfw_header() -> None: def generate_glfw_header() -> None:
lines = [ lines = [
'typedef enum {', 'typedef enum {',
@ -322,20 +309,14 @@ def generate_functional_table() -> None:
patch_file('kitty/key_encoding.c', 'special numbers', '\n'.join(enc_lines)) patch_file('kitty/key_encoding.c', 'special numbers', '\n'.join(enc_lines))
code_to_name = {v: k.upper() for k, v in name_to_code.items()} code_to_name = {v: k.upper() for k, v in name_to_code.items()}
csi_map = {v: name_to_code[k] for k, v in functional_encoding_overrides.items()} csi_map = {v: name_to_code[k] for k, v in functional_encoding_overrides.items()}
letter_trailer_codes: Dict[str, int] = { letter_trailer_codes = {
v: functional_encoding_overrides.get(k, name_to_code.get(k, 0)) v: functional_encoding_overrides.get(k, name_to_code.get(k))
for k, v in different_trailer_functionals.items() if v in 'ABCDEHFPQRSZ'} for k, v in different_trailer_functionals.items() if v in 'ABCDEHFPQRSZ'}
text = f'functional_key_number_to_name_map = {serialize_dict(code_to_name)}' text = f'functional_key_number_to_name_map = {serialize_dict(code_to_name)}'
text += f'\ncsi_number_to_functional_number_map = {serialize_dict(csi_map)}' text += f'\ncsi_number_to_functional_number_map = {serialize_dict(csi_map)}'
text += f'\nletter_trailer_to_csi_number_map = {letter_trailer_codes!r}' text += f'\nletter_trailer_to_csi_number_map = {letter_trailer_codes!r}'
text += f'\ntilde_trailers = {tilde_trailers!r}' text += f'\ntilde_trailers = {tilde_trailers!r}'
patch_file('kitty/key_encoding.py', 'csi mapping', text, start_marker='# ', end_marker='') patch_file('kitty/key_encoding.py', 'csi mapping', text, start_marker='# ', end_marker='')
text = f'var functional_key_number_to_name_map = map[int]string{serialize_go_dict(code_to_name)}\n'
text += f'\nvar csi_number_to_functional_number_map = map[int]int{serialize_go_dict(csi_map)}\n'
text += f'\nvar letter_trailer_to_csi_number_map = map[string]int{serialize_go_dict(letter_trailer_codes)}\n'
tt = ', '.join(f'{x}: true' for x in tilde_trailers)
text += '\nvar tilde_trailers = map[int]bool{' + f'{tt}' + '}\n'
patch_file('tools/tui/loop/key-encoding.go', 'csi mapping', text, start_marker='// ', end_marker='')
def generate_legacy_text_key_maps() -> None: def generate_legacy_text_key_maps() -> None:

View File

@ -1,48 +0,0 @@
#!/usr/bin/env python3
# vim:fileencoding=utf-8
import os
from typing import List
def to_linear(a: float) -> float:
if a <= 0.04045:
return a / 12.92
else:
return float(pow((a + 0.055) / 1.055, 2.4))
def generate_srgb_lut(line_prefix: str = '') -> List[str]:
values: List[str] = []
lines: List[str] = []
for i in range(256):
values.append('{:1.5f}f'.format(to_linear(i / 255.0)))
for i in range(16):
lines.append(line_prefix + ', '.join(values[i * 16:(i + 1) * 16]) + ',')
return lines
def generate_srgb_gamma_c() -> str:
lines: List[str] = []
lines.append('// Generated by gen-srgb-lut.py DO NOT edit')
lines.append('#include "srgb_gamma.h"')
lines.append('')
lines.append('const GLfloat srgb_lut[256] = {')
lines += generate_srgb_lut(' ')
lines.append('};')
return "\n".join(lines)
def main() -> None:
c = generate_srgb_gamma_c()
with open(os.path.join('kitty', 'srgb_gamma.c'), 'w') as f:
f.write(f'{c}\n')
if __name__ == '__main__':
main()

View File

@ -3,26 +3,17 @@
import os import os
import re import re
import subprocess
import sys import sys
from collections import defaultdict from collections import defaultdict
from contextlib import contextmanager from contextlib import contextmanager
from functools import lru_cache, partial from datetime import date
from functools import partial
from html.entities import html5 from html.entities import html5
from itertools import groupby from itertools import groupby
from operator import itemgetter from operator import itemgetter
from typing import ( from typing import (
Callable, Callable, DefaultDict, Dict, FrozenSet, Generator, Iterable, List,
DefaultDict, Optional, Set, Tuple, Union
Dict,
FrozenSet,
Generator,
Iterable,
List,
Optional,
Set,
Tuple,
Union,
) )
from urllib.request import urlopen from urllib.request import urlopen
@ -53,15 +44,6 @@ def get_data(fname: str, folder: str = 'UCD') -> Iterable[str]:
yield line yield line
@lru_cache(maxsize=2)
def unicode_version() -> Tuple[int, int, int]:
for line in get_data("ReadMe.txt"):
m = re.search(r'Version\s+(\d+)\.(\d+)\.(\d+)', line)
if m is not None:
return int(m.group(1)), int(m.group(2)), int(m.group(3))
raise ValueError('Could not find Unicode Version')
# Map of class names to set of codepoints in class # Map of class names to set of codepoints in class
class_maps: Dict[str, Set[int]] = {} class_maps: Dict[str, Set[int]] = {}
all_symbols: Set[int] = set() all_symbols: Set[int] = set()
@ -272,12 +254,8 @@ def get_ranges(items: List[int]) -> Generator[Union[int, Tuple[int, int]], None,
yield a, b yield a, b
def write_case(spec: Union[Tuple[int, ...], int], p: Callable[..., None], for_go: bool = False) -> None: def write_case(spec: Union[Tuple[int, ...], int], p: Callable[..., None]) -> None:
if isinstance(spec, tuple): if isinstance(spec, tuple):
if for_go:
v = ', '.join(f'0x{x:x}' for x in range(spec[0], spec[1] + 1))
p(f'\t\tcase {v}:')
else:
p('\t\tcase 0x{:x} ... 0x{:x}:'.format(*spec)) p('\t\tcase 0x{:x} ... 0x{:x}:'.format(*spec))
else: else:
p(f'\t\tcase 0x{spec:x}:') p(f'\t\tcase 0x{spec:x}:')
@ -287,8 +265,8 @@ def write_case(spec: Union[Tuple[int, ...], int], p: Callable[..., None], for_go
def create_header(path: str, include_data_types: bool = True) -> Generator[Callable[..., None], None, None]: def create_header(path: str, include_data_types: bool = True) -> Generator[Callable[..., None], None, None]:
with open(path, 'w') as f: with open(path, 'w') as f:
p = partial(print, file=f) p = partial(print, file=f)
p('// Unicode data, built from the Unicode Standard', '.'.join(map(str, unicode_version()))) p('// unicode data, built from the unicode standard on:', date.today())
p(f'// Code generated by {os.path.basename(__file__)}, DO NOT EDIT.', end='\n\n') p('// see gen-wcwidth.py')
if path.endswith('.h'): if path.endswith('.h'):
p('#pragma once') p('#pragma once')
if include_data_types: if include_data_types:
@ -369,19 +347,13 @@ def codepoint_to_mark_map(p: Callable[..., None], mark_map: List[int]) -> Dict[i
return rmap return rmap
def classes_to_regex(classes: Iterable[str], exclude: str = '', for_go: bool = True) -> Iterable[str]: def classes_to_regex(classes: Iterable[str], exclude: str = '') -> Iterable[str]:
chars: Set[int] = set() chars: Set[int] = set()
for c in classes: for c in classes:
chars |= class_maps[c] chars |= class_maps[c]
for x in map(ord, exclude): for x in map(ord, exclude):
chars.discard(x) chars.discard(x)
if for_go:
def as_string(codepoint: int) -> str:
if codepoint < 256:
return fr'\x{codepoint:02x}'
return fr'\x{{{codepoint:x}}}'
else:
def as_string(codepoint: int) -> str: def as_string(codepoint: int) -> str:
if codepoint < 256: if codepoint < 256:
return fr'\x{codepoint:02x}' return fr'\x{codepoint:02x}'
@ -444,144 +416,153 @@ def gen_ucd() -> None:
f.truncate() f.truncate()
f.write(raw) f.write(raw)
chars = ''.join(classes_to_regex(cz, exclude='\n\r')) with open('kittens/hints/url_regex.py', 'w') as f:
with open('tools/cmd/hints/url_regex.go', 'w') as f: f.write('# generated by gen-wcwidth.py, do not edit\n\n')
f.write('// generated by gen-wcwidth.py, do not edit\n\n') f.write("url_delimiters = '{}' # noqa".format(''.join(classes_to_regex(cz, exclude='\n\r'))))
f.write('package hints\n\n')
f.write(f'const URL_DELIMITERS = `{chars}`\n')
def gen_names() -> None: def gen_names() -> None:
aliases_map: Dict[int, Set[str]] = {} with create_header('kittens/unicode_input/names.h') as p:
for word, codepoints in word_search_map.items(): mark_to_cp = list(sorted(name_map))
for cp in codepoints: cp_to_mark = {cp: m for m, cp in enumerate(mark_to_cp)}
aliases_map.setdefault(cp, set()).add(word) # Mapping of mark to codepoint name
if len(name_map) > 0xffff: p(f'static const char* name_map[{len(mark_to_cp)}] = {{' ' // {{{')
raise Exception('Too many named codepoints') for cp in mark_to_cp:
with open('tools/unicode_names/names.txt', 'w') as f: w = name_map[cp].replace('"', '\\"')
print(len(name_map), len(word_search_map), file=f) p(f'\t"{w}",')
for cp in sorted(name_map): p("}; // }}}\n")
name = name_map[cp]
words = name.lower().split() # Mapping of mark to codepoint
aliases = aliases_map.get(cp, set()) - set(words) p(f'static const char_type mark_to_cp[{len(mark_to_cp)}] = {{' ' // {{{')
end = '\n' p(', '.join(map(str, mark_to_cp)))
if aliases: p('}; // }}}\n')
end = '\t' + ' '.join(sorted(aliases)) + end
print(cp, *words, end=end, file=f) # Function to get mark number for codepoint
p('static char_type mark_for_codepoint(char_type c) {')
codepoint_to_mark_map(p, mark_to_cp)
p('}\n')
p('static inline const char* name_for_codepoint(char_type cp) {')
p('\tchar_type m = mark_for_codepoint(cp); if (m == 0) return NULL;')
p('\treturn name_map[m];')
p('}\n')
# Array of all words
word_map = tuple(sorted(word_search_map))
word_rmap = {w: i for i, w in enumerate(word_map)}
p(f'static const char* all_words_map[{len(word_map)}] = {{' ' // {{{')
cwords = (w.replace('"', '\\"') for w in word_map)
p(', '.join(f'"{w}"' for w in cwords))
p('}; // }}}\n')
# Array of sets of marks for each word
word_to_marks = {word_rmap[w]: frozenset(map(cp_to_mark.__getitem__, cps)) for w, cps in word_search_map.items()}
all_mark_groups = frozenset(word_to_marks.values())
array = [0]
mg_to_offset = {}
for mg in all_mark_groups:
mg_to_offset[mg] = len(array)
array.append(len(mg))
array.extend(sorted(mg))
p(f'static const char_type mark_groups[{len(array)}] = {{' ' // {{{')
p(', '.join(map(str, array)))
p('}; // }}}\n')
offsets_array = []
for wi, w in enumerate(word_map):
mg = word_to_marks[wi]
offsets_array.append(mg_to_offset[mg])
p(f'static const char_type mark_to_offset[{len(offsets_array)}] = {{' ' // {{{')
p(', '.join(map(str, offsets_array)))
p('}; // }}}\n')
# The trie
p('typedef struct { uint32_t children_offset; uint32_t match_offset; } word_trie;\n')
all_trie_nodes: List['TrieNode'] = [] # noqa
class TrieNode:
def __init__(self) -> None:
self.match_offset = 0
self.children_offset = 0
self.children: Dict[int, int] = {}
def add_letter(self, letter: int) -> int:
if letter not in self.children:
self.children[letter] = len(all_trie_nodes)
all_trie_nodes.append(TrieNode())
return self.children[letter]
def __str__(self) -> str:
return f'{{ .children_offset={self.children_offset}, .match_offset={self.match_offset} }}'
root = TrieNode()
all_trie_nodes.append(root)
def add_word(word_idx: int, word: str) -> None:
parent = root
for letter in map(ord, word):
idx = parent.add_letter(letter)
parent = all_trie_nodes[idx]
parent.match_offset = offsets_array[word_idx]
for i, word in enumerate(word_map):
add_word(i, word)
children_array = [0]
for node in all_trie_nodes:
if node.children:
node.children_offset = len(children_array)
children_array.append(len(node.children))
for letter, child_offset in node.children.items():
children_array.append((child_offset << 8) | (letter & 0xff))
p(f'static const word_trie all_trie_nodes[{len(all_trie_nodes)}] = {{' ' // {{{')
p(',\n'.join(map(str, all_trie_nodes)))
p('\n}; // }}}\n')
p(f'static const uint32_t children_array[{len(children_array)}] = {{' ' // {{{')
p(', '.join(map(str, children_array)))
p('}; // }}}\n')
def gen_wcwidth() -> None: def gen_wcwidth() -> None:
seen: Set[int] = set() seen: Set[int] = set()
non_printing = class_maps['Cc'] | class_maps['Cf'] | class_maps['Cs']
def add(p: Callable[..., None], comment: str, chars_: Union[Set[int], FrozenSet[int]], ret: int, for_go: bool = False) -> None: def add(p: Callable[..., None], comment: str, chars_: Union[Set[int], FrozenSet[int]], ret: int) -> None:
chars = chars_ - seen chars = chars_ - seen
seen.update(chars) seen.update(chars)
p(f'\t\t// {comment} ({len(chars)} codepoints)' + ' {{' '{') p(f'\t\t// {comment} ({len(chars)} codepoints)' + ' {{' '{')
for spec in get_ranges(list(chars)): for spec in get_ranges(list(chars)):
write_case(spec, p, for_go) write_case(spec, p)
p(f'\t\t\treturn {ret};') p(f'\t\t\treturn {ret};')
p('\t\t// }}}\n') p('\t\t// }}}\n')
def add_all(p: Callable[..., None], for_go: bool = False) -> None: with create_header('kitty/wcwidth-std.h') as p:
seen.clear() p('static inline int\nwcwidth_std(int32_t code) {')
add(p, 'Flags', flag_codepoints, 2, for_go) p('\tif (LIKELY(0x20 <= code && code <= 0x7e)) return 1;')
add(p, 'Marks', marks | {0}, 0, for_go) p('\tswitch(code) {')
add(p, 'Non-printing characters', non_printing, -1, for_go)
add(p, 'Private use', class_maps['Co'], -3, for_go)
add(p, 'Text Presentation', narrow_emoji, 1, for_go)
add(p, 'East Asian ambiguous width', ambiguous, -2, for_go)
add(p, 'East Asian double width', doublewidth, 2, for_go)
add(p, 'Emoji Presentation', wide_emoji, 2, for_go)
add(p, 'Not assigned in the unicode character database', not_assigned, -4, for_go) non_printing = class_maps['Cc'] | class_maps['Cf'] | class_maps['Cs']
add(p, 'Flags', flag_codepoints, 2)
add(p, 'Marks', marks | {0}, 0)
add(p, 'Non-printing characters', non_printing, -1)
add(p, 'Private use', class_maps['Co'], -3)
add(p, 'Text Presentation', narrow_emoji, 1)
add(p, 'East Asian ambiguous width', ambiguous, -2)
add(p, 'East Asian double width', doublewidth, 2)
add(p, 'Emoji Presentation', wide_emoji, 2)
p('\t\tdefault:\n\t\t\treturn 1;') add(p, 'Not assigned in the unicode character database', not_assigned, -4)
p('\t\tdefault: return 1;')
p('\t}') p('\t}')
if for_go:
p('\t}')
else:
p('\treturn 1;\n}') p('\treturn 1;\n}')
with create_header('kitty/wcwidth-std.h') as p, open('tools/wcswidth/std.go', 'w') as gof:
gop = partial(print, file=gof)
gop('package wcswidth\n\n')
gop('func Runewidth(code rune) int {')
p('static inline int\nwcwidth_std(int32_t code) {')
p('\tif (LIKELY(0x20 <= code && code <= 0x7e)) { return 1; }')
p('\tswitch(code) {')
gop('\tswitch(code) {')
add_all(p)
add_all(gop, True)
p('static inline bool\nis_emoji_presentation_base(uint32_t code) {') p('static inline bool\nis_emoji_presentation_base(uint32_t code) {')
gop('func IsEmojiPresentationBase(code rune) bool {')
p('\tswitch(code) {') p('\tswitch(code) {')
gop('\tswitch(code) {')
for spec in get_ranges(list(emoji_presentation_bases)): for spec in get_ranges(list(emoji_presentation_bases)):
write_case(spec, p) write_case(spec, p)
write_case(spec, gop, for_go=True)
p('\t\t\treturn true;') p('\t\t\treturn true;')
gop('\t\t\treturn true;')
p('\t\tdefault: return false;') p('\t\tdefault: return false;')
p('\t}') p('\t}')
gop('\t\tdefault:\n\t\t\treturn false') p('\treturn 1;\n}')
gop('\t}')
p('\treturn true;\n}')
gop('\n}')
uv = unicode_version()
p(f'#define UNICODE_MAJOR_VERSION {uv[0]}')
p(f'#define UNICODE_MINOR_VERSION {uv[1]}')
p(f'#define UNICODE_PATCH_VERSION {uv[2]}')
gop('var UnicodeDatabaseVersion [3]int = [3]int{' f'{uv[0]}, {uv[1]}, {uv[2]}' + '}')
subprocess.check_call(['gofmt', '-w', '-s', gof.name])
def gen_rowcolumn_diacritics() -> None:
# codes of all row/column diacritics
codes = []
with open("./rowcolumn-diacritics.txt") as file:
for line in file.readlines():
if line.startswith('#'):
continue
code = int(line.split(";")[0], 16)
codes.append(code)
go_file = 'tools/utils/images/rowcolumn_diacritics.go'
with create_header('kitty/rowcolumn-diacritics.c') as p, create_header(go_file, include_data_types=False) as g:
p('#include "unicode-data.h"')
p('int diacritic_to_num(char_type code) {')
p('\tswitch (code) {')
g('package images')
g(f'var NumberToDiacritic = [{len(codes)}]rune''{')
g(', '.join(f'0x{x:x}' for x in codes) + ',')
g('}')
range_start_num = 1
range_start = 0
range_end = 0
def print_range() -> None:
if range_start >= range_end:
return
write_case((range_start, range_end), p)
p('\t\treturn code - ' + hex(range_start) + ' + ' +
str(range_start_num) + ';')
for code in codes:
if range_end == code:
range_end += 1
else:
print_range()
range_start_num += range_end - range_start
range_start = code
range_end = code + 1
print_range()
p('\t}')
p('\treturn 0;')
p('}')
subprocess.check_call(['gofmt', '-w', '-s', go_file])
parse_ucd() parse_ucd()
@ -592,4 +573,3 @@ gen_ucd()
gen_wcwidth() gen_wcwidth()
gen_emoji() gen_emoji()
gen_names() gen_names()
gen_rowcolumn_diacritics()

View File

@ -8,9 +8,10 @@ import shlex
import shutil import shutil
import subprocess import subprocess
cmdline = ( cmdline = (
'glad --out-path {dest} --api gl:core=3.1 ' 'glad --out-path {dest} --api gl:core=3.3 '
' --extensions GL_ARB_texture_storage,GL_ARB_copy_image,GL_ARB_multisample,GL_ARB_robustness,GL_ARB_instanced_arrays,GL_KHR_debug ' ' --extensions GL_ARB_texture_storage,GL_ARB_copy_image,GL_ARB_multisample,GL_ARB_robustness,GL_KHR_debug '
'c --header-only --debug' 'c --header-only --debug'
) )

View File

@ -1014,7 +1014,7 @@ static pthread_t main_thread;
static NSLock *tick_lock = NULL; static NSLock *tick_lock = NULL;
void _glfwDispatchTickCallback(void) { void _glfwDispatchTickCallback() {
if (tick_lock && tick_callback) { if (tick_lock && tick_callback) {
[tick_lock lock]; [tick_lock lock];
while(tick_callback_requested) { while(tick_callback_requested) {
@ -1026,7 +1026,7 @@ void _glfwDispatchTickCallback(void) {
} }
static void static void
request_tick_callback(void) { request_tick_callback() {
if (!tick_callback_requested) { if (!tick_callback_requested) {
tick_callback_requested = true; tick_callback_requested = true;
[NSApp performSelectorOnMainThread:@selector(tick_callback) withObject:nil waitUntilDone:NO]; [NSApp performSelectorOnMainThread:@selector(tick_callback) withObject:nil waitUntilDone:NO];

View File

@ -323,7 +323,7 @@ static double getFallbackRefreshRate(CGDirectDisplayID displayID)
////// GLFW internal API ////// ////// GLFW internal API //////
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
void _glfwClearDisplayLinks(void) { void _glfwClearDisplayLinks() {
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].displayLink) { if (_glfw.ns.displayLinks.entries[i].displayLink) {
CVDisplayLinkStop(_glfw.ns.displayLinks.entries[i].displayLink); CVDisplayLinkStop(_glfw.ns.displayLinks.entries[i].displayLink);

View File

@ -1010,18 +1010,6 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
_glfwInputCursorEnter(window, true); _glfwInputCursorEnter(window, true);
} }
- (void)viewDidChangeEffectiveAppearance
{
static int appearance = 0;
if (_glfw.callbacks.system_color_theme_change) {
int new_appearance = glfwGetCurrentSystemColorTheme();
if (new_appearance != appearance) {
appearance = new_appearance;
_glfw.callbacks.system_color_theme_change(appearance);
}
}
}
- (void)viewDidChangeBackingProperties - (void)viewDidChangeBackingProperties
{ {
if (!window) return; if (!window) return;
@ -1466,11 +1454,15 @@ is_ascii_control_char(char x) {
} }
void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) { void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
[w->ns.view updateIMEStateFor: ev->type focused:(bool)ev->focused]; [w->ns.view updateIMEStateFor: ev->type focused:(bool)ev->focused left:(CGFloat)ev->cursor.left top:(CGFloat)ev->cursor.top cellWidth:(CGFloat)ev->cursor.width cellHeight:(CGFloat)ev->cursor.height];
} }
- (void)updateIMEStateFor:(GLFWIMEUpdateType)which - (void)updateIMEStateFor:(GLFWIMEUpdateType)which
focused:(bool)focused focused:(bool)focused
left:(CGFloat)left
top:(CGFloat)top
cellWidth:(CGFloat)cellWidth
cellHeight:(CGFloat)cellHeight
{ {
if (which == GLFW_IME_UPDATE_FOCUS && !focused && [self hasMarkedText] && window) { if (which == GLFW_IME_UPDATE_FOCUS && !focused && [self hasMarkedText] && window) {
[input_context discardMarkedText]; [input_context discardMarkedText];
@ -1480,7 +1472,16 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
_glfw.ns.text[0] = 0; _glfw.ns.text[0] = 0;
} }
if (which != GLFW_IME_UPDATE_CURSOR_POSITION) return; if (which != GLFW_IME_UPDATE_CURSOR_POSITION) return;
left /= window->ns.xscale;
top /= window->ns.yscale;
cellWidth /= window->ns.xscale;
cellHeight /= window->ns.yscale;
debug_key("updateIMEPosition: left=%f, top=%f, width=%f, height=%f\n", left, top, cellWidth, cellHeight);
const NSRect frame = [window->ns.view frame];
const NSRect rectInView = NSMakeRect(left,
frame.size.height - top - cellHeight,
cellWidth, cellHeight);
markedRect = [window->ns.object convertRectToScreen: rectInView];
if (_glfwPlatformWindowFocused(window)) [[window->ns.view inputContext] invalidateCharacterCoordinates]; if (_glfwPlatformWindowFocused(window)) [[window->ns.view inputContext] invalidateCharacterCoordinates];
} }
@ -1506,21 +1507,6 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
actualRange:(NSRangePointer)actualRange actualRange:(NSRangePointer)actualRange
{ {
(void)range; (void)actualRange; (void)range; (void)actualRange;
if (_glfw.callbacks.get_ime_cursor_position) {
GLFWIMEUpdateEvent ev = { .type = GLFW_IME_UPDATE_CURSOR_POSITION };
if (_glfw.callbacks.get_ime_cursor_position((GLFWwindow*)window, &ev)) {
const CGFloat left = (CGFloat)ev.cursor.left / window->ns.xscale;
const CGFloat top = (CGFloat)ev.cursor.top / window->ns.yscale;
const CGFloat cellWidth = (CGFloat)ev.cursor.width / window->ns.xscale;
const CGFloat cellHeight = (CGFloat)ev.cursor.height / window->ns.yscale;
debug_key("updateIMEPosition: left=%f, top=%f, width=%f, height=%f\n", left, top, cellWidth, cellHeight);
const NSRect frame = [window->ns.view frame];
const NSRect rectInView = NSMakeRect(left,
frame.size.height - top - cellHeight,
cellWidth, cellHeight);
markedRect = [window->ns.object convertRectToScreen: rectInView];
}
}
return markedRect; return markedRect;
} }
@ -1590,71 +1576,6 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
return text; return text;
} }
// <https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/SysServices/Articles/using.html>
// Support services receiving "public.utf8-plain-text" and "NSStringPboardType"
- (id)validRequestorForSendType:(NSString *)sendType returnType:(NSString *)returnType
{
if (
(!sendType || [sendType isEqual:NSPasteboardTypeString] || [sendType isEqual:@"NSStringPboardType"]) &&
(!returnType || [returnType isEqual:NSPasteboardTypeString] || [returnType isEqual:@"NSStringPboardType"])
) {
if (_glfw.callbacks.has_current_selection && _glfw.callbacks.has_current_selection()) return self;
}
return [super validRequestorForSendType:sendType returnType:returnType];
}
// Selected text as input to be sent to Services
// For example, after selecting an absolute path, open the global menu bar kitty->Services and click `Show in Finder`.
- (BOOL)writeSelectionToPasteboard:(NSPasteboard *)pboard types:(NSArray *)types
{
if (!_glfw.callbacks.get_current_selection) return NO;
char *text = _glfw.callbacks.get_current_selection();
if (!text) return NO;
BOOL ans = NO;
if (text[0]) {
if ([types containsObject:NSPasteboardTypeString] == YES) {
[pboard declareTypes:@[NSPasteboardTypeString] owner:self];
ans = [pboard setString:@(text) forType:NSPasteboardTypeString];
} else if ([types containsObject:@"NSStringPboardType"] == YES) {
[pboard declareTypes:@[@"NSStringPboardType"] owner:self];
ans = [pboard setString:@(text) forType:@"NSStringPboardType"];
}
free(text);
}
return ans;
}
// Service output to be handled
// For example, open System Settings->Keyboard->Keyboard Shortcuts->Services->Text, enable `Convert Text to Full Width`, select some text and execute the service.
- (BOOL)readSelectionFromPasteboard:(NSPasteboard *)pboard
{
NSString* text = nil;
NSArray *types = [pboard types];
if ([types containsObject:NSPasteboardTypeString] == YES) {
text = [pboard stringForType:NSPasteboardTypeString]; // public.utf8-plain-text
} else if ([types containsObject:@"NSStringPboardType"] == YES) {
text = [pboard stringForType:@"NSStringPboardType"]; // for older services (need re-encode?)
} else {
return NO;
}
if (text && [text length] > 0) {
// The service wants us to replace the selection, but we can't replace anything but insert text.
const char *utf8 = polymorphic_string_as_utf8(text);
debug_key("Sending text received in readSelectionFromPasteboard as key event\n");
GLFWkeyevent glfw_keyevent = {.text=utf8, .ime_state=GLFW_IME_COMMIT_TEXT};
_glfwInputKeyboard(window, &glfw_keyevent);
// Restore pre-edit text after inserting the received text
if ([self hasMarkedText]) {
glfw_keyevent.text = [[markedText string] UTF8String];
glfw_keyevent.ime_state = GLFW_IME_PREEDIT_CHANGED;
_glfwInputKeyboard(window, &glfw_keyevent);
}
return YES;
}
return NO;
}
@end @end
// }}} // }}}
@ -1731,18 +1652,6 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
if (glfw_window && !glfw_window->decorated && glfw_window->ns.view) [self makeFirstResponder:glfw_window->ns.view]; if (glfw_window && !glfw_window->decorated && glfw_window->ns.view) [self makeFirstResponder:glfw_window->ns.view];
} }
- (void)zoom:(id)sender
{
if (![self isZoomed]) {
const NSSize original = [self resizeIncrements];
[self setResizeIncrements:NSMakeSize(1.0, 1.0)];
[super zoom:sender];
[self setResizeIncrements:original];
} else {
[super zoom:sender];
}
}
@end @end
// }}} // }}}
@ -1896,9 +1805,8 @@ int _glfwPlatformCreateWindow(_GLFWwindow* window,
if (window->monitor) if (window->monitor)
{ {
// Do not show the window here until after setting the window size, maximized state, and full screen _glfwPlatformShowWindow(window);
// _glfwPlatformShowWindow(window); _glfwPlatformFocusWindow(window);
// _glfwPlatformFocusWindow(window);
acquireMonitor(window); acquireMonitor(window);
} }
@ -2090,10 +1998,9 @@ void _glfwPlatformRestoreWindow(_GLFWwindow* window)
void _glfwPlatformMaximizeWindow(_GLFWwindow* window) void _glfwPlatformMaximizeWindow(_GLFWwindow* window)
{ {
if (![window->ns.object isZoomed]) { if (![window->ns.object isZoomed])
[window->ns.object zoom:nil]; [window->ns.object zoom:nil];
} }
}
void _glfwPlatformShowWindow(_GLFWwindow* window) void _glfwPlatformShowWindow(_GLFWwindow* window)
{ {
@ -2598,19 +2505,6 @@ bool _glfwPlatformToggleFullscreen(_GLFWwindow* w, unsigned int flags) {
if (in_fullscreen) made_fullscreen = false; if (in_fullscreen) made_fullscreen = false;
[window toggleFullScreen: nil]; [window toggleFullScreen: nil];
} }
// Update window button visibility
if (w->ns.titlebar_hidden) {
// The hidden buttons might be automatically reset to be visible after going full screen
// to show up in the auto-hide title bar, so they need to be set back to hidden.
BOOL button_hidden = YES;
// When title bar is configured to be hidden, it should be shown with buttons (auto-hide) after going to full screen.
if (!traditional) {
button_hidden = (BOOL) !made_fullscreen;
}
[[window standardWindowButton: NSWindowCloseButton] setHidden:button_hidden];
[[window standardWindowButton: NSWindowMiniaturizeButton] setHidden:button_hidden];
[[window standardWindowButton: NSWindowZoomButton] setHidden:button_hidden];
}
return made_fullscreen; return made_fullscreen;
} }
@ -2969,19 +2863,6 @@ GLFWAPI void glfwCocoaRequestRenderFrame(GLFWwindow *w, GLFWcocoarenderframefun
requestRenderFrame((_GLFWwindow*)w, callback); requestRenderFrame((_GLFWwindow*)w, callback);
} }
GLFWAPI int glfwGetCurrentSystemColorTheme(void) {
int theme_type = 0;
NSAppearance *changedAppearance = NSApp.effectiveAppearance;
NSAppearanceName newAppearance = [changedAppearance bestMatchFromAppearancesWithNames:@[NSAppearanceNameAqua, NSAppearanceNameDarkAqua]];
if([newAppearance isEqualToString:NSAppearanceNameDarkAqua]){
theme_type = 1;
} else {
theme_type = 2;
}
return theme_type;
}
GLFWAPI uint32_t GLFWAPI uint32_t
glfwGetCocoaKeyEquivalent(uint32_t glfw_key, int glfw_mods, int *cocoa_mods) { glfwGetCocoaKeyEquivalent(uint32_t glfw_key, int glfw_mods, int *cocoa_mods) {
*cocoa_mods = 0; *cocoa_mods = 0;

4
glfw/dbus_glfw.c vendored
View File

@ -174,7 +174,7 @@ glfw_dbus_dispatch(DBusConnection *conn) {
} }
void void
glfw_dbus_session_bus_dispatch(void) { glfw_dbus_session_bus_dispatch() {
if (session_bus) glfw_dbus_dispatch(session_bus); if (session_bus) glfw_dbus_dispatch(session_bus);
} }
@ -344,7 +344,7 @@ glfw_dbus_connect_to_session_bus(void) {
} }
DBusConnection * DBusConnection *
glfw_dbus_session_bus(void) { glfw_dbus_session_bus() {
if (!session_bus) glfw_dbus_connect_to_session_bus(); if (!session_bus) glfw_dbus_connect_to_session_bus();
return session_bus; return session_bus;
} }

View File

@ -14,10 +14,6 @@ is_openbsd = 'openbsd' in _plat
base = os.path.dirname(os.path.abspath(__file__)) base = os.path.dirname(os.path.abspath(__file__))
def null_func() -> None:
return None
class CompileKey(NamedTuple): class CompileKey(NamedTuple):
src: str src: str
dest: str dest: str
@ -27,7 +23,7 @@ class Command(NamedTuple):
desc: str desc: str
cmd: Sequence[str] cmd: Sequence[str]
is_newer_func: Callable[[], bool] is_newer_func: Callable[[], bool]
on_success: Callable[[], None] = null_func on_success: Callable[[], None] = lambda: None
key: Optional[CompileKey] = None key: Optional[CompileKey] = None
keyfile: Optional[str] = None keyfile: Optional[str] = None
@ -41,7 +37,6 @@ class Env:
library_paths: Dict[str, List[str]] = {} library_paths: Dict[str, List[str]] = {}
ldpaths: List[str] = [] ldpaths: List[str] = []
ccver: Tuple[int, int] ccver: Tuple[int, int]
vcs_rev: str = ''
# glfw stuff # glfw stuff
all_headers: List[str] = [] all_headers: List[str] = []
@ -53,13 +48,11 @@ class Env:
def __init__( def __init__(
self, cc: List[str] = [], cppflags: List[str] = [], cflags: List[str] = [], ldflags: List[str] = [], self, cc: List[str] = [], cppflags: List[str] = [], cflags: List[str] = [], ldflags: List[str] = [],
library_paths: Dict[str, List[str]] = {}, ldpaths: Optional[List[str]] = None, ccver: Tuple[int, int] = (0, 0), library_paths: Dict[str, List[str]] = {}, ldpaths: Optional[List[str]] = None, ccver: Tuple[int, int] = (0, 0)
vcs_rev: str = ''
): ):
self.cc, self.cppflags, self.cflags, self.ldflags, self.library_paths = cc, cppflags, cflags, ldflags, library_paths self.cc, self.cppflags, self.cflags, self.ldflags, self.library_paths = cc, cppflags, cflags, ldflags, library_paths
self.ldpaths = ldpaths or [] self.ldpaths = ldpaths or []
self.ccver = ccver self.ccver = ccver
self.vcs_rev = vcs_rev
def copy(self) -> 'Env': def copy(self) -> 'Env':
ans = Env(self.cc, list(self.cppflags), list(self.cflags), list(self.ldflags), dict(self.library_paths), list(self.ldpaths), self.ccver) ans = Env(self.cc, list(self.cppflags), list(self.cflags), list(self.ldflags), dict(self.library_paths), list(self.ldpaths), self.ccver)
@ -69,7 +62,6 @@ class Env:
ans.wayland_scanner = self.wayland_scanner ans.wayland_scanner = self.wayland_scanner
ans.wayland_scanner_code = self.wayland_scanner_code ans.wayland_scanner_code = self.wayland_scanner_code
ans.wayland_protocols = self.wayland_protocols ans.wayland_protocols = self.wayland_protocols
ans.vcs_rev = self.vcs_rev
return ans return ans

22
glfw/glfw3.h vendored
View File

@ -1368,22 +1368,6 @@ typedef void (* GLFWwindowclosefun)(GLFWwindow*);
*/ */
typedef void (* GLFWapplicationclosefun)(int); typedef void (* GLFWapplicationclosefun)(int);
/*! @brief The function pointer type for system color theme change callbacks.
*
* This is the function pointer type for system color theme changes.
* @code
* void function_name(int theme_type)
* @endcode
*
* @param[in] theme_type 0 for unknown, 1 for dark and 2 for light
*
* @sa @ref glfwSetSystemColorThemeChangeCallback
*
* @ingroup window
*/
typedef void (* GLFWsystemcolorthemechangefun)(int);
/*! @brief The function pointer type for window content refresh callbacks. /*! @brief The function pointer type for window content refresh callbacks.
* *
* This is the function pointer type for window content refresh callbacks. * This is the function pointer type for window content refresh callbacks.
@ -1735,7 +1719,6 @@ typedef void (* GLFWtickcallback)(void*);
typedef void (* GLFWactivationcallback)(GLFWwindow *window, const char *token, void *data); typedef void (* GLFWactivationcallback)(GLFWwindow *window, const char *token, void *data);
typedef bool (* GLFWdrawtextfun)(GLFWwindow *window, const char *text, uint32_t fg, uint32_t bg, uint8_t *output_buf, size_t width, size_t height, float x_offset, float y_offset, size_t right_margin); typedef bool (* GLFWdrawtextfun)(GLFWwindow *window, const char *text, uint32_t fg, uint32_t bg, uint8_t *output_buf, size_t width, size_t height, float x_offset, float y_offset, size_t right_margin);
typedef char* (* GLFWcurrentselectionfun)(void); typedef char* (* GLFWcurrentselectionfun)(void);
typedef bool (* GLFWhascurrentselectionfun)(void);
typedef void (* GLFWclipboarddatafreefun)(void* data); typedef void (* GLFWclipboarddatafreefun)(void* data);
typedef struct GLFWDataChunk { typedef struct GLFWDataChunk {
const char *data; const char *data;
@ -1748,7 +1731,6 @@ typedef enum {
} GLFWClipboardType; } GLFWClipboardType;
typedef GLFWDataChunk (* GLFWclipboarditerfun)(const char *mime_type, void *iter, GLFWClipboardType ctype); typedef GLFWDataChunk (* GLFWclipboarditerfun)(const char *mime_type, void *iter, GLFWClipboardType ctype);
typedef bool (* GLFWclipboardwritedatafun)(void *object, const char *data, size_t sz); typedef bool (* GLFWclipboardwritedatafun)(void *object, const char *data, size_t sz);
typedef bool (* GLFWimecursorpositionfun)(GLFWwindow *window, GLFWIMEUpdateEvent *ev);
/*! @brief Video mode type. /*! @brief Video mode type.
* *
@ -1907,8 +1889,6 @@ GLFWAPI void glfwUpdateTimer(unsigned long long timer_id, monotonic_t interval,
GLFWAPI void glfwRemoveTimer(unsigned long long); GLFWAPI void glfwRemoveTimer(unsigned long long);
GLFWAPI GLFWdrawtextfun glfwSetDrawTextFunction(GLFWdrawtextfun function); GLFWAPI GLFWdrawtextfun glfwSetDrawTextFunction(GLFWdrawtextfun function);
GLFWAPI GLFWcurrentselectionfun glfwSetCurrentSelectionCallback(GLFWcurrentselectionfun callback); GLFWAPI GLFWcurrentselectionfun glfwSetCurrentSelectionCallback(GLFWcurrentselectionfun callback);
GLFWAPI GLFWhascurrentselectionfun glfwSetHasCurrentSelectionCallback(GLFWhascurrentselectionfun callback);
GLFWAPI GLFWimecursorpositionfun glfwSetIMECursorPositionCallback(GLFWimecursorpositionfun callback);
/*! @brief Terminates the GLFW library. /*! @brief Terminates the GLFW library.
* *
@ -3922,8 +3902,6 @@ GLFWAPI GLFWwindowsizefun glfwSetWindowSizeCallback(GLFWwindow* window, GLFWwind
*/ */
GLFWAPI GLFWwindowclosefun glfwSetWindowCloseCallback(GLFWwindow* window, GLFWwindowclosefun callback); GLFWAPI GLFWwindowclosefun glfwSetWindowCloseCallback(GLFWwindow* window, GLFWwindowclosefun callback);
GLFWAPI GLFWapplicationclosefun glfwSetApplicationCloseCallback(GLFWapplicationclosefun callback); GLFWAPI GLFWapplicationclosefun glfwSetApplicationCloseCallback(GLFWapplicationclosefun callback);
GLFWAPI GLFWsystemcolorthemechangefun glfwSetSystemColorThemeChangeCallback(GLFWsystemcolorthemechangefun callback);
GLFWAPI int glfwGetCurrentSystemColorTheme(void);
/*! @brief Sets the refresh callback for the specified window. /*! @brief Sets the refresh callback for the specified window.
* *

34
glfw/ibus_glfw.c vendored
View File

@ -283,35 +283,29 @@ static const char*
get_ibus_address_file_name(void) { get_ibus_address_file_name(void) {
const char *addr; const char *addr;
static char ans[PATH_MAX]; static char ans[PATH_MAX];
static char display[64] = {0};
addr = getenv("IBUS_ADDRESS"); addr = getenv("IBUS_ADDRESS");
int offset = 0; int offset = 0;
if (addr && addr[0]) { if (addr && addr[0]) {
memcpy(ans, addr, GLFW_MIN(strlen(addr), sizeof(ans))); memcpy(ans, addr, GLFW_MIN(strlen(addr), sizeof(ans)));
return ans; return ans;
} }
const char* disp_num = NULL;
const char *host = "unix";
// See https://github.com/ibus/ibus/commit/8ce25208c3f4adfd290a032c6aa739d2b7580eb1 for why we need this dance.
const char *de = getenv("WAYLAND_DISPLAY");
if (de) {
disp_num = de;
} else {
const char *de = getenv("DISPLAY"); const char *de = getenv("DISPLAY");
if (!de || !de[0]) de = ":0.0"; if (!de || !de[0]) de = ":0.0";
strncpy(display, de, sizeof(display) - 1); char *display = _glfw_strdup(de);
char *dnum = strrchr(display, ':'); const char *host = display;
if (!dnum) { char *disp_num = strrchr(display, ':');
char *screen_num = strrchr(display, '.');
if (!disp_num) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Could not get IBUS address file name as DISPLAY env var has no colon"); _glfwInputError(GLFW_PLATFORM_ERROR, "Could not get IBUS address file name as DISPLAY env var has no colon");
free(display);
return NULL; return NULL;
} }
char *screen_num = strrchr(display, '.'); *disp_num = 0;
*dnum = 0; disp_num++;
dnum++;
if (screen_num) *screen_num = 0; if (screen_num) *screen_num = 0;
if (*display) host = display; if (!*host) host = "unix";
disp_num = dnum;
}
memset(ans, 0, sizeof(ans)); memset(ans, 0, sizeof(ans));
const char *conf_env = getenv("XDG_CONFIG_HOME"); const char *conf_env = getenv("XDG_CONFIG_HOME");
@ -321,6 +315,7 @@ get_ibus_address_file_name(void) {
conf_env = getenv("HOME"); conf_env = getenv("HOME");
if (!conf_env || !conf_env[0]) { if (!conf_env || !conf_env[0]) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Could not get IBUS address file name as no HOME env var is set"); _glfwInputError(GLFW_PLATFORM_ERROR, "Could not get IBUS address file name as no HOME env var is set");
free(display);
return NULL; return NULL;
} }
offset = snprintf(ans, sizeof(ans), "%s/.config", conf_env); offset = snprintf(ans, sizeof(ans), "%s/.config", conf_env);
@ -328,6 +323,7 @@ get_ibus_address_file_name(void) {
char *key = dbus_get_local_machine_id(); char *key = dbus_get_local_machine_id();
snprintf(ans + offset, sizeof(ans) - offset, "/ibus/bus/%s-%s-%s", key, host, disp_num); snprintf(ans + offset, sizeof(ans) - offset, "/ibus/bus/%s-%s-%s", key, host, disp_num);
dbus_free(key); dbus_free(key);
free(display);
return ans; return ans;
} }
@ -387,12 +383,12 @@ input_context_created(DBusMessage *msg, const char* errmsg, void *data) {
enum Capabilities caps = IBUS_CAP_FOCUS | IBUS_CAP_PREEDIT_TEXT; enum Capabilities caps = IBUS_CAP_FOCUS | IBUS_CAP_PREEDIT_TEXT;
if (!glfw_dbus_call_method_no_reply(ibus->conn, IBUS_SERVICE, ibus->input_ctx_path, IBUS_INPUT_INTERFACE, "SetCapabilities", DBUS_TYPE_UINT32, &caps, DBUS_TYPE_INVALID)) return; if (!glfw_dbus_call_method_no_reply(ibus->conn, IBUS_SERVICE, ibus->input_ctx_path, IBUS_INPUT_INTERFACE, "SetCapabilities", DBUS_TYPE_UINT32, &caps, DBUS_TYPE_INVALID)) return;
ibus->ok = true; ibus->ok = true;
glfw_ibus_set_focused(ibus, _glfwFocusedWindow() != NULL); glfw_ibus_set_focused(ibus, false);
glfw_ibus_set_cursor_geometry(ibus, 0, 0, 0, 0); glfw_ibus_set_cursor_geometry(ibus, 0, 0, 0, 0);
debug("Connected to IBUS daemon for IME input management\n"); debug("Connected to IBUS daemon for IME input management\n");
} }
static bool bool
setup_connection(_GLFWIBUSData *ibus) { setup_connection(_GLFWIBUSData *ibus) {
const char *client_name = "GLFW_Application"; const char *client_name = "GLFW_Application";
const char *address_file_name = get_ibus_address_file_name(); const char *address_file_name = get_ibus_address_file_name();

22
glfw/init.c vendored
View File

@ -382,14 +382,6 @@ GLFWAPI GLFWapplicationclosefun glfwSetApplicationCloseCallback(GLFWapplicationc
return cbfun; return cbfun;
} }
GLFWAPI GLFWapplicationclosefun glfwSetSystemColorThemeChangeCallback(GLFWsystemcolorthemechangefun cbfun)
{
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
_GLFW_SWAP_POINTERS(_glfw.callbacks.system_color_theme_change, cbfun);
return cbfun;
}
GLFWAPI GLFWdrawtextfun glfwSetDrawTextFunction(GLFWdrawtextfun cbfun) GLFWAPI GLFWdrawtextfun glfwSetDrawTextFunction(GLFWdrawtextfun cbfun)
{ {
_GLFW_REQUIRE_INIT_OR_RETURN(NULL); _GLFW_REQUIRE_INIT_OR_RETURN(NULL);
@ -403,17 +395,3 @@ GLFWAPI GLFWcurrentselectionfun glfwSetCurrentSelectionCallback(GLFWcurrentselec
_GLFW_SWAP_POINTERS(_glfw.callbacks.get_current_selection, cbfun); _GLFW_SWAP_POINTERS(_glfw.callbacks.get_current_selection, cbfun);
return cbfun; return cbfun;
} }
GLFWAPI GLFWhascurrentselectionfun glfwSetHasCurrentSelectionCallback(GLFWhascurrentselectionfun cbfun)
{
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
_GLFW_SWAP_POINTERS(_glfw.callbacks.has_current_selection, cbfun);
return cbfun;
}
GLFWAPI GLFWimecursorpositionfun glfwSetIMECursorPositionCallback(GLFWimecursorpositionfun cbfun)
{
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
_GLFW_SWAP_POINTERS(_glfw.callbacks.get_ime_cursor_position, cbfun);
return cbfun;
}

4
glfw/internal.h vendored
View File

@ -632,13 +632,11 @@ struct _GLFWlibrary
GLFWmonitorfun monitor; GLFWmonitorfun monitor;
GLFWjoystickfun joystick; GLFWjoystickfun joystick;
GLFWapplicationclosefun application_close; GLFWapplicationclosefun application_close;
GLFWsystemcolorthemechangefun system_color_theme_change;
GLFWdrawtextfun draw_text; GLFWdrawtextfun draw_text;
GLFWcurrentselectionfun get_current_selection; GLFWcurrentselectionfun get_current_selection;
GLFWhascurrentselectionfun has_current_selection;
GLFWimecursorpositionfun get_ime_cursor_position;
} callbacks; } callbacks;
// This is defined in the window API's platform.h // This is defined in the window API's platform.h
_GLFW_PLATFORM_LIBRARY_WINDOW_STATE; _GLFW_PLATFORM_LIBRARY_WINDOW_STATE;
// This is defined in the context API's context.h // This is defined in the context API's context.h

View File

@ -24,11 +24,6 @@ static uint32_t appearance = 0;
static bool is_gnome = false; static bool is_gnome = false;
static bool cursor_theme_changed = false; static bool cursor_theme_changed = false;
int
glfw_current_system_color_theme(void) {
return appearance;
}
#define HANDLER(name) static void name(DBusMessage *msg, const char* errmsg, void *data) { \ #define HANDLER(name) static void name(DBusMessage *msg, const char* errmsg, void *data) { \
(void)data; \ (void)data; \
if (errmsg) { \ if (errmsg) { \
@ -160,9 +155,6 @@ on_color_scheme_change(DBusMessage *message) {
if (val > 2) val = 0; if (val > 2) val = 0;
if (val != appearance) { if (val != appearance) {
appearance = val; appearance = val;
if (_glfw.callbacks.system_color_theme_change) {
_glfw.callbacks.system_color_theme_change(appearance);
}
} }
} }
break; break;

View File

@ -12,4 +12,3 @@
void glfw_initialize_desktop_settings(void); void glfw_initialize_desktop_settings(void);
void glfw_current_cursor_theme(const char **theme, int *size); void glfw_current_cursor_theme(const char **theme, int *size);
int glfw_current_system_color_theme(void);

4
glfw/wl_init.c vendored
View File

@ -789,10 +789,6 @@ glfwWaylandCheckForServerSideDecorations(void) {
return has_ssd ? "YES" : "NO"; return has_ssd ? "YES" : "NO";
} }
GLFWAPI int glfwGetCurrentSystemColorTheme(void) {
return glfw_current_system_color_theme();
}
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
////// GLFW platform API ////// ////// GLFW platform API //////
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////

28
glfw/wl_window.c vendored
View File

@ -75,7 +75,7 @@ get_activation_token(
if (token == NULL) fail("Wayland: failed to create activation request token"); if (token == NULL) fail("Wayland: failed to create activation request token");
if (_glfw.wl.activation_requests.capacity < _glfw.wl.activation_requests.sz + 1) { if (_glfw.wl.activation_requests.capacity < _glfw.wl.activation_requests.sz + 1) {
_glfw.wl.activation_requests.capacity = MAX(64u, _glfw.wl.activation_requests.capacity * 2); _glfw.wl.activation_requests.capacity = MAX(64u, _glfw.wl.activation_requests.capacity * 2);
_glfw.wl.activation_requests.array = realloc(_glfw.wl.activation_requests.array, _glfw.wl.activation_requests.capacity * sizeof(_glfw.wl.activation_requests.array[0])); _glfw.wl.activation_requests.array = realloc(_glfw.wl.activation_requests.array, _glfw.wl.activation_requests.capacity);
if (!_glfw.wl.activation_requests.array) { if (!_glfw.wl.activation_requests.array) {
_glfw.wl.activation_requests.capacity = 0; _glfw.wl.activation_requests.capacity = 0;
fail("Wayland: Out of memory while allocation activation request"); fail("Wayland: Out of memory while allocation activation request");
@ -275,6 +275,18 @@ static void setOpaqueRegion(_GLFWwindow* window, bool commit_surface)
wl_region_destroy(region); wl_region_destroy(region);
} }
static void
swap_buffers(_GLFWwindow *window) {
// this will attach the buffer to the surface,
// the client is responsible for clearing the buffer to an appropriate blank
window->swaps_disallowed = false;
GLFWwindow *current = glfwGetCurrentContext();
bool context_is_current = ((_GLFWwindow*)current)->id == window->id;
if (!context_is_current) glfwMakeContextCurrent((GLFWwindow*)window);
window->context.swapBuffers(window);
if (!context_is_current) glfwMakeContextCurrent(current);
}
static void static void
resizeFramebuffer(_GLFWwindow* window) { resizeFramebuffer(_GLFWwindow* window) {
@ -592,9 +604,8 @@ static void xdgSurfaceHandleConfigure(void* data,
int width = window->wl.pending.width; int width = window->wl.pending.width;
int height = window->wl.pending.height; int height = window->wl.pending.height;
if (!window->wl.surface_configured_once) { if (!window->wl.surface_configured_once) {
window->swaps_disallowed = false;
window->wl.waiting_for_swap_to_commit = true;
window->wl.surface_configured_once = true; window->wl.surface_configured_once = true;
swap_buffers(window);
} }
if (new_states != window->wl.current.toplevel_states || if (new_states != window->wl.current.toplevel_states ||
@ -617,11 +628,12 @@ static void xdgSurfaceHandleConfigure(void* data,
window->wl.current.decoration_mode = mode; window->wl.current.decoration_mode = mode;
} }
bool resized = false;
if (window->wl.pending_state) { if (window->wl.pending_state) {
int width = window->wl.pending.width, height = window->wl.pending.height; int width = window->wl.pending.width, height = window->wl.pending.height;
set_csd_window_geometry(window, &width, &height); set_csd_window_geometry(window, &width, &height);
bool resized = dispatchChangesAfterConfigure(window, width, height); resized = dispatchChangesAfterConfigure(window, width, height);
if (window->wl.decorations.serverSide || window->monitor || window->wl.current.toplevel_states & TOPLEVEL_STATE_FULLSCREEN) { if (window->wl.decorations.serverSide) {
free_csd_surfaces(window); free_csd_surfaces(window);
} else { } else {
ensure_csd_resources(window); ensure_csd_resources(window);
@ -1638,7 +1650,7 @@ write_chunk(void *object, const char *data, size_t sz) {
chunked_writer *cw = object; chunked_writer *cw = object;
if (cw->cap < cw->sz + sz) { if (cw->cap < cw->sz + sz) {
cw->cap = MAX(cw->cap * 2, cw->sz + 8*sz); cw->cap = MAX(cw->cap * 2, cw->sz + 8*sz);
cw->buf = realloc(cw->buf, cw->cap * sizeof(cw->buf[0])); cw->buf = realloc(cw->buf, cw->cap);
} }
memcpy(cw->buf + cw->sz, data, sz); memcpy(cw->buf + cw->sz, data, sz);
cw->sz += sz; cw->sz += sz;
@ -1952,12 +1964,12 @@ primary_selection_copy_callback_done(void *data, struct wl_callback *callback, u
wl_callback_destroy(callback); wl_callback_destroy(callback);
} }
void _glfwSetupWaylandDataDevice(void) { void _glfwSetupWaylandDataDevice() {
_glfw.wl.dataDevice = wl_data_device_manager_get_data_device(_glfw.wl.dataDeviceManager, _glfw.wl.seat); _glfw.wl.dataDevice = wl_data_device_manager_get_data_device(_glfw.wl.dataDeviceManager, _glfw.wl.seat);
if (_glfw.wl.dataDevice) wl_data_device_add_listener(_glfw.wl.dataDevice, &data_device_listener, NULL); if (_glfw.wl.dataDevice) wl_data_device_add_listener(_glfw.wl.dataDevice, &data_device_listener, NULL);
} }
void _glfwSetupWaylandPrimarySelectionDevice(void) { void _glfwSetupWaylandPrimarySelectionDevice() {
_glfw.wl.primarySelectionDevice = zwp_primary_selection_device_manager_v1_get_device(_glfw.wl.primarySelectionDeviceManager, _glfw.wl.seat); _glfw.wl.primarySelectionDevice = zwp_primary_selection_device_manager_v1_get_device(_glfw.wl.primarySelectionDeviceManager, _glfw.wl.seat);
if (_glfw.wl.primarySelectionDevice) zwp_primary_selection_device_v1_add_listener(_glfw.wl.primarySelectionDevice, &primary_selection_device_listener, NULL); if (_glfw.wl.primarySelectionDevice) zwp_primary_selection_device_v1_add_listener(_glfw.wl.primarySelectionDevice, &primary_selection_device_listener, NULL);
} }

4
glfw/x11_init.c vendored
View File

@ -614,10 +614,6 @@ Cursor _glfwCreateCursorX11(const GLFWimage* image, int xhot, int yhot)
////// GLFW platform API ////// ////// GLFW platform API //////
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
GLFWAPI int glfwGetCurrentSystemColorTheme(void) {
return 0;
}
int _glfwPlatformInit(void) int _glfwPlatformInit(void)
{ {
XInitThreads(); XInitThreads();

23
glfw/x11_window.c vendored
View File

@ -719,7 +719,6 @@ static bool createNativeWindow(_GLFWwindow* window,
static size_t static size_t
get_clipboard_data(const _GLFWClipboardData *cd, const char *mime, char **data) { get_clipboard_data(const _GLFWClipboardData *cd, const char *mime, char **data) {
*data = NULL; *data = NULL;
if (cd->get_data == NULL) { return 0; }
GLFWDataChunk chunk = cd->get_data(mime, NULL, cd->ctype); GLFWDataChunk chunk = cd->get_data(mime, NULL, cd->ctype);
char *buf = NULL; char *buf = NULL;
size_t sz = 0, cap = 0; size_t sz = 0, cap = 0;
@ -730,7 +729,7 @@ get_clipboard_data(const _GLFWClipboardData *cd, const char *mime, char **data)
if (!chunk.sz) break; if (!chunk.sz) break;
if (cap < sz + chunk.sz) { if (cap < sz + chunk.sz) {
cap = MAX(cap * 2, sz + 4 * chunk.sz); cap = MAX(cap * 2, sz + 4 * chunk.sz);
buf = realloc(buf, cap * sizeof(buf[0])); buf = realloc(buf, cap);
} }
memcpy(buf + sz, chunk.data, chunk.sz); memcpy(buf + sz, chunk.data, chunk.sz);
sz += chunk.sz; sz += chunk.sz;
@ -1034,7 +1033,7 @@ getSelectionString(Atom selection, Atom *targets, size_t num_targets, GLFWclipbo
} }
else if (actualType == XA_ATOM && targets[i] == _glfw.x11.TARGETS) { else if (actualType == XA_ATOM && targets[i] == _glfw.x11.TARGETS) {
found = true; found = true;
write_data(object, data, sizeof(Atom) * itemCount); write_data(object, data, itemCount);
} }
XFREE(data); XFREE(data);
@ -2862,7 +2861,7 @@ static MimeAtom atom_for_mime(const char *mime) {
MimeAtom ma = {.mime=_glfw_strdup(mime), .atom=XInternAtom(_glfw.x11.display, mime, 0)}; MimeAtom ma = {.mime=_glfw_strdup(mime), .atom=XInternAtom(_glfw.x11.display, mime, 0)};
if (_glfw.x11.mime_atoms.capacity < _glfw.x11.mime_atoms.sz + 1) { if (_glfw.x11.mime_atoms.capacity < _glfw.x11.mime_atoms.sz + 1) {
_glfw.x11.mime_atoms.capacity += 32; _glfw.x11.mime_atoms.capacity += 32;
_glfw.x11.mime_atoms.array = realloc(_glfw.x11.mime_atoms.array, _glfw.x11.mime_atoms.capacity * sizeof(_glfw.x11.mime_atoms.array[0])); _glfw.x11.mime_atoms.array = realloc(_glfw.x11.mime_atoms.array, _glfw.x11.mime_atoms.capacity);
} }
_glfw.x11.mime_atoms.array[_glfw.x11.mime_atoms.sz++] = ma; _glfw.x11.mime_atoms.array[_glfw.x11.mime_atoms.sz++] = ma;
return ma; return ma;
@ -2898,20 +2897,17 @@ void _glfwPlatformSetClipboard(GLFWClipboardType t) {
typedef struct chunked_writer { typedef struct chunked_writer {
char *buf; size_t sz, cap; char *buf; size_t sz, cap;
bool is_self_offer;
} chunked_writer; } chunked_writer;
static bool static bool
write_chunk(void *object, const char *data, size_t sz) { write_chunk(void *object, const char *data, size_t sz) {
chunked_writer *cw = object; chunked_writer *cw = object;
if (data) {
if (cw->cap < cw->sz + sz) { if (cw->cap < cw->sz + sz) {
cw->cap = MAX(cw->cap * 2, cw->sz + 8*sz); cw->cap = MAX(cw->cap * 2, cw->sz + 8*sz);
cw->buf = realloc(cw->buf, cw->cap * sizeof(cw->buf[0])); cw->buf = realloc(cw->buf, cw->cap);
} }
memcpy(cw->buf + cw->sz, data, sz); memcpy(cw->buf + cw->sz, data, sz);
cw->sz += sz; cw->sz += sz;
} else if (sz == 1) cw->is_self_offer = true;
return true; return true;
} }
@ -2919,10 +2915,6 @@ static void
get_available_mime_types(Atom which_clipboard, GLFWclipboardwritedatafun write_data, void *object) { get_available_mime_types(Atom which_clipboard, GLFWclipboardwritedatafun write_data, void *object) {
chunked_writer cw = {0}; chunked_writer cw = {0};
getSelectionString(which_clipboard, &_glfw.x11.TARGETS, 1, write_chunk, &cw, false); getSelectionString(which_clipboard, &_glfw.x11.TARGETS, 1, write_chunk, &cw, false);
if (cw.is_self_offer) {
write_data(object, NULL, 1);
return;
}
size_t count = 0; size_t count = 0;
bool ok = true; bool ok = true;
if (cw.buf) { if (cw.buf) {
@ -2954,12 +2946,11 @@ _glfwPlatformGetClipboard(GLFWClipboardType clipboard_type, const char* mime_typ
} }
size_t count = 0; size_t count = 0;
if (strcmp(mime_type, "text/plain") == 0) { if (strcmp(mime_type, "text/plain") == 0) {
// UTF8_STRING is what xclip uses by default, and there are people out there that expect to be able to paste from it with a single read operation. See https://github.com/kovidgoyal/kitty/issues/5842 // we need to do this because GTK/GNOME is developed by morons
// Also ancient versions of GNOME use DOS line endings even for text/plain;charset=utf-8. See https://github.com/kovidgoyal/kitty/issues/5528#issuecomment-1325348218 // they convert text/plain to DOS line endings
atoms[count++] = _glfw.x11.UTF8_STRING;
// we need to do this because GTK/GNOME is moronic they convert text/plain to DOS line endings, see
// https://gitlab.gnome.org/GNOME/gtk/-/issues/2307 // https://gitlab.gnome.org/GNOME/gtk/-/issues/2307
atoms[count++] = atom_for_mime("text/plain;charset=utf-8").atom; atoms[count++] = atom_for_mime("text/plain;charset=utf-8").atom;
atoms[count++] = _glfw.x11.UTF8_STRING;
atoms[count++] = atom_for_mime("text/plain").atom; atoms[count++] = atom_for_mime("text/plain").atom;
atoms[count++] = XA_STRING; atoms[count++] = XA_STRING;
} else { } else {

30
go.mod
View File

@ -1,30 +0,0 @@
module kitty
go 1.20
require (
github.com/ALTree/bigfloat v0.0.0-20220102081255-38c8b72a9924
github.com/alecthomas/chroma/v2 v2.7.0
github.com/bmatcuk/doublestar/v4 v4.6.0
github.com/disintegration/imaging v1.6.2
github.com/dlclark/regexp2 v1.9.0
github.com/google/go-cmp v0.5.9
github.com/google/uuid v1.3.0
github.com/jamesruan/go-rfc1924 v0.0.0-20170108144916-2767ca7c638f
github.com/seancfoley/ipaddress-go v1.5.4
github.com/shirou/gopsutil/v3 v3.23.3
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
golang.org/x/image v0.7.0
golang.org/x/sys v0.7.0
)
require (
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
github.com/seancfoley/bintree v1.2.1 // indirect
github.com/shoenig/go-m1cpu v0.1.5 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.0 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
)

104
go.sum
View File

@ -1,104 +0,0 @@
github.com/ALTree/bigfloat v0.0.0-20220102081255-38c8b72a9924 h1:DG4UyTVIujioxwJc8Zj8Nabz1L1wTgQ/xNBSQDfdP3I=
github.com/ALTree/bigfloat v0.0.0-20220102081255-38c8b72a9924/go.mod h1:+NaH2gLeY6RPBPPQf4aRotPPStg+eXc8f9ZaE4vRfD4=
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
github.com/alecthomas/chroma/v2 v2.7.0 h1:hm1rY6c/Ob4eGclpQ7X/A3yhqBOZNUTk9q+yhyLIViI=
github.com/alecthomas/chroma/v2 v2.7.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw=
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc=
github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dlclark/regexp2 v1.9.0 h1:pTK/l/3qYIKaRXuHnEnIf7Y5NxfRPfpb7dis6/gdlVI=
github.com/dlclark/regexp2 v1.9.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/jamesruan/go-rfc1924 v0.0.0-20170108144916-2767ca7c638f h1:Ko4+g6K16vSyUrtd/pPXuQnWsiHe5BYptEtTxfwYwCc=
github.com/jamesruan/go-rfc1924 v0.0.0-20170108144916-2767ca7c638f/go.mod h1:eHzfhOKbTGJEGPSdMHzU6jft192tHHt2Bu2vIZArvC0=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik=
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig=
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/seancfoley/bintree v1.2.1 h1:Z/iNjRKkXnn0CTW7jDQYtjW5fz2GH1yWvOTJ4MrMvdo=
github.com/seancfoley/bintree v1.2.1/go.mod h1:hIUabL8OFYyFVTQ6azeajbopogQc2l5C/hiXMcemWNU=
github.com/seancfoley/ipaddress-go v1.5.4 h1:ZdjewWC1J2y5ruQjWHwK6rA1tInWB6mz1ftz6uTm+Uw=
github.com/seancfoley/ipaddress-go v1.5.4/go.mod h1:fpvVPC+Jso+YEhNcNiww8HQmBgKP8T4T6BTp1SLxxIo=
github.com/shirou/gopsutil/v3 v3.23.3 h1:Syt5vVZXUDXPEXpIBt5ziWsJ4LdSAAxF4l/xZeQgSEE=
github.com/shirou/gopsutil/v3 v3.23.3/go.mod h1:lSBNN6t3+D6W5e5nXTxc8KIMMVxAcS+6IJlffjRRlMU=
github.com/shoenig/go-m1cpu v0.1.4/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
github.com/shoenig/go-m1cpu v0.1.5 h1:LF57Z/Fpb/WdGLjt2HZilNnmZOxg/q2bSKTQhgbrLrQ=
github.com/shoenig/go-m1cpu v0.1.5/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c=
github.com/shoenig/test v0.6.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,433 +0,0 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package ask
import (
"fmt"
"io"
"kitty/tools/cli/markup"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/utils/style"
"kitty/tools/wcswidth"
"os"
"regexp"
"strings"
"unicode"
)
var _ = fmt.Print
type Choice struct {
text string
idx int
color, letter string
}
func (self Choice) prefix() string {
return string([]rune(self.text)[:self.idx])
}
func (self Choice) display_letter() string {
return string([]rune(self.text)[self.idx])
}
func (self Choice) suffix() string {
return string([]rune(self.text)[self.idx+1:])
}
type Range struct {
start, end, y int
}
func (self *Range) has_point(x, y int) bool {
return y == self.y && self.start <= x && x <= self.end
}
func truncate_at_space(text string, width int) (string, string) {
truncated, p := wcswidth.TruncateToVisualLengthWithWidth(text, width)
if len(truncated) == len(text) {
return text, ""
}
i := strings.LastIndexByte(truncated, ' ')
if i > 0 && p-i < 12 {
p = i + 1
}
return text[:p], text[p:]
}
func extra_for(width, screen_width int) int {
return utils.Max(0, screen_width-width)/2 + 1
}
func GetChoices(o *Options) (response string, err error) {
response = ""
lp, err := loop.New()
if err != nil {
return "", err
}
lp.MouseTrackingMode(loop.BUTTONS_ONLY_MOUSE_TRACKING)
prefix_style_pat := regexp.MustCompile("^(?:\x1b\\[[^m]*?m)+")
choice_order := make([]Choice, 0, len(o.Choices))
clickable_ranges := make(map[string][]Range, 16)
allowed := utils.NewSet[string](utils.Max(2, len(o.Choices)))
response_on_accept := o.Default
switch o.Type {
case "yesno":
allowed.AddItems("y", "n")
if !allowed.Has(response_on_accept) {
response_on_accept = "y"
}
case "choices":
first_choice := ""
for i, x := range o.Choices {
letter, text, _ := strings.Cut(x, ":")
color := ""
if strings.Contains(letter, ";") {
letter, color, _ = strings.Cut(letter, ";")
}
letter = strings.ToLower(letter)
idx := strings.Index(strings.ToLower(text), letter)
idx = len([]rune(strings.ToLower(text)[:idx]))
allowed.Add(letter)
c := Choice{text: text, idx: idx, color: color, letter: letter}
choice_order = append(choice_order, c)
if i == 0 {
first_choice = letter
}
}
if !allowed.Has(response_on_accept) {
response_on_accept = first_choice
}
}
message := o.Message
hidden_text_start_pos := -1
hidden_text_end_pos := -1
hidden_text := ""
m := markup.New(true)
replacement_text := fmt.Sprintf("Press %s or click to show", m.Green(o.UnhideKey))
replacement_range := Range{-1, -1, -1}
if message != "" && o.HiddenTextPlaceholder != "" {
hidden_text_start_pos = strings.Index(message, o.HiddenTextPlaceholder)
if hidden_text_start_pos > -1 {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
return "", fmt.Errorf("Failed to read hidden text from STDIN: %w", err)
}
hidden_text = strings.TrimRightFunc(utils.UnsafeBytesToString(raw), unicode.IsSpace)
hidden_text_end_pos = hidden_text_start_pos + len(replacement_text)
suffix := message[hidden_text_start_pos+len(o.HiddenTextPlaceholder):]
message = message[:hidden_text_start_pos] + replacement_text + suffix
}
}
draw_long_text := func(screen_width int, text string, msg_lines []string) []string {
if text == "" {
msg_lines = append(msg_lines, "")
} else {
width := screen_width - 2
prefix := prefix_style_pat.FindString(text)
for text != "" {
var t string
t, text = truncate_at_space(text, width)
t = strings.TrimSpace(t)
msg_lines = append(msg_lines, strings.Repeat(" ", extra_for(wcswidth.Stringwidth(t), width))+m.Bold(prefix+t))
}
}
return msg_lines
}
ctx := style.Context{AllowEscapeCodes: true}
draw_choice_boxes := func(y, screen_width, screen_height int, choices ...Choice) {
clickable_ranges = map[string][]Range{}
width := screen_width - 2
current_line_length := 0
type Item struct{ letter, text string }
type Line = []Item
var current_line Line
lines := make([]Line, 0, 32)
sep := " "
sep_sz := len(sep) + 2 // for the borders
for _, choice := range choices {
clickable_ranges[choice.letter] = make([]Range, 0, 4)
text := " " + choice.prefix()
color := choice.color
if choice.color == "" {
color = "green"
}
text += ctx.SprintFunc("fg=" + color)(choice.display_letter())
text += choice.suffix() + " "
sz := wcswidth.Stringwidth(text)
if sz+sep_sz+current_line_length > width {
lines = append(lines, current_line)
current_line = nil
current_line_length = 0
}
current_line = append(current_line, Item{choice.letter, text})
current_line_length += sz + sep_sz
}
if len(current_line) > 0 {
lines = append(lines, current_line)
}
highlight := func(text string) string {
return m.Yellow(text)
}
top := func(text string, highlight_frame bool) (ans string) {
ans = "╭" + strings.Repeat("─", wcswidth.Stringwidth(text)) + "╮"
if highlight_frame {
ans = highlight(ans)
}
return
}
middle := func(text string, highlight_frame bool) (ans string) {
f := "│"
if highlight_frame {
f = highlight(f)
}
return f + text + f
}
bottom := func(text string, highlight_frame bool) (ans string) {
ans = "╰" + strings.Repeat("─", wcswidth.Stringwidth(text)) + "╯"
if highlight_frame {
ans = highlight(ans)
}
return
}
print_line := func(add_borders func(string, bool) string, is_last bool, items ...Item) {
type Position struct {
letter string
x, size int
}
texts := make([]string, 0, 8)
positions := make([]Position, 0, 8)
x := 0
for _, item := range items {
text := item.text
positions = append(positions, Position{item.letter, x, wcswidth.Stringwidth(text) + 2})
text = add_borders(text, item.letter == response_on_accept)
text += sep
x += wcswidth.Stringwidth(text)
texts = append(texts, text)
}
line := strings.TrimRightFunc(strings.Join(texts, ""), unicode.IsSpace)
offset := extra_for(wcswidth.Stringwidth(line), width)
for _, pos := range positions {
x = pos.x
x += offset
clickable_ranges[pos.letter] = append(clickable_ranges[pos.letter], Range{x, x + pos.size - 1, y})
}
end := "\r\n"
if is_last {
end = ""
}
lp.QueueWriteString(strings.Repeat(" ", offset) + line + end)
y++
}
lp.AllowLineWrapping(false)
defer func() { lp.AllowLineWrapping(true) }()
for i, boxed_line := range lines {
print_line(top, false, boxed_line...)
print_line(middle, false, boxed_line...)
is_last := i == len(lines)-1
print_line(bottom, is_last, boxed_line...)
}
}
draw_yesno := func(y, screen_width, screen_height int) {
yes := m.Green("Y") + "es"
no := m.BrightRed("N") + "o"
if y+3 <= screen_height {
draw_choice_boxes(y, screen_width, screen_height, Choice{"Yes", 0, "green", "y"}, Choice{"No", 0, "red", "n"})
} else {
sep := strings.Repeat(" ", 3)
text := yes + sep + no
w := wcswidth.Stringwidth(text)
x := extra_for(w, screen_width-2)
nx := x + wcswidth.Stringwidth(yes) + len(sep)
clickable_ranges = map[string][]Range{
"y": {{x, x + wcswidth.Stringwidth(yes) - 1, y}},
"n": {{nx, nx + wcswidth.Stringwidth(no) - 1, y}},
}
lp.QueueWriteString(strings.Repeat(" ", x) + text)
}
}
draw_choice := func(y, screen_width, screen_height int) {
if y+3 <= screen_height {
draw_choice_boxes(y, screen_width, screen_height, choice_order...)
return
}
clickable_ranges = map[string][]Range{}
current_line := ""
current_ranges := map[string]int{}
width := screen_width - 2
commit_line := func(add_newline bool) {
x := extra_for(wcswidth.Stringwidth(current_line), width)
text := strings.Repeat(" ", x) + current_line
if add_newline {
lp.Println(text)
} else {
lp.QueueWriteString(text)
}
for letter, sz := range current_ranges {
clickable_ranges[letter] = []Range{{x, x + sz - 3, y}}
x += sz
}
current_ranges = map[string]int{}
y++
current_line = ""
}
for _, choice := range choice_order {
text := choice.prefix()
spec := ""
if choice.color != "" {
spec = "fg=" + choice.color
} else {
spec = "fg=green"
}
if choice.letter == response_on_accept {
spec += " u=straight"
}
text += ctx.SprintFunc(spec)(choice.display_letter())
text += choice.suffix()
text += " "
sz := wcswidth.Stringwidth(text)
if sz+wcswidth.Stringwidth(current_line) >= width {
commit_line(true)
}
current_line += text
current_ranges[choice.letter] = sz
}
if current_line != "" {
commit_line(false)
}
}
draw_screen := func() error {
lp.StartAtomicUpdate()
defer lp.EndAtomicUpdate()
lp.ClearScreen()
msg_lines := make([]string, 0, 8)
sz, err := lp.ScreenSize()
if err != nil {
return err
}
if message != "" {
scanner := utils.NewLineScanner(message)
for scanner.Scan() {
msg_lines = draw_long_text(int(sz.WidthCells), scanner.Text(), msg_lines)
}
}
y := int(sz.HeightCells) - len(msg_lines)
y = utils.Max(0, (y/2)-2)
lp.QueueWriteString(strings.Repeat("\r\n", y))
for _, line := range msg_lines {
if replacement_text != "" {
idx := strings.Index(line, replacement_text)
if idx > -1 {
x := wcswidth.Stringwidth(line[:idx])
replacement_range = Range{x, x + wcswidth.Stringwidth(replacement_text), y}
}
}
lp.Println(line)
y++
}
if sz.HeightCells > 2 {
lp.Println()
y++
}
switch o.Type {
case "yesno":
draw_yesno(y, int(sz.WidthCells), int(sz.HeightCells))
case "choices":
draw_choice(y, int(sz.WidthCells), int(sz.HeightCells))
}
return nil
}
unhide := func() {
if hidden_text != "" && message != "" {
message = message[:hidden_text_start_pos] + hidden_text + message[hidden_text_end_pos:]
hidden_text = ""
draw_screen()
}
}
lp.OnInitialize = func() (string, error) {
lp.SetCursorVisible(false)
return "", draw_screen()
}
lp.OnFinalize = func() string {
lp.SetCursorVisible(true)
return ""
}
lp.OnText = func(text string, from_key_event, in_bracketed_paste bool) error {
text = strings.ToLower(text)
if allowed.Has(text) {
response = text
lp.Quit(0)
} else if hidden_text != "" && text == o.UnhideKey {
unhide()
} else if o.Type == "yesno" {
lp.Quit(1)
}
return nil
}
lp.OnKeyEvent = func(ev *loop.KeyEvent) error {
if ev.MatchesPressOrRepeat("esc") || ev.MatchesPressOrRepeat("ctrl+c") {
ev.Handled = true
lp.Quit(1)
} else if ev.MatchesPressOrRepeat("enter") {
ev.Handled = true
response = response_on_accept
lp.Quit(0)
}
return nil
}
lp.OnMouseEvent = func(ev *loop.MouseEvent) error {
if ev.Event_type == loop.MOUSE_CLICK {
for letter, ranges := range clickable_ranges {
for _, r := range ranges {
if r.has_point(ev.Cell.X, ev.Cell.Y) {
response = letter
lp.Quit(0)
return nil
}
}
}
if hidden_text != "" && replacement_range.has_point(ev.Cell.X, ev.Cell.Y) {
unhide()
}
}
return nil
}
lp.OnResize = func(old, news loop.ScreenSize) error {
return draw_screen()
}
err = lp.Run()
if err != nil {
return "", err
}
ds := lp.DeathSignalName()
if ds != "" {
fmt.Println("Killed by signal: ", ds)
lp.KillIfSignalled()
return "", fmt.Errorf("Filled by signal: %s", ds)
}
return response, nil
}

View File

@ -1,92 +0,0 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package ask
import (
"fmt"
"io"
"os"
"path/filepath"
"time"
"kitty/tools/tui/loop"
"kitty/tools/tui/readline"
"kitty/tools/utils"
)
var _ = fmt.Print
func get_line(o *Options) (result string, err error) {
lp, err := loop.New(loop.NoAlternateScreen, loop.NoRestoreColors)
if err != nil {
return
}
cwd, _ := os.Getwd()
ropts := readline.RlInit{Prompt: o.Prompt}
if o.Name != "" {
base := filepath.Join(utils.CacheDir(), "ask")
ropts.HistoryPath = filepath.Join(base, o.Name+".history.json")
os.MkdirAll(base, 0o755)
}
rl := readline.New(lp, ropts)
if o.Default != "" {
rl.SetText(o.Default)
}
lp.OnInitialize = func() (string, error) {
rl.Start()
return "", nil
}
lp.OnFinalize = func() string { rl.End(); return "" }
lp.OnResumeFromStop = func() error {
rl.Start()
return nil
}
lp.OnResize = rl.OnResize
lp.OnKeyEvent = func(event *loop.KeyEvent) error {
if event.MatchesPressOrRepeat("ctrl+c") {
return fmt.Errorf("Canceled by user")
}
err := rl.OnKeyEvent(event)
if err != nil {
if err == io.EOF {
lp.Quit(0)
return nil
}
if err == readline.ErrAcceptInput {
hi := readline.HistoryItem{Timestamp: time.Now(), Cmd: rl.AllText(), ExitCode: 0, Cwd: cwd}
rl.AddHistoryItem(hi)
result = rl.AllText()
lp.Quit(0)
return nil
}
return err
}
if event.Handled {
rl.Redraw()
return nil
}
return nil
}
lp.OnText = func(text string, from_key_event, in_bracketed_paste bool) error {
err := rl.OnText(text, from_key_event, in_bracketed_paste)
if err == nil {
rl.Redraw()
}
return err
}
err = lp.Run()
rl.Shutdown()
if err != nil {
return "", err
}
ds := lp.DeathSignalName()
if ds != "" {
return "", fmt.Errorf("Killed by signal: %s", ds)
}
return
}

View File

@ -1,73 +0,0 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package ask
import (
"errors"
"fmt"
"kitty/tools/cli"
"kitty/tools/cli/markup"
"kitty/tools/tui"
)
var _ = fmt.Print
type Response struct {
Items []string `json:"items"`
Response string `json:"response"`
}
func show_message(msg string) {
if msg != "" {
m := markup.New(true)
fmt.Println(m.Bold(msg))
}
}
func main(_ *cli.Command, o *Options, args []string) (rc int, err error) {
output := tui.KittenOutputSerializer()
result := &Response{Items: args}
if len(o.Prompt) > 2 && o.Prompt[0] == o.Prompt[len(o.Prompt)-1] && (o.Prompt[0] == '"' || o.Prompt[0] == '\'') {
o.Prompt = o.Prompt[1 : len(o.Prompt)-1]
}
switch o.Type {
case "yesno", "choices":
result.Response, err = GetChoices(o)
if err != nil {
return 1, err
}
case "password":
show_message(o.Message)
pw, err := tui.ReadPassword(o.Prompt, false)
if err != nil {
if errors.Is(err, tui.Canceled) {
pw = ""
} else {
return 1, err
}
}
result.Response = pw
case "line":
show_message(o.Message)
result.Response, err = get_line(o)
if err != nil {
return 1, err
}
default:
return 1, fmt.Errorf("Unknown type: %s", o.Type)
}
s, err := output(result)
if err != nil {
return 1, err
}
_, err = fmt.Println(s)
if err != nil {
return 1, err
}
return
}
func EntryPoint(parent *cli.Command) {
create_cmd(parent, main)
}

View File

@ -1,15 +1,76 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# 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 re
import sys import sys
from contextlib import suppress
from typing import ( from typing import (
List, TYPE_CHECKING, Callable, Dict, Iterator, List, NamedTuple, Optional, Tuple
Optional,
) )
from kitty.typing import BossType, TypedDict from kitty.cli import parse_args
from kitty.cli_stub import AskCLIOptions
from kitty.constants import cache_dir
from kitty.fast_data_types import truncate_point_for_length, wcswidth
from kitty.typing import BossType, KeyEventType, TypedDict
from kitty.utils import ScreenSize
from ..tui.handler import result_handler from ..tui.handler import Handler, result_handler
from ..tui.loop import Loop, MouseEvent, debug
from ..tui.operations import MouseTracking, alternate_screen, styled
if TYPE_CHECKING:
import readline
debug
else:
readline = None
def get_history_items() -> List[str]:
return list(map(readline.get_history_item, range(1, readline.get_current_history_length() + 1)))
def sort_key(item: str) -> Tuple[int, str]:
return len(item), item.lower()
class HistoryCompleter:
def __init__(self, name: Optional[str] = None):
self.matches: List[str] = []
self.history_path = None
if name:
ddir = os.path.join(cache_dir(), 'ask')
with suppress(FileExistsError):
os.makedirs(ddir)
self.history_path = os.path.join(ddir, name)
def complete(self, text: str, state: int) -> Optional[str]:
response = None
if state == 0:
history_values = get_history_items()
if text:
self.matches = sorted(
(h for h in history_values if h and h.startswith(text)), key=sort_key)
else:
self.matches = []
try:
response = self.matches[state]
except IndexError:
response = None
return response
def __enter__(self) -> 'HistoryCompleter':
if self.history_path:
with suppress(Exception):
readline.read_history_file(self.history_path)
readline.set_completer(self.complete)
return self
def __exit__(self, *a: object) -> None:
if self.history_path:
readline.write_history_file(self.history_path)
def option_text() -> str: def option_text() -> str:
@ -65,8 +126,377 @@ class Response(TypedDict):
items: List[str] items: List[str]
response: Optional[str] response: Optional[str]
class Choice(NamedTuple):
text: str
idx: int
color: str
letter: str
class Range(NamedTuple):
start: int
end: int
y: int
def has_point(self, x: int, y: int) -> bool:
return y == self.y and self.start <= x <= self.end
def truncate_at_space(text: str, width: int) -> Tuple[str, str]:
p = truncate_point_for_length(text, width)
if p < len(text):
i = text.rfind(' ', 0, p + 1)
if i > 0 and p - i < 12:
p = i + 1
return text[:p], text[p:]
def extra_for(width: int, screen_width: int) -> int:
return max(0, screen_width - width) // 2 + 1
class Password(Handler):
def __init__(self, cli_opts: AskCLIOptions, prompt: str, is_password: bool = True, initial_text: str = '') -> None:
self.cli_opts = cli_opts
self.prompt = prompt
self.initial_text = initial_text
from kittens.tui.line_edit import LineEdit
self.line_edit = LineEdit(is_password=is_password)
def initialize(self) -> None:
self.cmd.set_cursor_shape('beam')
if self.initial_text:
self.line_edit.on_text(self.initial_text, True)
self.draw_screen()
@Handler.atomic_update
def draw_screen(self) -> None:
self.cmd.clear_screen()
if self.cli_opts.message:
for line in self.cli_opts.message.splitlines():
self.print(line)
self.print()
self.line_edit.write(self.write, self.prompt)
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
self.line_edit.on_text(text, in_bracketed_paste)
self.draw_screen()
def on_key(self, key_event: KeyEventType) -> None:
if self.line_edit.on_key(key_event):
self.draw_screen()
return
if key_event.matches('enter'):
self.quit_loop(0)
if key_event.matches('esc'):
self.quit_loop(1)
def on_resize(self, screen_size: ScreenSize) -> None:
self.screen_size = screen_size
self.draw_screen()
def on_interrupt(self) -> None:
self.quit_loop(1)
on_eot = on_interrupt
@property
def response(self) -> str:
if self._tui_loop.return_code == 0:
return self.line_edit.current_input
return ''
class Choose(Handler): # {{{
mouse_tracking = MouseTracking.buttons_only
def __init__(self, cli_opts: AskCLIOptions) -> None:
self.prefix_style_pat = re.compile(r'(?:\x1b\[[^m]*?m)+')
self.cli_opts = cli_opts
self.choices: Dict[str, Choice] = {}
self.clickable_ranges: Dict[str, List[Range]] = {}
if cli_opts.type == 'yesno':
self.allowed = frozenset('yn')
else:
allowed = []
for choice in cli_opts.choices:
letter, text = choice.split(':', maxsplit=1)
color = ''
if ';' in letter:
letter, color = letter.split(';', maxsplit=1)
letter = letter.lower()
idx = text.lower().index(letter)
allowed.append(letter)
self.choices[letter] = Choice(text, idx, color, letter)
self.allowed = frozenset(allowed)
self.response = ''
self.response_on_accept = cli_opts.default or ''
if cli_opts.type in ('yesno', 'choices') and self.response_on_accept not in self.allowed:
self.response_on_accept = 'y' if cli_opts.type == 'yesno' else tuple(self.choices.keys())[0]
self.message = cli_opts.message
self.hidden_text_start_pos = self.hidden_text_end_pos = -1
self.hidden_text = ''
self.replacement_text = t = f'Press {styled(self.cli_opts.unhide_key, fg="green")} or click to show'
self.replacement_range = Range(-1, -1, -1)
if self.message and self.cli_opts.hidden_text_placeholder:
self.hidden_text_start_pos = self.message.find(self.cli_opts.hidden_text_placeholder)
if self.hidden_text_start_pos > -1:
self.hidden_text = sys.stdin.read().rstrip()
self.hidden_text_end_pos = self.hidden_text_start_pos + len(t)
suffix = self.message[self.hidden_text_start_pos + len(self.cli_opts.hidden_text_placeholder):]
self.message = self.message[:self.hidden_text_start_pos] + t + suffix
def initialize(self) -> None:
self.cmd.set_cursor_visible(False)
self.draw_screen()
def finalize(self) -> None:
self.cmd.set_cursor_visible(True)
def draw_long_text(self, text: str) -> Iterator[str]:
if not text:
yield ''
return
width = self.screen_size.cols - 2
m = self.prefix_style_pat.match(text)
prefix = m.group() if m else ''
while text:
t, text = truncate_at_space(text, width)
t = t.strip()
yield ' ' * extra_for(wcswidth(t), width) + styled(prefix + t, bold=True)
@Handler.atomic_update
def draw_screen(self) -> None:
self.cmd.clear_screen()
msg_lines: List[str] = []
if self.message:
for line in self.message.splitlines():
msg_lines.extend(self.draw_long_text(line))
y = self.screen_size.rows - len(msg_lines)
y = max(0, (y // 2) - 2)
self.print(end='\r\n'*y)
for line in msg_lines:
if self.replacement_text in line:
idx = line.find(self.replacement_text)
x = wcswidth(line[:idx])
self.replacement_range = Range(x, x + wcswidth(self.replacement_text), y)
self.print(line)
y += 1
if self.screen_size.rows > 2:
self.print()
y += 1
if self.cli_opts.type == 'yesno':
self.draw_yesno(y)
else:
self.draw_choice(y)
def draw_choice_boxes(self, y: int, *choices: Choice) -> None:
self.clickable_ranges.clear()
width = self.screen_size.cols - 2
current_line_length = 0
current_line: List[Tuple[str, str]] = []
lines: List[List[Tuple[str, str]]] = []
sep = ' '
sep_sz = len(sep) + 2 # for the borders
for choice in choices:
self.clickable_ranges[choice.letter] = []
text = ' ' + choice.text[:choice.idx]
text += styled(choice.text[choice.idx], fg=choice.color or 'green')
text += choice.text[choice.idx + 1:] + ' '
sz = wcswidth(text)
if sz + sep_sz + current_line_length > width:
lines.append(current_line)
current_line = []
current_line_length = 0
current_line.append((choice.letter, text))
current_line_length += sz + sep_sz
if current_line:
lines.append(current_line)
def top(text: str) -> str:
return '' + '' * wcswidth(text) + ''
def middle(text: str) -> str:
return f'{text}'
def bottom(text: str) -> str:
return '' + '' * wcswidth(text) + ''
def highlight(text: str, only_edges: bool = False) -> str:
if only_edges:
return styled(text[0], fg='yellow') + text[1:-1] + styled(text[-1], fg='yellow')
return styled(text, fg='yellow')
def print_line(add_borders: Callable[[str], str], *items: Tuple[str, str], is_last: bool = False) -> None:
nonlocal y
texts = []
positions = []
x = 0
for (letter, text) in items:
positions.append((letter, x, wcswidth(text) + 2))
text = add_borders(text)
if letter == self.response_on_accept:
text = highlight(text, only_edges=add_borders is middle)
text += sep
x += wcswidth(text)
texts.append(text)
line = ''.join(texts).rstrip()
offset = extra_for(wcswidth(line), width)
for (letter, x, sz) in positions:
x += offset
self.clickable_ranges[letter].append(Range(x, x + sz - 1, y))
self.print(' ' * offset, line, sep='', end='' if is_last else '\r\n')
y += 1
self.cmd.set_line_wrapping(False)
for boxed_line in lines:
print_line(top, *boxed_line)
print_line(middle, *boxed_line)
print_line(bottom, *boxed_line, is_last=boxed_line is lines[-1])
self.cmd.set_line_wrapping(True)
def draw_choice(self, y: int) -> None:
if y + 3 <= self.screen_size.rows:
self.draw_choice_boxes(y, *self.choices.values())
return
self.clickable_ranges.clear()
current_line = ''
current_ranges: Dict[str, int] = {}
width = self.screen_size.cols - 2
def commit_line(end: str = '\r\n') -> None:
nonlocal current_line, y
x = extra_for(wcswidth(current_line), width)
self.print(' ' * x + current_line, end=end)
for letter, sz in current_ranges.items():
self.clickable_ranges[letter] = [Range(x, x + sz - 3, y)]
x += sz
current_ranges.clear()
y += 1
current_line = ''
for letter, choice in self.choices.items():
text = choice.text[:choice.idx]
text += styled(choice.text[choice.idx], fg=choice.color or 'green', underline='straight' if letter == self.response_on_accept else None)
text += choice.text[choice.idx + 1:]
text += ' '
sz = wcswidth(text)
if sz + wcswidth(current_line) >= width:
commit_line()
current_line += text
current_ranges[letter] = sz
if current_line:
commit_line(end='')
def draw_yesno(self, y: int) -> None:
yes = styled('Y', fg='green') + 'es'
no = styled('N', fg='red') + 'o'
if y + 3 <= self.screen_size.rows:
self.draw_choice_boxes(y, Choice('Yes', 0, 'green', 'y'), Choice('No', 0, 'red', 'n'))
return
sep = ' ' * 3
text = yes + sep + no
w = wcswidth(text)
x = extra_for(w, self.screen_size.cols - 2)
nx = x + wcswidth(yes) + len(sep)
self.clickable_ranges = {'y': [Range(x, x + wcswidth(yes) - 1, y)], 'n': [Range(nx, nx + wcswidth(no) - 1, y)]}
self.print(' ' * x + text, end='')
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
text = text.lower()
if text in self.allowed:
self.response = text
self.quit_loop(0)
elif self.cli_opts.type == 'yesno':
self.on_interrupt()
elif self.hidden_text and text == self.cli_opts.unhide_key:
self.unhide()
def unhide(self) -> None:
if self.hidden_text and self.message:
self.message = self.message[:self.hidden_text_start_pos] + self.hidden_text + self.message[self.hidden_text_end_pos:]
self.hidden_text = ''
self.draw_screen()
def on_key(self, key_event: KeyEventType) -> None:
if key_event.matches('esc'):
self.on_interrupt()
elif key_event.matches('enter'):
self.response = self.response_on_accept
self.quit_loop(0)
def on_click(self, ev: MouseEvent) -> None:
for letter, ranges in self.clickable_ranges.items():
for r in ranges:
if r.has_point(ev.cell_x, ev.cell_y):
self.response = letter
self.quit_loop(0)
return
if self.hidden_text and self.replacement_range.has_point(ev.cell_x, ev.cell_y):
self.unhide()
def on_resize(self, screen_size: ScreenSize) -> None:
self.screen_size = screen_size
self.draw_screen()
def on_interrupt(self) -> None:
self.quit_loop(1)
on_eot = on_interrupt
# }}}
def main(args: List[str]) -> Response: def main(args: List[str]) -> Response:
raise SystemExit('This must be run as kitten ask') # For some reason importing readline in a key handler in the main kitty process
# causes a crash of the python interpreter, probably because of some global
# lock
global readline
msg = 'Ask the user for input'
try:
cli_opts, items = parse_args(args[1:], option_text, '', msg, 'kitty ask', result_class=AskCLIOptions)
except SystemExit as e:
if e.code != 0:
print(e.args[0])
input('Press Enter to quit')
raise SystemExit(e.code)
if cli_opts.type in ('yesno', 'choices'):
loop = Loop()
handler = Choose(cli_opts)
loop.loop(handler)
return {'items': items, 'response': handler.response}
prompt = cli_opts.prompt
if prompt[0] == prompt[-1] and prompt[0] in '\'"':
prompt = prompt[1:-1]
if cli_opts.type == 'password':
loop = Loop()
phandler = Password(cli_opts, prompt)
loop.loop(phandler)
return {'items': items, 'response': phandler.response}
import readline as rl
readline = rl
from kitty.shell import init_readline
init_readline()
response = None
with alternate_screen(), HistoryCompleter(cli_opts.name):
if cli_opts.message:
print(styled(cli_opts.message, bold=True))
with suppress(KeyboardInterrupt, EOFError):
if cli_opts.default:
def prefill_text() -> None:
readline.insert_text(cli_opts.default or '')
readline.redisplay()
readline.set_pre_input_hook(prefill_text)
response = input(prompt)
readline.set_pre_input_hook()
else:
response = input(prompt)
return {'items': items, 'response': response}
@result_handler() @result_handler()
@ -77,10 +507,6 @@ def handle_result(args: List[str], data: Response, target_window_id: int, boss:
if __name__ == '__main__': if __name__ == '__main__':
main(sys.argv) ans = main(sys.argv)
elif __name__ == '__doc__': if ans:
cd = sys.cli_docs # type: ignore print(ans)
cd['usage'] = ''
cd['options'] = option_text
cd['help_text'] = 'Ask the user for input'
cd['short_desc'] = 'Ask the user for input'

View File

@ -31,7 +31,6 @@ class Broadcast(Handler):
def __init__(self, opts: BroadcastCLIOptions, initial_strings: List[str]) -> None: def __init__(self, opts: BroadcastCLIOptions, initial_strings: List[str]) -> None:
self.opts = opts self.opts = opts
self.hide_input = False
self.initial_strings = initial_strings self.initial_strings = initial_strings
self.payload = {'exclude_active': True, 'data': '', 'match': opts.match, 'match_tab': opts.match_tab, 'session_id': uuid4()} self.payload = {'exclude_active': True, 'data': '', 'match': opts.match, 'match_tab': opts.match_tab, 'session_id': uuid4()}
self.line_edit = LineEdit() self.line_edit = LineEdit()
@ -41,7 +40,7 @@ class Broadcast(Handler):
def initialize(self) -> None: def initialize(self) -> None:
self.write_broadcast_session() self.write_broadcast_session()
self.print('Type the text to broadcast below, press', styled(self.opts.end_session, fg='yellow'), 'to quit:') self.print('Type the text to broadcast below, press', styled('Ctrl+Esc', fg='yellow'), 'to quit:')
for x in self.initial_strings: for x in self.initial_strings:
self.write_broadcast_text(x) self.write_broadcast_text(x)
self.write(SAVE_CURSOR) self.write(SAVE_CURSOR)
@ -57,7 +56,6 @@ class Broadcast(Handler):
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None: def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
self.write_broadcast_text(text) self.write_broadcast_text(text)
if not self.hide_input:
self.line_edit.on_text(text, in_bracketed_paste) self.line_edit.on_text(text, in_bracketed_paste)
self.commit_line() self.commit_line()
@ -70,33 +68,22 @@ class Broadcast(Handler):
self.write_broadcast_text('\x04') self.write_broadcast_text('\x04')
def on_key(self, key_event: KeyEventType) -> None: def on_key(self, key_event: KeyEventType) -> None:
if key_event.matches(self.opts.hide_input_toggle): if self.line_edit.on_key(key_event):
self.hide_input ^= True
self.cmd.set_cursor_visible(not self.hide_input)
if self.hide_input:
self.end_line()
self.print('Input hidden, press', styled(self.opts.hide_input_toggle, fg='yellow'), 'to unhide:')
self.end_line()
return
if key_event.matches(self.opts.end_session):
self.quit_loop(0)
return
if not self.hide_input and self.line_edit.on_key(key_event):
self.commit_line() self.commit_line()
if key_event.matches('enter'): if key_event.matches('enter'):
self.write_broadcast_text('\r') self.write_broadcast_text('\r')
self.end_line() self.print('')
self.line_edit.clear()
self.write(SAVE_CURSOR)
return
if key_event.matches('ctrl+esc'):
self.quit_loop(0)
return return
ek = encode_key_event(key_event) ek = encode_key_event(key_event)
ek = standard_b64encode(ek.encode('utf-8')).decode('ascii') ek = standard_b64encode(ek.encode('utf-8')).decode('ascii')
self.write_broadcast_data('kitty-key:' + ek) self.write_broadcast_data('kitty-key:' + ek)
def end_line(self) -> None:
self.print('')
self.line_edit.clear()
self.write(SAVE_CURSOR)
def write_broadcast_text(self, text: str) -> None: def write_broadcast_text(self, text: str) -> None:
self.write_broadcast_data('base64:' + standard_b64encode(text.encode('utf-8')).decode('ascii')) self.write_broadcast_data('base64:' + standard_b64encode(text.encode('utf-8')).decode('ascii'))
@ -111,19 +98,7 @@ class Broadcast(Handler):
self.write(session_command(self.payload, start)) self.write(session_command(self.payload, start))
OPTIONS = (''' OPTIONS = (MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t')).format
--hide-input-toggle
default=Ctrl+Alt+Esc
Key to press that will toggle hiding of the input in the broadcast window itself.
Useful while typing a password, prevents the password from being visible on the screen.
--end-session
default=Ctrl+Esc
Key to press to end the broadcast session.
''' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t')).format
help_text = 'Broadcast typed text to kitty windows. By default text is sent to all windows, unless one of the matching options is specified' help_text = 'Broadcast typed text to kitty windows. By default text is sent to all windows, unless one of the matching options is specified'
usage = '[initial text to send ...]' usage = '[initial text to send ...]'

View File

View File

@ -0,0 +1,86 @@
/*
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#pragma once
#include "data-types.h"
#if defined(_MSC_VER)
#define ISWINDOWS
#define STDCALL __stdcall
#ifndef ssize_t
#include <BaseTsd.h>
typedef SSIZE_T ssize_t;
#ifndef SSIZE_MAX
#if defined(_WIN64)
#define SSIZE_MAX _I64_MAX
#else
#define SSIZE_MAX LONG_MAX
#endif
#endif
#endif
#else
#define STDCALL
#endif
#include "vector.h"
typedef uint8_t len_t;
typedef uint32_t text_t;
#define LEN_MAX UINT8_MAX
#define IS_LOWERCASE(x) (x) >= 'a' && (x) <= 'z'
#define IS_UPPERCASE(x) (x) >= 'A' && (x) <= 'Z'
#define LOWERCASE(x) ((IS_UPPERCASE(x)) ? (x) + 32 : (x))
#define arraysz(x) (sizeof(x)/sizeof(x[0]))
typedef struct {
text_t* src;
ssize_t src_sz;
len_t haystack_len;
len_t *positions;
double score;
ssize_t idx;
} Candidate;
typedef struct {
Candidate *haystack;
size_t haystack_count;
text_t level1[LEN_MAX], level2[LEN_MAX], level3[LEN_MAX], needle[LEN_MAX];
len_t level1_len, level2_len, level3_len, needle_len;
size_t haystack_size;
text_t *output;
size_t output_sz, output_pos;
int oom;
} GlobalData;
typedef struct {
bool output_positions;
size_t limit;
int num_threads;
text_t mark_before[128], mark_after[128], delimiter[128];
size_t mark_before_sz, mark_after_sz, delimiter_sz;
} Options;
VECTOR_OF(len_t, Positions)
VECTOR_OF(text_t, Chars)
VECTOR_OF(Candidate, Candidates)
void output_results(GlobalData *, Candidate *haystack, size_t count, Options *opts, len_t needle_len);
void* alloc_workspace(len_t max_haystack_len, GlobalData*);
void* free_workspace(void *v);
double score_item(void *v, text_t *haystack, len_t haystack_len, len_t *match_positions);
unsigned int encode_codepoint(text_t ch, char* dest);
size_t unescape(const char *src, char *dest, size_t destlen);
int cpu_count(void);
void* alloc_threads(size_t num_threads);
#ifdef ISWINDOWS
bool start_thread(void* threads, size_t i, unsigned int (STDCALL *start_routine) (void *), void *arg);
ssize_t getdelim(char **lineptr, size_t *n, int delim, FILE *stream);
#else
bool start_thread(void* threads, size_t i, void *(*start_routine) (void *), void *arg);
#endif
void wait_for_thread(void *threads, size_t i);
void free_threads(void *threads);

244
kittens/choose/main.c Normal file
View File

@ -0,0 +1,244 @@
/*
* main.c
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "choose-data-types.h"
#include "charsets.h"
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <fcntl.h>
#ifndef ISWINDOWS
#include <unistd.h>
#endif
typedef struct {
size_t start, count;
void *workspace;
len_t max_haystack_len;
bool started;
GlobalData *global;
} JobData;
static unsigned int STDCALL
run_scoring(JobData *job_data) {
GlobalData *global = job_data->global;
for (size_t i = job_data->start; i < job_data->start + job_data->count; i++) {
global->haystack[i].score = score_item(job_data->workspace, global->haystack[i].src, global->haystack[i].haystack_len, global->haystack[i].positions);
}
return 0;
}
static void*
run_scoring_pthreads(void *job_data) {
run_scoring((JobData*)job_data);
return NULL;
}
#ifdef ISWINDOWS
#define START_FUNC run_scoring
#else
#define START_FUNC run_scoring_pthreads
#endif
static JobData*
create_job(size_t i, size_t blocksz, GlobalData *global) {
JobData *ans = (JobData*)calloc(1, sizeof(JobData));
if (ans == NULL) return NULL;
ans->start = i * blocksz;
if (ans->start >= global->haystack_count) ans->count = 0;
else ans->count = global->haystack_count - ans->start;
ans->max_haystack_len = 0;
for (size_t j = ans->start; j < ans->start + ans->count; j++) ans->max_haystack_len = MAX(ans->max_haystack_len, global->haystack[j].haystack_len);
if (ans->count > 0) {
ans->workspace = alloc_workspace(ans->max_haystack_len, global);
if (!ans->workspace) { free(ans); return NULL; }
}
ans->global = global;
return ans;
}
static JobData*
free_job(JobData *job) {
if (job) {
if (job->workspace) free_workspace(job->workspace);
free(job);
}
return NULL;
}
static int
run_threaded(int num_threads_asked, GlobalData *global) {
int ret = 0;
size_t i, blocksz;
size_t num_threads = MAX(1, num_threads_asked > 0 ? num_threads_asked : cpu_count());
if (global->haystack_size < 10000) num_threads = 1;
/* printf("num_threads: %lu asked: %d sysconf: %ld\n", num_threads, num_threads_asked, sysconf(_SC_NPROCESSORS_ONLN)); */
void *threads = alloc_threads(num_threads);
JobData **job_data = calloc(num_threads, sizeof(JobData*));
if (threads == NULL || job_data == NULL) { ret = 1; goto end; }
blocksz = global->haystack_count / num_threads + global->haystack_count % num_threads;
for (i = 0; i < num_threads; i++) {
job_data[i] = create_job(i, blocksz, global);
if (job_data[i] == NULL) { ret = 1; goto end; }
}
if (num_threads == 1) {
run_scoring(job_data[0]);
} else {
for (i = 0; i < num_threads; i++) {
job_data[i]->started = false;
if (job_data[i]->count > 0) {
if (!start_thread(threads, i, START_FUNC, job_data[i])) ret = 1;
else job_data[i]->started = true;
}
}
}
end:
if (num_threads > 1 && job_data) {
for (i = 0; i < num_threads; i++) {
if (job_data[i] && job_data[i]->started) wait_for_thread(threads, i);
}
}
if (job_data) { for (i = 0; i < num_threads; i++) job_data[i] = free_job(job_data[i]); }
free(job_data);
free_threads(threads);
return ret;
}
static int
run_search(Options *opts, GlobalData *global, const char * const *lines, const size_t* sizes, size_t num_lines) {
const char *linebuf = NULL;
size_t idx = 0;
ssize_t sz = 0;
int ret = 0;
Candidates candidates = {0};
Chars chars = {0};
ALLOC_VEC(text_t, chars, 8192 * 20);
if (chars.data == NULL) return 1;
ALLOC_VEC(Candidate, candidates, 8192);
if (candidates.data == NULL) { FREE_VEC(chars); return 1; }
for (size_t i = 0; i < num_lines; i++) {
sz = sizes[i];
linebuf = lines[i];
if (sz > 0) {
ENSURE_SPACE(text_t, chars, sz);
ENSURE_SPACE(Candidate, candidates, 1);
sz = decode_utf8_string(linebuf, sz, &(NEXT(chars)));
NEXT(candidates).src_sz = sz;
NEXT(candidates).haystack_len = (len_t)(MIN(LEN_MAX, sz));
global->haystack_size += NEXT(candidates).haystack_len;
NEXT(candidates).idx = idx++;
INC(candidates, 1); INC(chars, sz);
}
}
// Prepare the haystack allocating space for positions arrays and settings
// up the src pointers to point to the correct locations
Candidate *haystack = &ITEM(candidates, 0);
len_t *positions = (len_t*)calloc(SIZE(candidates), sizeof(len_t) * global->needle_len);
if (positions) {
text_t *cdata = &ITEM(chars, 0);
for (size_t i = 0, off = 0; i < SIZE(candidates); i++) {
haystack[i].positions = positions + (i * global->needle_len);
haystack[i].src = cdata + off;
off += haystack[i].src_sz;
}
global->haystack = haystack;
global->haystack_count = SIZE(candidates);
ret = run_threaded(opts->num_threads, global);
if (ret == 0) output_results(global, haystack, SIZE(candidates), opts, global->needle_len);
else { REPORT_OOM; }
} else { ret = 1; REPORT_OOM; }
FREE_VEC(chars); free(positions); FREE_VEC(candidates);
return ret;
}
static size_t
copy_unicode_object(PyObject *src, text_t *dest, size_t dest_sz) {
PyUnicode_READY(src);
int kind = PyUnicode_KIND(src);
void *data = PyUnicode_DATA(src);
size_t len = PyUnicode_GetLength(src);
for (size_t i = 0; i < len && i < dest_sz; i++) {
dest[i] = PyUnicode_READ(kind, data, i);
}
return len;
}
static PyObject*
match(PyObject *self, PyObject *args) {
(void)(self);
int output_positions;
unsigned long limit;
PyObject *lines, *levels, *needle, *mark_before, *mark_after, *delimiter;
Options opts = {0};
GlobalData global = {0};
if (!PyArg_ParseTuple(args, "O!O!UpkiUUU",
&PyList_Type, &lines, &PyTuple_Type, &levels, &needle,
&output_positions, &limit, &opts.num_threads,
&mark_before, &mark_after, &delimiter
)) return NULL;
opts.output_positions = output_positions ? true : false;
opts.limit = limit;
global.level1_len = copy_unicode_object(PyTuple_GET_ITEM(levels, 0), global.level1, arraysz(global.level1));
global.level2_len = copy_unicode_object(PyTuple_GET_ITEM(levels, 1), global.level2, arraysz(global.level2));
global.level3_len = copy_unicode_object(PyTuple_GET_ITEM(levels, 2), global.level3, arraysz(global.level3));
global.needle_len = copy_unicode_object(needle, global.needle, arraysz(global.needle));
opts.mark_before_sz = copy_unicode_object(mark_before, opts.mark_before, arraysz(opts.mark_before));
opts.mark_after_sz = copy_unicode_object(mark_after, opts.mark_after, arraysz(opts.mark_after));
opts.delimiter_sz = copy_unicode_object(delimiter, opts.delimiter, arraysz(opts.delimiter));
size_t num_lines = PyList_GET_SIZE(lines);
char **clines = malloc(sizeof(char*) * num_lines);
if (!clines) { return PyErr_NoMemory(); }
size_t *sizes = malloc(sizeof(size_t) * num_lines);
if (!sizes) { free(clines); clines = NULL; return PyErr_NoMemory(); }
for (size_t i = 0; i < num_lines; i++) {
clines[i] = PyBytes_AS_STRING(PyList_GET_ITEM(lines, i));
sizes[i] = PyBytes_GET_SIZE(PyList_GET_ITEM(lines, i));
}
Py_BEGIN_ALLOW_THREADS;
run_search(&opts, &global, (const char* const *)clines, sizes, num_lines);
Py_END_ALLOW_THREADS;
free(clines); free(sizes);
if (global.oom) { free(global.output); return PyErr_NoMemory(); }
if (global.output) {
PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, global.output, global.output_pos);
free(global.output);
return ans;
}
Py_RETURN_NONE;
}
static PyMethodDef module_methods[] = {
{"match", match, METH_VARARGS, ""},
{NULL, NULL, 0, NULL} /* Sentinel */
};
static struct PyModuleDef module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "subseq_matcher", /* name of module */
.m_doc = NULL,
.m_size = -1,
.m_methods = module_methods
};
EXPORTED PyMODINIT_FUNC
PyInit_subseq_matcher(void) {
return PyModule_Create(&module);
}

39
kittens/choose/main.py Normal file
View File

@ -0,0 +1,39 @@
#!/usr/bin/env python3
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import sys
from typing import List
from kitty.key_encoding import KeyEvent
from ..tui.handler import Handler
from ..tui.loop import Loop
class ChooseHandler(Handler):
def initialize(self) -> None:
pass
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
pass
def on_key(self, key_event: KeyEvent) -> None:
pass
def on_interrupt(self) -> None:
self.quit_loop(1)
def on_eot(self) -> None:
self.quit_loop(1)
def main(args: List[str]) -> None:
loop = Loop()
handler = ChooseHandler()
loop.loop(handler)
raise SystemExit(loop.return_code)
if __name__ == '__main__':
main(sys.argv)

38
kittens/choose/match.py Normal file
View File

@ -0,0 +1,38 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
from typing import Iterable, List, Union
from . import subseq_matcher
def match(
input_data: Union[str, bytes, Iterable[Union[str, bytes]]],
query: str,
threads: int = 0,
positions: bool = False,
level1: str = '/',
level2: str = '-_0123456789',
level3: str = '.',
limit: int = 0,
mark_before: str = '',
mark_after: str = '',
delimiter: str = '\n'
) -> List[str]:
if isinstance(input_data, str):
idata = [x.encode('utf-8') for x in input_data.split(delimiter)]
elif isinstance(input_data, bytes):
idata = input_data.split(delimiter.encode('utf-8'))
else:
idata = [x.encode('utf-8') if isinstance(x, str) else x for x in input_data]
query = query.lower()
level1 = level1.lower()
level2 = level2.lower()
level3 = level3.lower()
data = subseq_matcher.match(
idata, (level1, level2, level3), query,
positions, limit, threads,
mark_before, mark_after, delimiter)
if data is None:
return []
return list(filter(None, data.split(delimiter or '\n')))

101
kittens/choose/output.c Normal file
View File

@ -0,0 +1,101 @@
/*
* output.c
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "choose-data-types.h"
#include "../../kitty/iqsort.h"
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#ifdef ISWINDOWS
#include <io.h>
#define STDOUT_FILENO 1
static ssize_t ms_write(int fd, const void* buf, size_t count) { return _write(fd, buf, (unsigned int)count); }
#define write ms_write
#else
#include <unistd.h>
#endif
#include <errno.h>
#define FIELD(x, which) (((Candidate*)(x))->which)
static bool
ensure_space(GlobalData *global, size_t sz) {
if (global->output_sz < sz + global->output_pos || !global->output) {
size_t before = global->output_sz;
global->output_sz += MAX(sz, (64u * 1024u));
global->output = realloc(global->output, sizeof(text_t) * global->output_sz);
if (!global->output) {
global->output_sz = before;
return false;
}
}
return true;
}
static void
output_text(GlobalData *global, const text_t *data, size_t sz) {
if (ensure_space(global, sz)) {
memcpy(global->output + global->output_pos, data, sizeof(text_t) * sz);
global->output_pos += sz;
}
}
static void
output_with_marks(GlobalData *global, Options *opts, text_t *src, size_t src_sz, len_t *positions, len_t poslen) {
size_t pos, i = 0;
for (pos = 0; pos < poslen; pos++, i++) {
output_text(global, src + i, MIN(src_sz, positions[pos]) - i);
i = positions[pos];
if (i < src_sz) {
if (opts->mark_before_sz > 0) output_text(global, opts->mark_before, opts->mark_before_sz);
output_text(global, src + i, 1);
if (opts->mark_after_sz > 0) output_text(global, opts->mark_after, opts->mark_after_sz);
}
}
i = positions[poslen - 1];
if (i + 1 < src_sz) output_text(global, src + i + 1, src_sz - i - 1);
}
static void
output_positions(GlobalData *global, len_t *positions, len_t num) {
wchar_t buf[128];
for (len_t i = 0; i < num; i++) {
int pnum = swprintf(buf, arraysz(buf), L"%u", positions[i]);
if (pnum > 0 && ensure_space(global, pnum + 1)) {
for (int k = 0; k < pnum; k++) global->output[global->output_pos++] = buf[k];
global->output[global->output_pos++] = (i == num - 1) ? ':' : ',';
}
}
}
static void
output_result(GlobalData *global, Candidate *c, Options *opts, len_t needle_len) {
if (opts->output_positions) output_positions(global, c->positions, needle_len);
if (opts->mark_before_sz > 0 || opts->mark_after_sz > 0) {
output_with_marks(global, opts, c->src, c->src_sz, c->positions, needle_len);
} else {
output_text(global, c->src, c->src_sz);
}
output_text(global, opts->delimiter, opts->delimiter_sz);
}
void
output_results(GlobalData *global, Candidate *haystack, size_t count, Options *opts, len_t needle_len) {
Candidate *c;
#define lt(b, a) ( (a)->score < (b)->score || ((a)->score == (b)->score && (a->idx < b->idx)) )
QSORT(Candidate, haystack, count, lt);
#undef lt
size_t left = opts->limit > 0 ? opts->limit : count;
for (size_t i = 0; i < left; i++) {
c = haystack + i;
if (c->score > 0) output_result(global, c, opts, needle_len);
}
}

182
kittens/choose/score.c Normal file
View File

@ -0,0 +1,182 @@
/*
* score.c
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "choose-data-types.h"
#include <stdlib.h>
#include <string.h>
#include <float.h>
#include <stdio.h>
typedef struct {
len_t *positions_buf; // buffer to store positions for every char in needle
len_t **positions; // Array of pointers into positions_buf
len_t *positions_count; // Array of counts for positions
len_t needle_len; // Length of the needle
len_t max_haystack_len; // Max length of a string in the haystack
len_t haystack_len; // Length of the current string in the haystack
len_t *address; // Array of offsets into the positions array
double max_score_per_char;
uint8_t *level_factors; // Array of score factors for every character in the current haystack that matches a character in the needle
text_t *level1, *level2, *level3; // The characters in the levels
len_t level1_len, level2_len, level3_len;
text_t *needle; // The current needle
text_t *haystack; //The current haystack
} WorkSpace;
void*
alloc_workspace(len_t max_haystack_len, GlobalData *global) {
WorkSpace *ans = calloc(1, sizeof(WorkSpace));
if (ans == NULL) return NULL;
ans->positions_buf = (len_t*) calloc(global->needle_len, sizeof(len_t) * max_haystack_len);
ans->positions = (len_t**)calloc(global->needle_len, sizeof(len_t*));
ans->positions_count = (len_t*)calloc(2*global->needle_len, sizeof(len_t));
ans->level_factors = (uint8_t*)calloc(max_haystack_len, sizeof(uint8_t));
if (ans->positions == NULL || ans->positions_buf == NULL || ans->positions_count == NULL || ans->level_factors == NULL) { free_workspace(ans); return NULL; }
ans->needle = global->needle;
ans->needle_len = global->needle_len;
ans->max_haystack_len = max_haystack_len;
ans->level1 = global->level1; ans->level2 = global->level2; ans->level3 = global->level3;
ans->level1_len = global->level1_len; ans->level2_len = global->level2_len; ans->level3_len = global->level3_len;
ans->address = ans->positions_count + sizeof(len_t) * global->needle_len;
for (len_t i = 0; i < global->needle_len; i++) ans->positions[i] = ans->positions_buf + i * max_haystack_len;
return ans;
}
#define NUKE(x) free(x); x = NULL;
void*
free_workspace(void *v) {
WorkSpace *w = (WorkSpace*)v;
NUKE(w->positions_buf);
NUKE(w->positions);
NUKE(w->positions_count);
NUKE(w->level_factors);
free(w);
return NULL;
}
static bool
has_char(text_t *text, len_t sz, text_t ch) {
for(len_t i = 0; i < sz; i++) {
if(text[i] == ch) return true;
}
return false;
}
static uint8_t
level_factor_for(text_t current, text_t last, WorkSpace *w) {
text_t lch = LOWERCASE(last);
if (has_char(w->level1, w->level1_len, lch)) return 90;
if (has_char(w->level2, w->level2_len, lch)) return 80;
if (IS_LOWERCASE(last) && IS_UPPERCASE(current)) return 80; // CamelCase
if (has_char(w->level3, w->level3_len, lch)) return 70;
return 0;
}
static void
init_workspace(WorkSpace *w, text_t *haystack, len_t haystack_len) {
// Calculate the positions and level_factors arrays for the specified haystack
bool level_factor_calculated = false;
memset(w->positions_count, 0, sizeof(*(w->positions_count)) * 2 * w->needle_len);
memset(w->level_factors, 0, sizeof(*(w->level_factors)) * w->max_haystack_len);
for (len_t i = 0; i < haystack_len; i++) {
level_factor_calculated = false;
for (len_t j = 0; j < w->needle_len; j++) {
if (w->needle[j] == LOWERCASE(haystack[i])) {
if (!level_factor_calculated) {
level_factor_calculated = true;
w->level_factors[i] = i > 0 ? level_factor_for(haystack[i], haystack[i-1], w) : 0;
}
w->positions[j][w->positions_count[j]++] = i;
}
}
}
w->haystack = haystack;
w->haystack_len = haystack_len;
w->max_score_per_char = (1.0 / haystack_len + 1.0 / w->needle_len) / 2.0;
}
static bool
has_atleast_one_match(WorkSpace *w) {
int p = -1;
bool found;
for (len_t i = 0; i < w->needle_len; i++) {
if (w->positions_count[i] == 0) return false; // All characters of the needle are not present in the haystack
found = false;
for (len_t j = 0; j < w->positions_count[i]; j++) {
if (w->positions[i][j] > p) { p = w->positions[i][j]; found = true; break; }
}
if (!found) return false; // Characters of needle not present in sequence in haystack
}
return true;
}
#define POSITION(x) w->positions[x][w->address[x]]
static bool
increment_address(WorkSpace *w) {
len_t pos = w->needle_len - 1;
while(true) {
w->address[pos]++;
if (w->address[pos] < w->positions_count[pos]) return true;
if (pos == 0) break;
w->address[pos--] = 0;
}
return false;
}
static bool
address_is_monotonic(WorkSpace *w) {
// Check if the character positions pointed to by the current address are monotonic
for (len_t i = 1; i < w->needle_len; i++) {
if (POSITION(i) <= POSITION(i-1)) return false;
}
return true;
}
static double
calc_score(WorkSpace *w) {
double ans = 0;
len_t distance, pos;
for (len_t i = 0; i < w->needle_len; i++) {
pos = POSITION(i);
if (i == 0) distance = pos < LEN_MAX ? pos + 1 : LEN_MAX;
else {
distance = pos - POSITION(i-1);
if (distance < 2) {
ans += w->max_score_per_char; // consecutive characters
continue;
}
}
if (w->level_factors[pos]) ans += (100 * w->max_score_per_char) / w->level_factors[pos]; // at a special location
else ans += (0.75 * w->max_score_per_char) / distance;
}
return ans;
}
static double
process_item(WorkSpace *w, len_t *match_positions) {
double highscore = 0, score;
do {
if (!address_is_monotonic(w)) continue;
score = calc_score(w);
if (score > highscore) {
highscore = score;
for (len_t i = 0; i < w->needle_len; i++) match_positions[i] = POSITION(i);
}
} while(increment_address(w));
return highscore;
}
double
score_item(void *v, text_t *haystack, len_t haystack_len, len_t *match_positions) {
WorkSpace *w = (WorkSpace*)v;
init_workspace(w, haystack, haystack_len);
if (!has_atleast_one_match(w)) return 0;
return process_item(w, match_positions);
}

View File

@ -0,0 +1,9 @@
from typing import List, Optional, Tuple
def match(
lines: List[bytes], levels: Tuple[str, str, str], needle: str,
output_positions: bool, limit: int, num_threads: int, mark_before: str,
mark_after: str, delimiter: str
) -> Optional[str]:
pass

View File

@ -0,0 +1,50 @@
/*
* unix_compat.c
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "choose-data-types.h"
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#ifdef __APPLE__
#ifndef _SC_NPROCESSORS_ONLN
#define _SC_NPROCESSORS_ONLN 58
#endif
#endif
int
cpu_count() {
return sysconf(_SC_NPROCESSORS_ONLN);
}
void*
alloc_threads(size_t num_threads) {
return calloc(num_threads, sizeof(pthread_t));
}
bool
start_thread(void* threads, size_t i, void *(*start_routine) (void *), void *arg) {
int rc;
if ((rc = pthread_create(((pthread_t*)threads) + i, NULL, start_routine, arg))) {
fprintf(stderr, "Failed to create thread, with error: %s\n", strerror(rc));
return false;
}
return true;
}
void
wait_for_thread(void *threads, size_t i) {
pthread_join(((pthread_t*)(threads))[i], NULL);
}
void
free_threads(void *threads) {
free(threads);
}

42
kittens/choose/vector.h Normal file
View File

@ -0,0 +1,42 @@
/*
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#pragma once
#include "data-types.h"
#define REPORT_OOM global->oom = 1;
#define VECTOR_OF(TYPE, NAME) typedef struct { \
TYPE *data; \
size_t size; \
size_t capacity; \
} NAME;
#define ALLOC_VEC(TYPE, vec, cap) \
vec.size = 0; vec.capacity = cap; \
vec.data = (TYPE*)malloc(vec.capacity * sizeof(TYPE)); \
if (vec.data == NULL) { REPORT_OOM; }
#define FREE_VEC(vec) \
if (vec.data) { free(vec.data); vec.data = NULL; } \
vec.size = 0; vec.capacity = 0;
#define ENSURE_SPACE(TYPE, vec, amt) \
if (vec.size + amt >= vec.capacity) { \
vec.capacity = MAX(vec.capacity * 2, vec.size + amt); \
void *temp = realloc(vec.data, sizeof(TYPE) * vec.capacity); \
if (temp == NULL) { REPORT_OOM; ret = 1; free(vec.data); vec.data = NULL; vec.size = 0; vec.capacity = 0; break; } \
else vec.data = temp; \
}
#define NEXT(vec) (vec.data[vec.size])
#define INC(vec, amt) vec.size += amt;
#define SIZE(vec) (vec.size)
#define ITEM(vec, n) (vec.data[n])

View File

@ -0,0 +1,107 @@
/*
* windows_compat.c
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "choose-data-types.h"
#include <windows.h>
#include <process.h>
#include <stdio.h>
#include <errno.h>
int
cpu_count() {
SYSTEM_INFO sysinfo;
GetSystemInfo(&sysinfo);
return sysinfo.dwNumberOfProcessors;
}
void*
alloc_threads(size_t num_threads) {
return calloc(num_threads, sizeof(uintptr_t));
}
bool
start_thread(void* vt, size_t i, unsigned int (STDCALL *start_routine) (void *), void *arg) {
uintptr_t *threads = (uintptr_t*)vt;
errno = 0;
threads[i] = _beginthreadex(NULL, 0, start_routine, arg, 0, NULL);
if (threads[i] == 0) {
perror("Failed to create thread, with error");
return false;
}
return true;
}
void
wait_for_thread(void *vt, size_t i) {
uintptr_t *threads = vt;
WaitForSingleObject((HANDLE)threads[i], INFINITE);
CloseHandle((HANDLE)threads[i]);
threads[i] = 0;
}
void
free_threads(void *threads) {
free(threads);
}
ssize_t
getdelim(char **lineptr, size_t *n, int delim, FILE *stream) {
char c, *cur_pos, *new_lineptr;
size_t new_lineptr_len;
if (lineptr == NULL || n == NULL || stream == NULL) {
errno = EINVAL;
return -1;
}
if (*lineptr == NULL) {
*n = 8192; /* init len */
if ((*lineptr = (char *)malloc(*n)) == NULL) {
errno = ENOMEM;
return -1;
}
}
cur_pos = *lineptr;
for (;;) {
c = getc(stream);
if (ferror(stream) || (c == EOF && cur_pos == *lineptr))
return -1;
if (c == EOF)
break;
if ((*lineptr + *n - cur_pos) < 2) {
if (SSIZE_MAX / 2 < *n) {
#ifdef EOVERFLOW
errno = EOVERFLOW;
#else
errno = ERANGE; /* no EOVERFLOW defined */
#endif
return -1;
}
new_lineptr_len = *n * 2;
if ((new_lineptr = (char *)realloc(*lineptr, new_lineptr_len)) == NULL) {
errno = ENOMEM;
return -1;
}
*lineptr = new_lineptr;
*n = new_lineptr_len;
}
*cur_pos++ = c;
if (c == delim)
break;
}
*cur_pos = '\0';
return (ssize_t)(cur_pos - *lineptr);
}

View File

@ -1,237 +0,0 @@
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package clipboard
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"os"
"strings"
"kitty/tools/tty"
"kitty/tools/tui/loop"
"kitty/tools/utils"
)
var _ = fmt.Print
var _ = fmt.Print
func encode_read_from_clipboard(use_primary bool) string {
dest := "c"
if use_primary {
dest = "p"
}
return fmt.Sprintf("\x1b]52;%s;?\x1b\\", dest)
}
type base64_streaming_enc struct {
output func(string) loop.IdType
last_written_id loop.IdType
}
func (self *base64_streaming_enc) Write(p []byte) (int, error) {
if len(p) > 0 {
self.last_written_id = self.output(string(p))
}
return len(p), nil
}
var ErrTooMuchPipedData = errors.New("Too much piped data")
func read_all_with_max_size(r io.Reader, max_size int) ([]byte, error) {
b := make([]byte, 0, utils.Min(8192, max_size))
for {
if len(b) == cap(b) {
new_size := utils.Min(2*cap(b), max_size)
if new_size <= cap(b) {
return b, ErrTooMuchPipedData
}
b = append(make([]byte, 0, new_size), b...)
}
n, err := r.Read(b[len(b):cap(b)])
b = b[:len(b)+n]
if err != nil {
if err == io.EOF {
err = nil
}
return b, err
}
}
}
func preread_stdin() (data_src io.Reader, tempfile *os.File, err error) {
// we pre-read STDIN because otherwise if the output of a command is being piped in
// and that command itself transmits on the tty we will break. For example
// kitten @ ls | kitten clipboard
var stdin_data []byte
stdin_data, err = read_all_with_max_size(os.Stdin, 2*1024*1024)
if err == nil {
os.Stdin.Close()
} else if err != ErrTooMuchPipedData {
os.Stdin.Close()
err = fmt.Errorf("Failed to read from STDIN pipe with error: %w", err)
return
}
if err == ErrTooMuchPipedData {
tempfile, err = utils.CreateAnonymousTemp("")
if err != nil {
return nil, nil, fmt.Errorf("Failed to create a temporary from STDIN pipe with error: %w", err)
}
tempfile.Write(stdin_data)
_, err = io.Copy(tempfile, os.Stdin)
os.Stdin.Close()
if err != nil {
return nil, nil, fmt.Errorf("Failed to copy data from STDIN pipe to temp file with error: %w", err)
}
tempfile.Seek(0, os.SEEK_SET)
data_src = tempfile
} else if stdin_data != nil {
data_src = bytes.NewBuffer(stdin_data)
}
return
}
func run_plain_text_loop(opts *Options) (err error) {
stdin_is_tty := tty.IsTerminal(os.Stdin.Fd())
var data_src io.Reader
var tempfile *os.File
if !stdin_is_tty {
data_src, tempfile, err = preread_stdin()
if err != nil {
return err
}
if tempfile != nil {
defer tempfile.Close()
}
}
lp, err := loop.New(loop.NoAlternateScreen, loop.NoRestoreColors, loop.NoMouseTracking)
if err != nil {
return
}
dest := "c"
if opts.UsePrimary {
dest = "p"
}
send_to_loop := func(data string) loop.IdType {
return lp.QueueWriteString(data)
}
enc_writer := base64_streaming_enc{output: send_to_loop}
enc := base64.NewEncoder(base64.StdEncoding, &enc_writer)
transmitting := true
after_read_from_stdin := func() {
transmitting = false
if opts.GetClipboard {
lp.QueueWriteString(encode_read_from_clipboard(opts.UsePrimary))
} else if opts.WaitForCompletion {
lp.QueueWriteString("\x1bP+q544e\x1b\\")
} else {
lp.Quit(0)
}
}
buf := make([]byte, 8192)
write_one_chunk := func() error {
n, err := data_src.Read(buf[:cap(buf)])
if err != nil && !errors.Is(err, io.EOF) {
send_to_loop("\x1b\\")
return err
}
if n > 0 {
enc.Write(buf[:n])
}
if errors.Is(err, io.EOF) {
enc.Close()
send_to_loop("\x1b\\")
after_read_from_stdin()
}
return nil
}
lp.OnInitialize = func() (string, error) {
if data_src != nil {
send_to_loop(fmt.Sprintf("\x1b]52;%s;", dest))
return "", write_one_chunk()
}
after_read_from_stdin()
return "", nil
}
lp.OnWriteComplete = func(id loop.IdType) error {
if id == enc_writer.last_written_id {
return write_one_chunk()
}
return nil
}
var clipboard_contents []byte
lp.OnEscapeCode = func(etype loop.EscapeCodeType, data []byte) (err error) {
switch etype {
case loop.DCS:
if strings.HasPrefix(utils.UnsafeBytesToString(data), "1+r") {
lp.Quit(0)
}
case loop.OSC:
q := utils.UnsafeBytesToString(data)
if strings.HasPrefix(q, "52;") {
parts := strings.SplitN(q, ";", 3)
if len(parts) < 3 {
lp.Quit(0)
return
}
data, err := base64.StdEncoding.DecodeString(parts[2])
if err != nil {
return fmt.Errorf("Invalid base64 encoded data from terminal with error: %w", err)
}
clipboard_contents = data
lp.Quit(0)
}
}
return
}
esc_count := 0
lp.OnKeyEvent = func(event *loop.KeyEvent) error {
if event.MatchesPressOrRepeat("ctrl+c") || event.MatchesPressOrRepeat("esc") {
if transmitting {
return nil
}
event.Handled = true
esc_count++
if esc_count < 2 {
key := "Esc"
if event.MatchesPressOrRepeat("ctrl+c") {
key = "Ctrl+C"
}
lp.QueueWriteString(fmt.Sprintf("Waiting for response from terminal, press %s again to abort. This could cause garbage to be spewed to the screen.\r\n", key))
} else {
return fmt.Errorf("Aborted by user!")
}
}
return nil
}
err = lp.Run()
if err != nil {
return
}
ds := lp.DeathSignalName()
if ds != "" {
fmt.Println("Killed by signal: ", ds)
lp.KillIfSignalled()
return
}
if len(clipboard_contents) > 0 {
_, err = os.Stdout.Write(clipboard_contents)
if err != nil {
err = fmt.Errorf("Failed to write to STDOUT with error: %w", err)
}
}
return
}

View File

@ -1,32 +0,0 @@
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package clipboard
import (
"os"
"kitty/tools/cli"
)
func run_mime_loop(opts *Options, args []string) (err error) {
cwd, err = os.Getwd()
if err != nil {
return err
}
if opts.GetClipboard {
return run_get_loop(opts, args)
}
return run_set_loop(opts, args)
}
func clipboard_main(cmd *cli.Command, opts *Options, args []string) (rc int, err error) {
if len(args) > 0 {
return 0, run_mime_loop(opts, args)
}
return 0, run_plain_text_loop(opts)
}
func EntryPoint(parent *cli.Command) {
create_cmd(parent, clipboard_main)
}

View File

@ -1,91 +1,152 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import codecs
import io
import os
import select
import sys import sys
from typing import List, NoReturn, Optional
from kitty.cli import parse_args
from kitty.cli_stub import ClipboardCLIOptions
from kitty.fast_data_types import parse_input_from_terminal
from ..tui.operations import (
raw_mode, request_from_clipboard, write_to_clipboard
)
OPTIONS = r''' OPTIONS = r'''
--get-clipboard -g --get-clipboard
default=False
type=bool-set type=bool-set
Output the current contents of the clipboard to STDOUT. Note that by default Output the current contents of the clipboard to STDOUT. Note that by default
kitty will prompt for permission to access the clipboard. Can be controlled kitty will prompt for permission to access the clipboard. Can be controlled
by :opt:`clipboard_control`. by :opt:`clipboard_control`.
--use-primary -p --use-primary
default=False
type=bool-set type=bool-set
Use the primary selection rather than the clipboard on systems that support it, Use the primary selection rather than the clipboard on systems that support it,
such as Linux. such as X11.
--mime -m
type=list
The mimetype of the specified file. Useful when the auto-detected mimetype is
likely to be incorrect or the filename has no extension and therefore no mimetype
can be detected. If more than one file is specified, this option should be specified multiple
times, once for each specified file. When copying data from the clipboard, you can use wildcards
to match MIME types. For example: :code:`--mime 'text/*'` will match any textual MIME type
available on the clipboard, usually the first matching MIME type is copied. The special MIME
type :code:`.` will return the list of available MIME types currently on the system clipboard.
--alias -a
type=list
Specify aliases for MIME types. Aliased MIME types are considered equivalent.
When copying to clipboard both the original and alias are made available on the
clipboard. When copying from clipboard if the original is not found, the alias
is used, as a fallback. Can be specified multiple times to create multiple
aliases. For example: :code:`--alias text/plain=text/x-rst` makes :code:`text/plain` an alias
of :code:`text/rst`. Aliases are not used in filter mode. An alias for
:code:`text/plain` is automatically created if :code:`text/plain` is not present in the input data, but some
other :code:`text/*` MIME is present.
--wait-for-completion --wait-for-completion
default=False
type=bool-set type=bool-set
Wait till the copy to clipboard is complete before exiting. Useful if running Wait till the copy to clipboard is complete before exiting. Useful if running
the kitten in a dedicated, ephemeral window. Only needed in filter mode. the kitten in a dedicated, ephemeral window.
'''.format '''.format
help_text = '''\ help_text = '''\
Read or write to the system clipboard. Read or write to the system clipboard.
This kitten operates most simply in :italic:`filter mode`. To set the clipboard text, pipe in the new text on STDIN. Use the
To set the clipboard text, pipe in the new text on :file:`STDIN`. Use the :option:`--get-clipboard` option to output the current clipboard contents to
:option:`--get-clipboard` option to output the current clipboard text content to :file:`stdout`. Note that reading the clipboard will cause a permission
:file:`STDOUT`. Note that copying from the clipboard will cause a permission
popup, see :opt:`clipboard_control` for details. popup, see :opt:`clipboard_control` for details.
For more control, specify filename arguments. Then, different MIME types can be copied to/from
the clipboard. Some examples:
.. code:: sh
# Copy an image to the clipboard:
kitty +kitten clipboard picture.png
# Copy an image and some text to the clipboard:
kitty +kitten clipboard picture.jpg text.txt
# Copy text from STDIN and an image to the clipboard:
echo hello | kitty +kitten clipboard picture.png /dev/stdin
# Copy any raster image available on the clipboard to a PNG file:
kitty +kitten clipboard -g picture.png
# Copy an image to a file and text to STDOUT:
kitty +kitten clipboard -g picture.png /dev/stdout
# List the formats available on the system clipboard
kitty +kitten clipboard -g -m . /dev/stdout
''' '''
usage = '[files to copy to/from]' usage = ''
got_capability_response = False
got_clipboard_response = False
clipboard_contents = ''
clipboard_from_primary = False
def ignore(x: str) -> None:
pass
def on_text(x: str) -> None:
if '\x03' in x:
raise KeyboardInterrupt()
if '\x04' in x:
raise EOFError()
def on_dcs(dcs: str) -> None:
global got_capability_response
if dcs.startswith('1+r'):
got_capability_response = True
def on_osc(osc: str) -> None:
global clipboard_contents, clipboard_from_primary, got_clipboard_response
idx = osc.find(';')
if idx <= 0:
return
q = osc[:idx]
if q == '52':
got_clipboard_response = True
widx = osc.find(';', idx + 1)
if widx < idx:
clipboard_from_primary = osc.find('p', idx + 1) > -1
clipboard_contents = ''
else:
from base64 import standard_b64decode
clipboard_from_primary = osc.find('p', idx+1, widx) > -1
data = memoryview(osc.encode('ascii'))
clipboard_contents = standard_b64decode(data[widx+1:]).decode('utf-8')
def wait_loop(tty_fd: int) -> None:
os.set_blocking(tty_fd, False)
decoder = codecs.getincrementaldecoder('utf-8')('ignore')
with raw_mode(tty_fd):
buf = ''
while not got_capability_response and not got_clipboard_response:
rd = select.select([tty_fd], [], [])[0]
if rd:
raw = os.read(tty_fd, io.DEFAULT_BUFFER_SIZE)
if not raw:
raise EOFError()
data = decoder.decode(raw)
buf = (buf + data) if buf else data
buf = parse_input_from_terminal(on_text, on_dcs, ignore, on_osc, ignore, ignore, buf, False)
def main(args: List[str]) -> NoReturn:
cli_opts, items = parse_args(args[1:], OPTIONS, usage, help_text, 'kitty +kitten clipboard', result_class=ClipboardCLIOptions)
if items:
raise SystemExit('Unrecognized extra command line arguments')
data: Optional[bytes] = None
if not sys.stdin.isatty():
data = sys.stdin.buffer.read()
wait_for_capability_response = False
data_to_write = []
if data:
data_to_write.append(write_to_clipboard(data, cli_opts.use_primary).encode('ascii'))
if not cli_opts.get_clipboard and cli_opts.wait_for_completion:
data_to_write.append(b'\x1bP+q544e\x1b\\')
wait_for_capability_response = True
if cli_opts.get_clipboard:
data_to_write.append(request_from_clipboard(cli_opts.use_primary).encode('ascii'))
wait_for_capability_response = True
tty_fd = os.open(os.ctermid(), os.O_RDWR | os.O_CLOEXEC)
retcode = 0
with open(tty_fd, 'wb', closefd=True) as ttyf:
for x in data_to_write:
ttyf.write(x)
ttyf.flush()
if wait_for_capability_response:
try:
wait_loop(tty_fd)
except KeyboardInterrupt:
sys.excepthook = lambda *a: None
raise
except EOFError:
retcode = 1
if clipboard_contents:
print(end=clipboard_contents)
raise SystemExit(retcode)
if __name__ == '__main__': if __name__ == '__main__':
raise SystemExit('This should be run as kitten clipboard') main(sys.argv)
elif __name__ == '__doc__': elif __name__ == '__doc__':
from kitty.cli import CompletionSpec
cd = sys.cli_docs # type: ignore cd = sys.cli_docs # type: ignore
cd['usage'] = usage cd['usage'] = usage
cd['options'] = OPTIONS cd['options'] = OPTIONS
cd['help_text'] = help_text cd['help_text'] = help_text
cd['short_desc'] = 'Copy/paste with the system clipboard, even over SSH'
cd['args_completion'] = CompletionSpec.from_string('type:file mime:* group:Files')

View File

@ -1,456 +0,0 @@
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package clipboard
import (
"bytes"
"encoding/base64"
"fmt"
"image"
"os"
"path/filepath"
"strings"
"sync"
"kitty/tools/tty"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/utils/images"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
var _ = fmt.Print
var cwd string
const OSC_NUMBER = "5522"
type Output struct {
arg string
ext string
arg_is_stream bool
mime_type string
remote_mime_type string
image_needs_conversion bool
is_stream bool
dest_is_tty bool
dest *os.File
err error
started bool
all_data_received bool
}
func (self *Output) cleanup() {
if self.dest != nil {
self.dest.Close()
if !self.is_stream {
os.Remove(self.dest.Name())
}
self.dest = nil
}
}
func (self *Output) add_data(data []byte) {
if self.err != nil {
return
}
if self.dest == nil {
if !self.image_needs_conversion && self.arg_is_stream {
self.is_stream = true
self.dest = os.Stdout
if self.arg == "/dev/stderr" {
self.dest = os.Stderr
}
self.dest_is_tty = tty.IsTerminal(self.dest.Fd())
} else {
d := cwd
if strings.ContainsRune(self.arg, os.PathSeparator) && !self.arg_is_stream {
d = filepath.Dir(self.arg)
}
f, err := os.CreateTemp(d, "."+filepath.Base(self.arg))
if err != nil {
self.err = err
return
}
self.dest = f
}
self.started = true
}
if self.dest_is_tty {
data = bytes.ReplaceAll(data, utils.UnsafeStringToBytes("\n"), utils.UnsafeStringToBytes("\r\n"))
}
_, self.err = self.dest.Write(data)
}
func (self *Output) write_image(img image.Image) (err error) {
var output *os.File
if self.arg_is_stream {
output = os.Stdout
if self.arg == "/dev/stderr" {
output = os.Stderr
}
} else {
output, err = os.Create(self.arg)
if err != nil {
return err
}
}
defer func() {
output.Close()
if err != nil && !self.arg_is_stream {
os.Remove(output.Name())
}
}()
return images.Encode(output, img, self.mime_type)
}
func (self *Output) commit() {
if self.err != nil {
return
}
if self.image_needs_conversion {
self.dest.Seek(0, os.SEEK_SET)
img, _, err := image.Decode(self.dest)
self.dest.Close()
os.Remove(self.dest.Name())
if err == nil {
err = self.write_image(img)
}
if err != nil {
self.err = fmt.Errorf("Failed to encode image data to %s with error: %w", self.mime_type, err)
}
} else {
self.dest.Close()
if !self.is_stream {
f, err := os.OpenFile(self.arg, os.O_CREATE|os.O_RDONLY, 0666)
if err == nil {
fi, err := f.Stat()
if err == nil {
self.dest.Chmod(fi.Mode().Perm())
}
f.Close()
os.Remove(f.Name())
}
self.err = os.Rename(self.dest.Name(), self.arg)
if self.err != nil {
os.Remove(self.dest.Name())
self.err = fmt.Errorf("Failed to rename temporary file used for downloading to destination: %s with error: %w", self.arg, self.err)
}
}
}
self.dest = nil
}
func (self *Output) assign_mime_type(available_mimes []string, aliases map[string][]string) (err error) {
if self.mime_type == "." {
self.remote_mime_type = "."
return
}
if slices.Contains(available_mimes, self.mime_type) {
self.remote_mime_type = self.mime_type
return
}
if len(aliases[self.mime_type]) > 0 {
for _, alias := range aliases[self.mime_type] {
if slices.Contains(available_mimes, alias) {
self.remote_mime_type = alias
return
}
}
}
for _, mt := range available_mimes {
if matched, _ := filepath.Match(self.mime_type, mt); matched {
self.remote_mime_type = mt
return
}
}
if images.EncodableImageTypes[self.mime_type] {
for _, mt := range available_mimes {
if images.DecodableImageTypes[mt] {
self.remote_mime_type = mt
self.image_needs_conversion = true
return
}
}
}
if is_textual_mime(self.mime_type) {
for _, mt := range available_mimes {
if mt == "text/plain" {
self.remote_mime_type = mt
return
}
}
}
return fmt.Errorf("The MIME type %s for %s not available on the clipboard", self.mime_type, self.arg)
}
func escape_metadata_value(k, x string) (ans string) {
if k == "mime" {
x = base64.StdEncoding.EncodeToString(utils.UnsafeStringToBytes(x))
}
return x
}
func unescape_metadata_value(k, x string) (ans string) {
if k == "mime" {
b, err := base64.StdEncoding.DecodeString(x)
if err == nil {
x = string(b)
}
}
return x
}
func encode_bytes(metadata map[string]string, payload []byte) string {
ans := strings.Builder{}
ans.Grow(2048)
ans.WriteString("\x1b]")
ans.WriteString(OSC_NUMBER)
ans.WriteString(";")
for k, v := range metadata {
if !strings.HasSuffix(ans.String(), ";") {
ans.WriteString(":")
}
ans.WriteString(k)
ans.WriteString("=")
ans.WriteString(escape_metadata_value(k, v))
}
if len(payload) > 0 {
ans.WriteString(";")
ans.WriteString(base64.StdEncoding.EncodeToString(payload))
}
ans.WriteString("\x1b\\")
return ans.String()
}
func encode(metadata map[string]string, payload string) string {
return encode_bytes(metadata, utils.UnsafeStringToBytes(payload))
}
func error_from_status(status string) error {
switch status {
case "ENOSYS":
return fmt.Errorf("no primary selection available on this system")
case "EPERM":
return fmt.Errorf("permission denied")
case "EBUSY":
return fmt.Errorf("a temporary error occurred, try again later.")
default:
return fmt.Errorf("%s", status)
}
}
func parse_escape_code(etype loop.EscapeCodeType, data []byte) (metadata map[string]string, payload []byte, err error) {
if etype != loop.OSC || !bytes.HasPrefix(data, utils.UnsafeStringToBytes(OSC_NUMBER+";")) {
return
}
parts := bytes.SplitN(data, utils.UnsafeStringToBytes(";"), 3)
metadata = make(map[string]string)
if len(parts) > 2 && len(parts[2]) > 0 {
payload, err = base64.StdEncoding.DecodeString(utils.UnsafeBytesToString(parts[2]))
if err != nil {
err = fmt.Errorf("Received OSC %s packet from terminal with invalid base64 encoded payload", OSC_NUMBER)
return
}
}
if len(parts) > 1 {
for _, record := range bytes.Split(parts[1], utils.UnsafeStringToBytes(":")) {
rp := bytes.SplitN(record, utils.UnsafeStringToBytes("="), 2)
v := ""
if len(rp) == 2 {
v = string(rp[1])
}
k := string(rp[0])
metadata[k] = unescape_metadata_value(k, v)
}
}
return
}
func parse_aliases(raw []string) (map[string][]string, error) {
ans := make(map[string][]string, len(raw))
for _, x := range raw {
k, v, found := strings.Cut(x, "=")
if !found {
return nil, fmt.Errorf("%s is not valid MIME alias specification", x)
}
ans[k] = append(ans[k], v)
ans[v] = append(ans[v], k)
}
return ans, nil
}
func run_get_loop(opts *Options, args []string) (err error) {
lp, err := loop.New(loop.NoAlternateScreen, loop.NoRestoreColors, loop.NoMouseTracking)
if err != nil {
return err
}
var available_mimes []string
var wg sync.WaitGroup
var getting_data_for string
requested_mimes := make(map[string]*Output)
reading_available_mimes := true
outputs := make([]*Output, len(args))
aliases, merr := parse_aliases(opts.Alias)
if merr != nil {
return merr
}
for i, arg := range args {
outputs[i] = &Output{arg: arg, arg_is_stream: arg == "/dev/stdout" || arg == "/dev/stderr", ext: filepath.Ext(arg)}
if len(opts.Mime) > i {
outputs[i].mime_type = opts.Mime[i]
} else {
if outputs[i].arg_is_stream {
outputs[i].mime_type = "text/plain"
} else {
outputs[i].mime_type = utils.GuessMimeType(outputs[i].arg)
}
}
if outputs[i].mime_type == "" {
return fmt.Errorf("Could not detect the MIME type for: %s use --mime to specify it manually", arg)
}
}
defer func() {
for _, o := range outputs {
if o.dest != nil {
o.cleanup()
}
}
}()
basic_metadata := map[string]string{"type": "read"}
if opts.UsePrimary {
basic_metadata["loc"] = "primary"
}
lp.OnInitialize = func() (string, error) {
lp.QueueWriteString(encode(basic_metadata, "."))
return "", nil
}
lp.OnEscapeCode = func(etype loop.EscapeCodeType, data []byte) (err error) {
metadata, payload, err := parse_escape_code(etype, data)
if err != nil {
return err
}
if metadata == nil {
return nil
}
if reading_available_mimes {
switch metadata["status"] {
case "DATA":
available_mimes = utils.Map(strings.TrimSpace, strings.Split(utils.UnsafeBytesToString(payload), " "))
case "OK":
case "DONE":
reading_available_mimes = false
if len(available_mimes) == 0 {
return fmt.Errorf("The clipboard is empty")
}
for _, o := range outputs {
err = o.assign_mime_type(available_mimes, aliases)
if err != nil {
return err
}
if o.remote_mime_type == "." {
o.started = true
o.add_data(utils.UnsafeStringToBytes(strings.Join(available_mimes, "\n")))
o.all_data_received = true
} else {
requested_mimes[o.remote_mime_type] = o
}
}
if len(requested_mimes) > 0 {
lp.QueueWriteString(encode(basic_metadata, strings.Join(maps.Keys(requested_mimes), " ")))
} else {
lp.Quit(0)
}
default:
return fmt.Errorf("Failed to read list of available data types in the clipboard with error: %w", error_from_status(metadata["status"]))
}
} else {
switch metadata["status"] {
case "DATA":
current_mime := metadata["mime"]
o := requested_mimes[current_mime]
if o != nil {
if getting_data_for != current_mime {
if prev := requested_mimes[getting_data_for]; prev != nil && !prev.all_data_received {
prev.all_data_received = true
wg.Add(1)
go func() {
prev.commit()
wg.Done()
}()
}
getting_data_for = current_mime
}
if !o.all_data_received {
o.add_data(payload)
}
}
case "OK":
case "DONE":
if prev := requested_mimes[getting_data_for]; getting_data_for != "" && prev != nil && !prev.all_data_received {
prev.all_data_received = true
wg.Add(1)
go func() {
prev.commit()
wg.Done()
}()
getting_data_for = ""
}
lp.Quit(0)
default:
return fmt.Errorf("Failed to read data from the clipboard with error: %w", error_from_status(metadata["status"]))
}
}
return
}
esc_count := 0
lp.OnKeyEvent = func(event *loop.KeyEvent) error {
if event.MatchesPressOrRepeat("ctrl+c") || event.MatchesPressOrRepeat("esc") {
event.Handled = true
esc_count++
if esc_count < 2 {
key := "Esc"
if event.MatchesPressOrRepeat("ctrl+c") {
key = "Ctrl+C"
}
lp.QueueWriteString(fmt.Sprintf("Waiting for response from terminal, press %s again to abort. This could cause garbage to be spewed to the screen.\r\n", key))
} else {
return fmt.Errorf("Aborted by user!")
}
}
return nil
}
err = lp.Run()
wg.Wait()
if err != nil {
return
}
ds := lp.DeathSignalName()
if ds != "" {
fmt.Println("Killed by signal: ", ds)
lp.KillIfSignalled()
return
}
for _, o := range outputs {
if o.err != nil {
err = fmt.Errorf("Failed to get %s with error: %w", o.arg, o.err)
return
}
if !o.started {
err = fmt.Errorf("No data for %s with MIME type: %s", o.arg, o.mime_type)
return
}
}
return
}

View File

@ -1,229 +0,0 @@
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package clipboard
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"kitty/tools/tui/loop"
"kitty/tools/utils"
)
var _ = fmt.Print
type Input struct {
src io.Reader
arg string
ext string
is_stream bool
mime_type string
extra_mime_types []string
}
func is_textual_mime(x string) bool {
return strings.HasPrefix(x, "text/") || utils.KnownTextualMimes[x]
}
func is_text_plain_mime(x string) bool {
return x == "text/plain"
}
func (self *Input) has_mime_matching(predicate func(string) bool) bool {
if predicate(self.mime_type) {
return true
}
for _, i := range self.extra_mime_types {
if predicate(i) {
return true
}
}
return false
}
func write_loop(inputs []*Input, opts *Options) (err error) {
lp, err := loop.New(loop.NoAlternateScreen, loop.NoRestoreColors, loop.NoMouseTracking)
if err != nil {
return err
}
var waiting_for_write loop.IdType
var buf [4096]byte
aliases, aerr := parse_aliases(opts.Alias)
if aerr != nil {
return aerr
}
num_text_mimes := 0
has_text_plain := false
for _, i := range inputs {
i.extra_mime_types = aliases[i.mime_type]
if i.has_mime_matching(is_textual_mime) {
num_text_mimes++
if !has_text_plain && i.has_mime_matching(is_text_plain_mime) {
has_text_plain = true
}
}
}
if num_text_mimes > 0 && !has_text_plain {
for _, i := range inputs {
if i.has_mime_matching(is_textual_mime) {
i.extra_mime_types = append(i.extra_mime_types, "text/plain")
break
}
}
}
make_metadata := func(ptype, mime string) map[string]string {
ans := map[string]string{"type": ptype}
if opts.UsePrimary {
ans["loc"] = "primary"
}
if mime != "" {
ans["mime"] = mime
}
return ans
}
lp.OnInitialize = func() (string, error) {
waiting_for_write = lp.QueueWriteString(encode(make_metadata("write", ""), ""))
return "", nil
}
write_chunk := func() error {
if len(inputs) == 0 {
return nil
}
i := inputs[0]
n, err := i.src.Read(buf[:])
if n > 0 {
waiting_for_write = lp.QueueWriteString(encode_bytes(make_metadata("wdata", i.mime_type), buf[:n]))
}
if err != nil {
if errors.Is(err, io.EOF) {
if len(i.extra_mime_types) > 0 {
lp.QueueWriteString(encode(make_metadata("walias", i.mime_type), strings.Join(i.extra_mime_types, " ")))
}
inputs = inputs[1:]
if len(inputs) == 0 {
lp.QueueWriteString(encode(make_metadata("wdata", ""), ""))
waiting_for_write = 0
}
return lp.OnWriteComplete(waiting_for_write)
}
return fmt.Errorf("Failed to read from %s with error: %w", i.arg, err)
}
return nil
}
lp.OnWriteComplete = func(msg_id loop.IdType) error {
if waiting_for_write == msg_id {
return write_chunk()
}
return nil
}
lp.OnEscapeCode = func(etype loop.EscapeCodeType, data []byte) (err error) {
metadata, _, err := parse_escape_code(etype, data)
if err != nil {
return err
}
if metadata != nil && metadata["type"] == "write" {
switch metadata["status"] {
case "DONE":
lp.Quit(0)
case "EIO":
return fmt.Errorf("Could not write to clipboard an I/O error occurred while the terminal was processing the data")
case "EINVAL":
return fmt.Errorf("Could not write to clipboard base64 encoding invalid")
case "ENOSYS":
return fmt.Errorf("Could not write to primary selection as the system does not support it")
case "EPERM":
return fmt.Errorf("Could not write to clipboard as permission was denied")
case "EBUSY":
return fmt.Errorf("Could not write to clipboard, a temporary error occurred, try again later.")
default:
return fmt.Errorf("Could not write to clipboard unknowns status returned from terminal: %#v", metadata["status"])
}
}
return
}
esc_count := 0
lp.OnKeyEvent = func(event *loop.KeyEvent) error {
if event.MatchesPressOrRepeat("ctrl+c") || event.MatchesPressOrRepeat("esc") {
event.Handled = true
esc_count++
if esc_count < 2 {
key := "Esc"
if event.MatchesPressOrRepeat("ctrl+c") {
key = "Ctrl+C"
}
lp.QueueWriteString(fmt.Sprintf("Waiting for response from terminal, press %s again to abort. This could cause garbage to be spewed to the screen.\r\n", key))
} else {
return fmt.Errorf("Aborted by user!")
}
}
return nil
}
err = lp.Run()
if err != nil {
return
}
ds := lp.DeathSignalName()
if ds != "" {
fmt.Println("Killed by signal: ", ds)
lp.KillIfSignalled()
return
}
return
}
func run_set_loop(opts *Options, args []string) (err error) {
inputs := make([]*Input, len(args))
to_process := make([]*Input, len(args))
defer func() {
for _, i := range inputs {
if i.src != nil {
rc, ok := i.src.(io.Closer)
if ok {
rc.Close()
}
}
}
}()
for i, arg := range args {
if arg == "/dev/stdin" {
f, _, err := preread_stdin()
if err != nil {
return err
}
inputs[i] = &Input{arg: arg, src: f, is_stream: true}
} else {
f, err := os.Open(arg)
if err != nil {
return fmt.Errorf("Failed to open %s with error: %w", arg, err)
}
inputs[i] = &Input{arg: arg, src: f, ext: filepath.Ext(arg)}
}
if i < len(opts.Mime) {
inputs[i].mime_type = opts.Mime[i]
} else if inputs[i].is_stream {
inputs[i].mime_type = "text/plain"
} else if inputs[i].ext != "" {
inputs[i].mime_type = utils.GuessMimeType(inputs[i].arg)
}
if inputs[i].mime_type == "" {
return fmt.Errorf("Could not guess MIME type for %s use the --mime option to specify a MIME type", arg)
}
to_process[i] = inputs[i]
if to_process[i].is_stream {
}
}
return write_loop(to_process, opts)
}

View File

@ -0,0 +1 @@
See https://sw.kovidgoyal.net/kitty/kittens/diff/

View File

@ -1,9 +1,8 @@
from typing import Dict class GlobalData:
def __init__(self) -> None:
self.title = ''
self.cmd = ''
def syntax_aliases(x: str) -> Dict[str, str]: global_data = GlobalData
ans = {}
for x in x.split():
k, _, v = x.partition(':')
ans[k] = v
return ans

View File

@ -1,390 +0,0 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package diff
import (
"crypto/md5"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"unicode/utf8"
"kitty/tools/utils"
)
var _ = fmt.Print
var path_name_map, remote_dirs map[string]string
var mimetypes_cache, data_cache, hash_cache *utils.LRUCache[string, string]
var size_cache *utils.LRUCache[string, int64]
var lines_cache *utils.LRUCache[string, []string]
var highlighted_lines_cache *utils.LRUCache[string, []string]
var is_text_cache *utils.LRUCache[string, bool]
func init_caches() {
path_name_map = make(map[string]string, 32)
remote_dirs = make(map[string]string, 32)
const sz = 4096
size_cache = utils.NewLRUCache[string, int64](sz)
mimetypes_cache = utils.NewLRUCache[string, string](sz)
data_cache = utils.NewLRUCache[string, string](sz)
is_text_cache = utils.NewLRUCache[string, bool](sz)
lines_cache = utils.NewLRUCache[string, []string](sz)
highlighted_lines_cache = utils.NewLRUCache[string, []string](sz)
hash_cache = utils.NewLRUCache[string, string](sz)
}
func add_remote_dir(val string) {
x := filepath.Base(val)
idx := strings.LastIndex(x, "-")
if idx > -1 {
x = x[idx+1:]
} else {
x = ""
}
remote_dirs[val] = x
}
func mimetype_for_path(path string) string {
return mimetypes_cache.MustGetOrCreate(path, func(path string) string {
mt := utils.GuessMimeTypeWithFileSystemAccess(path)
if mt == "" {
mt = "application/octet-stream"
}
if utils.KnownTextualMimes[mt] {
if _, a, found := strings.Cut(mt, "/"); found {
mt = "text/" + a
}
}
return mt
})
}
func data_for_path(path string) (string, error) {
return data_cache.GetOrCreate(path, func(path string) (string, error) {
ans, err := os.ReadFile(path)
return utils.UnsafeBytesToString(ans), err
})
}
func size_for_path(path string) (int64, error) {
return size_cache.GetOrCreate(path, func(path string) (int64, error) {
s, err := os.Stat(path)
if err != nil {
return 0, err
}
return s.Size(), nil
})
}
func is_image(path string) bool {
return strings.HasPrefix(mimetype_for_path(path), "image/")
}
func is_path_text(path string) bool {
return is_text_cache.MustGetOrCreate(path, func(path string) bool {
if is_image(path) {
return false
}
s1, err := os.Stat(path)
if err == nil {
s2, err := os.Stat("/dev/null")
if err == nil && os.SameFile(s1, s2) {
return false
}
}
d, err := data_for_path(path)
if err != nil {
return false
}
return utf8.ValidString(d)
})
}
func hash_for_path(path string) (string, error) {
return hash_cache.GetOrCreate(path, func(path string) (string, error) {
ans, err := data_for_path(path)
if err != nil {
return "", err
}
hash := md5.Sum(utils.UnsafeStringToBytes(ans))
return utils.UnsafeBytesToString(hash[:]), err
})
}
// Remove all control codes except newlines
func sanitize_control_codes(x string) string {
pat := utils.MustCompile("[\x00-\x09\x0b-\x1f\x7f\u0080-\u009f]")
return pat.ReplaceAllLiteralString(x, "░")
}
func sanitize_tabs_and_carriage_returns(x string) string {
return strings.NewReplacer("\t", conf.Replace_tab_by, "\r", "⏎").Replace(x)
}
func sanitize(x string) string {
return sanitize_control_codes(sanitize_tabs_and_carriage_returns(x))
}
func text_to_lines(text string) []string {
lines := make([]string, 0, 512)
splitlines_like_git(text, false, func(line string) { lines = append(lines, line) })
return lines
}
func lines_for_path(path string) ([]string, error) {
return lines_cache.GetOrCreate(path, func(path string) ([]string, error) {
ans, err := data_for_path(path)
if err != nil {
return nil, err
}
return text_to_lines(sanitize(ans)), nil
})
}
func highlighted_lines_for_path(path string) ([]string, error) {
plain_lines, err := lines_for_path(path)
if err != nil {
return nil, err
}
if ans, found := highlighted_lines_cache.Get(path); found && len(ans) == len(plain_lines) {
return ans, nil
}
return plain_lines, nil
}
type Collection struct {
changes, renames, type_map map[string]string
adds, removes *utils.Set[string]
all_paths []string
paths_to_highlight *utils.Set[string]
added_count, removed_count int
}
func (self *Collection) add_change(left, right string) {
self.changes[left] = right
self.all_paths = append(self.all_paths, left)
self.paths_to_highlight.Add(left)
self.paths_to_highlight.Add(right)
self.type_map[left] = `diff`
}
func (self *Collection) add_rename(left, right string) {
self.renames[left] = right
self.all_paths = append(self.all_paths, left)
self.type_map[left] = `rename`
}
func (self *Collection) add_add(right string) {
self.adds.Add(right)
self.all_paths = append(self.all_paths, right)
self.paths_to_highlight.Add(right)
self.type_map[right] = `add`
if is_path_text(right) {
num, _ := lines_for_path(right)
self.added_count += len(num)
}
}
func (self *Collection) add_removal(left string) {
self.removes.Add(left)
self.all_paths = append(self.all_paths, left)
self.paths_to_highlight.Add(left)
self.type_map[left] = `removal`
if is_path_text(left) {
num, _ := lines_for_path(left)
self.removed_count += len(num)
}
}
func (self *Collection) finalize() {
utils.StableSortWithKey(self.all_paths, func(path string) string {
return path_name_map[path]
})
}
func (self *Collection) Len() int { return len(self.all_paths) }
func (self *Collection) Items() int { return len(self.all_paths) }
func (self *Collection) Apply(f func(path, typ, changed_path string) error) error {
for _, path := range self.all_paths {
typ := self.type_map[path]
changed_path := ""
switch typ {
case "diff":
changed_path = self.changes[path]
case "rename":
changed_path = self.renames[path]
}
if err := f(path, typ, changed_path); err != nil {
return err
}
}
return nil
}
func allowed(path string, patterns ...string) bool {
name := filepath.Base(path)
for _, pat := range patterns {
if matched, err := filepath.Match(pat, name); err == nil && matched {
return false
}
}
return true
}
func remote_hostname(path string) (string, string) {
for q, val := range remote_dirs {
if strings.HasPrefix(path, q) {
return q, val
}
}
return "", ""
}
func resolve_remote_name(path, defval string) string {
remote_dir, rh := remote_hostname(path)
if remote_dir != "" && rh != "" {
r, err := filepath.Rel(remote_dir, path)
if err == nil {
return rh + ":" + r
}
}
return defval
}
func walk(base string, patterns []string, names *utils.Set[string], pmap, path_name_map map[string]string) error {
base, err := filepath.Abs(base)
if err != nil {
return err
}
return filepath.WalkDir(base, func(path string, d fs.DirEntry, err error) error {
is_allowed := allowed(path, patterns...)
if !is_allowed {
if d.IsDir() {
return fs.SkipDir
}
return nil
}
if d.IsDir() {
return nil
}
path, err = filepath.Abs(path)
if err != nil {
return err
}
name, err := filepath.Rel(base, path)
if err != nil {
return err
}
if name != "." {
path_name_map[path] = name
names.Add(name)
pmap[name] = path
}
return nil
})
}
func (self *Collection) collect_files(left, right string) error {
left_names, right_names := utils.NewSet[string](16), utils.NewSet[string](16)
left_path_map, right_path_map := make(map[string]string, 16), make(map[string]string, 16)
err := walk(left, conf.Ignore_name, left_names, left_path_map, path_name_map)
if err != nil {
return err
}
err = walk(right, conf.Ignore_name, right_names, right_path_map, path_name_map)
common_names := left_names.Intersect(right_names)
changed_names := utils.NewSet[string](common_names.Len())
for n := range common_names.Iterable() {
ld, err := data_for_path(left_path_map[n])
var rd string
if err == nil {
rd, err = data_for_path(right_path_map[n])
}
if err != nil {
return err
}
if ld != rd {
changed_names.Add(n)
self.add_change(left_path_map[n], right_path_map[n])
}
}
removed := left_names.Subtract(common_names)
added := right_names.Subtract(common_names)
ahash, rhash := make(map[string]string, added.Len()), make(map[string]string, removed.Len())
for a := range added.Iterable() {
ahash[a], err = hash_for_path(right_path_map[a])
if err != nil {
return err
}
}
for r := range removed.Iterable() {
rhash[r], err = hash_for_path(left_path_map[r])
if err != nil {
return err
}
}
for name, rh := range rhash {
found := false
for n, ah := range ahash {
if ah == rh {
ld, _ := data_for_path(left_path_map[name])
rd, _ := data_for_path(right_path_map[n])
if ld == rd {
self.add_rename(left_path_map[name], right_path_map[n])
added.Discard(n)
found = true
break
}
}
}
if !found {
self.add_removal(left_path_map[name])
}
}
for name := range added.Iterable() {
self.add_add(right_path_map[name])
}
return nil
}
func create_collection(left, right string) (ans *Collection, err error) {
ans = &Collection{
changes: make(map[string]string),
renames: make(map[string]string),
type_map: make(map[string]string),
adds: utils.NewSet[string](32),
removes: utils.NewSet[string](32),
paths_to_highlight: utils.NewSet[string](32),
all_paths: make([]string, 0, 32),
}
left_stat, err := os.Stat(left)
if err != nil {
return nil, err
}
if left_stat.IsDir() {
err = ans.collect_files(left, right)
if err != nil {
return nil, err
}
} else {
pl, err := filepath.Abs(left)
if err != nil {
return nil, err
}
pr, err := filepath.Abs(right)
if err != nil {
return nil, err
}
path_name_map[pl] = resolve_remote_name(pl, left)
path_name_map[pr] = resolve_remote_name(pr, right)
ans.add_change(pl, pr)
}
ans.finalize()
return ans, err
}

235
kittens/diff/collect.py Normal file
View File

@ -0,0 +1,235 @@
#!/usr/bin/env python3
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import os
from contextlib import suppress
from fnmatch import fnmatch
from functools import lru_cache
from hashlib import md5
from typing import (
TYPE_CHECKING, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Union
)
from kitty.guess_mime_type import guess_type
from kitty.utils import control_codes_pat
if TYPE_CHECKING:
from .highlight import DiffHighlight # noqa
path_name_map: Dict[str, str] = {}
remote_dirs: Dict[str, str] = {}
def add_remote_dir(val: str) -> None:
remote_dirs[val] = os.path.basename(val).rpartition('-')[-1]
class Segment:
__slots__ = ('start', 'end', 'start_code', 'end_code')
def __init__(self, start: int, start_code: str):
self.start = start
self.start_code = start_code
self.end: Optional[int] = None
self.end_code: Optional[str] = None
def __repr__(self) -> str:
return f'Segment(start={self.start!r}, start_code={self.start_code!r}, end={self.end!r}, end_code={self.end_code!r})'
class Collection:
ignore_names: Tuple[str, ...] = ()
def __init__(self) -> None:
self.changes: Dict[str, str] = {}
self.renames: Dict[str, str] = {}
self.adds: Set[str] = set()
self.removes: Set[str] = set()
self.all_paths: List[str] = []
self.type_map: Dict[str, str] = {}
self.added_count = self.removed_count = 0
def add_change(self, left_path: str, right_path: str) -> None:
self.changes[left_path] = right_path
self.all_paths.append(left_path)
self.type_map[left_path] = 'diff'
def add_rename(self, left_path: str, right_path: str) -> None:
self.renames[left_path] = right_path
self.all_paths.append(left_path)
self.type_map[left_path] = 'rename'
def add_add(self, right_path: str) -> None:
self.adds.add(right_path)
self.all_paths.append(right_path)
self.type_map[right_path] = 'add'
if isinstance(data_for_path(right_path), str):
self.added_count += len(lines_for_path(right_path))
def add_removal(self, left_path: str) -> None:
self.removes.add(left_path)
self.all_paths.append(left_path)
self.type_map[left_path] = 'removal'
if isinstance(data_for_path(left_path), str):
self.removed_count += len(lines_for_path(left_path))
def finalize(self) -> None:
def key(x: str) -> str:
return path_name_map.get(x, '')
self.all_paths.sort(key=key)
def __iter__(self) -> Iterator[Tuple[str, str, Optional[str]]]:
for path in self.all_paths:
typ = self.type_map[path]
if typ == 'diff':
data: Optional[str] = self.changes[path]
elif typ == 'rename':
data = self.renames[path]
else:
data = None
yield path, typ, data
def __len__(self) -> int:
return len(self.all_paths)
def remote_hostname(path: str) -> Tuple[Optional[str], Optional[str]]:
for q in remote_dirs:
if path.startswith(q):
return q, remote_dirs[q]
return None, None
def resolve_remote_name(path: str, default: str) -> str:
remote_dir, rh = remote_hostname(path)
if remote_dir and rh:
return f'{rh}:{os.path.relpath(path, remote_dir)}'
return default
def allowed_items(items: Sequence[str], ignore_patterns: Sequence[str]) -> Iterator[str]:
for name in items:
for pat in ignore_patterns:
if fnmatch(name, pat):
break
else:
yield name
def walk(base: str, names: Set[str], pmap: Dict[str, str], ignore_names: Tuple[str, ...]) -> None:
for dirpath, dirnames, filenames in os.walk(base):
dirnames[:] = allowed_items(dirnames, ignore_names)
for filename in allowed_items(filenames, ignore_names):
path = os.path.abspath(os.path.join(dirpath, filename))
path_name_map[path] = name = os.path.relpath(path, base)
names.add(name)
pmap[name] = path
def collect_files(collection: Collection, left: str, right: str) -> None:
left_names: Set[str] = set()
right_names: Set[str] = set()
left_path_map: Dict[str, str] = {}
right_path_map: Dict[str, str] = {}
walk(left, left_names, left_path_map, collection.ignore_names)
walk(right, right_names, right_path_map, collection.ignore_names)
common_names = left_names & right_names
changed_names = {n for n in common_names if data_for_path(left_path_map[n]) != data_for_path(right_path_map[n])}
for n in changed_names:
collection.add_change(left_path_map[n], right_path_map[n])
removed = left_names - common_names
added = right_names - common_names
ahash = {a: hash_for_path(right_path_map[a]) for a in added}
rhash = {r: hash_for_path(left_path_map[r]) for r in removed}
for name, rh in rhash.items():
for n, ah in ahash.items():
if ah == rh and data_for_path(left_path_map[name]) == data_for_path(right_path_map[n]):
collection.add_rename(left_path_map[name], right_path_map[n])
added.discard(n)
break
else:
collection.add_removal(left_path_map[name])
for name in added:
collection.add_add(right_path_map[name])
def sanitize(text: str) -> str:
ntext = text.replace('\r\n', '\n')
return control_codes_pat().sub('', ntext)
@lru_cache(maxsize=1024)
def mime_type_for_path(path: str) -> str:
return guess_type(path, allow_filesystem_access=True) or 'application/octet-stream'
@lru_cache(maxsize=1024)
def raw_data_for_path(path: str) -> bytes:
with open(path, 'rb') as f:
return f.read()
def is_image(path: Optional[str]) -> bool:
return mime_type_for_path(path).startswith('image/') if path else False
@lru_cache(maxsize=1024)
def data_for_path(path: str) -> Union[str, bytes]:
raw_bytes = raw_data_for_path(path)
if not is_image(path) and not os.path.samefile(path, os.devnull):
with suppress(UnicodeDecodeError):
return raw_bytes.decode('utf-8')
return raw_bytes
class LinesForPath:
replace_tab_by = ' ' * 4
@lru_cache(maxsize=1024)
def __call__(self, path: str) -> Tuple[str, ...]:
data = data_for_path(path)
assert isinstance(data, str)
data = data.replace('\t', self.replace_tab_by)
return tuple(sanitize(data).splitlines())
lines_for_path = LinesForPath()
@lru_cache(maxsize=1024)
def hash_for_path(path: str) -> bytes:
return md5(raw_data_for_path(path)).digest()
def create_collection(left: str, right: str) -> Collection:
collection = Collection()
if os.path.isdir(left):
collect_files(collection, left, right)
else:
pl, pr = os.path.abspath(left), os.path.abspath(right)
path_name_map[pl] = resolve_remote_name(pl, left)
path_name_map[pr] = resolve_remote_name(pr, right)
collection.add_change(pl, pr)
collection.finalize()
return collection
highlight_data: Dict[str, 'DiffHighlight'] = {}
def set_highlight_data(data: Dict[str, 'DiffHighlight']) -> None:
global highlight_data
highlight_data = data
def highlights_for_path(path: str) -> 'DiffHighlight':
return highlight_data.get(path, [])

View File

@ -1,53 +0,0 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package diff
import (
"fmt"
"os"
"path/filepath"
"testing"
"kitty/tools/utils"
"github.com/google/go-cmp/cmp"
)
var _ = fmt.Print
func TestDiffCollectWalk(t *testing.T) {
tdir := t.TempDir()
j := func(x ...string) string { return filepath.Join(append([]string{tdir}, x...)...) }
os.MkdirAll(j("a", "b"), 0o700)
os.WriteFile(j("a/b/c"), nil, 0o600)
os.WriteFile(j("b"), nil, 0o600)
os.WriteFile(j("d"), nil, 0o600)
os.WriteFile(j("e"), nil, 0o600)
os.WriteFile(j("#d#"), nil, 0o600)
os.WriteFile(j("e~"), nil, 0o600)
os.MkdirAll(j("f"), 0o700)
os.WriteFile(j("f/g"), nil, 0o600)
os.WriteFile(j("h space"), nil, 0o600)
expected_names := utils.NewSetWithItems("d", "e", "f/g", "h space")
expected_pmap := map[string]string{
"d": j("d"),
"e": j("e"),
"f/g": j("f/g"),
"h space": j("h space"),
}
names := utils.NewSet[string](16)
pmap := make(map[string]string, 16)
if err := walk(tdir, []string{"*~", "#*#", "b"}, names, pmap, map[string]string{}); err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(
utils.Sort(expected_names.AsSlice(), func(a, b string) bool { return a < b }),
utils.Sort(names.AsSlice(), func(a, b string) bool { return a < b }),
); diff != "" {
t.Fatal(diff)
}
if diff := cmp.Diff(expected_pmap, pmap); diff != "" {
t.Fatal(diff)
}
}

72
kittens/diff/config.py Normal file
View File

@ -0,0 +1,72 @@
#!/usr/bin/env python3
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import os
from typing import Any, Dict, Iterable, Optional
from kitty.cli_stub import DiffCLIOptions
from kitty.conf.utils import (
load_config as _load_config, parse_config_base, resolve_config
)
from kitty.constants import config_dir
from kitty.rgb import color_as_sgr
from .options.types import Options as DiffOptions, defaults
formats: Dict[str, str] = {
'title': '',
'margin': '',
'text': '',
}
def set_formats(opts: DiffOptions) -> None:
formats['text'] = '48' + color_as_sgr(opts.background)
formats['title'] = '38' + color_as_sgr(opts.title_fg) + ';48' + color_as_sgr(opts.title_bg) + ';1'
formats['margin'] = '38' + color_as_sgr(opts.margin_fg) + ';48' + color_as_sgr(opts.margin_bg)
formats['added_margin'] = '38' + color_as_sgr(opts.margin_fg) + ';48' + color_as_sgr(opts.added_margin_bg)
formats['removed_margin'] = '38' + color_as_sgr(opts.margin_fg) + ';48' + color_as_sgr(opts.removed_margin_bg)
formats['added'] = '48' + color_as_sgr(opts.added_bg)
formats['removed'] = '48' + color_as_sgr(opts.removed_bg)
formats['filler'] = '48' + color_as_sgr(opts.filler_bg)
formats['margin_filler'] = '48' + color_as_sgr(opts.margin_filler_bg or opts.filler_bg)
formats['hunk_margin'] = '38' + color_as_sgr(opts.margin_fg) + ';48' + color_as_sgr(opts.hunk_margin_bg)
formats['hunk'] = '38' + color_as_sgr(opts.margin_fg) + ';48' + color_as_sgr(opts.hunk_bg)
formats['removed_highlight'] = '48' + color_as_sgr(opts.highlight_removed_bg)
formats['added_highlight'] = '48' + color_as_sgr(opts.highlight_added_bg)
SYSTEM_CONF = '/etc/xdg/kitty/diff.conf'
defconf = os.path.join(config_dir, 'diff.conf')
def load_config(*paths: str, overrides: Optional[Iterable[str]] = None) -> DiffOptions:
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
overrides = tuple(overrides) if overrides is not None else ()
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:
config = tuple(resolve_config(SYSTEM_CONF, defconf, args.config))
overrides = (a.replace('=', ' ', 1) for a in args.override or ())
opts = load_config(*config, overrides=overrides)
set_formats(opts)
for (sc, action) in opts.map:
opts.key_definitions[sc] = action
return opts

View File

@ -1,264 +0,0 @@
// Copied from the Go stdlib, with modifications.
//https://github.com/golang/go/raw/master/src/internal/diff/diff.go
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package diff
import (
"bytes"
"fmt"
"sort"
"strings"
)
// A pair is a pair of values tracked for both the x and y side of a diff.
// It is typically a pair of line indexes.
type pair struct{ x, y int }
// Diff returns an anchored diff of the two texts old and new
// in the “unified diff” format. If old and new are identical,
// Diff returns a nil slice (no output).
//
// Unix diff implementations typically look for a diff with
// the smallest number of lines inserted and removed,
// which can in the worst case take time quadratic in the
// number of lines in the texts. As a result, many implementations
// either can be made to run for a long time or cut off the search
// after a predetermined amount of work.
//
// In contrast, this implementation looks for a diff with the
// smallest number of “unique” lines inserted and removed,
// where unique means a line that appears just once in both old and new.
// We call this an “anchored diff” because the unique lines anchor
// the chosen matching regions. An anchored diff is usually clearer
// than a standard diff, because the algorithm does not try to
// reuse unrelated blank lines or closing braces.
// The algorithm also guarantees to run in O(n log n) time
// instead of the standard O(n²) time.
//
// Some systems call this approach a “patience diff,” named for
// the “patience sorting” algorithm, itself named for a solitaire card game.
// We avoid that name for two reasons. First, the name has been used
// for a few different variants of the algorithm, so it is imprecise.
// Second, the name is frequently interpreted as meaning that you have
// to wait longer (to be patient) for the diff, meaning that it is a slower algorithm,
// when in fact the algorithm is faster than the standard one.
func Diff(oldName, old, newName, new string, num_of_context_lines int) []byte {
if old == new {
return nil
}
x := lines(old)
y := lines(new)
// Print diff header.
var out bytes.Buffer
fmt.Fprintf(&out, "diff %s %s\n", oldName, newName)
fmt.Fprintf(&out, "--- %s\n", oldName)
fmt.Fprintf(&out, "+++ %s\n", newName)
// Loop over matches to consider,
// expanding each match to include surrounding lines,
// and then printing diff chunks.
// To avoid setup/teardown cases outside the loop,
// tgs returns a leading {0,0} and trailing {len(x), len(y)} pair
// in the sequence of matches.
var (
done pair // printed up to x[:done.x] and y[:done.y]
chunk pair // start lines of current chunk
count pair // number of lines from each side in current chunk
ctext []string // lines for current chunk
)
for _, m := range tgs(x, y) {
if m.x < done.x {
// Already handled scanning forward from earlier match.
continue
}
// Expand matching lines as far possible,
// establishing that x[start.x:end.x] == y[start.y:end.y].
// Note that on the first (or last) iteration we may (or definitey do)
// have an empty match: start.x==end.x and start.y==end.y.
start := m
for start.x > done.x && start.y > done.y && x[start.x-1] == y[start.y-1] {
start.x--
start.y--
}
end := m
for end.x < len(x) && end.y < len(y) && x[end.x] == y[end.y] {
end.x++
end.y++
}
// Emit the mismatched lines before start into this chunk.
// (No effect on first sentinel iteration, when start = {0,0}.)
for _, s := range x[done.x:start.x] {
ctext = append(ctext, "-"+s)
count.x++
}
for _, s := range y[done.y:start.y] {
ctext = append(ctext, "+"+s)
count.y++
}
// If we're not at EOF and have too few common lines,
// the chunk includes all the common lines and continues.
C := num_of_context_lines // number of context lines
if (end.x < len(x) || end.y < len(y)) &&
(end.x-start.x < C || (len(ctext) > 0 && end.x-start.x < 2*C)) {
for _, s := range x[start.x:end.x] {
ctext = append(ctext, " "+s)
count.x++
count.y++
}
done = end
continue
}
// End chunk with common lines for context.
if len(ctext) > 0 {
n := end.x - start.x
if n > C {
n = C
}
for _, s := range x[start.x : start.x+n] {
ctext = append(ctext, " "+s)
count.x++
count.y++
}
done = pair{start.x + n, start.y + n}
// Format and emit chunk.
// Convert line numbers to 1-indexed.
// Special case: empty file shows up as 0,0 not 1,0.
if count.x > 0 {
chunk.x++
}
if count.y > 0 {
chunk.y++
}
fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", chunk.x, count.x, chunk.y, count.y)
for _, s := range ctext {
out.WriteString(s)
}
count.x = 0
count.y = 0
ctext = ctext[:0]
}
// If we reached EOF, we're done.
if end.x >= len(x) && end.y >= len(y) {
break
}
// Otherwise start a new chunk.
chunk = pair{end.x - C, end.y - C}
for _, s := range x[chunk.x:end.x] {
ctext = append(ctext, " "+s)
count.x++
count.y++
}
done = end
}
return out.Bytes()
}
// lines returns the lines in the file x, including newlines.
// If the file does not end in a newline, one is supplied
// along with a warning about the missing newline.
func lines(x string) []string {
l := strings.SplitAfter(x, "\n")
if l[len(l)-1] == "" {
l = l[:len(l)-1]
} else {
// Treat last line as having a message about the missing newline attached,
// using the same text as BSD/GNU diff (including the leading backslash).
l[len(l)-1] += "\n\\ No newline at end of file\n"
}
return l
}
// tgs returns the pairs of indexes of the longest common subsequence
// of unique lines in x and y, where a unique line is one that appears
// once in x and once in y.
//
// The longest common subsequence algorithm is as described in
// Thomas G. Szymanski, “A Special Case of the Maximal Common
// Subsequence Problem,” Princeton TR #170 (January 1975),
// available at https://research.swtch.com/tgs170.pdf.
func tgs(x, y []string) []pair {
// Count the number of times each string appears in a and b.
// We only care about 0, 1, many, counted as 0, -1, -2
// for the x side and 0, -4, -8 for the y side.
// Using negative numbers now lets us distinguish positive line numbers later.
m := make(map[string]int)
for _, s := range x {
if c := m[s]; c > -2 {
m[s] = c - 1
}
}
for _, s := range y {
if c := m[s]; c > -8 {
m[s] = c - 4
}
}
// Now unique strings can be identified by m[s] = -1+-4.
//
// Gather the indexes of those strings in x and y, building:
// xi[i] = increasing indexes of unique strings in x.
// yi[i] = increasing indexes of unique strings in y.
// inv[i] = index j such that x[xi[i]] = y[yi[j]].
var xi, yi, inv []int
for i, s := range y {
if m[s] == -1+-4 {
m[s] = len(yi)
yi = append(yi, i)
}
}
for i, s := range x {
if j, ok := m[s]; ok && j >= 0 {
xi = append(xi, i)
inv = append(inv, j)
}
}
// Apply Algorithm A from Szymanski's paper.
// In those terms, A = J = inv and B = [0, n).
// We add sentinel pairs {0,0}, and {len(x),len(y)}
// to the returned sequence, to help the processing loop.
J := inv
n := len(xi)
T := make([]int, n)
L := make([]int, n)
for i := range T {
T[i] = n + 1
}
for i := 0; i < n; i++ {
k := sort.Search(n, func(k int) bool {
return T[k] >= J[i]
})
T[k] = J[i]
L[i] = k + 1
}
k := 0
for _, v := range L {
if k < v {
k = v
}
}
seq := make([]pair, 2+k)
seq[1+k] = pair{len(x), len(y)} // sentinel at end
lastj := n
for i := n - 1; i >= 0; i-- {
if L[i] == k && J[i] < lastj {
seq[k] = pair{xi[i], yi[J[i]]}
k--
}
}
seq[0] = pair{0, 0} // sentinel at start
return seq
}

View File

@ -0,0 +1,14 @@
from typing import List, Optional, Tuple
from .collect import Segment
def split_with_highlights(
line: str, truncate_points: List[int], fg_highlights: List[Segment],
bg_highlight: Optional[Segment]
) -> List[str]:
pass
def changed_center(left_prefix: str, right_postfix: str) -> Tuple[int, int]:
pass

View File

@ -1,207 +0,0 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package diff
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"kitty/tools/utils"
"kitty/tools/utils/images"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
)
var _ = fmt.Print
var _ = os.WriteFile
var ErrNoLexer = errors.New("No lexer available for this format")
var DefaultStyle = (&utils.Once[*chroma.Style]{Run: func() *chroma.Style {
// Default style generated by python style.py default pygments.styles.default.DefaultStyle
// with https://raw.githubusercontent.com/alecthomas/chroma/master/_tools/style.py
return styles.Register(chroma.MustNewStyle("default", chroma.StyleEntries{
chroma.TextWhitespace: "#bbbbbb",
chroma.Comment: "italic #3D7B7B",
chroma.CommentPreproc: "noitalic #9C6500",
chroma.Keyword: "bold #008000",
chroma.KeywordPseudo: "nobold",
chroma.KeywordType: "nobold #B00040",
chroma.Operator: "#666666",
chroma.OperatorWord: "bold #AA22FF",
chroma.NameBuiltin: "#008000",
chroma.NameFunction: "#0000FF",
chroma.NameClass: "bold #0000FF",
chroma.NameNamespace: "bold #0000FF",
chroma.NameException: "bold #CB3F38",
chroma.NameVariable: "#19177C",
chroma.NameConstant: "#880000",
chroma.NameLabel: "#767600",
chroma.NameEntity: "bold #717171",
chroma.NameAttribute: "#687822",
chroma.NameTag: "bold #008000",
chroma.NameDecorator: "#AA22FF",
chroma.LiteralString: "#BA2121",
chroma.LiteralStringDoc: "italic",
chroma.LiteralStringInterpol: "bold #A45A77",
chroma.LiteralStringEscape: "bold #AA5D1F",
chroma.LiteralStringRegex: "#A45A77",
chroma.LiteralStringSymbol: "#19177C",
chroma.LiteralStringOther: "#008000",
chroma.LiteralNumber: "#666666",
chroma.GenericHeading: "bold #000080",
chroma.GenericSubheading: "bold #800080",
chroma.GenericDeleted: "#A00000",
chroma.GenericInserted: "#008400",
chroma.GenericError: "#E40000",
chroma.GenericEmph: "italic",
chroma.GenericStrong: "bold",
chroma.GenericPrompt: "bold #000080",
chroma.GenericOutput: "#717171",
chroma.GenericTraceback: "#04D",
chroma.Error: "border:#FF0000",
chroma.Background: " bg:#f8f8f8",
}))
}}).Get
// Clear the background colour.
func clear_background(style *chroma.Style) *chroma.Style {
builder := style.Builder()
bg := builder.Get(chroma.Background)
bg.Background = 0
bg.NoInherit = true
builder.AddEntry(chroma.Background, bg)
style, _ = builder.Build()
return style
}
func ansi_formatter(w io.Writer, style *chroma.Style, it chroma.Iterator) error {
const SGR_PREFIX = "\033["
const SGR_SUFFIX = "m"
style = clear_background(style)
before, after := make([]byte, 0, 64), make([]byte, 0, 64)
nl := []byte{'\n'}
write_sgr := func(which []byte) {
if len(which) > 1 {
w.Write(utils.UnsafeStringToBytes(SGR_PREFIX))
w.Write(which[:len(which)-1])
w.Write(utils.UnsafeStringToBytes(SGR_SUFFIX))
}
}
write := func(text string) {
write_sgr(before)
w.Write(utils.UnsafeStringToBytes(text))
write_sgr(after)
}
for token := it(); token != chroma.EOF; token = it() {
entry := style.Get(token.Type)
before, after = before[:0], after[:0]
if !entry.IsZero() {
if entry.Bold == chroma.Yes {
before = append(before, '1', ';')
after = append(after, '2', '2', '1', ';')
}
if entry.Underline == chroma.Yes {
before = append(before, '4', ';')
after = append(after, '2', '4', ';')
}
if entry.Italic == chroma.Yes {
before = append(before, '3', ';')
after = append(after, '2', '3', ';')
}
if entry.Colour.IsSet() {
before = append(before, fmt.Sprintf("38:2:%d:%d:%d;", entry.Colour.Red(), entry.Colour.Green(), entry.Colour.Blue())...)
after = append(after, '3', '9', ';')
}
}
// independently format each line in a multiline token, needed for the diff kitten highlighting to work, also
// pagers like less reset SGR formatting at line boundaries
text := sanitize(token.Value)
for text != "" {
idx := strings.IndexByte(text, '\n')
if idx < 0 {
write(text)
break
}
write(text[:idx])
w.Write(nl)
text = text[idx+1:]
}
}
return nil
}
func highlight_file(path string) (highlighted string, err error) {
filename_for_detection := filepath.Base(path)
ext := filepath.Ext(filename_for_detection)
if ext != "" {
ext = strings.ToLower(ext[1:])
r := conf.Syntax_aliases[ext]
if r != "" {
filename_for_detection = "file." + r
}
}
text, err := data_for_path(path)
if err != nil {
return "", err
}
lexer := lexers.Match(filename_for_detection)
if lexer == nil {
if err == nil {
lexer = lexers.Analyse(text)
}
}
if lexer == nil {
return "", fmt.Errorf("Cannot highlight %#v: %w", path, ErrNoLexer)
}
lexer = chroma.Coalesce(lexer)
name := conf.Pygments_style
var style *chroma.Style
if name == "default" {
style = DefaultStyle()
} else {
style = styles.Get(name)
}
if style == nil {
if conf.Background.IsDark() && !conf.Foreground.IsDark() {
style = styles.Get("monokai")
if style == nil {
style = styles.Get("github-dark")
}
} else {
style = DefaultStyle()
}
if style == nil {
style = styles.Fallback
}
}
iterator, err := lexer.Tokenise(nil, text)
if err != nil {
return "", err
}
formatter := chroma.FormatterFunc(ansi_formatter)
w := strings.Builder{}
w.Grow(len(text) * 2)
err = formatter.Format(&w, style, iterator)
// os.WriteFile(filepath.Base(path+".highlighted"), []byte(w.String()), 0o600)
return w.String(), err
}
func highlight_all(paths []string) {
ctx := images.Context{}
ctx.Parallel(0, len(paths), func(nums <-chan int) {
for i := range nums {
path := paths[i]
raw, err := highlight_file(path)
if err == nil {
highlighted_lines_cache.Set(path, text_to_lines(raw))
}
}
})
}

187
kittens/diff/highlight.py Normal file
View File

@ -0,0 +1,187 @@
#!/usr/bin/env python3
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import concurrent
import os
import re
from concurrent.futures import ProcessPoolExecutor
from typing import (
IO, Dict, Iterable, Iterator, List, Optional, Tuple, Union, cast
)
from pygments import highlight # type: ignore
from pygments.formatter import Formatter # type: ignore
from pygments.lexers import get_lexer_for_filename # type: ignore
from pygments.util import ClassNotFound # type: ignore
from kitty.multiprocessing import get_process_pool_executor
from kitty.rgb import color_as_sgr, parse_sharp
from .collect import Collection, Segment, data_for_path, lines_for_path
class StyleNotFound(Exception):
pass
class DiffFormatter(Formatter): # type: ignore
def __init__(self, style: str = 'default') -> None:
try:
Formatter.__init__(self, style=style)
initialized = True
except ClassNotFound:
initialized = False
if not initialized:
raise StyleNotFound(f'pygments style "{style}" not found')
self.styles: Dict[str, Tuple[str, str]] = {}
for token, token_style in self.style:
start = []
end = []
fstart = fend = ''
# a style item is a tuple in the following form:
# colors are readily specified in hex: 'RRGGBB'
col = token_style['color']
if col:
pc = parse_sharp(col)
if pc is not None:
start.append('38' + color_as_sgr(pc))
end.append('39')
if token_style['bold']:
start.append('1')
end.append('22')
if token_style['italic']:
start.append('3')
end.append('23')
if token_style['underline']:
start.append('4')
end.append('24')
if start:
fstart = '\033[{}m'.format(';'.join(start))
fend = '\033[{}m'.format(';'.join(end))
self.styles[token] = fstart, fend
def format(self, tokensource: Iterable[Tuple[str, str]], outfile: IO[str]) -> None:
for ttype, value in tokensource:
not_found = True
if value.rstrip('\n'):
while ttype and not_found:
tok = self.styles.get(ttype)
if tok is None:
ttype = ttype[:-1]
else:
on, off = tok
lines = value.split('\n')
for line in lines:
if line:
outfile.write(on + line + off)
if line is not lines[-1]:
outfile.write('\n')
not_found = False
if not_found:
outfile.write(value)
formatter: Optional[DiffFormatter] = None
def initialize_highlighter(style: str = 'default') -> None:
global formatter
formatter = DiffFormatter(style)
def highlight_data(code: str, filename: str, aliases: Optional[Dict[str, str]] = None) -> Optional[str]:
if aliases:
base, ext = os.path.splitext(filename)
alias = aliases.get(ext[1:])
if alias is not None:
filename = f'{base}.{alias}'
try:
lexer = get_lexer_for_filename(filename, stripnl=False)
except ClassNotFound:
return None
return cast(str, highlight(code, lexer, formatter))
split_pat = re.compile(r'(\033\[.*?m)')
def highlight_line(line: str) -> List[Segment]:
ans: List[Segment] = []
current: Optional[Segment] = None
pos = 0
for x in split_pat.split(line):
if x.startswith('\033'):
if current is None:
current = Segment(pos, x)
else:
current.end = pos
current.end_code = x
ans.append(current)
current = None
else:
pos += len(x)
return ans
DiffHighlight = List[List[Segment]]
def highlight_for_diff(path: str, aliases: Dict[str, str]) -> DiffHighlight:
ans: DiffHighlight = []
lines = lines_for_path(path)
hd = highlight_data('\n'.join(lines), path, aliases)
if hd is not None:
for line in hd.splitlines():
ans.append(highlight_line(line))
return ans
process_pool_executor: Optional[ProcessPoolExecutor] = None
def get_highlight_processes() -> Iterator[int]:
if process_pool_executor is None:
return
for pid in process_pool_executor._processes:
yield pid
def highlight_collection(collection: Collection, aliases: Optional[Dict[str, str]] = None) -> Union[str, Dict[str, DiffHighlight]]:
global process_pool_executor
jobs = {}
ans: Dict[str, DiffHighlight] = {}
with get_process_pool_executor(prefer_fork=True) as executor:
process_pool_executor = executor
for path, item_type, other_path in collection:
if item_type != 'rename':
for p in (path, other_path):
if p:
is_binary = isinstance(data_for_path(p), bytes)
if not is_binary:
jobs[executor.submit(highlight_for_diff, p, aliases or {})] = p
for future in concurrent.futures.as_completed(jobs):
path = jobs[future]
try:
highlights = future.result()
except Exception as e:
import traceback
tb = traceback.format_exc()
return f'Running syntax highlighting for {path} generated an exception: {e} with traceback:\n{tb}'
ans[path] = highlights
return ans
def main() -> None:
# kitty +runpy "from kittens.diff.highlight import main; main()" file
import sys
from .options.types import defaults
initialize_highlighter()
with open(sys.argv[-1]) as f:
highlighted = highlight_data(f.read(), f.name, defaults.syntax_aliases)
if highlighted is None:
raise SystemExit(f'Unknown filetype: {sys.argv[-1]}')
print(highlighted)

Some files were not shown because too many files have changed in this diff Show More