Compare commits
No commits in common. "bold_is_bright" and "v0.26.5" have entirely different histories.
bold_is_br
...
v0.26.5
@ -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
5
.gitattributes
vendored
@ -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
4
.github/FUNDING.yml
vendored
@ -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
|
||||||
|
|||||||
9
.github/workflows/ci.py
vendored
9
.github/workflows/ci.py
vendored
@ -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}')
|
||||||
|
|
||||||
|
|||||||
50
.github/workflows/ci.yml
vendored
50
.github/workflows/ci.yml
vendored
@ -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
|
||||||
|
|
||||||
|
|||||||
10
.github/workflows/codeql-analysis.yml
vendored
10
.github/workflows/codeql-analysis.yml
vendored
@ -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
7
.gitignore
vendored
@ -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
|
||||||
|
|||||||
1
Brewfile
1
Brewfile
@ -5,4 +5,3 @@ brew "python"
|
|||||||
brew "imagemagick"
|
brew "imagemagick"
|
||||||
brew "harfbuzz"
|
brew "harfbuzz"
|
||||||
brew "sphinx-doc"
|
brew "sphinx-doc"
|
||||||
brew "go"
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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']
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
@ -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`
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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]
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
|
||||||
48
docs/conf.py
48
docs/conf.py
@ -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:
|
||||||
|
|||||||
@ -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
|
||||||
------------------------
|
------------------------
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
189
docs/faq.rst
189
docs/faq.rst
@ -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
|
||||||
|
|||||||
@ -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``.
|
|
||||||
|
|||||||
@ -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:: python
|
||||||
|
|
||||||
.. 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
|
|
||||||
|
|
||||||
#!/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**
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,14 +581,8 @@ 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:
|
||||||
|
|
||||||
@ -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!!
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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 :)
|
||||||
|
|||||||
@ -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, {})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|||||||
@ -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
295
docs/kitty_at_template.py
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import termios
|
||||||
|
import time
|
||||||
|
import tty
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from ctypes import (
|
||||||
|
CDLL, POINTER, byref, c_char_p, c_int, c_size_t, c_void_p,
|
||||||
|
create_string_buffer
|
||||||
|
)
|
||||||
|
from ctypes.util import find_library
|
||||||
|
|
||||||
|
_plat = sys.platform.lower()
|
||||||
|
is_macos: bool = 'darwin' in _plat
|
||||||
|
|
||||||
|
|
||||||
|
def build_crypto_tools(): # {{{
|
||||||
|
class EVP_PKEY_POINTER(c_void_p):
|
||||||
|
|
||||||
|
algorithm = 0
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
EVP_PKEY_free(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def public(self):
|
||||||
|
sz = c_size_t(0)
|
||||||
|
EVP_PKEY_get_raw_public_key(self, None, byref(sz))
|
||||||
|
buf = create_string_buffer(sz.value)
|
||||||
|
EVP_PKEY_get_raw_public_key(self, buf, byref(sz))
|
||||||
|
return buf.raw
|
||||||
|
|
||||||
|
def derive_secret(self, pubkey):
|
||||||
|
pubkey = EVP_PKEY_new_raw_public_key(self.algorithm, None, pubkey, len(pubkey))
|
||||||
|
ctx = EVP_PKEY_CTX_new(self, None)
|
||||||
|
EVP_PKEY_derive_init(ctx)
|
||||||
|
EVP_PKEY_derive_set_peer(ctx, pubkey)
|
||||||
|
sz = c_size_t(0)
|
||||||
|
EVP_PKEY_derive(ctx, None, byref(sz))
|
||||||
|
buf = create_string_buffer(sz.value)
|
||||||
|
EVP_PKEY_derive(ctx, buf, byref(sz))
|
||||||
|
return hashlib.sha256(buf.raw).digest()
|
||||||
|
|
||||||
|
class EVP_PKEY_CTX_POINTER(c_void_p):
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
EVP_PKEY_CTX_free(self)
|
||||||
|
|
||||||
|
class EVP_CIPHER_CTX_POINTER(c_void_p):
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
EVP_CIPHER_CTX_free(self)
|
||||||
|
|
||||||
|
class EVP_CIPHER_POINTER(c_void_p):
|
||||||
|
pass
|
||||||
|
|
||||||
|
cl = find_library('crypto')
|
||||||
|
if not cl:
|
||||||
|
raise SystemExit('Failed to find libcrypto on your system, make sure OpenSSL is installed')
|
||||||
|
crypto = CDLL(cl)
|
||||||
|
libc = CDLL(None)
|
||||||
|
|
||||||
|
def create_crypto_func(name, *argtypes, restype=c_int, int_return_ok=lambda x: x == 1):
|
||||||
|
|
||||||
|
impl = getattr(crypto, name)
|
||||||
|
impl.restype = restype
|
||||||
|
impl.argtypes = argtypes
|
||||||
|
|
||||||
|
def func(*a):
|
||||||
|
res = impl(*a)
|
||||||
|
if restype is c_int:
|
||||||
|
if not int_return_ok(res):
|
||||||
|
print('Call to', name, 'failed with return code:', res, file=sys.stderr)
|
||||||
|
abort_on_openssl_error()
|
||||||
|
elif restype is not None and issubclass(restype, c_void_p):
|
||||||
|
if res.value is None:
|
||||||
|
print('Call to', name, 'failed with NULL return', file=sys.stderr)
|
||||||
|
abort_on_openssl_error()
|
||||||
|
return res
|
||||||
|
return func
|
||||||
|
|
||||||
|
OBJ_txt2nid = create_crypto_func('OBJ_txt2nid', c_char_p, int_return_ok=bool)
|
||||||
|
EVP_PKEY_CTX_new_id = create_crypto_func('EVP_PKEY_CTX_new_id', c_int, c_void_p, restype=EVP_PKEY_CTX_POINTER)
|
||||||
|
EVP_PKEY_CTX_new = create_crypto_func('EVP_PKEY_CTX_new', EVP_PKEY_POINTER, c_void_p, restype=EVP_PKEY_CTX_POINTER)
|
||||||
|
EVP_PKEY_keygen_init = create_crypto_func('EVP_PKEY_keygen_init', EVP_PKEY_CTX_POINTER)
|
||||||
|
EVP_PKEY_keygen = create_crypto_func('EVP_PKEY_keygen', EVP_PKEY_CTX_POINTER, POINTER(EVP_PKEY_POINTER))
|
||||||
|
ERR_print_errors_fp = create_crypto_func('ERR_print_errors_fp', c_void_p, restype=None)
|
||||||
|
EVP_PKEY_free = create_crypto_func('EVP_PKEY_free', EVP_PKEY_POINTER, restype=None)
|
||||||
|
EVP_PKEY_CTX_free = create_crypto_func('EVP_PKEY_CTX_free', EVP_PKEY_CTX_POINTER, restype=None)
|
||||||
|
EVP_PKEY_get_raw_public_key = create_crypto_func('EVP_PKEY_get_raw_public_key', EVP_PKEY_POINTER, c_char_p, POINTER(c_size_t))
|
||||||
|
EVP_PKEY_new_raw_public_key = create_crypto_func('EVP_PKEY_new_raw_public_key', c_int, c_void_p, c_char_p, c_size_t, restype=EVP_PKEY_POINTER)
|
||||||
|
EVP_PKEY_derive_init = create_crypto_func('EVP_PKEY_derive_init', EVP_PKEY_CTX_POINTER)
|
||||||
|
EVP_PKEY_derive_set_peer = create_crypto_func('EVP_PKEY_derive_set_peer', EVP_PKEY_CTX_POINTER, EVP_PKEY_POINTER)
|
||||||
|
EVP_PKEY_derive = create_crypto_func('EVP_PKEY_derive', EVP_PKEY_CTX_POINTER, c_char_p, POINTER(c_size_t))
|
||||||
|
EVP_CIPHER_CTX_free = create_crypto_func('EVP_CIPHER_CTX_free', EVP_CIPHER_CTX_POINTER, restype=None)
|
||||||
|
EVP_get_cipherbyname = create_crypto_func('EVP_get_cipherbyname', c_char_p, restype=EVP_CIPHER_POINTER)
|
||||||
|
EVP_CIPHER_key_length = create_crypto_func('EVP_CIPHER_key_length', EVP_CIPHER_POINTER, int_return_ok=bool)
|
||||||
|
EVP_CIPHER_iv_length = create_crypto_func('EVP_CIPHER_iv_length', EVP_CIPHER_POINTER, int_return_ok=bool)
|
||||||
|
EVP_CIPHER_CTX_block_size = create_crypto_func('EVP_CIPHER_CTX_block_size', EVP_CIPHER_CTX_POINTER, int_return_ok=bool)
|
||||||
|
EVP_CIPHER_CTX_new = create_crypto_func('EVP_CIPHER_CTX_new', restype=EVP_CIPHER_CTX_POINTER)
|
||||||
|
EVP_EncryptInit_ex = create_crypto_func('EVP_EncryptInit_ex', EVP_CIPHER_CTX_POINTER, EVP_CIPHER_POINTER, c_void_p, c_char_p, c_char_p)
|
||||||
|
EVP_EncryptUpdate = create_crypto_func('EVP_EncryptUpdate', EVP_CIPHER_CTX_POINTER, c_char_p, POINTER(c_int), c_char_p, c_int)
|
||||||
|
EVP_EncryptFinal_ex = create_crypto_func('EVP_EncryptFinal_ex', EVP_CIPHER_CTX_POINTER, c_char_p, POINTER(c_int))
|
||||||
|
EVP_CIPHER_CTX_ctrl = create_crypto_func('EVP_CIPHER_CTX_ctrl', EVP_CIPHER_CTX_POINTER, c_int, c_int, c_char_p)
|
||||||
|
try:
|
||||||
|
EVP_CIPHER_CTX_tag_length = create_crypto_func('EVP_CIPHER_CTX_tag_length', EVP_CIPHER_CTX_POINTER, int_return_ok=bool)
|
||||||
|
except AttributeError: # need openssl >= 3
|
||||||
|
def EVP_CIPHER_CTX_tag_length(cipher):
|
||||||
|
return 16
|
||||||
|
EVP_CTRL_AEAD_GET_TAG, EVP_CTRL_AEAD_SET_TAG = 0x10, 0x11 # these are defines in the header dont know how to get them programmatically
|
||||||
|
EVP_CTRL_AEAD_SET_TAG
|
||||||
|
|
||||||
|
def abort_on_openssl_error():
|
||||||
|
stderr = c_void_p.in_dll(libc, 'stderr')
|
||||||
|
ERR_print_errors_fp(stderr)
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
def elliptic_curve_keypair(algorithm='X25519'):
|
||||||
|
nid = OBJ_txt2nid(algorithm.encode())
|
||||||
|
pctx = EVP_PKEY_CTX_new_id(nid, None)
|
||||||
|
EVP_PKEY_keygen_init(pctx)
|
||||||
|
key = EVP_PKEY_POINTER()
|
||||||
|
EVP_PKEY_keygen(pctx, byref(key))
|
||||||
|
key.algorithm = nid
|
||||||
|
return key
|
||||||
|
|
||||||
|
def encrypt(plaintext, symmetric_key, algorithm='aes-256-gcm'):
|
||||||
|
cipher = EVP_get_cipherbyname(algorithm.encode())
|
||||||
|
if len(symmetric_key) != EVP_CIPHER_key_length(cipher):
|
||||||
|
raise KeyError(f'The symmetric key has length {len(symmetric_key)} != {EVP_CIPHER_key_length(cipher)} needed for {algorithm}')
|
||||||
|
ctx = EVP_CIPHER_CTX_new()
|
||||||
|
iv = os.urandom(EVP_CIPHER_iv_length(cipher))
|
||||||
|
EVP_EncryptInit_ex(ctx, cipher, None, symmetric_key, iv)
|
||||||
|
bs = EVP_CIPHER_CTX_block_size(ctx)
|
||||||
|
ciphertext = create_string_buffer(len(plaintext) + 2 * bs)
|
||||||
|
outlen = c_int(len(ciphertext))
|
||||||
|
EVP_EncryptUpdate(ctx, ciphertext, byref(outlen), plaintext, len(plaintext))
|
||||||
|
ans = ciphertext[:outlen.value]
|
||||||
|
outlen = c_int(len(ciphertext))
|
||||||
|
EVP_EncryptFinal_ex(ctx, ciphertext, byref(outlen))
|
||||||
|
if outlen.value:
|
||||||
|
ans += ciphertext[:outlen.value]
|
||||||
|
tag = create_string_buffer(EVP_CIPHER_CTX_tag_length(cipher))
|
||||||
|
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, len(tag), tag)
|
||||||
|
return iv, ans, tag.raw
|
||||||
|
return elliptic_curve_keypair, encrypt
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
|
# utils {{{
|
||||||
|
def encrypt_cmd(cmd, password, pubkey=None):
|
||||||
|
elliptic_curve_keypair, encrypt = build_crypto_tools()
|
||||||
|
if pubkey is None:
|
||||||
|
pubkey = os.environ['KITTY_PUBLIC_KEY']
|
||||||
|
v, d = pubkey.split(':', 1)
|
||||||
|
if v != '1':
|
||||||
|
raise SystemExit(f'Unsupported encryption protocol: {v}')
|
||||||
|
pubkey = base64.b85decode(d)
|
||||||
|
k = elliptic_curve_keypair()
|
||||||
|
sk = k.derive_secret(pubkey)
|
||||||
|
cmd['timestamp'] = time.time_ns()
|
||||||
|
cmd['password'] = password
|
||||||
|
data = json.dumps(cmd).encode()
|
||||||
|
iv, encrypted, tag = encrypt(data, sk)
|
||||||
|
|
||||||
|
def e(x):
|
||||||
|
return base64.b85encode(x).decode('ascii')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'encrypted': e(encrypted), 'iv': e(iv), 'tag': e(tag), 'pubkey': e(k.public), 'version': cmd['version']
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def raw_mode(fd):
|
||||||
|
old = termios.tcgetattr(fd)
|
||||||
|
try:
|
||||||
|
tty.setraw(fd)
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
||||||
|
|
||||||
|
|
||||||
|
def config_dir():
|
||||||
|
if 'KITTY_CONFIG_DIRECTORY' in os.environ:
|
||||||
|
return os.path.abspath(os.path.expanduser(os.environ['KITTY_CONFIG_DIRECTORY']))
|
||||||
|
locations = []
|
||||||
|
if 'XDG_CONFIG_HOME' in os.environ:
|
||||||
|
locations.append(os.path.abspath(os.path.expanduser(os.environ['XDG_CONFIG_HOME'])))
|
||||||
|
locations.append(os.path.expanduser('~/.config'))
|
||||||
|
if is_macos:
|
||||||
|
locations.append(os.path.expanduser('~/Library/Preferences'))
|
||||||
|
for loc in filter(None, os.environ.get('XDG_CONFIG_DIRS', '').split(os.pathsep)):
|
||||||
|
locations.append(os.path.abspath(os.path.expanduser(loc)))
|
||||||
|
for loc in locations:
|
||||||
|
if loc:
|
||||||
|
q = os.path.join(loc, 'kitty')
|
||||||
|
if os.access(q, os.W_OK) and os.path.exists(os.path.join(q, 'kitty.conf')):
|
||||||
|
return q
|
||||||
|
for loc in locations:
|
||||||
|
if loc:
|
||||||
|
q = os.path.join(loc, 'kitty')
|
||||||
|
if os.path.isdir(q) and os.access(q, os.W_OK):
|
||||||
|
return q
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_custom_file(path):
|
||||||
|
path = os.path.expanduser(path)
|
||||||
|
path = os.path.expandvars(path)
|
||||||
|
if not os.path.isabs(path):
|
||||||
|
cdir = config_dir()
|
||||||
|
if cdir:
|
||||||
|
path = os.path.join(cdir, path)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def get_password(opts):
|
||||||
|
if opts.use_password == 'never':
|
||||||
|
return ''
|
||||||
|
ans = ''
|
||||||
|
if opts.password:
|
||||||
|
ans = opts.password
|
||||||
|
if not ans and opts.password_file:
|
||||||
|
if opts.password_file == '-':
|
||||||
|
if sys.stdin.isatty():
|
||||||
|
from getpass import getpass
|
||||||
|
ans = getpass()
|
||||||
|
else:
|
||||||
|
ans = sys.stdin.read().rstrip()
|
||||||
|
try:
|
||||||
|
tty_fd = os.open(os.ctermid(), os.O_RDONLY | os.O_CLOEXEC)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
with open(tty_fd, closefd=True):
|
||||||
|
os.dup2(tty_fd, sys.stdin.fileno())
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
with open(resolve_custom_file(opts.password_file)) as f:
|
||||||
|
ans = f.read().rstrip()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
if not ans and opts.password_env:
|
||||||
|
ans = os.environ.get(opts.password_env, '')
|
||||||
|
if not ans and opts.use_password == 'always':
|
||||||
|
raise SystemExit('No password was found')
|
||||||
|
if ans and len(ans) > 1024:
|
||||||
|
raise SystemExit('Specified password is too long')
|
||||||
|
return ans
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
|
arg_parser = argparse.ArgumentParser(prog='kitty@', description='Control kitty remotely.')
|
||||||
|
arg_parser.add_argument('--password', default='', help='''\
|
||||||
|
A password to use when contacting kitty. This will cause kitty to ask the user
|
||||||
|
for permission to perform the specified action, unless the password has been
|
||||||
|
accepted before or is pre-configured in kitty.conf''')
|
||||||
|
arg_parser.add_argument('--password-file', default='rc-pass', help='''\
|
||||||
|
A file from which to read the password. Trailing whitespace is ignored. Relative
|
||||||
|
paths are resolved from the kitty configuration directory. Use - to read from STDIN.
|
||||||
|
Used if no --password is supplied. Defaults to checking for the
|
||||||
|
rc-pass file in the kitty configuration directory.''')
|
||||||
|
arg_parser.add_argument('--password-env', default='KITTY_RC_PASSWORD', help='''\
|
||||||
|
The name of an environment variable to read the password from.
|
||||||
|
Used if no --password-file is supplied. Defaults to checking the KITTY_RC_PASSWORD.''')
|
||||||
|
arg_parser.add_argument('--use-password', default='if-available', choices=('if-available', 'always', 'never'), help='''\
|
||||||
|
If no password is available, kitty will usually just send the remote control command
|
||||||
|
without a password. This option can be used to force it to always or never use
|
||||||
|
the supplied password.''')
|
||||||
|
|
||||||
|
args = arg_parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def populate_cmd(cmd):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
password = get_password(args)
|
||||||
|
cmd = {'version': [0, 20, 0]} # use a random version that's fairly old
|
||||||
|
populate_cmd(cmd)
|
||||||
|
if password:
|
||||||
|
encrypt_cmd(cmd, password)
|
||||||
|
|
||||||
|
# cmd = {'version': [0, 14, 2], 'cmd': 'ls'}
|
||||||
|
# cmd = encrypt_cmd(cmd, 'test')
|
||||||
|
# with open(os.open(os.ctermid(), os.O_RDWR | os.O_CLOEXEC), 'w') as tty_file, raw_mode(tty_file.fileno()):
|
||||||
|
# print(end=f'\x1bP@kitty-cmd{json.dumps(cmd)}\x1b\\', flush=True, file=tty_file)
|
||||||
|
# os.read(tty_file.fileno(), 4096)
|
||||||
@ -68,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``
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -32,4 +32,3 @@ please do so by opening issues in the `GitHub bug tracker
|
|||||||
unscroll
|
unscroll
|
||||||
color-stack
|
color-stack
|
||||||
deccara
|
deccara
|
||||||
clipboard
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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__':
|
||||||
|
|||||||
767
gen-go-code.py
767
gen-go-code.py
@ -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() # }}}
|
|
||||||
@ -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:
|
||||||
|
|||||||
@ -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()
|
|
||||||
276
gen-wcwidth.py
276
gen-wcwidth.py
@ -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()
|
|
||||||
|
|||||||
@ -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'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -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];
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,9 +1998,8 @@ 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
4
glfw/dbus_glfw.c
vendored
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
12
glfw/glfw.py
12
glfw/glfw.py
@ -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
22
glfw/glfw3.h
vendored
@ -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
34
glfw/ibus_glfw.c
vendored
@ -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
22
glfw/init.c
vendored
@ -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
4
glfw/internal.h
vendored
@ -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
|
||||||
|
|||||||
8
glfw/linux_desktop_settings.c
vendored
8
glfw/linux_desktop_settings.c
vendored
@ -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;
|
||||||
|
|||||||
1
glfw/linux_desktop_settings.h
vendored
1
glfw/linux_desktop_settings.h
vendored
@ -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
4
glfw/wl_init.c
vendored
@ -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
28
glfw/wl_window.c
vendored
@ -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
4
glfw/x11_init.c
vendored
@ -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
23
glfw/x11_window.c
vendored
@ -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
30
go.mod
@ -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
104
go.sum
@ -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=
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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'
|
|
||||||
|
|||||||
@ -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 ...]'
|
||||||
|
|
||||||
|
|||||||
0
kittens/choose/__init__.py
Normal file
0
kittens/choose/__init__.py
Normal file
86
kittens/choose/choose-data-types.h
Normal file
86
kittens/choose/choose-data-types.h
Normal 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
244
kittens/choose/main.c
Normal 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
39
kittens/choose/main.py
Normal 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
38
kittens/choose/match.py
Normal 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
101
kittens/choose/output.c
Normal 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
182
kittens/choose/score.c
Normal 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);
|
||||||
|
}
|
||||||
9
kittens/choose/subseq_matcher.pyi
Normal file
9
kittens/choose/subseq_matcher.pyi
Normal 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
|
||||||
50
kittens/choose/unix_compat.c
Normal file
50
kittens/choose/unix_compat.c
Normal 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
42
kittens/choose/vector.h
Normal 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])
|
||||||
107
kittens/choose/windows_compat.c
Normal file
107
kittens/choose/windows_compat.c
Normal 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);
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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')
|
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
1
kittens/diff/README.asciidoc
Normal file
1
kittens/diff/README.asciidoc
Normal file
@ -0,0 +1 @@
|
|||||||
|
See https://sw.kovidgoyal.net/kitty/kittens/diff/
|
||||||
@ -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
|
|
||||||
|
|||||||
@ -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
235
kittens/diff/collect.py
Normal 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, [])
|
||||||
@ -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
72
kittens/diff/config.py
Normal 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
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
14
kittens/diff/diff_speedup.pyi
Normal file
14
kittens/diff/diff_speedup.pyi
Normal 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
|
||||||
@ -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
187
kittens/diff/highlight.py
Normal 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
Loading…
x
Reference in New Issue
Block a user