Compare commits

..

1 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
a80bab2588 fix: use total advance width for rendered_width in core_text to fix alignment 2026-06-23 14:08:04 +00:00
53 changed files with 350 additions and 2928 deletions

View File

@@ -4,10 +4,8 @@
import glob
import io
import json
import lzma
import os
import platform
import shlex
import shutil
import subprocess
@@ -22,9 +20,7 @@ NERD_URL = 'https://github.com/ryanoasis/nerd-fonts/releases/latest/download/Ner
is_bundle = os.environ.get('KITTY_BUNDLE') == '1'
is_codeql = os.environ.get('KITTY_CODEQL') == '1'
is_macos = 'darwin' in sys.platform.lower()
running_under_sanitizer = os.environ.get('KITTY_SANITIZE') == '1'
SW = ''
SLANG_INSTALL_DIR = '/tmp/slang'
def do_print_crash_reports() -> None:
@@ -104,33 +100,6 @@ def install_fonts() -> None:
tf.extractall(fonts_dir)
def install_slang_compiler() -> None:
os_name = 'macos' if is_macos else 'linux'
machine = platform.machine().lower()
arch = 'aarch64' if machine in ('aarch64', 'arm64') else 'x86_64'
with open('bypy/sources.json') as f:
data = json.loads(f.read())
for dep in data:
if dep['name'].startswith('slang '):
version = dep['name'].split()[-1]
break
url = f'https://github.com/shader-slang/slang/releases/download/v{version}/slang-{version}-{os_name}-{arch}.tar.gz'
install_dir = SLANG_INSTALL_DIR
os.makedirs(install_dir, exist_ok=True)
data = download_with_retry(url)
with tarfile.open(fileobj=io.BytesIO(data), mode='r:gz') as tf:
try:
tf.extractall(install_dir, filter='fully_trusted')
except TypeError:
# filter parameter not supported on older Python versions
tf.extractall(install_dir)
pc_dir = os.path.join(install_dir, 'lib', 'pkgconfig')
pp = os.environ.get('PKG_CONFIG_PATH', '')
os.environ['PKG_CONFIG_PATH'] = f'{pc_dir}:{pp}' if pp else pc_dir
def install_deps() -> None:
print('Installing kitty dependencies...')
sys.stdout.flush()
@@ -148,8 +117,7 @@ def install_deps() -> None:
run('sudo apt-get install -y --fix-missing libgl1-mesa-dev libxi-dev libxrandr-dev libxinerama-dev ca-certificates'
' libxcursor-dev libxcb-xkb-dev libdbus-1-dev libxkbcommon-dev libharfbuzz-dev libx11-xcb-dev zsh'
' libpng-dev liblcms2-dev libfontconfig-dev libxkbcommon-x11-dev libcanberra-dev libxxhash-dev uuid-dev'
' libsimde-dev libsystemd-dev libcairo2-dev zsh bash dash systemd-coredump gdb'
' libwayland-dev wayland-protocols glslang-tools')
' libsimde-dev libsystemd-dev libcairo2-dev zsh bash dash systemd-coredump gdb')
# for some reason these directories are world writable which causes zsh
# compinit to break
run('sudo chmod -R og-w /usr/share/zsh')
@@ -160,21 +128,16 @@ def install_deps() -> None:
if sys.version_info[:2] < (3, 7):
cmd += ' importlib-resources dataclasses'
run(cmd)
install_slang_compiler()
install_fonts()
def set_slangc() -> None:
slangc = os.path.join(SW if is_bundle else SLANG_INSTALL_DIR, 'bin', 'slangc')
os.environ['SLANGC'] = slangc
def build_kitty() -> None:
python = shutil.which('python3') if is_bundle else sys.executable
cmd = f'{python} setup.py build --verbose'
if running_under_sanitizer:
if is_macos:
cmd += ' --debug' # for better crash report to debug SIGILL issue
if os.environ.get('KITTY_SANITIZE') == '1':
cmd += ' --debug --sanitize'
set_slangc()
run(cmd)
@@ -182,17 +145,12 @@ def test_kitty() -> None:
if is_macos:
run('ulimit -c unlimited')
run('sudo chmod -R 777 /cores')
if running_under_sanitizer:
os.environ['MallocNanoZone'] = '0'
set_slangc()
run('./test.py', print_crash_reports=True)
def package_kitty() -> None:
set_slangc()
python = 'python3' if is_macos else 'python'
run(f'{python} setup.py linux-package --update-check-interval=0 --verbose')
run('make FAIL_WARN=1 docs')
if is_macos:
run('python3 setup.py kitty.app --update-check-interval=0 --verbose')
run('kitty.app/Contents/MacOS/kitty +runpy "from kitty.constants import *; print(kitty_exe())"')
@@ -207,8 +165,7 @@ def replace_in_file(path: str, src: str, dest: str) -> None:
def setup_bundle_env() -> None:
global SW
os.environ['SW'] = SW = '/Users/Shared/kitty-build/sw/sw' if is_macos else os.path.join(
os.environ['GITHUB_WORKSPACE'], 'sw')
os.environ['SW'] = SW = '/Users/Shared/kitty-build/sw/sw' if is_macos else os.path.join(os.environ['GITHUB_WORKSPACE'], 'sw')
os.environ['PKG_CONFIG_PATH'] = os.path.join(SW, 'lib', 'pkgconfig')
if is_macos:
os.environ['PATH'] = '{}:{}'.format('/usr/local/opt/sphinx-doc/bin', os.environ['PATH'])
@@ -324,11 +281,11 @@ def main() -> None:
if action == 'build':
build_kitty()
elif action == 'package':
build_kitty()
test_kitty()
package_kitty()
elif action == 'test':
test_kitty()
elif action == 'test':
test_kitty()
elif action == 'govulncheck':
subprocess.check_call(['go', 'install', 'golang.org/x/vuln/cmd/govulncheck@latest'])
subprocess.check_call(['govulncheck', '-mode=binary', 'kitty/launcher/kitten'])

View File

@@ -117,12 +117,21 @@ jobs:
- name: Build kitty package
run: python .github/workflows/ci.py package
- name: Build kitty
run: python setup.py build --debug
- name: Run mypy
run: which python && python -m mypy --version && ./test.py mypy
- name: Run go vet
run: go version && go vet -tags testing ./...
- name: Build man page
run: make FAIL_WARN=1 man
- name: Build HTML docs
run: make FAIL_WARN=1 html
- name: Build static kittens
run: python setup.py build-static-binaries
@@ -134,7 +143,6 @@ jobs:
os: [ubuntu-latest, macos-latest]
env:
KITTY_BUNDLE: 1
KITTY_SANITIZE: 1
steps:
- name: Checkout source code
uses: actions/checkout@v6.0.3
@@ -173,9 +181,18 @@ jobs:
with:
go-version-file: go.mod
- name: Build kitty
run: python3 .github/workflows/ci.py build
- name: Test kitty
run: python3 .github/workflows/ci.py test
- name: Install deps for docs
run: python3 -m pip install -r docs/requirements.txt
- name: Builds docs
run: make FAIL_WARN=1 docs
- name: Build kitty package
run: python3 .github/workflows/ci.py package

View File

@@ -2,99 +2,17 @@
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
import json
import os
import posixpath
import re
import subprocess
import sys
from collections import defaultdict, namedtuple
from collections import namedtuple
from datetime import datetime
from enum import Enum
from functools import cached_property
from typing import IO, Mapping, Optional
from typing import IO, List, Mapping, Optional
Frame = namedtuple('Frame', 'image_name image_base image_offset symbol symbol_offset')
Register = namedtuple('Register', 'name value')
# Cache mapping filename (basename) -> absolute path, built once on first use.
_build_file_cache: Optional[dict[str, str]] = None
def _build_filename_cache() -> dict[str, str]:
"""Walk the repo build tree and map each filename to its absolute path.
The script lives at <repo>/.github/workflows/, so the repo root is two
levels up. We scan the whole repo tree so we find both in-tree build
artefacts (e.g. kitty/fast_data_types.so) and those under build/.
"""
cache: dict[str, str] = {}
repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
for dirpath, _dirnames, filenames in os.walk(repo_root):
for fname in filenames:
# Keep the first occurrence (shallowest path wins).
if fname not in cache:
cache[fname] = os.path.join(dirpath, fname)
return cache
def _resolve_image_path(image_path: str) -> str:
"""Return the real filesystem path for *image_path*.
Crash reports on macOS redact parts of paths for privacy, replacing
directory components with ``*`` (e.g. ``/Users/USER/*/fast_data_types.so``).
These are *not* glob patterns the ``*`` is a literal privacy placeholder.
When such a path is detected, look up the basename in the build-file cache.
"""
if '*' not in image_path and '?' not in image_path:
return image_path
global _build_file_cache
if _build_file_cache is None:
_build_file_cache = _build_filename_cache()
basename = os.path.basename(image_path)
return _build_file_cache.get(basename, image_path)
def _get_source_locations(frames: list[Frame]) -> dict[int, str]:
"""Use atos to look up source file and line number for each frame.
Returns a mapping of frame index -> 'source_file:line_number' string.
Only frames with a known image path and base address are processed.
"""
# Group frames by (image_path, load_address) so we can batch atos calls.
by_image: dict[tuple[str, int], list[tuple[int, int]]] = defaultdict(list) # (path, base) -> [(address, frame_idx)]
for i, frame in enumerate(frames):
if frame.image_name and frame.image_base is not None and frame.image_offset is not None:
addr = frame.image_base + frame.image_offset
by_image[(frame.image_name, frame.image_base)].append((addr, i))
result: dict[int, str] = {}
for (image_path, load_addr), addr_frame_pairs in by_image.items():
addresses = [addr for addr, _ in addr_frame_pairs]
frame_indices = [idx for _, idx in addr_frame_pairs]
# Paths in crash reports may have privacy-redacted components (e.g.
# /Users/USER/*/fast_data_types.so). Resolve to a real path using the
# cached build-file index before passing to atos.
resolved_path = _resolve_image_path(image_path)
try:
cmd = ['atos', '-o', resolved_path, '-l', hex(load_addr)] + [hex(a) for a in addresses]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if proc.returncode == 0:
lines = proc.stdout.splitlines()
for frame_idx, line in zip(frame_indices, lines):
# atos output: "func_name (in binary) (source_file:line)"
# Extract the trailing "(file:line)" part. Use [^:]+ for the
# file portion since colons in filenames are rare/invalid on
# macOS, and this avoids false-matches with parentheses in paths.
m = re.search(r'\(([^:()]+:\d+)\)\s*$', line)
if m:
result[frame_idx] = m.group(1)
except (OSError, subprocess.SubprocessError):
pass
return result
def surround(x: str, start: int, end: int) -> str:
if sys.stdout.isatty():
@@ -351,7 +269,7 @@ class UserModeCrashReport(CrashReportBase):
return int(self._parse_field('Triggered by Thread'))
@cached_property
def frames(self) -> list[Frame]:
def frames(self) -> List[Frame]:
result = []
if self._is_json:
thread_index = self.faulting_thread
@@ -386,7 +304,7 @@ class UserModeCrashReport(CrashReportBase):
return result
@cached_property
def registers(self) -> list[Register]:
def registers(self) -> List[Register]:
result = []
if self._is_json:
thread_index = self._data['faultingThread']
@@ -490,9 +408,8 @@ class UserModeCrashReport(CrashReportBase):
result += '\n\n'
source_locations = _get_source_locations(self.frames)
result += bold('Frames:\n')
for i, frame in enumerate(self.frames):
for frame in self.frames:
image_base = '_HEADER'
if frame.image_base is not None:
image_base = f'0x{frame.image_base:x}'
@@ -501,14 +418,12 @@ class UserModeCrashReport(CrashReportBase):
result += f' + 0x{frame.image_offset:x}'
if frame.symbol is not None:
result += f' ({frame.symbol} + 0x{frame.symbol_offset:x})'
if i in source_locations:
result += f' [{source_locations[i]}]'
result += '\n'
return result
def get_crash_report_from_file(crash_report_file: IO[str]) -> CrashReportBase:
def get_crash_report_from_file(crash_report_file: IO) -> CrashReportBase:
metadata = json.loads(crash_report_file.readline())
try:

1
.gitignore vendored
View File

@@ -13,7 +13,6 @@
/tags
/build/
/fonts/
/shaders/
/linux-package/
/kitty.app/
/glad/out/

View File

@@ -279,12 +279,8 @@ func dependencies(args []string) {
which = "macos"
case "linux":
which = "linux"
switch runtime.GOARCH {
case "amd64":
case "arm64", "arm64be":
url = strings.Replace(url, "-64.", "-arm64.", 1)
default:
exit(fmt.Sprintf("Pre-built binaries are not available for the %s architecture", runtime.GOARCH))
if runtime.GOARCH != "amd64" {
exit("Pre-built dependencies are only available for the amd64 CPU architecture")
}
}
if which == "" {
@@ -393,7 +389,6 @@ func build(args []string) {
}
root := root_dir()
os.Setenv("DEVELOP_ROOT", root)
os.Setenv("SLANGC", filepath.Join(root, "bin", "slangc"))
prepend("PKG_CONFIG_PATH", filepath.Join(root, "lib", "pkgconfig"))
if runtime.GOOS == "darwin" {
os.Setenv("PKGCONFIG_EXE", filepath.Join(root, "bin", "pkg-config"))

View File

@@ -11,7 +11,7 @@ import sys
import tempfile
from contextlib import suppress
from bypy.constants import BIN, LIBDIR, PREFIX, PYTHON, ismacos, worker_env
from bypy.constants import LIBDIR, PREFIX, PYTHON, ismacos, worker_env
from bypy.constants import SRC as KITTY_DIR
from bypy.utils import run_shell, walk
@@ -39,7 +39,6 @@ def run(*args, **extra_env):
env.update(extra_env)
env['SW'] = PREFIX
env['LD_LIBRARY_PATH'] = LIBDIR
env['SLANGC'] = os.path.join(BIN, 'slangc')
if ismacos:
env['PKGCONFIG_EXE'] = os.path.join(PREFIX, 'bin', 'pkg-config')
cwd = env.pop('cwd', KITTY_DIR)
@@ -90,7 +89,7 @@ def build_frozen_tools(kitty_exe):
def sanitize_source_folder(path: str) -> None:
for q in walk(path):
if os.path.splitext(q)[1] not in ('.py', '.glsl', '.slang', '.ttf', '.otf', '.json'):
if os.path.splitext(q)[1] not in ('.py', '.glsl', '.ttf', '.otf', '.json'):
os.unlink(q)
@@ -100,7 +99,6 @@ def build_c_extensions(ext_dir, args):
shutil.copytree(
KITTY_DIR, writeable_src_dir, symlinks=True,
ignore=shutil.ignore_patterns('b', 'build', 'dist', '*_commands.json', '*.o', '*.so', '*.dylib', '*.pyd'))
shutil.rmtree(os.path.join(writeable_src_dir, 'shaders'))
with suppress(FileNotFoundError):
os.unlink(os.path.join(writeable_src_dir, 'kitty', 'launcher', 'kitty'))

View File

@@ -10,7 +10,7 @@ import subprocess
import tarfile
import time
from bypy.constants import BIN, LIBDIR, OUTPUT_DIR, PREFIX, python_major_minor_version
from bypy.constants import OUTPUT_DIR, PREFIX, 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
@@ -90,14 +90,6 @@ def copy_libs(env):
shutil.copy2(x, dest)
dest = os.path.join(dest, os.path.basename(x))
subprocess.check_call(['chrpath', '-d', dest])
# Copy slang
x = 'libslang-compiler.so'
shutil.copy2(os.path.join(LIBDIR, f'{x}.0.0.0.0'), env.lib_dir)
os.symlink(f'{x}.0.0.0.0', os.path.join(env.lib_dir, x))
for x in ('glsl-module', 'glslang'):
x = f'libslang-{x}-0.0.0.so'
shutil.copy2(os.path.join(LIBDIR, x), env.lib_dir)
shutil.copy2(os.path.join(BIN, 'slangc'), env.bin_dir)
def add_ca_certs(env):
@@ -146,6 +138,10 @@ def copy_python(env):
shutil.rmtree(env.py_dir)
def build_launcher(env):
iv['build_frozen_launcher']([path_to_freeze_dir(), env.obj_dir])
def is_elf(path):
with open(path, 'rb') as f:
return f.read(4) == b'\x7fELF'
@@ -232,7 +228,7 @@ def main():
env = Env(os.path.join(ext_dir, kitty_constants['appname']))
copy_libs(env)
copy_python(env)
iv['build_frozen_launcher']([path_to_freeze_dir(), env.obj_dir])
build_launcher(env)
files = find_binaries(env)
fix_permissions(files)
add_ca_certs(env)

View File

@@ -12,7 +12,7 @@ import sys
import tempfile
import zipfile
from bypy.constants import BIN, LIBDIR, 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.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
@@ -301,7 +301,7 @@ class Freeze(object):
self.fix_dependencies_in_lib(join(self.frameworks_dir, basename(path)))
@flush
def add_misc_libraries(self) -> None:
def add_misc_libraries(self):
for x in (
'sqlite3.0',
'z.1',
@@ -319,22 +319,6 @@ class Freeze(object):
dest = join(self.frameworks_dir, x)
self.set_id(dest, f'{self.FID}/{x}')
self.fix_dependencies_in_lib(dest)
# Copy slang
x = 'libslang-compiler.0.0.0.0.dylib'
shutil.copy2(os.path.join(LIBDIR, x), self.frameworks_dir)
os.symlink(x, os.path.join(self.frameworks_dir, 'libslang-compiler.dylib'))
dest = join(self.frameworks_dir, x)
self.set_id(dest, f'{self.FID}/{x}')
self.fix_dependencies_in_lib(dest)
for x in ('glsl-module', 'glslang'):
x = f'libslang-{x}-0.0.0.dylib'
shutil.copy2(os.path.join(LIBDIR, x), self.frameworks_dir)
dest = join(self.frameworks_dir, x)
self.set_id(dest, f'{self.FID}/{x}')
dest = os.path.join(self.contents_dir, 'MacOS')
shutil.copy2(os.path.join(BIN, 'slangc'), dest)
dest = os.path.join(dest, 'slangc')
self.fix_dependencies_in_lib(dest)
@flush
def add_package_dir(self, x, dest=None):

View File

@@ -279,15 +279,6 @@
}
},
{
"name": "slang 2026.12.2",
"unix": {
"file_extension": "tar.gz",
"hash": "sha256:1ed5ebf7886849ea621d6dabee20248e8551c80134aa8c4433746879cb20dea3",
"urls": ["git-submodules://github.com/shader-slang/slang.git"]
}
},
{
"name": "wayland 1.24.0",
"os": "linux",

View File

@@ -89,7 +89,6 @@ Run-time dependencies:
* ``liblcms2``
* ``libxxhash``
* ``openssl``
* ``shader-slang`` (for the slangc compiler)
* ``pixman`` (not needed on macOS)
* ``cairo`` (not needed on macOS)
* ``freetype`` (not needed on macOS)

View File

@@ -192,10 +192,6 @@ Detailed list of changes
- kitty binary builds are now built on Ubuntu 22 upgraded from Ubuntu 18 for improved performance from better compilers
- macOS: Fix incorrect horizontal alignment when using text sizing protocol (:iss:`10179`)
- ``edit-in-kitty``: Return exit code from underlying editor process on exit (:iss:`10198`)
0.47.4 [2026-06-15]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -330,6 +330,3 @@ Kittens created by kitty users
`weechat-hints <https://github.com/GermainZ/kitty-weechat-hints>`_
URL hints kitten for WeeChat that works without having to use WeeChat's
raw-mode.
`pdf-kitten <https://github.com/gelaechter/pdf-kitten>`__
Allows you to export kitty's output to PDF, making use of weasyprint.

View File

@@ -9,7 +9,7 @@ Hyperlinked grep
.. note::
As of ripgrep versions newer than 13.0 it supports hyperlinks
As of ripgrep versions newer that 13.0 it supports hyperlinks
natively so you can just add the following alias in your shell rc file:
``alias rg="rg --hyperlink-format=kitty"`` no need to use this kitten.
But, see below for instructions on how to customize kitty to have it open
@@ -45,7 +45,7 @@ Now, run a search with::
Hold down the :kbd:`Ctrl+Shift` keys and click on any of the result lines, to
open the file in :program:`vim` at the matching line. If you use some editor
other than :program:`vim`, you should adjust the :file:`open-actions.conf` file
accordingly. To open links with the keyboard instead, use
accordingly. TO open links with the keyboard instead, use
:sc:`open_selected_hyperlink`.
Finally, add an alias to your shell's rc files to invoke the kitten as
@@ -67,7 +67,7 @@ which items are linked with a :code:`--kitten hyperlink` flag. For example,
:code:`--kitten hyperlink=matching_lines` will only add hyperlinks to the
match lines. :code:`--kitten hyperlink=file_headers,context_lines` will link
file headers and context lines but not match lines. :code:`--kitten
hyperlink=none` will cause the command line to be passed 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`
may be specified multiple times.

View File

@@ -2,13 +2,234 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import json
import os
import re
import subprocess
import sys
from enum import Enum
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Sequence, Tuple
_plat = sys.platform.lower()
is_linux = 'linux' in _plat
is_openbsd = 'openbsd' in _plat
base = os.path.dirname(os.path.abspath(__file__))
def null_func() -> None:
return None
class CompileKey(NamedTuple):
src: str
dest: str
class Command(NamedTuple):
desc: str
cmd: Sequence[str]
is_newer_func: Callable[[], bool]
on_success: Callable[[], None] = null_func
key: Optional[CompileKey] = None
keyfile: Optional[str] = None
class ISA(Enum):
X86 = 0x03
AMD64 = 0x3e
ARM64 = 0xb7
Other = 0x0
class BinaryArch(NamedTuple):
bits: int = 64
isa: ISA = ISA.AMD64
class CompilerType(Enum):
gcc = 'gcc'
clang = 'clang'
unknown = 'unknown'
class Env:
cc: List[str] = []
cppflags: List[str] = []
cflags: List[str] = []
ldflags: List[str] = []
library_paths: Dict[str, List[str]] = {}
ldpaths: List[str] = []
ccver: Tuple[int, int]
vcs_rev: str = ''
binary_arch: BinaryArch = BinaryArch()
native_optimizations: bool = False
primary_version: int = 0
secondary_version: int = 0
xt_version: str = ''
has_copy_file_range: bool = False
# glfw stuff
all_headers: List[str] = []
sources: List[str] = []
wayland_packagedir: str = ''
wayland_scanner: str = ''
wayland_scanner_code: str = ''
wayland_protocols: Tuple[str, ...] = ()
def __init__(
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),
vcs_rev: str = '', binary_arch: BinaryArch = BinaryArch(),
native_optimizations: bool = False,
):
self.cc, self.cppflags, self.cflags, self.ldflags, self.library_paths = cc, cppflags, cflags, ldflags, library_paths
self.ldpaths = ldpaths or []
self.ccver = ccver
self.vcs_rev = vcs_rev
self.binary_arch = binary_arch
self.native_optimizations = native_optimizations
self._cc_version_string = ''
self._compiler_type: Optional[CompilerType] = None
@property
def cc_version_string(self) -> str:
if not self._cc_version_string:
self._cc_version_string = subprocess.check_output(self.cc + ['--version']).decode()
return self._cc_version_string
@property
def compiler_type(self) -> CompilerType:
if self._compiler_type is None:
raw = self.cc_version_string
if 'Free Software Foundation' in raw:
self._compiler_type = CompilerType.gcc
elif 'clang' in raw.lower().split():
self._compiler_type = CompilerType.clang
else:
self._compiler_type = CompilerType.unknown
return self._compiler_type
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.all_headers = list(self.all_headers)
ans._cc_version_string = self._cc_version_string
ans.sources = list(self.sources)
ans.wayland_packagedir = self.wayland_packagedir
ans.wayland_scanner = self.wayland_scanner
ans.wayland_scanner_code = self.wayland_scanner_code
ans.wayland_protocols = self.wayland_protocols
ans.vcs_rev = self.vcs_rev
ans.binary_arch = self.binary_arch
ans.native_optimizations = self.native_optimizations
ans.primary_version = self.primary_version
ans.secondary_version = self.secondary_version
ans.xt_version = self.xt_version
ans.has_copy_file_range = self.has_copy_file_range
return ans
def wayland_protocol_file_name(base: str, ext: str = 'c') -> str:
base = os.path.basename(base).rpartition('.')[0]
return f'wayland-{base}-client-protocol.{ext}'
def init_env(
env: Env,
pkg_config: Callable[..., List[str]],
pkg_version: Callable[[str], Tuple[int, int]],
at_least_version: Callable[..., None],
test_compile: Callable[..., Any],
module: str = 'x11'
) -> Env:
ans = env.copy()
ans.cflags.append('-fPIC')
ans.cppflags.append(f'-D_GLFW_{module.upper()}')
ans.cppflags.append('-D_GLFW_BUILD_DLL')
with open(os.path.join(base, 'source-info.json')) as f:
sinfo = json.load(f)
module_sources = list(sinfo[module]['sources'])
if module in ('x11', 'wayland'):
remove = 'null_joystick.c' if is_linux else 'linux_joystick.c'
module_sources.remove(remove)
ans.sources = sinfo['common']['sources'] + module_sources
ans.all_headers = [x for x in os.listdir(base) if x.endswith('.h')]
if module in ('x11', 'wayland'):
ans.cflags.append('-pthread')
ans.ldpaths.extend('-pthread -lm'.split())
if not is_openbsd:
ans.ldpaths.extend('-lrt -ldl'.split())
major, minor = pkg_version('xkbcommon')
if (major, minor) < (0, 5):
raise SystemExit('libxkbcommon >= 0.5 required')
if major < 1:
ans.cflags.append('-DXKB_HAS_NO_UTF32')
if module == 'x11':
for dep in 'x11 xrandr xinerama xcursor xkbcommon xkbcommon-x11 x11-xcb dbus-1'.split():
ans.cflags.extend(pkg_config(dep, '--cflags-only-I'))
ans.ldpaths.extend(pkg_config(dep, '--libs'))
elif module == 'cocoa':
ans.cppflags.append('-DGL_SILENCE_DEPRECATION')
for f_ in 'Cocoa IOKit CoreFoundation CoreVideo QuartzCore UniformTypeIdentifiers'.split():
ans.ldpaths.extend(('-framework', f_))
elif module == 'wayland':
at_least_version('wayland-protocols', *sinfo['wayland_protocols'])
ans.wayland_packagedir = os.path.abspath(pkg_config('wayland-protocols', '--variable=pkgdatadir')[0])
ans.wayland_scanner = os.path.abspath(pkg_config('wayland-scanner', '--variable=wayland_scanner')[0])
scanner_version = tuple(map(int, pkg_config('wayland-scanner', '--modversion')[0].strip().split('.')))
ans.wayland_scanner_code = 'private-code' if scanner_version >= (1, 14, 91) else 'code'
ans.wayland_protocols = tuple(sinfo[module]['protocols'])
for p in ans.wayland_protocols:
ans.sources.append(wayland_protocol_file_name(p))
ans.all_headers.append(wayland_protocol_file_name(p, 'h'))
for dep in 'wayland-client wayland-cursor xkbcommon dbus-1'.split():
ans.cflags.extend(pkg_config(dep, '--cflags-only-I'))
ans.ldpaths.extend(pkg_config(dep, '--libs'))
has_memfd_create = test_compile(env.cc, '-Werror', src='''#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>
int main(void) {
return syscall(__NR_memfd_create, "test", 0);
}''')
if has_memfd_create:
ans.cppflags.append('-DHAS_MEMFD_CREATE')
return ans
def build_wayland_protocols(
env: Env,
parallel_run: Callable[[List[Command]], None],
emphasis: Callable[[str], str],
newer: Callable[..., bool],
dest_dir: str
) -> None:
items = []
for protocol in env.wayland_protocols:
if '/' in protocol:
src = os.path.join(env.wayland_packagedir, protocol)
if not os.path.exists(src):
raise SystemExit(f'The wayland-protocols package on your system is missing the {protocol} protocol definition file')
else:
src = os.path.join(os.path.dirname(os.path.abspath(__file__)), protocol)
if not os.path.exists(src):
raise SystemExit(f'The local Wayland protocol {protocol} is missing from kitty sources')
for ext in 'hc':
dest = wayland_protocol_file_name(src, ext)
dest = os.path.join(dest_dir, dest)
if newer(dest, src):
q = 'client-header' if ext == 'h' else env.wayland_scanner_code
items.append(Command(
f'Generating {emphasis(os.path.basename(dest))} ...',
[env.wayland_scanner, q, src, dest], lambda: True))
if items:
parallel_run(items)
class Arg:

4
go.mod
View File

@@ -18,7 +18,7 @@ require (
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1
github.com/kovidgoyal/go-parallel v1.1.1
github.com/kovidgoyal/go-shm v1.0.0
github.com/kovidgoyal/imaging v1.8.22
github.com/kovidgoyal/imaging v1.8.21
github.com/nwaples/rardecode/v2 v2.2.5
github.com/seancfoley/ipaddress-go v1.7.1
github.com/sgtdi/fswatcher v1.3.0
@@ -26,7 +26,7 @@ require (
github.com/ulikunitz/xz v0.5.15
github.com/zeebo/xxh3 v1.1.0
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b
golang.org/x/image v0.43.0
golang.org/x/image v0.42.0
golang.org/x/sys v0.46.0
golang.org/x/text v0.38.0
howett.net/plist v1.0.1

8
go.sum
View File

@@ -40,8 +40,8 @@ github.com/kovidgoyal/go-parallel v1.1.1 h1:1OzpNjtrUkBPq3UaqrnvOoB2F9RttSt811ui
github.com/kovidgoyal/go-parallel v1.1.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw=
github.com/kovidgoyal/go-shm v1.0.0 h1:HJEel9D1F9YhULvClEHJLawoRSj/1u/EDV7MJbBPgQo=
github.com/kovidgoyal/go-shm v1.0.0/go.mod h1:Yzb80Xf9L3kaoB2RGok9hHwMIt7Oif61kT6t3+VnZds=
github.com/kovidgoyal/imaging v1.8.22 h1:CtpoRXQpS79xxJsKu8+LUJJE/0i4FLquJZy0QH+QNlM=
github.com/kovidgoyal/imaging v1.8.22/go.mod h1:y8wo4JTv4D+skbtQf6fHg8nA1qtagvCcn8J2Nu5k2Jg=
github.com/kovidgoyal/imaging v1.8.21 h1:95S2+dowTeKJJHNpf6lnScvIennTr2H0zQotu+ptNQw=
github.com/kovidgoyal/imaging v1.8.21/go.mod h1:976F+zjiQeZ7sd87Pxlm0a64S/w9bImSIWg3sSk1rdQ=
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/nwaples/rardecode/v2 v2.2.5 h1:L5doqgGfQwI7qADJMqnkrSB86rpPsqQDrHeO0HWa5JY=
@@ -76,8 +76,8 @@ github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b h1:r+vk0EmXNmekl0S0BascoeeoHk/L7wmaW2QF90K+kYI=
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/image v0.42.0 h1:1gSs6ehNWXLbkHBIPcWztk3D/6aIA/8hauiAYtlodVY=
golang.org/x/image v0.42.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=

View File

@@ -7,7 +7,7 @@ from functools import partial
from typing import NamedTuple
from .fast_data_types import BORDERS_PROGRAM, current_focused_os_window_id, get_options, init_borders_program, set_borders_rects
from .shaders.legacy import program_for
from .shaders import program_for
from .typing_compat import LayoutType
from .utils import color_as_int
from .window_list import WindowGroup, WindowList

View File

@@ -148,7 +148,7 @@ from .session import (
most_recent_session,
save_as_session,
)
from .shaders.legacy import load_shader_programs
from .shaders import load_shader_programs
from .simple_cli_definitions import grab_keyboard_docs
from .tabs import SpecialWindow, SpecialWindowInstance, Tab, TabDict, TabManager
from .types import _T, AsyncResponse, LayerShellConfig, SingleInstanceData, WindowSystemMouseEvent, ac

View File

@@ -370,18 +370,17 @@ static const unsigned write_buf_limit = 100 * 1024 * 1024;
children_mutex(unlock);
void
schedule_write_to_child_if_possible(id_type id, const char *data, size_t sz, bool *found, bool *too_much_data, size_t keep_space) {
schedule_write_to_child_if_possible(id_type id, const char *data, size_t sz, bool *found, bool *too_much_data) {
children_mutex(lock);
ChildMonitor *self = the_monitor;
*found = false; *too_much_data = false;
size_t limit = write_buf_limit > keep_space ? write_buf_limit - keep_space : 0;
for (size_t i = 0; i < self->count; i++) {
if (children[i].id == id) {
Screen *screen = children[i].screen;
screen_mutex(lock, write);
size_t space_left = screen->write_buf_sz - screen->write_buf_used;
if (space_left < sz) {
if (screen->write_buf_used + sz > limit) {
if (screen->write_buf_used + sz > write_buf_limit) {
*too_much_data = true;
screen_mutex(unlock, write);
break;

View File

@@ -35,8 +35,6 @@ kitty_run_data: dict[str, Any] = getattr(sys, 'kitty_run_data', {})
launched_by_launch_services = kitty_run_data.get('launched_by_launch_services', False)
is_quick_access_terminal_app = kitty_run_data.get('is_quick_access_terminal_app', False)
unserialize_launch_flag = 'kitty-unserialize-data='
slangc = os.environ.get('SLANGC', 'slangc').split()
if getattr(sys, 'frozen', False):
extensions_dir: str = kitty_run_data['extensions_dir']
@@ -63,9 +61,7 @@ if getattr(sys, 'frozen', False):
ans = os.path.join(ans, 'kitty')
return ans
kitty_base_dir = get_frozen_base()
if rpath := kitty_run_data.get('bundle_exe_dir'):
slangc = [os.path.join(rpath, 'slangc')]
del get_frozen_base, rpath
del get_frozen_base
else:
kitty_base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
extensions_dir = os.path.join(kitty_base_dir, 'kitty')
@@ -158,7 +154,6 @@ logo_png_file = os.path.join(kitty_base_dir, 'logo', 'kitty.png')
beam_cursor_data_file = os.path.join(kitty_base_dir, 'logo', 'beam-cursor.png')
shell_integration_dir = os.path.join(kitty_base_dir, 'shell-integration')
fonts_dir = os.path.join(kitty_base_dir, 'fonts')
shaders_dir = os.path.join(kitty_base_dir, 'shaders')
try:
shell_path = os.environ.get('SHELL') or pwd.getpwuid(os.geteuid()).pw_shell or '/bin/sh'
except KeyError:

View File

@@ -683,20 +683,6 @@ py_get_config_dir(PyObject *self UNUSED, PyObject *args UNUSED) {
#include "launcher/cli-parser.h"
static PyObject*
set_uint_at_address(PyObject *self UNUSED, PyObject *const *args, Py_ssize_t nargs) {
if (nargs != 2) { PyErr_SetString(PyExc_TypeError, "set_uint_at_address() requires exactly 2 arguments"); return NULL; }
void *ptr = PyLong_AsVoidPtr(args[0]);
if (ptr == NULL) {
if (!PyErr_Occurred()) PyErr_SetString(PyExc_ValueError, "NULL address is not valid");
return NULL;
}
unsigned long value = PyLong_AsUnsignedLong(args[1]);
if (value == (unsigned long)-1 && PyErr_Occurred()) return NULL;
*((unsigned int*)ptr) = (unsigned int)value;
Py_RETURN_NONE;
}
static PyMethodDef module_methods[] = {
METHODB(replace_c0_codes_except_nl_space_tab, METH_O),
METHODB(read_file, METH_O),
@@ -730,7 +716,6 @@ static PyMethodDef module_methods[] = {
{"timed_debug_print", (PyCFunction)py_timed_debug_print, METH_VARARGS, ""},
{"find_in_memoryview", (PyCFunction)find_in_memoryview, METH_VARARGS, ""},
{"run_at_exit_cleanup_functions", (PyCFunction)py_run_atexit_cleanup_functions, METH_NOARGS, ""},
{"set_uint_at_address", (PyCFunction)(void (*)(void))set_uint_at_address, METH_FASTCALL, "Write an unsigned integer value to a memory address"},
#ifdef __APPLE__
METHODB(user_cache_dir, METH_NOARGS),
METHODB(process_group_map, METH_NOARGS),

View File

@@ -315,7 +315,7 @@ const char* cursor_as_sgr(const Cursor *);
PyObject* cm_thread_write(PyObject *self, PyObject *args);
bool schedule_write_to_child(id_type id, unsigned int num, ...);
bool schedule_write_to_child_python(id_type id, const char *prefix, PyObject* tuple_of_str_or_bytes, const char *suffix);
void schedule_write_to_child_if_possible(id_type id, const char *data, size_t sz, bool *found, bool *too_much_data, size_t keep_space);
void schedule_write_to_child_if_possible(id_type id, const char *data, size_t sz, bool *found, bool *too_much_data);
bool set_iutf8(int, bool);
DynamicColor colorprofile_to_color(const ColorProfile *self, DynamicColor entry, DynamicColor defval);

View File

@@ -236,8 +236,6 @@ test_write_chunk(id_type id, const char *buf, size_t sz) {
return true;
}
static const size_t keep_write_to_child_queue_space = 32 * 1024;
static size_t
send_payload_to_child(id_type id, uint32_t client_id, const char *header, size_t header_sz, const char *data, const size_t data_sz, bool as_base64) {
size_t offset = 0;
@@ -248,7 +246,7 @@ send_payload_to_child(id_type id, uint32_t client_id, const char *header, size_t
buf[header_sz++] = 0x1b; buf[header_sz++] = '\\';
if (!test_write_chunk(id, buf, header_sz)) {
bool found, too_much_data;
schedule_write_to_child_if_possible(id, buf, header_sz, &found, &too_much_data, keep_write_to_child_queue_space);
schedule_write_to_child_if_possible(id, buf, header_sz, &found, &too_much_data);
if (too_much_data) return 0;
}
return 1;
@@ -272,7 +270,7 @@ send_payload_to_child(id_type id, uint32_t client_id, const char *header, size_t
buf[p++] = 0x1b; buf[p++] = '\\';
if (!test_write_chunk(id, buf, p)) {
bool found, too_much_data;
schedule_write_to_child_if_possible(id, buf, p, &found, &too_much_data, keep_write_to_child_queue_space);
schedule_write_to_child_if_possible(id, buf, p, &found, &too_much_data);
if (too_much_data) break;
if (!found) return data_sz;
}

View File

@@ -1860,4 +1860,3 @@ def request_callback_with_thumbnail(
scale: float = 0.25, max_width: int = 480
) -> None: ...
def png_from_32bit_rgba_data(data: bytes, width: int, height: int, flip_vertically: bool = False) -> bytes: ...
def set_uint_at_address(address: int, value: int) -> None: ...

View File

@@ -239,7 +239,7 @@ read_STAT_font_table(const uint8_t *table, size_t table_len, PyObject *name_look
if (_PyTuple_Resize(&design_axes, count) == -1) return false;
count = 0;
const uint8_t *start_of_axis_values_offsets_array = table + offset_to_start_of_axis_value_entries;
Py_ssize_t num_of_multi_axis_styles = 0;
Py_ssize_t i = 0;
if (_PyTuple_Resize(&multi_axis_styles, count_of_axis_value_entries) == -1) return false;
for (
const uint8_t *pos = start_of_axis_values_offsets_array;
@@ -273,16 +273,11 @@ read_STAT_font_table(const uint8_t *table, size_t table_len, PyObject *name_look
app("sd sd", "value", value, "linked_value", linked_value);
} break;
case 4: if ((uint8_t*)(p) + 6 * axis_index <= table_limit) {
if (num_of_multi_axis_styles >= PyTuple_GET_SIZE(multi_axis_styles)) {
PyErr_Format(PyExc_IndexError, "corrupted STAT table in font too many multi_axis_styles (%d >= %d)", (int)num_of_multi_axis_styles, (int)PyTuple_GET_SIZE(multi_axis_styles));
return false;
}
RAII_PyObject(values, PyTuple_New(axis_index));
if (!values) return false;
for (Py_ssize_t n = 0; n < PyTuple_GET_SIZE(values); n++) {
for (uint16_t n = 0; n < axis_index; n++, p += 3) {
uint16_t actual_axis_index = next;
double value = load_fixed((uint32_t*)p);
p += 2;
p32 = (uint32_t*)p; double value = next32;
PyObject *e = Py_BuildValue("{sH sd}", "design_index", actual_axis_index, "value", value);
if (!e) return false;
PyTuple_SET_ITEM(values, n, e);
@@ -290,11 +285,11 @@ read_STAT_font_table(const uint8_t *table, size_t table_len, PyObject *name_look
PyObject *e = Py_BuildValue("{sH sN sO}", "flags", flags,
"name", get_best_name(name_lookup_table, value_name_id), "values", values);
if (!e) return false;
PyTuple_SET_ITEM(multi_axis_styles, num_of_multi_axis_styles, e); num_of_multi_axis_styles++;
PyTuple_SET_ITEM(multi_axis_styles, i++, e);
} break;
}
}
if (_PyTuple_Resize(&multi_axis_styles, num_of_multi_axis_styles) == -1) return false;
if (_PyTuple_Resize(&multi_axis_styles, i) == -1) return false;
ok:
if (PyDict_SetItemString(output, "design_axes", design_axes) != 0) return false;
if (PyDict_SetItemString(output, "multi_axis_styles", multi_axis_styles) != 0) return false;

View File

@@ -120,7 +120,7 @@ Where to launch the child process:
:code:`os-panel`
Similar to :code:`os-window`, except that it creates the new OS Window as a desktop panel.
Only works on platforms that support this, such as Wayland compositors that support the layer
Only works on platforms that support this, such as Wayand compositors that support the layer
shell protocol. Use the :option:`kitten @ launch --os-panel` option to configure the panel.
#placeholder_for_formatting#
@@ -153,7 +153,7 @@ Add the newly created window/tab to the specified session. Use :code:`.` to add
to the session of the :option:`source window <launch --source-window>`, if any. See :ref:`sessions`
for what a session is and how to use one. By default, the window is added to the
session of the :option:`source window <launch --source-window>` when :option:`launch --cwd`
is set to use the working directory from that window, otherwise the newly created window
is set to use the the working directory from that window, otherwise the newly created window
does not belong to any session. To force adding to no session, use the value :code:`!`.
Adding a newly created window to a session is purely temporary, it does not change the actual
session file, for that you have to resave the session. Note that using this flag in a launch
@@ -943,7 +943,6 @@ class EditCmd:
self.file_size = -1
self.version = 0
self.source_window_id = self.editor_window_id = -1
self.editor_exit_code: int | None = None
simple = 'file_inode', 'file_data', 'abort_signaled', 'version'
for k, v in parse_message(msg, simple):
if k == 'file_inode':
@@ -1010,17 +1009,6 @@ class EditCmd:
def on_edit_window_close(self, window: Window) -> None:
self.check_status()
if self.editor_exit_code is None:
# The PID death callback fires in the same main-loop tick as the
# window-close callback (report_reaped_pids runs after parse_input).
# Schedule a short-interval fallback timer in case the PID was not
# registered in time (e.g., editor crashed before monitor_pid ran).
add_timer(self._fallback_done, 0.1, False)
def _fallback_done(self, timer_id: int | None = None) -> None:
if self.editor_exit_code is None:
self.editor_exit_code = 0
self.check_status()
def check_status(self, timer_id: int | None = None) -> None:
if self.abort_signaled:
@@ -1035,13 +1023,9 @@ class EditCmd:
self.send_data(source_window, 'UPDATE', data)
editor_window = boss.window_id_map.get(self.editor_window_id)
if editor_window is None:
if self.editor_exit_code is None:
# Wait for the PID death callback to provide the editor's exit code.
# It will call check_status() again once the exit code is known.
return
edits_in_flight.pop(self.source_window_id, None)
if source_window is not None:
self.send_data(source_window, 'DONE', str(self.editor_exit_code).encode())
self.send_data(source_window, 'DONE')
self.abort_signaled = self.abort_signaled or 'closed'
else:
self.schedule_check()
@@ -1158,19 +1142,6 @@ def remote_edit(msg: str, window: Window) -> None:
q.abort_signaled = 'replaced'
edits_in_flight[window.id] = c
w.actions_on_close.append(c.on_edit_window_close)
editor_pid = w.child.pid
if editor_pid:
def on_editor_pid_death(wait_status: int, err: Exception | None) -> None:
c.editor_exit_code = os.waitstatus_to_exitcode(wait_status) if err is None else 1
c.check_status()
try:
get_boss().monitor_pid(editor_pid, on_editor_pid_death)
except RuntimeError:
# PID monitoring table is full. The fallback timer in
# on_edit_window_close will set editor_exit_code to 0.
log_error('edit-in-kitty: PID monitoring table full, editor exit code will not be tracked')
else:
c.editor_exit_code = 0
c.schedule_check()

View File

@@ -59,7 +59,7 @@ from .options.types import Options
from .options.utils import DELETE_ENV_VAR
from .os_window_size import edge_spacing, initial_window_size_func
from .session import create_sessions, get_os_window_sizing_data
from .shaders.legacy import CompileError, load_shader_programs
from .shaders import CompileError, load_shader_programs
from .types import LayerShellConfig
from .utils import (
cleanup_ssh_control_masters,

View File

@@ -3,15 +3,26 @@
import re
from collections.abc import Callable, Generator, Iterable, Sequence
from ctypes import POINTER, c_uint, c_void_p, cast
from re import Pattern
from typing import Union
from .fast_data_types import set_uint_at_address
from .utils import resolve_custom_file
pointer_to_uint = POINTER(c_uint)
MarkerFunc = Callable[[str, int, int, int], Generator[None, None, None]]
def get_output_variables(left_address: int, right_address: int, color_address: int) -> tuple[c_uint, c_uint, c_uint]:
return (
cast(c_void_p(left_address), pointer_to_uint).contents,
cast(c_void_p(right_address), pointer_to_uint).contents,
cast(c_void_p(color_address), pointer_to_uint).contents,
)
def marker_from_regex(expression: Union[str, 'Pattern[str]'], color: int, flags: int = re.UNICODE) -> MarkerFunc:
color = max(1, min(color, 3))
if isinstance(expression, str):
@@ -20,10 +31,11 @@ def marker_from_regex(expression: Union[str, 'Pattern[str]'], color: int, flags:
pat = expression
def marker(text: str, left_address: int, right_address: int, color_address: int) -> Generator[None, None, None]:
set_uint_at_address(color_address, color)
left, right, colorv = get_output_variables(left_address, right_address, color_address)
colorv.value = color
for match in pat.finditer(text):
set_uint_at_address(left_address, match.start())
set_uint_at_address(right_address, match.end() - 1)
left.value = match.start()
right.value = match.end() - 1
yield
return marker
@@ -40,11 +52,12 @@ def marker_from_multiple_regex(regexes: Iterable[tuple[int, str]], flags: int =
pat = re.compile(expr, flags=flags)
def marker(text: str, left_address: int, right_address: int, color_address: int) -> Generator[None, None, None]:
left, right, color = get_output_variables(left_address, right_address, color_address)
for match in pat.finditer(text):
set_uint_at_address(left_address, match.start())
set_uint_at_address(right_address, match.end() - 1)
left.value = match.start()
right.value = match.end() - 1
grp = match.lastgroup
set_uint_at_address(color_address, color_map[grp] if grp is not None else 0)
color.value = color_map[grp] if grp is not None else 0
yield
return marker
@@ -56,10 +69,11 @@ def marker_from_text(expression: str, color: int) -> MarkerFunc:
def marker_from_function(func: Callable[[str], Iterable[tuple[int, int, int]]]) -> MarkerFunc:
def marker(text: str, left_address: int, right_address: int, color_address: int) -> Generator[None, None, None]:
left, right, colorv = get_output_variables(left_address, right_address, color_address)
for (ll, r, c) in func(text):
set_uint_at_address(left_address, ll)
set_uint_at_address(right_address, r)
set_uint_at_address(color_address, c)
left.value = ll
right.value = r
colorv.value = c
yield
return marker

View File

@@ -7,8 +7,8 @@ from functools import lru_cache, partial
from itertools import count
from typing import Any, Literal, NamedTuple, Optional
from kitty.constants import read_kitty_resource
from kitty.fast_data_types import (
from .constants import read_kitty_resource
from .fast_data_types import (
BGIMAGE_PROGRAM,
BLINK,
BLIT_PROGRAM,

View File

@@ -1,28 +0,0 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
module alpha_blend;
// Alpha blend two colors returning the resulting color pre-multiplied by its alpha
// and its alpha. See https://en.wikipedia.org/wiki/Alpha_compositing
public float4 alpha_blend(float4 over, float4 under) {
float alpha = lerp(under.a, 1.0f, over.a);
float3 combined_color = lerp(under.rgb * under.a, over.rgb, over.a);
return float4(combined_color, alpha);
}
// Same as alpha_blend() except that it assumes over and under are both premultiplied.
public float4 alpha_blend_premul(float4 over, float4 under) {
float inv_over_alpha = 1.0f - over.a;
float alpha = over.a + under.a * inv_over_alpha;
return float4(over.rgb + under.rgb * inv_over_alpha, alpha);
}
// Same as alpha_blend_premul with under_alpha = 1 outputs a blended color
// with alpha 1 which is effectively pre-multiplied since alpha is 1
public float4 alpha_blend_premul(float4 over, float3 under) {
float inv_over_alpha = 1.0f - over.a;
return float4(over.rgb + under.rgb * inv_over_alpha, 1.0f);
}

View File

@@ -1,90 +0,0 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
import alpha_blend;
// Constants and Macros
#define left 0
#define top 1
#define right 2
#define bottom 3
#define tex_left 0.0
#define tex_top 0.0
#define tex_right 1.0
#define tex_bottom 1.0
#define x_axis 0
#define y_axis 1
static const float2 tex_map[4] = {
float2(tex_left, tex_top),
float2(tex_left, tex_bottom),
float2(tex_right, tex_bottom),
float2(tex_right, tex_top)
};
struct VertexOutput {
float2 texcoord : TEXCOORD;
float4 position : SV_Position;
};
// Helper Functions
float scale_factor(float window_size, float image_size) {
return window_size / image_size;
}
float tiling_factor(int i, float4 sizes, float tiled) {
int window = i;
int image = i + 2;
return tiled * scale_factor(sizes[window], sizes[image]) + (1.0 - tiled);
}
// Main Vertex Shader Entry Point
[shader("vertex")]
VertexOutput vertex_main(
uint vertex_id : SV_VertexID,
uniform float tiled,
uniform float4 sizes,
uniform float4 positions,
) {
const float2 pos_map[4] = {
float2(positions[left], positions[top]),
float2(positions[left], positions[bottom]),
float2(positions[right], positions[bottom]),
float2(positions[right], positions[top])
};
VertexOutput output;
// Calculate outputs
float2 tex_coords = tex_map[vertex_id];
output.texcoord = float2(
tex_coords[x_axis] * tiling_factor(x_axis, sizes, tiled),
tex_coords[y_axis] * tiling_factor(y_axis, sizes, tiled)
);
output.position = float4(pos_map[vertex_id], 0.0, 1.0);
return output;
}
// Global Texture Bindings
uniform Sampler2D image;
// Helper Functions
float4 alpha_blend(float4 color, float4 bg) {
// Performs standard linear alpha blending: src * src.a + dst * (1 - src.a)
float3 blended_rgb = color.rgb * color.a + bg.rgb * (1.0 - color.a);
float blended_alpha = color.a + bg.a * (1.0 - color.a);
return float4(blended_rgb, blended_alpha);
}
// Main Fragment Shader Entry Point
[shader("fragment")]
float4 fragment_main(float2 texcoord: TEXCOORD, uniform float4 background) : SV_Target {
// Sample the texture using Slang's intrinsic texture object syntax
float4 color = image.Sample(texcoord);
// Compute final color with alpha blending
float4 premult_color = alpha_blend(color, background);
return premult_color;
}

View File

@@ -1,33 +0,0 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
// Maps a vertex id to src and destination co-ordinates
module blit_common;
public struct BlitOutput {
public float2 texcoord;
public float2 position;
};
#define left 0
#define top 1
#define right 2
#define bottom 3
// Static constant array mapping vertex IDs
static const int2 vertex_pos_map[4] = {
{right, top},
{right, bottom},
{left, bottom},
{left, top}
};
public BlitOutput get_coords_for_blit(uint vertex_id, float4 src_rect, float4 dest_rect) {
int2 pos = vertex_pos_map[vertex_id];
BlitOutput output;
output.texcoord = float2(src_rect[pos.x], src_rect[pos.y]);
output.position = float2(dest_rect[pos.x], dest_rect[pos.y]);
return output;
}

View File

@@ -1,32 +0,0 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
import blit_common;
import alpha_blend;
import utils;
import linear2srgb;
struct VSOutput {
float2 texcoord : TEXCOORD;
float4 position : SV_Position;
};
[shader("vertex")]
VSOutput vertex_main(
uint vertex_id : SV_VertexID,
uniform float4 src_rect,
uniform float4 dest_rect,
) {
BlitOutput ans = get_coords_for_blit(vertex_id, src_rect, dest_rect);
return {ans.texcoord, float4(ans.position[0], ans.position[1], 0.0, 1.0)};
}
uniform Sampler2D image;
[shader("fragment")]
float4 fragment_main(float2 texcoord : TEXCOORD) : SV_Target {
float4 color_premul = image.Sample(texcoord);
return vec4_premul(linear2srgb(color_premul.rgb / color_premul.a), color_premul.a);
}

View File

@@ -1,569 +0,0 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
// https://github.com/shader-slang/slang/issues/11874
// warnings-disable: 41012
import utils;
import hsluv;
import alpha_blend;
import linear2srgb;
#define NUM_COLORS 256
extern static const bool DO_FG_OVERRIDE;
extern static const uint FG_OVERRIDE_ALGO;
extern static const float FG_OVERRIDE_THRESHOLD;
extern static const bool TEXT_NEW_GAMMA;
extern static const bool ONLY_FOREGROUND;
extern static const bool ONLY_BACKGROUND;
static const bool OVERRIDE_FG_COLORS = DO_FG_OVERRIDE == 1 && !ONLY_BACKGROUND;
// Inputs {{{
struct CellRenderDataStruct
{
float use_cell_bg_for_selection_fg, use_cell_fg_for_selection_fg, use_cell_for_selection_bg;
uint default_fg, highlight_fg, highlight_bg, main_cursor_fg, main_cursor_bg, url_color, url_style, inverted, extra_cursor_fg, extra_cursor_bg;
uint columns, lines, sprites_xnum, sprites_ynum, cursor_shape, cell_width, cell_height;
uint cursor_x1, cursor_x2, cursor_y1, cursor_y2;
float cursor_opacity, inactive_text_alpha, dim_opacity, blink_opacity;
// must have unique entries with 0 being default_bg and unset being UINT32_MAX
uint bg_colors0, bg_colors1, bg_colors2, bg_colors3, bg_colors4, bg_colors5, bg_colors6, bg_colors7;
float bg_opacities0, bg_opacities1, bg_opacities2, bg_opacities3, bg_opacities4, bg_opacities5, bg_opacities6, bg_opacities7;
};
// Uniform Blocks (adjust binding slots as needed for your pipeline layout)
ConstantBuffer<CellRenderDataStruct> crd;
struct ColorTableStruct
{
uint color_table[NUM_COLORS + MARK_MASK + MARK_MASK + 2];
};
ConstantBuffer<ColorTableStruct> ColorTable;
#define color_table ColorTable.color_table
uniform float gamma_lut[256];
uniform Sampler2D<uint4> sprite_decorations_map;
//
static const int fg_index_map[3] = {0, 1, 0};
static const uint2 cell_pos_map[4] = {
uint2(1u, 0u), // right, top
uint2(1u, 1u), // right, bottom
uint2(0u, 1u), // left, bottom
uint2(0u, 0u) // left, top
};
static const uint cursor_shape_map[] = { // maps cursor shape to foreground sprite index
0u, // NO_CURSOR
0u, // BLOCK (this is rendered as background)
2u, // BEAM
3u, // UNDERLINE
4u // UNFOCUSED
};
// Vertex Input Attributes with explicit fixed locations. We use fixed
// locations as all variants of the cell program share the same VAOs
struct VertexInput
{
[[vk::location(0)]] uint3 colors;
[[vk::location(1)]] uint2 sprite_idx;
[[vk::location(2)]] uint is_selected;
};
// }}}
// Utility functions {{{
static const uint BYTE_MASK = 0xFF;
static const uint SPRITE_INDEX_MASK = 0x7fffffff;
static const uint SPRITE_COLORED_MASK = 0x80000000;
static const uint SPRITE_COLORED_SHIFT = 31u;
static const uint BIT_MASK = 1u;
// Linear space luminance values
static const float3 Y = float3(0.2126, 0.7152, 0.0722);
float3 color_to_vec(uint c) {
uint r, g, b;
r = (c >> 16) & BYTE_MASK;
g = (c >> 8) & BYTE_MASK;
b = c & BYTE_MASK;
return float3(gamma_lut[r], gamma_lut[g], gamma_lut[b]);
}
float one_if_equal_zero_otherwise(float a, float b) {
return (1.0f - zero_or_one(abs(float(a) - float(b))));
}
// We need an integer variant to accommodate GPU driver bugs, see
// https://github.com/kovidgoyal/kitty/issues/9072
uint one_if_equal_zero_otherwise(int a, int b) {
return (1u - uint(zero_or_one(abs(float(a) - float(b)))));
}
uint one_if_equal_zero_otherwise(uint a, uint b) {
return (1u - uint(zero_or_one(abs(float(a) - float(b)))));
}
uint resolve_color(uint c, uint defval) {
int t = int(c & BYTE_MASK);
uint is_one = one_if_equal_zero_otherwise(t, 1);
uint is_two = one_if_equal_zero_otherwise(t, 2);
uint is_neither_one_nor_two = 1u - is_one - is_two;
return is_one * color_table[(c >> 8) & BYTE_MASK] + is_two * (c >> 8) + is_neither_one_nor_two * defval;
}
float3 to_color(uint c, uint defval) {
return color_to_vec(resolve_color(c, defval));
}
[ForceInline]
float3 q_func(float type_val, uint which, float3 val) {
return one_if_equal_zero_otherwise(type_val, float(which)) * val;
}
float3 resolve_dynamic_color(uint c, float3 special_val, float3 defval) {
float type_val = float((c >> 24) & BYTE_MASK);
return (
q_func(type_val, COLOR_IS_RGB, color_to_vec(c)) +
q_func(type_val, COLOR_IS_INDEX, color_to_vec(color_table[c & BYTE_MASK])) +
q_func(type_val, COLOR_IS_SPECIAL, special_val) +
q_func(type_val, COLOR_NOT_SET, defval)
);
}
float contrast_ratio(float under_luminance, float over_luminance) {
return clamp((max(under_luminance, over_luminance) + 0.05f) / (min(under_luminance, over_luminance) + 0.05f), 1.f, 21.f);
}
float contrast_ratio(float3 a, float3 b) {
return contrast_ratio(dot(a, Y), dot(b, Y));
}
struct ColorPair {
float3 bg, fg;
};
float contrast_ratio(ColorPair a) {
return contrast_ratio(a.bg, a.fg);
}
ColorPair if_less_than_pair(float a, float b, ColorPair thenval, ColorPair elseval) {
return ColorPair(
if_less_than(a, b, thenval.bg, elseval.bg),
if_less_than(a, b, thenval.fg, elseval.fg)
);
}
ColorPair if_one_then_pair(float condition, ColorPair thenval, ColorPair elseval) {
return ColorPair(
if_one_then(condition, thenval.bg, elseval.bg),
if_one_then(condition, thenval.fg, elseval.fg)
);
}
ColorPair resolve_extra_cursor_colors_for_special_cursor(float3 cell_bg, float3 cell_fg) {
ColorPair cell = ColorPair(cell_fg, cell_bg);
ColorPair base = ColorPair(color_to_vec(crd.default_fg), color_to_vec(crd.bg_colors0));
float cr = contrast_ratio(cell);
float br = contrast_ratio(base);
ColorPair higher_contrast_pair = if_less_than_pair(cr, br, base, cell);
return if_less_than_pair(cr, 2.5f, higher_contrast_pair, cell);
}
ColorPair resolve_extra_cursor_colors(float3 cell_bg, float3 cell_fg, ColorPair main_cursor) {
ColorPair ans = ColorPair(
resolve_dynamic_color(crd.extra_cursor_bg, main_cursor.bg, main_cursor.bg),
resolve_dynamic_color(crd.extra_cursor_fg, cell_bg, main_cursor.fg)
);
ColorPair special = resolve_extra_cursor_colors_for_special_cursor(cell_bg, cell_fg);
return if_one_then_pair(zero_or_one(abs(float(crd.extra_cursor_bg & BYTE_MASK) - float(COLOR_IS_SPECIAL))), ans, special);
}
uint3 to_sprite_coords(uint idx) {
uint sprites_per_page = crd.sprites_xnum * crd.sprites_ynum;
uint z = idx / sprites_per_page;
uint num_on_last_page = idx - sprites_per_page * z;
uint y = num_on_last_page / crd.sprites_xnum;
uint x = num_on_last_page - crd.sprites_xnum * y;
return uint3(x, y, z);
}
float3 to_sprite_pos(uint2 pos, uint idx) {
uint3 c = to_sprite_coords(idx);
float2 s_xpos = float2(float(c.x), float(c.x) + 1.0f) * (1.0f / float(crd.sprites_xnum));
float2 s_ypos = float2(float(c.y), float(c.y) + 1.0f) * (1.0f / float(crd.sprites_ynum));
uint texture_height_px = (crd.cell_height + 1u) * crd.sprites_ynum;
float row_height = 1.0f / float(texture_height_px);
s_ypos[1] -= row_height; // skip the decorations_exclude row
return float3(s_xpos[pos.x], s_ypos[pos.y], float(c.z));
}
uint to_underline_exclusion_pos(uint2 sprite_idx) {
uint3 c = to_sprite_coords(sprite_idx[0]);
uint cell_top_px = c.y * (crd.cell_height + 1u);
return cell_top_px + crd.cell_height;
}
uint read_sprite_decorations_idx(uint2 sprite_idx) {
int idx = int(sprite_idx[0] & SPRITE_INDEX_MASK);
int width, height;
sprite_decorations_map.GetDimensions(width, height);
int2 sz = int2(width, height);
int y = idx / sz[0];
int x = idx - y * sz[0];
return sprite_decorations_map[int2(x, y)].r;
}
uint2 get_decorations_indices(uint2 sprite_idx, uint in_url /* [0, 1] */, uint text_attrs) {
uint decorations_idx = read_sprite_decorations_idx(sprite_idx);
uint has_decorations = uint(zero_or_one(float(decorations_idx)));
uint strike_style = ((text_attrs >> STRIKE_SHIFT) & BIT_MASK); // 0 or 1
uint strike_idx = decorations_idx * strike_style;
uint underline_style = ((text_attrs >> DECORATION_SHIFT) & DECORATION_MASK);
underline_style = in_url * crd.url_style + (1u - in_url) * underline_style; // [0, 5]
uint has_underline = uint(step(0.5f, float(underline_style))); // [0, 1]
return has_decorations * uint2(strike_idx, has_underline * (decorations_idx + underline_style));
}
uint is_cursor(uint x, uint y) {
uint clamped_x = clamp(x, crd.cursor_x1, crd.cursor_x2);
uint clamped_y = clamp(y, crd.cursor_y1, crd.cursor_y2);
return one_if_equal_zero_otherwise(x, clamped_x) * one_if_equal_zero_otherwise(y, clamped_y);
}
// }}}
struct CellData {
float has_cursor;
float has_block_cursor;
uint2 pos;
uint cursor_fg_sprite_idx;
ColorPair cursor;
};
struct VertexOutput {
float4 position : SV_Position;
float3 background;
float4 effective_background_premul;
float effective_text_alpha;
float3 sprite_pos;
float3 underline_pos;
float3 cursor_pos;
float3 strike_pos;
nointerpolation uint underline_exclusion_pos;
float3 cell_foreground;
float4 cursor_color_premult;
float3 decoration_fg;
float colored_sprite;
};
CellData set_vertex_position(
float3 cell_fg,
float3 cell_bg,
uint instance_id,
uint vertex_id,
float row_offset,
uint2 sprite_idx,
uint is_selected,
inout VertexOutput vo,
) {
CellData cell_out;
float dx = 2.0 / float(crd.columns);
float dy = 2.0 / float(crd.lines);
// The current cell being rendered
uint row = instance_id / crd.columns;
uint column = instance_id - row * crd.columns;
// The position of this vertex, at a corner of the cell
float left = -1.0 + float(column) * dx;
float top = 1.0 - (float(row) + row_offset) * dy;
uint2 pos = cell_pos_map[vertex_id];
float2 select_x = float2(left, left + dx);
float2 select_y = float2(top, top - dy);
float posX = select_x[pos.x];
float posY = select_y[pos.y];
vo.position = float4(posX, posY, 0.0, 1.0);
// The character sprite being rendered
if (!ONLY_BACKGROUND) {
vo.sprite_pos = to_sprite_pos(pos, sprite_idx[0] & SPRITE_INDEX_MASK);
vo.colored_sprite = float((sprite_idx[0] & SPRITE_COLORED_MASK) >> SPRITE_COLORED_SHIFT);
}
// Cursor shape and colors
float has_main_cursor = float(is_cursor(column, row));
float multicursor_shape = float((is_selected >> 2) & 3u);
float multicursor_uses_main_cursor_shape = float((is_selected >> 4) & BIT_MASK);
multicursor_shape = if_one_then(multicursor_uses_main_cursor_shape, crd.cursor_shape, multicursor_shape);
float final_cursor_shape = if_one_then(has_main_cursor, crd.cursor_shape, multicursor_shape);
float has_cursor = zero_or_one(final_cursor_shape);
float is_block_cursor = has_cursor * one_if_equal_zero_otherwise(final_cursor_shape, 1.0);
ColorPair main_cursor;
main_cursor.bg = color_to_vec(crd.main_cursor_bg);
main_cursor.fg = color_to_vec(crd.main_cursor_fg);
ColorPair extra_cursor = resolve_extra_cursor_colors(cell_bg, cell_fg, main_cursor);
ColorPair cursor = if_one_then_pair(has_main_cursor, main_cursor, extra_cursor);
cell_out.has_cursor = has_cursor;
cell_out.has_block_cursor = is_block_cursor;
cell_out.pos = pos;
cell_out.cursor_fg_sprite_idx = cursor_shape_map[int(final_cursor_shape)];
cell_out.cursor = cursor;
return cell_out;
}
float background_opacity_for(uint bg, uint colorval, float opacity_if_matched) {
float not_matched = step(1.0, abs(float(colorval) - float(bg)));
return not_matched + opacity_if_matched * (1.0 - not_matched);
}
float calc_background_opacity(uint bg) {
return (
background_opacity_for(bg, crd.bg_colors0, crd.bg_opacities0) *
background_opacity_for(bg, crd.bg_colors1, crd.bg_opacities1) *
background_opacity_for(bg, crd.bg_colors2, crd.bg_opacities2) *
background_opacity_for(bg, crd.bg_colors3, crd.bg_opacities3) *
background_opacity_for(bg, crd.bg_colors4, crd.bg_opacities4) *
background_opacity_for(bg, crd.bg_colors5, crd.bg_opacities5) *
background_opacity_for(bg, crd.bg_colors6, crd.bg_opacities6) *
background_opacity_for(bg, crd.bg_colors7, crd.bg_opacities7)
);
}
// Override foreground colors {{{
float3 fg_override_luminance(float colored_sprite, float under_luminance, float over_lumininace, float3 under, float3 over) {
// If the difference in luminance is too small,
// force the foreground color to be black or white.
float diff_luminance = abs(under_luminance - over_lumininace);
float override_level = (1.f - colored_sprite) * step(diff_luminance, FG_OVERRIDE_THRESHOLD);
float original_level = 1.f - override_level;
return original_level * over + override_level * float3(step(under_luminance, 0.5f));
}
float3 fg_override_contrast(float under_luminance, float over_luminance, float3 under, float3 over) {
float ratio = contrast_ratio(under_luminance, over_luminance);
float3 diff = abs(under - over);
float3 over_hsluv = rgbToHsluv(over);
const float min_contrast_ratio = FG_OVERRIDE_THRESHOLD;
float target_lum_a = clamp((under_luminance + 0.05) * min_contrast_ratio - 0.05, 0.0, 1.0);
float target_lum_b = clamp((under_luminance + 0.05) / min_contrast_ratio - 0.05, 0.0, 1.0);
float3 result_a = clamp(hsluvToRgb(float3(over_hsluv.x, over_hsluv.y, target_lum_a * 100.0)), 0.0, 1.0);
float3 result_b = clamp(hsluvToRgb(float3(over_hsluv.x, over_hsluv.y, target_lum_b * 100.0)), 0.0, 1.0);
float result_a_ratio = contrast_ratio(under_luminance, dot(result_a, Y));
float result_b_ratio = contrast_ratio(under_luminance, dot(result_b, Y));
float3 result = lerp(result_a, result_b, step(result_a_ratio, result_b_ratio));
float fallback_condition = max(step(diff.x + diff.y + diff.z, 0.001), step(min_contrast_ratio, ratio));
return lerp(result, over, fallback_condition);
}
float3 override_foreground_color(float3 over, float3 under, float colored_sprite) {
float under_luminance = dot(under, Y);
float over_lumininace = dot(over.rgb, Y);
if (FG_OVERRIDE_ALGO == 1) return fg_override_luminance(colored_sprite, under_luminance, over_lumininace, under, over);
return fg_override_contrast(under_luminance, over_lumininace, under, over);
}
// }}}
[shader("vertex")]
VertexOutput vertex_main(
VertexInput vi,
uint vertex_id : SV_VertexID,
uint instance_id : SV_InstanceID,
uniform uint draw_bg_bitfield,
uniform float row_offset,
) {
VertexOutput vo;
// set cell color indices {{{
uint2 default_colors = uint2(crd.default_fg, crd.bg_colors0);
uint text_attrs = vi.sprite_idx[1];
uint is_reversed = ((text_attrs >> REVERSE_SHIFT) & BIT_MASK);
uint is_inverted = is_reversed + crd.inverted;
int fg_index = fg_index_map[is_inverted];
int bg_index = 1 - fg_index;
int mark = int(text_attrs >> MARK_SHIFT) & MARK_MASK;
uint has_mark = uint(step(1, float(mark)));
uint bg_as_uint = resolve_color(vi.colors[bg_index], default_colors[bg_index]);
bg_as_uint = has_mark * color_table[NUM_COLORS + mark - 1] + (BIT_MASK - has_mark) * bg_as_uint;
float cell_has_default_bg = 1.f - step(1.f, abs(float(bg_as_uint - crd.bg_colors0))); // 1 if has default bg else 0
float3 bg = color_to_vec(bg_as_uint);
uint fg_as_uint = resolve_color(vi.colors[fg_index], default_colors[fg_index]);
fg_as_uint = has_mark * color_table[NUM_COLORS + MARK_MASK + mark] + (1u - has_mark) * fg_as_uint;
float3 foreground = color_to_vec(fg_as_uint);
CellData cell_data = set_vertex_position(foreground, bg, instance_id, vertex_id, row_offset, vi.sprite_idx, vi.is_selected, vo);
// }}}
// Foreground {{{
if (!ONLY_BACKGROUND) { // background does not depend on foreground
float has_dim = float((text_attrs >> DIM_SHIFT) & BIT_MASK), has_blink = float((text_attrs >> BLINK_SHIFT) & BIT_MASK);
vo.effective_text_alpha = crd.inactive_text_alpha * if_one_then(has_dim, crd.dim_opacity, 1.0) * if_one_then(
has_blink, crd.blink_opacity, 1.0);
float in_url = float((vi.is_selected >> 1) & BIT_MASK);
vo.decoration_fg = if_one_then(in_url, color_to_vec(crd.url_color), to_color(vi.colors[2], fg_as_uint));
// Selection
float3 selection_color = if_one_then(crd.use_cell_bg_for_selection_fg, bg, color_to_vec(crd.highlight_fg));
selection_color = if_one_then(crd.use_cell_fg_for_selection_fg, foreground, selection_color);
foreground = if_one_then(float(vi.is_selected & BIT_MASK), selection_color, foreground);
vo.decoration_fg = if_one_then(float(vi.is_selected & BIT_MASK), selection_color, vo.decoration_fg);
// Underline and strike through (rendered via sprites)
uint2 decs = get_decorations_indices(vi.sprite_idx, uint(in_url), text_attrs);
vo.strike_pos = to_sprite_pos(cell_data.pos, decs[0]);
vo.underline_pos = to_sprite_pos(cell_data.pos, decs[1]);
vo.underline_exclusion_pos = to_underline_exclusion_pos(vi.sprite_idx);
// Cursor
vo.cursor_color_premult = float4(cell_data.cursor.bg * crd.cursor_opacity, crd.cursor_opacity);
float3 final_cursor_text_color = lerp(foreground, cell_data.cursor.fg, crd.cursor_opacity);
foreground = if_one_then(cell_data.has_block_cursor, final_cursor_text_color, foreground);
vo.decoration_fg = if_one_then(cell_data.has_block_cursor, final_cursor_text_color, vo.decoration_fg);
vo.cursor_pos = to_sprite_pos(cell_data.pos, cell_data.cursor_fg_sprite_idx * uint(cell_data.has_cursor));
}
// }}}
// Background {{{
float bg_alpha = calc_background_opacity(bg_as_uint);
// we use max so that opacity of the block cursor cell background goes from bg_alpha to 1
float effective_cursor_opacity = max(crd.cursor_opacity, bg_alpha);
// is_special_cell is either 0 or 1
float is_special_cell = cell_data.has_block_cursor + float(vi.is_selected & BIT_MASK);
is_special_cell += float(is_reversed); // reverse video cells should be opaque as well
is_special_cell = zero_or_one(is_special_cell);
cell_has_default_bg = if_one_then(is_special_cell, 0., cell_has_default_bg);
// special cells must always be fully opaque, otherwise leave bg_alpha untouched
bg_alpha = if_one_then(is_special_cell, 1.f, bg_alpha);
// Selection and cursor
bg_alpha = if_one_then(cell_data.has_block_cursor, effective_cursor_opacity, bg_alpha);
bg = if_one_then(float(vi.is_selected & BIT_MASK), if_one_then(crd.use_cell_for_selection_bg, color_to_vec(fg_as_uint), color_to_vec(crd.highlight_bg)), bg);
float3 background_rgb = if_one_then(cell_data.has_block_cursor, lerp(bg, cell_data.cursor.bg, crd.cursor_opacity), bg);
vo.background = background_rgb;
// }}}
if (!ONLY_BACKGROUND && OVERRIDE_FG_COLORS) {
vo.decoration_fg = override_foreground_color(vo.decoration_fg, background_rgb, vo.colored_sprite);
foreground = override_foreground_color(foreground, background_rgb, vo.colored_sprite);
}
if (!ONLY_FOREGROUND) {
float4 bgpremul = vec4_premul(background_rgb, bg_alpha);
// draw_bg_bitfield has bit 0 set to draw default bg cells and bit 1 set to draw non-default bg cells
float cell_has_non_default_bg = 1.f - cell_has_default_bg;
uint draw_bg_mask = uint(2.f * cell_has_non_default_bg + cell_has_default_bg); // 1 if has default bg else 2
float draw_bg = step(0.5, float(draw_bg_bitfield & draw_bg_mask));
bgpremul *= draw_bg;
vo.effective_background_premul = bgpremul;
}
if (!ONLY_BACKGROUND) vo.cell_foreground = foreground;
return vo;
}
uniform Sampler2DArray sprites;
// Scaling factor for the extra text-alpha adjustment for luminance-difference.
static const float text_gamma_scaling = 0.5;
float clamp_to_unit_float(float x) {
// Clamp value to suitable output range
return clamp(x, 0.0f, 1.0f);
}
float4 foreground_contrast_new(float4 over, float3 under, float text_contrast, float text_gamma_adjustment) {
float under_luminance = dot(under, Y);
float over_lumininace = dot(over.rgb, Y);
// Apply additional gamma-adjustment scaled by the luminance difference, the darker the foreground the more adjustment we apply.
// A multiplicative contrast is also available to increase saturation.
over.a = clamp_to_unit_float(lerp(over.a, pow(over.a, text_gamma_adjustment), (1 - over_lumininace + under_luminance) * text_gamma_scaling) * text_contrast);
return over;
}
float4 foreground_contrast_old(float4 over, float3 under) {
// Simulation of gamma-incorrect blending
float under_luminance = dot(under, Y);
float over_lumininace = dot(over.rgb, Y);
// This is the original gamma-incorrect rendering, it is the solution of the following equation:
//
// linear2srgb(over * overA2 + under * (1 - overA2)) = linear2srgb(over) * over.a + linear2srgb(under) * (1 - over.a)
// ^ gamma correct blending with new alpha ^ gamma incorrect blending with old alpha
over.a = clamp_to_unit_float((srgb2linear(linear2srgb(over_lumininace) * over.a + linear2srgb(under_luminance) * (1.0f - over.a)) - under_luminance) / (over_lumininace - under_luminance));
return over;
}
float4 foreground_contrast(float4 over, float3 under, float text_contrast, float text_gamma_adjustment) {
if (TEXT_NEW_GAMMA) return foreground_contrast_new(over, under, text_contrast, text_gamma_adjustment);
return foreground_contrast_old(over, under);
}
float4 load_text_foreground_color(float3 sprite_pos, float colored_sprite, float3 cell_foreground) {
// For colored sprites use the color from the sprite rather than the text foreground
// Return non-premultiplied foreground color
float4 text_fg = sprites.Sample(sprite_pos);
return float4(lerp(cell_foreground, text_fg.xyz, colored_sprite), text_fg.w);
}
float4 calculate_premul_foreground_from_sprites(
float3 sprite_pos, float3 underline_pos, float3 cursor_pos, float3 strike_pos, uint underline_exclusion_pos,
float4 text_fg, float3 decoration_fg, float4 cursor_color_premult,
float effective_text_alpha,
) {
// Return premul foreground color from decorations (cursor, underline, strikethrough)
int width, height, layer;
sprites.GetDimensions(width, height, layer);
float3 sz = float3(float(width), float(height), float(layer));
float underline_alpha = sprites.Sample(underline_pos).w;
int3 fetch_coord = int3(int(sprite_pos.x * sz.x), int(underline_exclusion_pos), int(sprite_pos.z));
float underline_exclusion = sprites[fetch_coord].w;
underline_alpha *= 1.0 - underline_exclusion;
float strike_alpha = sprites.Sample(strike_pos).w;
float cursor_alpha = sprites.Sample(cursor_pos).w;
float combined_alpha = min(text_fg.w + strike_alpha, 1.0);
float4 ans = alpha_blend(
float4(text_fg.rgb, combined_alpha * effective_text_alpha),
float4(decoration_fg, underline_alpha * effective_text_alpha)
);
return lerp(ans, cursor_color_premult, cursor_alpha * cursor_color_premult.w);
}
float4 adjust_foreground_contrast_with_background(float4 text_fg, float3 bg, float text_contrast, float text_gamma_adjustment) {
return foreground_contrast(text_fg, bg, text_contrast, text_gamma_adjustment);
}
[shader("fragment")]
float4 fragment_main(
VertexOutput vo,
uniform float text_contrast,
uniform float text_gamma_adjustment,
) : SV_Target {
float4 ans_premul = 0;
if (!ONLY_FOREGROUND) ans_premul = vo.effective_background_premul;
if (!ONLY_BACKGROUND) {
// blend in the foreground color
float4 text_fg = load_text_foreground_color(vo.sprite_pos, vo.colored_sprite, vo.cell_foreground);
text_fg = adjust_foreground_contrast_with_background(text_fg, vo.background, text_contrast, text_gamma_adjustment);
float4 text_fg_premul = calculate_premul_foreground_from_sprites(
vo.sprite_pos, vo.underline_pos, vo.cursor_pos, vo.strike_pos, vo.underline_exclusion_pos,
text_fg, vo.decoration_fg, vo.cursor_color_premult, vo.effective_text_alpha);
if (ONLY_FOREGROUND) ans_premul = text_fg_premul;
else ans_premul = alpha_blend_premul(text_fg_premul, ans_premul);
}
return ans_premul;
}

View File

@@ -1,49 +0,0 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
import blit_common;
import alpha_blend;
import utils;
extern static const bool is_alpha_mask = false;
extern static const bool texture_is_not_premultiplied = false;
struct VSOutput
{
float2 texcoord : TEXCOORD;
float4 position : SV_Position;
};
[shader("vertex")]
VSOutput vertex_main(
uint vertex_id : SV_VertexID,
uniform float4 src_rect,
uniform float4 dest_rect,
) {
BlitOutput ans = get_coords_for_blit(vertex_id, src_rect, dest_rect);
return {ans.texcoord, float4(ans.position[0], ans.position[1], 0.0, 1.0)};
}
uniform Sampler2D image;
[shader("fragment")]
float4 fragment_main(
float2 texcoord : TEXCOORD,
uniform float3 amask_fg,
uniform float4 amask_bg_premult,
uniform float extra_alpha
) : SV_Target {
float4 color = image.Sample(texcoord);
if (is_alpha_mask) {
color = float4(amask_fg, color.r);
color = vec4_premul(color);
color = alpha_blend_premul(color, amask_bg_premult);
} else color.a *= extra_alpha;
if (texture_is_not_premultiplied) color = vec4_premul(color);
return color;
}

View File

@@ -1,216 +0,0 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
module hsluv;
// Helper Functions
float divide(float num, float denom) {
return num / (abs(denom) + 1e-15) * sign(denom);
}
float3 divide(float3 num, float3 denom) {
return num / (abs(denom) + 1e-15) * sign(denom);
}
float3 hsluv_intersectLineLine(float3 line1x, float3 line1y, float3 line2x, float3 line2y) {
return (line1y - line2y) / (line2x - line1x);
}
float3 hsluv_distanceFromPole(float3 pointx, float3 pointy) {
return sqrt(pointx * pointx + pointy * pointy);
}
float3 hsluv_lengthOfRayUntilIntersect(float theta, float3 x, float3 y) {
float3 len = divide(y, sin(theta) - x * cos(theta));
len = lerp(len, (float3)1000.0, step(len, (float3)0.0));
return len;
}
float hsluv_maxSafeChromaForL(float L) {
// Transposed from GLSL column-major constructor to Slang row-major layout
float3x3 m2 = float3x3(
3.2409699419045214, -1.5373831775700935, -0.49861076029300328,
-0.96924363628087983, 1.8759675015077207, 0.041555057407175613,
0.055630079696993609,-0.20397695888897657, 1.0569715142428786
);
float sub0 = L + 16.0;
float sub1 = sub0 * sub0 * sub0 * 0.000000641;
float sub2 = lerp(L / 903.2962962962963, sub1, step(0.0088564516790356308, sub1));
float3 top1 = (284517.0 * m2[0] - 94839.0 * m2[2]) * sub2;
float3 bottom = (632260.0 * m2[2] - 126452.0 * m2[1]) * sub2;
float3 top2 = (838422.0 * m2[2] + 769860.0 * m2[1] + 731718.0 * m2[0]) * L * sub2;
float3 bounds0x = top1 / bottom;
float3 bounds0y = top2 / bottom;
float3 bounds1x = top1 / (bottom + 126452.0);
float3 bounds1y = (top2 - 769860.0 * L) / (bottom + 126452.0);
float3 xs0 = hsluv_intersectLineLine(bounds0x, bounds0y, -1.0 / bounds0x, (float3)0.0);
float3 xs1 = hsluv_intersectLineLine(bounds1x, bounds1y, -1.0 / bounds1x, (float3)0.0);
float3 lengths0 = hsluv_distanceFromPole(xs0, bounds0y + xs0 * bounds0x);
float3 lengths1 = hsluv_distanceFromPole(xs1, bounds1y + xs1 * bounds1x);
return min(lengths0.x,
min(lengths1.x,
min(lengths0.y,
min(lengths1.y,
min(lengths0.z,
lengths1.z)))));
}
float hsluv_maxChromaForLH(float L, float H) {
float hrad = radians(H);
// Transposed from GLSL column-major constructor to Slang row-major layout
float3x3 m2 = float3x3(
3.2409699419045214, -1.5373831775700935, -0.49861076029300328,
-0.96924363628087983, 1.8759675015077207, 0.041555057407175613,
0.055630079696993609,-0.20397695888897657, 1.0569715142428786
);
float sub1 = pow(L + 16.0, 3.0) / 1560896.0;
float sub2 = lerp(L / 903.2962962962963, sub1, step(0.0088564516790356308, sub1));
float3 top1 = (284517.0 * m2[0] - 94839.0 * m2[2]) * sub2;
float3 bottom = (632260.0 * m2[2] - 126452.0 * m2[1]) * sub2;
float3 top2 = (838422.0 * m2[2] + 769860.0 * m2[1] + 731718.0 * m2[0]) * L * sub2;
float3 bound0x = top1 / bottom;
float3 bound0y = top2 / bottom;
float3 bound1x = top1 / (bottom + 126452.0);
float3 bound1y = (top2 - 769860.0 * L) / (bottom + 126452.0);
float3 lengths0 = hsluv_lengthOfRayUntilIntersect(hrad, bound0x, bound0y);
float3 lengths1 = hsluv_lengthOfRayUntilIntersect(hrad, bound1x, bound1y);
return min(lengths0.x,
min(lengths1.x,
min(lengths0.y,
min(lengths1.y,
min(lengths0.z,
lengths1.z)))));
}
float3 hsluv_fromLinear(float3 c) {
return lerp(c * 12.92, 1.055 * pow(max(c, (float3)0), (float3)(1.0 / 2.4)) - 0.055, step(0.0031308, c));
}
float3 hsluv_toLinear(float3 c) {
return lerp(c / 12.92, pow(max((c + 0.055) / (1.0 + 0.055), (float3)0), (float3)2.4), step(0.04045, c));
}
float hsluv_yToL(float Y) {
return lerp(Y * 903.2962962962963, 116.0 * pow(max(Y, 0), 1.0 / 3.0) - 16.0, step(0.0088564516790356308, Y));
}
float hsluv_lToY(float L) {
return lerp(L / 903.2962962962963, pow((max(L, 0) + 16.0) / 116.0, 3.0), step(8.0, L));
}
float3 xyzToRgb(float3 tuple) {
// Transposed layout from GLSL column-major matrix construction
const float3x3 m = float3x3(
3.2409699419045214, -0.96924363628087983, 0.055630079696993609,
-1.5373831775700935, 1.8759675015077207, -0.20397695888897657,
-0.49861076029300328, 0.041555057407175613, 1.0569715142428786
);
// GLSL `tuple * m` is transformed to Slang/HLSL `mul(m, tuple)`
return hsluv_fromLinear(mul(m, tuple));
}
float3 rgbToXyz(float3 tuple) {
// Transposed layout from GLSL column-major matrix construction
const float3x3 m = float3x3(
0.41239079926595948, 0.21263900587151036, 0.019330818715591851,
0.35758433938387796, 0.71516867876775593, 0.11919477979462599,
0.18048078840183429, 0.072192315360733715, 0.95053215224966058
);
// GLSL `tuple * m` is transformed to Slang/HLSL `mul(m, tuple)`
return mul(m, hsluv_toLinear(tuple));
}
float3 xyzToLuv(float3 tuple) {
float X = tuple.x;
float Y = tuple.y;
float Z = tuple.z;
float L = hsluv_yToL(Y);
float div = 1.0 / max(dot(tuple, float3(1, 15, 3)), 1e-15);
return float3(
1.0,
(52.0 * (X * div) - 2.57179),
(117.0 * (Y * div) - 6.08816)
) * L;
}
float3 luvToXyz(float3 tuple) {
float L = tuple.x;
float U = divide(tuple.y, 13.0 * L) + 0.19783000664283681;
float V = divide(tuple.z, 13.0 * L) + 0.468319994938791;
float Y = hsluv_lToY(L);
float X = 2.25 * U * Y / V;
float Z = (3.0 / V - 5.0) * Y - (X / 3.0);
return float3(X, Y, Z);
}
float3 luvToLch(float3 tuple) {
float L = tuple.x;
float U = tuple.y;
float V = tuple.z;
float C = length(tuple.yz);
float H = degrees(atan2(V, U)); // Slang standard library uses atan2(y, x)
H += 360.0 * step(H, 0.0);
return float3(L, C, H);
}
float3 lchToLuv(float3 tuple) {
float hrad = radians(tuple.z);
return float3(
tuple.x,
cos(hrad) * tuple.y,
sin(hrad) * tuple.y
);
}
float3 hsluvToLch(float3 tuple) {
tuple.y *= hsluv_maxChromaForLH(tuple.z, tuple.x) * 0.01;
return tuple.zyx;
}
float3 lchToHsluv(float3 tuple) {
tuple.y = divide(tuple.y, hsluv_maxChromaForLH(tuple.x, tuple.z) * 0.01);
return tuple.zyx;
}
float3 lchToRgb(float3 tuple) {
return xyzToRgb(luvToXyz(lchToLuv(tuple)));
}
float3 rgbToLch(float3 tuple) {
return luvToLch(xyzToLuv(rgbToXyz(tuple)));
}
public float3 hsluvToRgb(float3 tuple) {
return lchToRgb(hsluvToLch(tuple));
}
public float3 rgbToHsluv(float3 tuple) {
return lchToHsluv(rgbToLch(tuple));
}
float3 luvToRgb(float3 tuple) {
return xyzToRgb(luvToXyz(tuple));
}

View File

@@ -1,38 +0,0 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
module linear2srgb;
// Scalar sRGB to Linear conversion
public float srgb2linear(float x) {
float lower = x / 12.92;
float upper = pow((x + 0.055f) / 1.055f, 2.4f);
// GLSL mix() is replaced by Slang's lerp()
return lerp(lower, upper, step(0.04045f, x));
}
// Scalar Linear to sRGB conversion
public float linear2srgb(float x) {
float lower = 12.92 * x;
float upper = 1.055 * pow(x, 1.0f / 2.4f) - 0.055f;
return lerp(lower, upper, step(0.0031308f, x));
}
// Vector Linear to sRGB conversion
public float3 linear2srgb(float3 x) {
float3 lower = 12.92 * x;
float3 upper = 1.055 * pow(x, float3(1.0f / 2.4f)) - 0.055f;
return lerp(lower, upper, step(float3(0.0031308f), x));
}
// Vector sRGB to Linear conversion
public float3 srgb2linear(float3 c) {
// You can call the scalar version per-component,
// or pass the whole vector if you overload it for float3.
return float3(srgb2linear(c.r), srgb2linear(c.g), srgb2linear(c.b));
}

View File

@@ -1,68 +0,0 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
import alpha_blend;
#define left 0
#define top 1
#define right 2
#define bottom 3
static const int2 vertex_pos_map[4] = {
int2(right, top),
int2(right, bottom),
int2(left, bottom),
int2(left, top)
};
static const float4 dest_rect = float4(-1, 1, 1, -1);
[shader("vertex")]
float4 vertex_main(uint vertex_id : SV_VertexID) : SV_Position {
int2 pos = vertex_pos_map[vertex_id];
return float4(dest_rect[pos.x], dest_rect[pos.y], 0, 1);
}
// Signed distance function for a rounded rectangle
float rounded_rectangle_sdf(float2 p, float2 b, float r) {
// signed distance field
// first term is used for points outside the rectangle
float2 q = abs(p) - b;
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
}
[shader("fragment")]
float4 fragment_main(
uniform float4 rect,
uniform float2 params,
uniform float4 color,
uniform float4 background_color,
float4 frag_coord : SV_Position // SV_Position provides gl_FragCoord equivalent in Slang
) : SV_Target {
float2 size = rect.zw; // GLSL .ba maps to .zw or .ba in Slang (using .zw is typical)
float2 origin = rect.xy;
float thickness = params[0];
float corner_radius = params[1];
// Position must be relative to the center of the rectangle of (size) located at (origin)
// frag_coord.xy maps directly to gl_FragCoord.xy
float2 position = frag_coord.xy - size / 2.0 - origin;
// Calculate distance to rounded rectangle
float dist = rounded_rectangle_sdf(position, size * 0.5 - corner_radius, corner_radius);
// The border is outer - inner rects
float outer_edge = -dist;
float inner_edge = outer_edge - thickness;
// Smooth borders (anti-alias)
static const float step_size = 1.0;
float alpha = smoothstep(-step_size, step_size, outer_edge) - smoothstep(-step_size, step_size, inner_edge);
float4 ans = color;
ans.a *= alpha;
// pre-multiplied output
return alpha_blend(ans, background_color);
}

View File

@@ -1,88 +0,0 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
import blit_common;
import linear2srgb;
struct VSOutput {
float2 texcoord : TEXCOORD;
float4 position : SV_Position;
};
[shader("vertex")]
VSOutput vertex_main(
uint vertex_id : SV_VertexID,
uniform float4 src_rect,
uniform float4 dest_rect,
) {
BlitOutput ans = get_coords_for_blit(vertex_id, src_rect, dest_rect);
return {ans.texcoord, float4(ans.position[0], ans.position[1], 0.0, 1.0)};
}
uniform Sampler2D image;
float3 safe_unpremult_to_linear(float4 s) {
// Avoid division by zero by replacing 0.0 alpha with 1.0.
// If alpha is 0.0, the division is safe, and lerp masks the result to 0.0 anyway.
float safe_alpha = lerp(1.0f, s.a, step(0.00001f, s.a));
float3 unpremult = s.rgb / safe_alpha;
// Select between the computed linear color and float3(0.0) based on alpha presence
return lerp(float3(0.0f), srgb2linear(unpremult), step(0.00001f, s.a));
}
[shader("fragment")]
float4 fragment_main(float2 texcoord : TEXCOORD, uniform float2 src_size) : SV_Target {
float4 s00, s10, s01, s11;
float2 texel_size = 1.0 / src_size;
// Use Slang target switches to dynamically compile for specific backend features
__target_switch
{
// Modern backends with full texture gathering capabilities
case spirv: case hlsl: case metal: {
float2 gather_coord = texcoord - (0.5 * texel_size);
float4 r_gather = image.GatherRed(gather_coord);
float4 g_gather = image.GatherGreen(gather_coord);
float4 b_gather = image.GatherBlue(gather_coord);
float4 a_gather = image.GatherAlpha(gather_coord);
s00 = float4(r_gather.w, g_gather.w, b_gather.w, a_gather.w); // Bottom-Left
s10 = float4(r_gather.z, g_gather.z, b_gather.z, a_gather.z); // Bottom-Right
s01 = float4(r_gather.x, g_gather.x, b_gather.x, a_gather.x); // Top-Left
s11 = float4(r_gather.y, g_gather.y, b_gather.y, a_gather.y); // Top-Right
}
// Fallback for older targets or legacy GLSL versions
default: {
s00 = image.Sample(texcoord + float2(-0.25, -0.25) * texel_size);
s10 = image.Sample(texcoord + float2( 0.25, -0.25) * texel_size);
s01 = image.Sample(texcoord + float2(-0.25, 0.25) * texel_size);
s11 = image.Sample(texcoord + float2( 0.25, 0.25) * texel_size);
}
}
// Unpremultiply and convert to linear for each sample
float3 linear00 = safe_unpremult_to_linear(s00);
float3 linear10 = safe_unpremult_to_linear(s10);
float3 linear01 = safe_unpremult_to_linear(s01);
float3 linear11 = safe_unpremult_to_linear(s11);
// Average the alpha values
float avg_alpha = (s00.a + s10.a + s01.a + s11.a) * 0.25;
// For proper downsampling with transparency, weight colors by their alpha
float3 weighted_sum = linear00 * s00.a + linear10 * s10.a + linear01 * s01.a + linear11 * s11.a;
float total_weight = s00.a + s10.a + s01.a + s11.a;
// Calculate the weighted average color in linear space
float safe_total_weight = lerp(1.0f, total_weight, step(0.00001f, total_weight));
float3 avg_linear = lerp(float3(0.0f), weighted_sum / safe_total_weight, step(0.00001f, total_weight));
// Convert back to sRGB
float3 srgb_color = linear2srgb(avg_linear);
// Output unpremultiplied sRGB color
return float4(srgb_color, avg_alpha);
}

View File

@@ -1,716 +0,0 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2026, Kovid Goyal <kovid at kovidgoyal.net>
import fcntl
import json
import os
import re
import runpy
import shutil
import sys
import time
from collections import OrderedDict
from contextlib import contextmanager, suppress
from enum import StrEnum
from functools import lru_cache
from itertools import chain
from pathlib import Path
from types import MappingProxyType
from typing import Any, Callable, Iterable, Iterator, NamedTuple
from kitty.constants import read_kitty_resource, shaders_dir, slangc
from kitty.fast_data_types import (
BLINK,
COLOR_IS_INDEX,
COLOR_IS_RGB,
COLOR_IS_SPECIAL,
COLOR_NOT_SET,
DECORATION,
DECORATION_MASK,
DIM,
GLSL_VERSION,
MARK,
MARK_MASK,
REVERSE,
STRIKETHROUGH,
get_boss,
get_options,
)
from kitty.options.types import Options, defaults
@lru_cache(maxsize=64)
def get_shader_src(name: str) -> str:
return read_kitty_resource(f'{name}.slang', 'kitty.shaders').decode()
@lru_cache(maxsize=2)
def self_mtime() -> float:
with suppress(Exception):
return os.path.getmtime(__file__)
return 0
@lru_cache(maxsize=2)
def slangc_version() -> str:
import subprocess
return subprocess.check_output(slangc + ['-version'], stderr=subprocess.STDOUT).decode().strip()
def is_dir_slangc_version_ok(path: str) -> bool:
with suppress(OSError), open(os.path.join(path, 'slangc.version')) as f:
return f.read().strip() == slangc_version()
return False
def ensure_cache_dir(path: str) -> None:
os.makedirs(path, exist_ok=True)
# slang IR is version dependent and the compiler often crashes when loading .slang-module from another version
if not is_dir_slangc_version_ok(path):
shutil.rmtree(path)
os.makedirs(path)
with open(os.path.join(path, 'slangc.version'), 'w') as f:
f.write(slangc_version())
class Stage(StrEnum):
vertex = 'vertex'
fragment = 'fragment'
class EntryPoint(NamedTuple):
stage: Stage
name: str
def asdict(self) -> dict[str, str]:
return {'stage': str(self.stage), 'name': self.name}
@classmethod
def fromdict(self, s: dict[str, str]) -> 'EntryPoint':
return EntryPoint(Stage(s['stage']), s['name'])
class Specialization(NamedTuple):
name: str
variables: MappingProxyType[str, str]
@property
def filename_insert(self) -> str:
return f'.{self.name}' if self.name else '.default-specialization'
class SlangFile(NamedTuple):
path: str = ''
text: str = ''
imports: frozenset[str] = frozenset()
entry_points: frozenset[EntryPoint] = frozenset()
module: str = ''
specializable_variables: MappingProxyType[str, str] = MappingProxyType({})
disable_warnings: frozenset[str] = frozenset()
opts: Options | None = None
def asdict(self, skip_source: bool = False) -> dict[str, Any]:
' Return a dict useable for serialization to JSON '
ans = self._asdict()
ans['imports'] = tuple(ans['imports'])
ans['entry_points'] = tuple(ep.asdict() for ep in ans['entry_points'])
ans['specializable_variables'] = dict(ans['specializable_variables'])
ans['disable_warnings'] = tuple(ans['disable_warnings'])
if skip_source:
ans['text'] = ''
ans['path'] = os.path.basename(ans['path'])
del ans['opts']
return ans
@classmethod
def fromdict(cls, s: dict[str, Any]) -> 'SlangFile':
return SlangFile(
s['path'], s['text'], frozenset(s['imports']),
frozenset(EntryPoint.fromdict(x) for x in s['entry_points']),
s['module'], MappingProxyType(s['specializable_variables']), frozenset(s['disable_warnings']))
@property
def should_compile_to_ir(self) -> bool:
return bool(self.module or self.entry_points)
@property
def defines(self) -> MappingProxyType[str, str]:
ans = {}
match os.path.basename(self.path):
case 'cell.slang':
ans['MARK_MASK'] = str(MARK_MASK)
ans['REVERSE_SHIFT'] = str(REVERSE)
ans['STRIKE_SHIFT'] = str(STRIKETHROUGH)
ans['DIM_SHIFT'] = str(DIM)
ans['BLINK_SHIFT'] = str(BLINK)
ans['DECORATION_SHIFT'] = str(DECORATION)
ans['MARK_SHIFT'] = str(MARK)
ans['DECORATION_MASK'] = str(DECORATION_MASK)
ans['COLOR_NOT_SET'] = str(COLOR_NOT_SET)
ans['COLOR_IS_SPECIAL'] = str(COLOR_IS_SPECIAL)
ans['COLOR_IS_INDEX'] = str(COLOR_IS_INDEX)
ans['COLOR_IS_RGB'] = str(COLOR_IS_RGB)
return MappingProxyType(ans)
def get_options(self) -> Options:
return self.opts or defaults
@property
def specializations(self) -> Iterator[Specialization]:
def s(name: str = '', **kwargs: str) -> Specialization:
return Specialization(name, MappingProxyType(kwargs))
match os.path.basename(self.path):
case 'graphics.slang':
yield s()
yield s('alpha_mask', is_alpha_mask='true')
yield s('premult', texture_is_not_premultiplied='true')
case 'cell.slang':
opts = self.get_options()
text_fg_override_threshold: float = opts.text_fg_override_threshold[0]
match opts.text_fg_override_threshold[1]:
case '%':
text_fg_override_threshold = max(0, min(text_fg_override_threshold, 100.0)) * 0.01
algo = '1'
case 'ratio':
text_fg_override_threshold = max(0, min(text_fg_override_threshold, 21.0))
algo = '2'
base = {k:str(v) for k, v in dict(
DO_FG_OVERRIDE='true' if text_fg_override_threshold else 'false',
FG_OVERRIDE_ALGO=algo,
FG_OVERRIDE_THRESHOLD=text_fg_override_threshold,
TEXT_NEW_GAMMA='false' if opts.text_composition_strategy == 'legacy' else 'true',
ONLY_FOREGROUND='false',
ONLY_BACKGROUND='false',
).items()}
yield s('', **base)
base['ONLY_FOREGROUND'] = 'true'
yield s('fg', **base)
base['ONLY_FOREGROUND'] = 'false'
base['ONLY_BACKGROUND'] = 'true'
yield s('bg', **base)
case _:
yield s()
def parse_slang_text(src_code: str, path: str = '') -> SlangFile:
text = re.sub(r'/\*[\s\S]*?\*/', '', src_code)
entry_points, imports = [], set()
module = ''
found_entry_point = ''
specializable_variables = {}
disable_warnings = []
for line in text.splitlines():
line = line.strip()
if not line:
continue
if line.startswith('//'):
if line.startswith('// warnings-disable: '):
words = line.split()
for word in words[2:]:
for w in word.split(','):
disable_warnings.append(w)
continue
words = line.split()
if found_entry_point:
if words[0].startswith('['): # ]
continue
for q in words:
if '(' in q:
name = q.partition('(')[0] # ))
match found_entry_point:
case 'vertex':
entry_points.append(EntryPoint(Stage.vertex, name))
case 'fragment' | 'pixel':
entry_points.append(EntryPoint(Stage.fragment, name))
break
found_entry_point = ''
else:
match words[0]:
case 'module':
module = words[1].removesuffix(';')
case 'import':
imports.add(words[1].removesuffix(';'))
case 'extern':
if len(words) > 3 and words[1:3] == ['static', 'const']:
specializable_variables[line.partition('=')[0].split()[-1].rstrip(';')] = line
case _:
if words[0].startswith('[shader('): # ])
text = words[0].partition('(')[2].partition(')')[0].strip()
found_entry_point = text[1:-1]
return SlangFile(
path, src_code, frozenset(imports), frozenset(entry_points), module,
MappingProxyType(specializable_variables), frozenset(disable_warnings))
@lru_cache(4096)
def parse_slang_file(path: str) -> SlangFile:
with open(path) as f:
text = f.read()
return parse_slang_text(text, path)
def build_import_graph(dirpath: str) -> dict[str, SlangFile]:
graph: dict[str, SlangFile] = {}
for root, _, files in os.walk(os.path.abspath(dirpath)):
for file in files:
if file.endswith('.slang'):
full_path = os.path.abspath(os.path.join(root, file))
relpath = os.path.relpath(full_path, root)
modname = os.path.splitext(relpath.replace(os.sep, '.'))[0]
graph[modname] = parse_slang_file(full_path)
return graph
def topological_sort(graph: dict[str, SlangFile]) -> list[str]:
visited = set()
order = []
def visit(node: str) -> None:
if node in visited or node not in graph:
return
for dep in graph[node].imports:
visit(dep)
visited.add(node)
order.append(node)
for node in graph:
visit(node)
return order
def get_ordered_sources_in_tree(dirpath: str) -> OrderedDict[str, SlangFile]:
g = build_import_graph(dirpath)
return OrderedDict({k: g[k] for k in topological_sort(g)})
def future() -> float:
return time.time() + 1000000
def safe_mtime(path: str, defval: float = 0) -> float:
with suppress(OSError):
return os.path.getmtime(path)
return defval if defval >= 0 else future()
def read_deps_file(path: str) -> Iterator[str]:
with open(path) as f:
for line in f:
line = line.partition(':')[2].strip()
yield from line.split()
def get_newest_dep_time(path: str) -> float:
with suppress(OSError):
ans = 0.
for deppath in read_deps_file(path):
mtime = os.path.getmtime(deppath)
ans = max(mtime, ans)
return max(ans, self_mtime())
return future()
class Command(NamedTuple):
needs_build: bool
description: str
cmd: list[str]
def commands_to_compile_dir_to_ir(sources: dict[str, SlangFile], src_dir: str, output_dirpath: str) -> Iterator[Command]:
cmdbase = list(slangc) + ['-warnings-as-errors', 'all']
for name, sfile in sources.items():
if sfile.should_compile_to_ir:
parts = name.split('.')
base_dest = os.path.join(output_dirpath, *parts)
slang_module = f'{base_dest}.slang-module'
deps_file = f'{base_dest}.deps'
module_mtime = safe_mtime(slang_module)
needs_build = module_mtime < get_newest_dep_time(deps_file)
defines = [f'-D{k}={v}' for k, v in sfile.defines.items()]
yield Command(needs_build, f'Compiling |{name}.slang| ...', cmdbase + defines + [
'-I', output_dirpath, '-I', src_dir, '-depfile', deps_file,
'-target', 'none', '-o', slang_module, '--', sfile.path,
])
def iter_entry_point_shaders(sources: dict[str, SlangFile], dest_dir: str) -> Iterator[tuple[str, str, list[str], SlangFile]]:
cmdbase = list(slangc) + ['-warnings-as-errors', 'all']
for name, sfile in sources.items():
if not sfile.entry_points:
continue
parts = name.split('.')
base_dest = os.path.join(dest_dir, *parts)
slang_module = f'{base_dest}.slang-module'
cmd = list(cmdbase)
if sfile.disable_warnings:
cmd += ['-warnings-disable', ','.join(sfile.disable_warnings)]
cmd += ['-I', dest_dir, slang_module]
yield base_dest, slang_module, cmd, sfile
def serialize_source_metadata(sources: dict[str, SlangFile], dest_dir: str) -> None:
for base_dest, slang_module, scmd, sfile in iter_entry_point_shaders(sources, dest_dir):
dest = f'{base_dest}.json'
with open(dest, 'w') as f:
f.write(json.dumps(sfile.asdict(skip_source=True), indent=2, sort_keys=True))
def commands_to_compile_to_spirv(sources: dict[str, SlangFile], dest_dir: str, built_files: list[str]) -> Iterator[Command]:
# glsl 450 is vulkan 1.1 and spirv 1.3 released 2008
base_cmd = ['-target', 'spirv', '-profile', 'glsl_450', '-capability', 'vk_mem_model', '-fvk-use-entrypoint-name']
for base_dest, slang_module, scmd, sfile in iter_entry_point_shaders(sources, dest_dir):
for x in sfile.specializations:
cmd = list(scmd)
dest = f'{base_dest}.{x.name}.spv' if x.name else f'{base_dest}.spv'
if x.variables:
cmd.insert(-1, f'{base_dest}{x.filename_insert}.slang-module')
cmd += base_cmd + ['-o', dest, '-reflection-json', dest + '.json']
output_mtime = safe_mtime(dest)
module_mtime = os.path.getmtime(slang_module)
needs_build = output_mtime < module_mtime
if needs_build:
built_files.append(dest)
yield Command(needs_build, f'Linking |{os.path.basename(dest)}| ...', cmd)
# GLSL {{{
def commands_to_compile_to_glsl(sources: dict[str, SlangFile], dest_dir: str, built_glsl_files: list[str]) -> Iterator[Command]:
glsl_version = max(150, GLSL_VERSION) # slangc fails with glsl_140 https://github.com/shader-slang/slang/issues/11898
for base_dest, slang_module, cmd, sfile in iter_entry_point_shaders(sources, dest_dir):
module_mtime = os.path.getmtime(slang_module)
extra_cmd = ['-line-directive-mode', 'none', '-target', 'glsl', '-profile', f'glsl_{glsl_version}']
for ep in sfile.entry_points:
for sp in sfile.specializations:
v = {Stage.vertex: 'vert', Stage.fragment: 'frag'}[ep.stage]
c = list(cmd)
dest = f'{base_dest}{sp.filename_insert}.{v}.glsl' if sp.name else f'{base_dest}.{v}.glsl'
if sp.variables:
c.insert(-1, f'{base_dest}{sp.filename_insert}.slang-module')
c += extra_cmd + ['-entry', ep.name, '-stage', ep.stage.name, '-o', dest]
output_mtime = safe_mtime(dest)
needs_build = output_mtime < module_mtime
if needs_build:
built_glsl_files.append(dest)
yield Command(needs_build, f'Linking |{os.path.basename(slang_module)}| to GLSL {ep.stage.value} shader ...', c)
def fixup_opengl_code(glsl_code: str, path: str) -> tuple[str, dict[str, Any]]:
is_fragment_shader = 'frag' in os.path.basename(path).split()
lines = []
in_uniform_block = False
in_uniform_block_contents = False
uniform_block_is_struct = False
current_uniform_struct_members: dict[str, str] = {}
uniform_blocks = {}
current_uniform_names: list[str] = []
uniform_names: dict[str, str] = {}
uniform_structs = {}
def add_uniform_name(name: str, uniform_names: dict[str, str] = uniform_names) -> str:
name = name.rstrip(';')
uniform_name = name.rpartition('_')[0]
if uniform_name in uniform_names:
raise KeyError(f'The uniform name {uniform_name} is used with multiple suffixes in {path}')
uniform_names[uniform_name] = name
return name
src_lines = glsl_code.splitlines()
for i, line in enumerate(src_lines):
if in_uniform_block:
if in_uniform_block_contents:
if line.startswith('}'):
in_uniform_block = in_uniform_block_contents = False
block_name = line.lstrip('}').rstrip(';').strip()
if uniform_block_is_struct:
uniform_structs[block_name.rpartition('_')[0]] = {
'name': block_name, 'members': current_uniform_struct_members}
else:
uniform_blocks[block_name] = current_uniform_names
line = '// ' + line
current_uniform_names = []
else:
if uniform_block_is_struct:
current_uniform_names.append(add_uniform_name(line.split()[-1], current_uniform_struct_members))
else:
line = line.strip()
current_uniform_names.append(add_uniform_name(line.split()[-1]))
line = 'uniform ' + line
elif line.startswith('{'): # }}
if not uniform_block_is_struct:
line = '// ' + line
in_uniform_block_contents = True
current_uniform_names = []
else:
if line.startswith('#version '):
line = f'#version {GLSL_VERSION}'
if not is_fragment_shader:
line += '\n#extension GL_ARB_explicit_attrib_location : require'
elif line.startswith('#extension ') or line in ('layout(row_major) buffer;', 'layout(push_constant)'):
line = '// ' + line
elif line.startswith('layout(binding ='):
line = '// ' + line
elif line.startswith('layout(location =') and not is_fragment_shader:
line = '// ' + line
elif line.startswith('flat layout(location ='):
line = 'flat'
elif line: # ))))
words = line.split()
if 'uniform' in words and line.startswith('layout('): # )
in_uniform_block = True
in_uniform_block_contents = False
uniform_block_is_struct = line.startswith('layout(std140') # )
if uniform_block_is_struct:
current_uniform_struct_members = {}
else:
line = '// ' + line
elif words[0] == 'uniform' and len(words) > 2 and words[1].startswith('sampler'):
add_uniform_name(words[2])
lines.append(line)
ans = '\n'.join(lines)
for block_name, names in uniform_blocks.items():
for u in names:
u = u.partition('[')[0]
ans = ans.replace(f'{block_name}.{u}', u)
ans = ans.replace('gl_VertexIndex', 'gl_VertexID')
ans = ans.replace('gl_BaseVertex', '0')
ans = ans.replace('gl_InstanceIndex', 'gl_InstanceID')
ans = ans.replace('gl_BaseInstance', '0')
return ans, {'loose_uniforms': uniform_names, 'uniform_structs': uniform_structs}
def fixup_opengl_files(*paths: str) -> None:
' Convert the GLSL output of slangc to something that will work with OpenGL 3.1 '
for path in paths:
with open(path, 'r+') as f:
glsl_code = f.read()
try:
fixed, metadata = fixup_opengl_code(glsl_code, path)
except Exception:
os.unlink(path)
raise
f.seek(0)
f.truncate()
f.write(fixed)
with open(path + '.json', 'w') as f:
f.write(json.dumps(metadata))
# }}}
ParallelRun = Callable[[Iterable[tuple[bool, str, list[str]]]], None]
def copy_files_preserving_structure(source_dir: str, dest_dir: str, extension: str) -> None:
'''
Copies all files with a specific extension from a source directory
to a destination directory while preserving the subdirectory structure.
'''
source = Path(source_dir)
destination = Path(dest_dir)
if not extension.startswith('.'):
extension = f".{extension}"
# Recursively find all matching files
for file_path in source.rglob(f"*{extension}"):
if file_path.is_file():
# Calculate relative path to maintain folder hierarchy
relative_path = file_path.relative_to(source)
target_path = destination / relative_path
target_path.parent.mkdir(parents=True, exist_ok=True)
# Copy file while preserving original metadata
shutil.copy2(file_path, target_path)
def create_specialisations(sources: dict[str, SlangFile], dest_dir: str) -> Iterator[Command]:
for base_dest, slang_module, cmd, sfile in iter_entry_point_shaders(sources, dest_dir):
if sfile.entry_points and sfile.specializations:
for sp in sfile.specializations:
dest = f'{base_dest}{sp.filename_insert}.slang'
payload = existing = ''
if sp.variables:
lines = []
for key, val in sp.variables.items():
declaration = sfile.specializable_variables[key].rpartition('=')[0]
if not declaration:
declaration = sfile.specializable_variables[key].rstrip(';')
declaration = declaration.replace('extern ', 'export ', 1)
lines.append(f'{declaration} = {val};')
payload = '\n'.join(lines)
with suppress(FileNotFoundError), open(dest) as f:
existing = f.read()
if needs_build := payload != existing:
if payload:
with open(dest, 'w') as fw:
fw.write(payload)
else:
os.remove(dest)
yield Command(needs_build, f'Compiling specialisation |{os.path.basename(dest)}| ...',
list(slangc) + [dest, '-o', dest + '-module'])
def compile_builtin_shaders(build_dir: str, dest_dir: str, parallel_run: ParallelRun) -> None:
ensure_cache_dir(build_dir)
ensure_cache_dir(dest_dir)
src_dir = os.path.abspath('kitty/shaders')
source_tree = get_ordered_sources_in_tree(src_dir)
serialize_source_metadata(source_tree, dest_dir)
# First ensure all IR is generated
parallel_run(commands_to_compile_dir_to_ir(source_tree, src_dir, build_dir))
# Copy IR to dest_dir
copy_files_preserving_structure(build_dir, dest_dir, '.slang-module')
# Create the specializations
parallel_run(create_specialisations(source_tree, dest_dir))
# Now Vulkan shaders
built_spirv_files: list[str] = []
spirv_commands = commands_to_compile_to_spirv(source_tree, dest_dir, built_spirv_files)
# Now glsl files
built_glsl_files: list[str] = []
glsl_commands = commands_to_compile_to_glsl(source_tree, dest_dir, built_glsl_files)
# Now run all commands
parallel_run(chain(spirv_commands, glsl_commands))
fixup_opengl_files(*built_glsl_files)
if shutil.which('glslangValidator'):
from kitty.shaders.validate_shaders import validation_command_for_file
parallel_run((True, f'Validating |{os.path.basename(x)}| ...', validation_command_for_file(x)) for x in built_glsl_files)
@contextmanager
def lock_directory(target_dir: str) -> Iterator[None]:
'''
Context manager to exclusively lock a directory using a hidden lock file.
Works across all Unix-like operating systems.
'''
os.makedirs(target_dir, exist_ok=True)
lock_file_path = os.path.join(target_dir, '.shaders.lock')
lock_fd = os.open(lock_file_path, os.O_CREAT | os.O_WRONLY)
try:
fcntl.flock(lock_fd, fcntl.LOCK_EX)
yield
finally:
fcntl.flock(lock_fd, fcntl.LOCK_UN)
os.close(lock_fd)
def run_commands(cmds: Iterable[Command], cwd: str | None = None) -> None:
import subprocess
workers = []
for c in cmds:
if c.needs_build:
try:
p = subprocess.Popen(c.cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, cwd=cwd)
except FileNotFoundError:
raise Exception(f'Could not find slangc compiler ({slangc}) in PATH: {os.environ.get("PATH")}')
workers.append((c, p))
errors = []
for (c, p) in workers:
if p.wait() != 0:
assert p.stderr is not None
stderr = p.stderr.read().decode('utf-8', 'replace')
errors.append((c, stderr))
if errors:
raise Exception(f'Compiling shader failed. Command that was run: {errors[0][0]}\n{errors[0][1]}')
def specialize_shaders_to(sources: dict[str, SlangFile], dest_dir: str) -> None:
for name in sources:
shutil.copy2(os.path.join(shaders_dir, f'{name}.slang-module'), dest_dir)
specialisation_cmds = create_specialisations(sources, dest_dir)
run_commands(specialisation_cmds, dest_dir)
_ = []
spirv = commands_to_compile_to_spirv(sources, dest_dir, _)
glsl_built_files: list[str] = []
glsl = commands_to_compile_to_glsl(sources, dest_dir, glsl_built_files)
run_commands(chain(spirv, glsl), dest_dir)
fixup_opengl_files(*glsl_built_files)
@lru_cache(maxsize=2)
def per_process_cache_dir() -> str:
' A dir that has the lifetime of this process '
import atexit
from tempfile import mkdtemp
ans = mkdtemp()
boss = get_boss()
try:
boss.atexit.rmtree(ans)
except Exception: # happens if no boss exists
atexit.register(shutil.rmtree, ans)
return ans
specialize_cache: dict[str, tuple[tuple[Specialization, ...], dict[str, bytes]]] = {}
def specialize_cell_shader(
create_cache_dir: Callable[[], str] = per_process_cache_dir,
opts: Options | None = None
) -> dict[str, bytes]:
' Specialize the cell shader based on the specified options '
with open(os.path.join(shaders_dir, 'cell.json')) as f:
builtin_sfile = SlangFile.fromdict(json.load(f))
d = builtin_sfile._asdict()
if opts is None:
with suppress(RuntimeError):
opts = get_options()
d['opts'] = opts
sfile = SlangFile(**d)
builtin, current = tuple(builtin_sfile.specializations), tuple(sfile.specializations)
if builtin == current: # options not changed from defaults
return {}
dest_dir = create_cache_dir()
cache_key = f'cell-{dest_dir}'
if (cx := specialize_cache.get(cache_key)) and cx[0] == current:
return cx[1]
with lock_directory(dest_dir):
ensure_cache_dir(dest_dir)
specialize_shaders_to({'cell': sfile}, dest_dir)
ans = {}
for x in os.listdir(dest_dir):
if x.rpartition('.')[2] in ('spv', 'glsl', 'msl'):
with open(os.path.join(dest_dir, x), 'rb') as fb:
ans[x] = fb.read()
specialize_cache[cache_key] = (current, ans)
return ans
def main() -> None:
if not shutil.which(slangc[0]):
raise SystemExit(f'The shader slang compiler ({slangc[0]}) not in PATH: {os.environ.get("PATH")}')
setup = runpy.run_path('setup.py')
Command = setup['Command']
parallel_run = setup['parallel_run']
emphasis = setup['emphasis']
def prun(cmds: Iterable[tuple[bool, str, list[str]]]) -> None:
needed = []
for (needs_build, desc, cmd) in cmds:
if needs_build:
desc = re.sub(r'\|(.+?)\|', lambda m: emphasis(m.group(1)), desc)
needed.append(Command(desc, cmd, lambda: True))
parallel_run(needed)
compile_builtin_shaders(sys.argv[-2], sys.argv[-1], prun)
def test_slang_build() -> None:
import subprocess
if shutil.which(slangc[0]) is None:
raise AssertionError(f'The shader slang compiler ({slangc[0]}) not in PATH: {os.environ.get("PATH")}')
q = os.path.join(shaders_dir, 'graphics.spv')
if not os.path.isfile(q):
raise AssertionError(f'The compiled graphics shader {q} does not exist')
if not get_shader_src('graphics'):
raise AssertionError('Could not load graphics.slang shader source')
src = b'''
#language slang 2026
[shader("vertex")]
float4 main(uint vertex_id : SV_VertexID) : SV_Position { return float4(vertex_id, 1, 0, 1); }
'''
cp = subprocess.run(slangc + '-lang slang -entry main -stage vertex -target glsl -o /dev/stdout -- -'.split(),
input=src, capture_output=True)
if cp.returncode != 0:
raise AssertionError(f'Test compile of shader to GLSL failed with returncode: {cp.returncode} and stderr: {cp.stderr.decode()}')
if __name__ == '__main__':
main()

View File

@@ -1,32 +0,0 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
// Main Vertex Shader Entry Point
[shader("vertex")]
float4 vertex_main(
uint vertex_id : SV_VertexID,
uniform float4 edges, // [ left, top, right, bottom ]
) : SV_Position {
// Extract boundaries from the edges vector
float left = edges[0];
float top = edges[1];
float right = edges[2];
float bottom = edges[3];
// Static mapping table for vertex positions
const float2 pos_map[4] = {
float2(left, top),
float2(left, bottom),
float2(right, bottom),
float2(right, top)
};
// Calculate final position
return float4(pos_map[vertex_id], 0.0, 1.0);
}
[shader("fragment")]
float4 fragment_main(uniform float4 tint_color) : SV_Target {
return tint_color;
}

View File

@@ -1,41 +0,0 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
struct VertexOutput
{
float2 frag_pos : TEXCOORD;
float4 position : SV_Position;
};
// Main Vertex Shader Entry Point
[shader("vertex")]
VertexOutput vertex_main(uint vertex_id : SV_VertexID, uniform float4 x_coords, uniform float4 y_coords) {
VertexOutput output;
float2 pos = float2(x_coords[vertex_id], y_coords[vertex_id]);
output.position = float4(pos, 1.0, 1.0);
output.frag_pos = pos;
return output;
}
// Main Fragment Shader Entry Point
[shader("fragment")]
float4 fragment_main(
float2 frag_pos : TEXCOORD,
uniform float2 cursor_edge_x,
uniform float2 cursor_edge_y,
uniform float3 trail_color,
uniform float trail_opacity
) : SV_Target {
float opacity = trail_opacity;
// Evaluate if the fragment falls inside the bounding box of the cursor
float in_x = step(cursor_edge_x[0], frag_pos.x) * step(frag_pos.x, cursor_edge_x[1]);
float in_y = step(cursor_edge_y[1], frag_pos.y) * step(frag_pos.y, cursor_edge_y[0]);
// Mask out opacity inside the active cursor area
opacity *= 1.0f - (in_x * in_y);
// Output color with premultiplied alpha formatting
return float4(trail_color * opacity, opacity);
}

View File

@@ -1,34 +0,0 @@
#language slang 2026
// Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
// Distributed under terms of the GPLv3 license.
module utils;
// Return 0 if x < 1 otherwise 1
public __generic<T : __BuiltinFloatingPointType, int N = 1>
vector<T, N> zero_or_one(vector<T, N> x) {
return step((vector<T, N>)1.0f, x);
}
// condition must be zero or one. When 1 thenval is returned otherwise elseval
public __generic<T : __BuiltinFloatingPointType, int N = 1>
vector<T, N> if_one_then(vector<T, N> condition, vector<T, N> thenval, vector<T, N> elseval) {
return lerp(elseval, thenval, condition);
}
// a < b ? thenval : elseval
public __generic<T : __BuiltinFloatingPointType, int N = 1>
vector<T, N> if_less_than(vector<T, N> a, vector<T, N> b, vector<T, N> thenval, vector<T, N> elseval) {
return lerp(thenval, elseval, step(b, a));
}
// Replaces vec4(rgb * a, a)
public float4 vec4_premul(float3 rgb, float a) {
return float4(rgb * a, a);
}
// Overloaded variation replacing vec4(rgba.rgb * rgba.a, rgba.a)
public float4 vec4_premul(float4 rgba) {
return float4(rgba.rgb * rgba.a, rgba.a);
}

View File

@@ -1,76 +0,0 @@
#!/usr/bin/env python3
import subprocess
import sys
from pathlib import Path
from typing import Iterable
# Map the custom extensions to the required glslangValidator stage strings
stage_mapping = {
'.vert.glsl': 'vert',
'.frag.glsl': 'frag',
'.vertex.glsl': 'vert',
'.fragment.glsl': 'frag',
}
def validation_command_for_file(path: str | Path) -> list[str]:
file_path = Path(path)
matched_ext = next(ext for ext in stage_mapping if file_path.name.endswith(ext))
stage = stage_mapping[matched_ext]
return ['glslangValidator', '-S', stage, str(file_path)]
def validate_glsl_files(shader_files: Iterable[str | Path], verbose: bool = False) -> None:
error_count = 0
# Process each shader file
for file_path in sorted(Path(f) for f in shader_files):
# Identify extension matching suffix
if verbose:
print(f'Validating: {file_path.name}')
result = subprocess.run(validation_command_for_file(file_path))
# Check exit code
if result.returncode != 0:
error_count += 1
print(f'❌ Failed: {file_path.name}', file=sys.stderr)
else:
if verbose:
print(f'✅ Passed: {file_path.name}')
if verbose:
print('-' * 50)
# Print execution summary
if error_count == 0:
if verbose:
print("Success: All shaders validated successfully!")
else:
raise SystemExit(f"Failure: {error_count} shader(s) failed validation.")
def validate_glsl_dir(directory_path: str, verbose: bool = False) -> None:
'''
Validates all GLSL shaders in the specified directory with names matching
name.vert.glsl or name.frag.glsl using glslangValidator.
'''
target_dir = Path(directory_path)
if not target_dir.is_dir():
raise SystemExit(f"Error: Directory '{directory_path}' does not exist.")
# Find all files matching the patterns
shader_files: list[Path] = []
for ext in stage_mapping.keys():
shader_files.extend(target_dir.glob(f'*{ext}'))
if not shader_files:
if verbose:
print(f"No matching shaders (*.vert.glsl or *.frag.glsl) found in '{target_dir}'.")
return
validate_glsl_files(shader_files)
if __name__ == "__main__":
dir_to_scan = sys.argv[1] if len(sys.argv) > 1 else 'shaders'
validate_glsl_dir(dir_to_scan, verbose=True)

View File

@@ -34,22 +34,7 @@ _Pragma("clang diagnostic push")
_Pragma("clang diagnostic ignored \"-Wbitwise-instead-of-logical\"")
#endif
#include <simde/x86/avx2.h>
#if defined(__aarch64__) || (defined(__arm64__) && defined(__APPLE__))
// On ARM64, include native NEON header directly instead of simde/arm/neon.h
// to avoid a GCC bug triggered by some versions of simde where
// simde_vcadd_rot270_f16 calls SIMDE_SHUFFLE_VECTOR_(16, 4, ...) with 4
// indices for a 4-byte (2-element) int16 vector, producing the error:
// "excess elements in vector initializer"
# include <arm_neon.h>
typedef uint8x8_t simde_uint8x8_t;
typedef uint8x16_t simde_uint8x16_t;
# define simde_vshrn_n_u16 vshrn_n_u16
# define simde_vreinterpretq_u16_u8 vreinterpretq_u16_u8
# define simde_vget_lane_u64 vget_lane_u64
# define simde_vreinterpret_u64_u8 vreinterpret_u64_u8
#else
# include <simde/arm/neon.h>
#endif
#include <simde/arm/neon.h>
#if defined(__clang__) && __clang_major__ > 13
_Pragma("clang diagnostic pop")
#endif

View File

@@ -19,7 +19,7 @@ from . import BaseTest
class TestBuild(BaseTest):
def test_exe(self) -> None:
from kitty.constants import kitten_exe, kitty_exe, slangc, str_version
from kitty.constants import kitten_exe, kitty_exe, str_version
exe = kitty_exe()
self.assertTrue(os.access(exe, os.X_OK))
self.assertTrue(os.path.isfile(exe))
@@ -28,19 +28,14 @@ class TestBuild(BaseTest):
self.assertTrue(os.access(exe, os.X_OK))
self.assertTrue(os.path.isfile(exe))
self.assertIn(str_version, subprocess.check_output([exe, '--version']).decode())
self.assertTrue(shutil.which(slangc[0]), f'slang compiler: {slangc[0]} not found on PATH: {os.environ["PATH"]}')
def test_loading_extensions(self) -> None:
import kitty.fast_data_types as fdt
from kittens.transfer import rsync
del fdt, rsync
def test_slang_build(self) -> None:
from kitty.shaders.slang import test_slang_build
test_slang_build()
def test_loading_shaders(self) -> None:
from kitty.shaders.legacy import Program
from kitty.shaders import Program
for name in 'cell border bgimage tint graphics'.split():
Program(name)
@@ -132,10 +127,8 @@ class TestBuild(BaseTest):
exe = os.path.join(tdir, 'dictation_probe')
with open(src, 'w') as f:
f.write(probe)
nm = subprocess.run(['nm', '-g', cocoa_module], capture_output=True, text=True)
asan_flags = ['-fsanitize=address,undefined', '-fno-omit-frame-pointer'] if '__asan_init' in nm.stdout else []
cp = subprocess.run(
['clang', '-framework', 'AppKit'] + asan_flags + [src, '-o', exe],
['clang', '-framework', 'AppKit', src, '-o', exe],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
)
self.assertEqual(cp.returncode, 0, cp.stdout)

View File

@@ -940,50 +940,3 @@ class TestDataTypes(BaseTest):
s.reset()
s.draw('\0')
self.ae(str(s.line(0)), '')
def test_set_uint_at_address(self):
import platform
from kitty.fast_data_types import set_uint_at_address
from kitty.marks import marker_from_function, marker_from_multiple_regex, marker_from_regex, marker_from_text
# Test set_uint_at_address directly (skip on intel macs due to ctypes issues)
if not (is_macos and platform.machine() == 'x86_64'):
from ctypes import addressof, c_uint
val = c_uint(0)
addr = addressof(val)
set_uint_at_address(addr, 42)
self.ae(val.value, 42)
set_uint_at_address(addr, 0)
self.ae(val.value, 0)
set_uint_at_address(addr, 0xFFFF)
self.ae(val.value, 0xFFFF)
# Test marker functions using set_uint_at_address via Screen
s = self.create_screen()
s.draw('abaa')
s.set_marker(marker_from_regex('a', 3))
self.ae(s.marked_cells(), [(0, 0, 3), (2, 0, 3), (3, 0, 3)])
s.set_marker()
self.ae(s.marked_cells(), [])
s = self.create_screen()
s.draw('aXbX')
s.set_marker(marker_from_multiple_regex([(1, 'a'), (2, 'X')]))
self.ae(s.marked_cells(), [(0, 0, 1), (1, 0, 2), (3, 0, 2)])
s = self.create_screen(cols=20)
s.draw('hello world')
s.set_marker(marker_from_text('world', 2))
self.ae(s.marked_cells(), [(6, 0, 2), (7, 0, 2), (8, 0, 2), (9, 0, 2), (10, 0, 2)])
def mark_func(text):
for i, ch in enumerate(text):
if ch == 'x':
yield i, i, 1
s = self.create_screen()
s.draw('axbxc')
s.set_marker(marker_from_function(mark_func))
self.ae(s.marked_cells(), [(1, 0, 1), (3, 0, 1)])

View File

@@ -1,161 +0,0 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2026, Kovid Goyal <kovid at kovidgoyal.net>
import os
import tempfile
from kitty.shaders.slang import EntryPoint, SlangFile, Stage, build_import_graph, parse_slang_text, topological_sort
from . import BaseTest
class TestSlang(BaseTest):
def test_slang_parser(self):
def check(src: str, expected: SlangFile) -> None:
actual = parse_slang_text(src)
actual = actual._replace(text='')
self.assertEqual(expected, actual)
# Basic vertex + fragment entry points
check('''
[shader("vertex")]
void drawTriangle(float4 pos : POSITION) {
// vertex code
}
[shader("fragment")]
[numthreads(1, 1, 1)] // Handles intermediate attributes seamlessly
float4 psMain() : SV_Target {
return float4(1, 0, 0, 1);
}
''', SlangFile('', '', frozenset(), frozenset({EntryPoint(Stage.vertex, 'drawTriangle'), EntryPoint(Stage.fragment, 'psMain')})))
# Empty source
check('', SlangFile())
# Only line comments and block comments, no code
check('// just a comment\n/* block comment */', SlangFile('', '', frozenset(), frozenset()))
# Module and import declarations
check('''
module mymodule;
import utils;
import helpers;
''', SlangFile('', '', frozenset({'utils', 'helpers'}), frozenset(), 'mymodule'))
# pixel stage maps to Stage.fragment
check('''
[shader("pixel")]
float4 pixelMain() : SV_Target { return float4(0); }
''', SlangFile('', '', frozenset(), frozenset({EntryPoint(Stage.fragment, 'pixelMain')})))
# Block comment stripping removes multi-line comments before parsing
check('''
/* This is a block comment
spanning multiple lines */
[shader("vertex")]
void vertMain() {}
''', SlangFile('', '', frozenset(), frozenset({EntryPoint(Stage.vertex, 'vertMain')})))
# Block comment containing a shader attribute must not create a false entry point
check('''
/* [shader("vertex")]
void shouldNotBeDetected() {} */
[shader("fragment")]
void fragMain() {}
''', SlangFile('', '', frozenset(), frozenset({EntryPoint(Stage.fragment, 'fragMain')})))
# Multiple [attr] lines between [shader(...)] and the function declaration are skipped
check('''
[shader("fragment")]
[numthreads(4, 4, 1)]
[SomeOtherAttribute]
float4 fragMain() : SV_Target { return float4(0); }
''', SlangFile('', '', frozenset(), frozenset({EntryPoint(Stage.fragment, 'fragMain')})))
# Multiple entry points: vertex, pixel, and fragment stages
check('''
[shader("vertex")]
void vsMain(float4 pos : POSITION) {}
[shader("pixel")]
float4 psMain() : SV_Target { return float4(0); }
[shader("fragment")]
float4 fsMain() : SV_Target { return float4(0); }
''', SlangFile('', '', frozenset(), frozenset({
EntryPoint(Stage.vertex, 'vsMain'),
EntryPoint(Stage.fragment, 'psMain'),
EntryPoint(Stage.fragment, 'fsMain'),
})))
# module, imports and entry points together
check('''
module myshader;
import common;
[shader("vertex")]
void vsMain() {}
''', SlangFile('', '', frozenset({'common'}), frozenset({EntryPoint(Stage.vertex, 'vsMain')}), 'myshader'))
def test_slang_ordering(self):
# Test topological_sort with a manually constructed linear chain: a <- b <- c
graph: dict[str, SlangFile] = {
'a': SlangFile('', '', frozenset(), frozenset(), 'a'),
'b': SlangFile('', '', frozenset({'a'}), frozenset(), 'b'),
'c': SlangFile('', '', frozenset({'b'}), frozenset(), 'c'),
}
order = topological_sort(graph)
self.assertLess(order.index('a'), order.index('b'))
self.assertLess(order.index('b'), order.index('c'))
# Diamond dependency: base <- left, base <- right, left + right <- top
diamond: dict[str, SlangFile] = {
'base': SlangFile('', '', frozenset(), frozenset(), 'base'),
'left': SlangFile('', '', frozenset({'base'}), frozenset(), 'left'),
'right': SlangFile('', '', frozenset({'base'}), frozenset(), 'right'),
'top': SlangFile('', '', frozenset({'left', 'right'}), frozenset(), 'top'),
}
order2 = topological_sort(diamond)
self.assertLess(order2.index('base'), order2.index('left'))
self.assertLess(order2.index('base'), order2.index('right'))
self.assertLess(order2.index('left'), order2.index('top'))
self.assertLess(order2.index('right'), order2.index('top'))
# Node with an import not present in the graph is silently skipped
partial: dict[str, SlangFile] = {
'x': SlangFile('', '', frozenset({'missing'}), frozenset(), 'x'),
}
self.assertEqual(topological_sort(partial), ['x'])
# Empty graph
self.assertEqual(topological_sort({}), [])
# build_import_graph reads .slang files from a directory tree and parses them
with tempfile.TemporaryDirectory() as tmpdir:
files = {
'a': 'module a;\n',
'b': 'module b;\nimport a;\n',
'c': 'module c;\nimport b;\n',
}
for name, content in files.items():
with open(os.path.join(tmpdir, name + '.slang'), 'w') as f:
f.write(content)
graph2 = build_import_graph(tmpdir)
self.assertEqual(set(graph2.keys()), {'a', 'b', 'c'})
self.assertEqual(graph2['a'].imports, frozenset())
self.assertEqual(graph2['b'].imports, frozenset({'a'}))
self.assertEqual(graph2['c'].imports, frozenset({'b'}))
self.assertEqual(graph2['a'].module, 'a')
# Topological sort of file-based graph respects import dependencies
order3 = topological_sort(graph2)
self.assertLess(order3.index('a'), order3.index('b'))
self.assertLess(order3.index('b'), order3.index('c'))
# Non-.slang files are ignored
with open(os.path.join(tmpdir, 'ignored.txt'), 'w') as f:
f.write('not a slang file\n')
graph3 = build_import_graph(tmpdir)
self.assertNotIn('ignored', graph3)

267
setup.py
View File

@@ -18,13 +18,14 @@ import tempfile
import textwrap
import time
from contextlib import suppress
from enum import Enum
from functools import lru_cache, partial
from pathlib import Path
from typing import Any, Callable, Dict, FrozenSet, Iterable, Iterator, List, NamedTuple, Optional, Sequence, Set, Tuple, Union, cast
from typing import Callable, Dict, FrozenSet, Iterable, Iterator, List, Optional, Sequence, Set, Tuple, Union, cast
from glfw import glfw
from glfw.glfw import ISA, BinaryArch, Command, CompileKey, CompilerType
src_base = os.path.dirname(os.path.abspath(__file__))
glfw_base = os.path.join(src_base, 'glfw')
setattr(sys, 'running_from_setup', True)
def check_version_info() -> None:
@@ -44,6 +45,7 @@ def check_version_info() -> None:
exit(f'kitty requires Python {minver}. Current Python version: {".".join(map(str, sys.version_info[:3]))}')
check_version_info()
verbose = False
build_dir = 'build'
constants = os.path.join('kitty', 'constants.py')
@@ -63,133 +65,18 @@ is_macos = 'darwin' in _plat
is_openbsd = 'openbsd' in _plat
is_freebsd = 'freebsd' in _plat
is_netbsd = 'netbsd' in _plat
is_linux = 'linux' in _plat
is_dragonflybsd = 'dragonfly' in _plat
is_bsd = is_freebsd or is_netbsd or is_dragonflybsd or is_openbsd
is_windows = sys.platform == 'win32'
is_arm = platform.processor() == 'arm' or platform.machine() in ('arm64', 'aarch64')
c_std = '' if is_openbsd else '-std=c11'
Env = glfw.Env
env = Env()
PKGCONFIG = os.environ.get('PKGCONFIG_EXE', 'pkg-config')
link_targets: List[str] = []
macos_universal_arches = ('arm64', 'x86_64') if is_arm else ('x86_64', 'arm64')
def null_func() -> None:
return None
class CompileKey(NamedTuple):
src: str
dest: str
class Command(NamedTuple):
desc: str
cmd: Sequence[str]
is_newer_func: Callable[[], bool]
on_success: Callable[[], None] = null_func
key: Optional[CompileKey] = None
keyfile: Optional[str] = None
class ISA(Enum):
X86 = 0x03
AMD64 = 0x3e
ARM64 = 0xb7
Other = 0x0
class BinaryArch(NamedTuple):
bits: int = 64
isa: ISA = ISA.AMD64
class CompilerType(Enum):
gcc = 'gcc'
clang = 'clang'
unknown = 'unknown'
class Env:
cc: List[str] = []
cppflags: List[str] = []
cflags: List[str] = []
ldflags: List[str] = []
library_paths: Dict[str, List[str]] = {}
ldpaths: List[str] = []
ccver: Tuple[int, int]
vcs_rev: str = ''
binary_arch: BinaryArch = BinaryArch()
native_optimizations: bool = False
primary_version: int = 0
secondary_version: int = 0
xt_version: str = ''
has_copy_file_range: bool = False
# glfw stuff
all_headers: List[str] = []
sources: List[str] = []
wayland_packagedir: str = ''
wayland_scanner: str = ''
wayland_scanner_code: str = ''
wayland_protocols: Tuple[str, ...] = ()
def __init__(
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),
vcs_rev: str = '', binary_arch: BinaryArch = BinaryArch(),
native_optimizations: bool = False,
):
self.cc, self.cppflags, self.cflags, self.ldflags, self.library_paths = cc, cppflags, cflags, ldflags, library_paths
self.ldpaths = ldpaths or []
self.ccver = ccver
self.vcs_rev = vcs_rev
self.binary_arch = binary_arch
self.native_optimizations = native_optimizations
self._cc_version_string = ''
self._compiler_type: Optional[CompilerType] = None
@property
def cc_version_string(self) -> str:
if not self._cc_version_string:
self._cc_version_string = subprocess.check_output(self.cc + ['--version']).decode()
return self._cc_version_string
@property
def compiler_type(self) -> CompilerType:
if self._compiler_type is None:
raw = self.cc_version_string
if 'Free Software Foundation' in raw:
self._compiler_type = CompilerType.gcc
elif 'clang' in raw.lower().split():
self._compiler_type = CompilerType.clang
else:
self._compiler_type = CompilerType.unknown
return self._compiler_type
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.all_headers = list(self.all_headers)
ans._cc_version_string = self._cc_version_string
ans.sources = list(self.sources)
ans.wayland_packagedir = self.wayland_packagedir
ans.wayland_scanner = self.wayland_scanner
ans.wayland_scanner_code = self.wayland_scanner_code
ans.wayland_protocols = self.wayland_protocols
ans.vcs_rev = self.vcs_rev
ans.binary_arch = self.binary_arch
ans.native_optimizations = self.native_optimizations
ans.primary_version = self.primary_version
ans.secondary_version = self.secondary_version
ans.xt_version = self.xt_version
ans.has_copy_file_range = self.has_copy_file_range
return ans
env = Env()
def LinkKey(output: str) -> CompileKey:
return CompileKey('', output)
@@ -1151,114 +1038,11 @@ def find_c_files() -> Tuple[List[str], List[str]]:
return ans, headers
def wayland_protocol_file_name(base: str, ext: str = 'c') -> str:
base = os.path.basename(base).rpartition('.')[0]
return f'wayland-{base}-client-protocol.{ext}'
def glfw_init_env(
env: Env,
pkg_config: Callable[..., List[str]],
pkg_version: Callable[[str], Tuple[int, int]],
at_least_version: Callable[..., None],
test_compile: Callable[..., Any],
module: str = 'x11'
) -> Env:
ans = env.copy()
ans.cflags.append('-fPIC')
ans.cppflags.append(f'-D_GLFW_{module.upper()}')
ans.cppflags.append('-D_GLFW_BUILD_DLL')
with open(os.path.join(glfw_base, 'source-info.json')) as f:
sinfo = json.load(f)
module_sources = list(sinfo[module]['sources'])
if module in ('x11', 'wayland'):
remove = 'null_joystick.c' if is_linux else 'linux_joystick.c'
module_sources.remove(remove)
ans.sources = sinfo['common']['sources'] + module_sources
ans.all_headers = [x for x in os.listdir(glfw_base) if x.endswith('.h')]
if module in ('x11', 'wayland'):
ans.cflags.append('-pthread')
ans.ldpaths.extend('-pthread -lm'.split())
if not is_openbsd:
ans.ldpaths.extend('-lrt -ldl'.split())
major, minor = pkg_version('xkbcommon')
if (major, minor) < (0, 5):
raise SystemExit('libxkbcommon >= 0.5 required')
if major < 1:
ans.cflags.append('-DXKB_HAS_NO_UTF32')
if module == 'x11':
for dep in 'x11 xrandr xinerama xcursor xkbcommon xkbcommon-x11 x11-xcb dbus-1'.split():
ans.cflags.extend(pkg_config(dep, '--cflags-only-I'))
ans.ldpaths.extend(pkg_config(dep, '--libs'))
elif module == 'cocoa':
ans.cppflags.append('-DGL_SILENCE_DEPRECATION')
for f_ in 'Cocoa IOKit CoreFoundation CoreVideo QuartzCore UniformTypeIdentifiers'.split():
ans.ldpaths.extend(('-framework', f_))
elif module == 'wayland':
at_least_version('wayland-protocols', *sinfo['wayland_protocols'])
ans.wayland_packagedir = os.path.abspath(pkg_config('wayland-protocols', '--variable=pkgdatadir')[0])
ans.wayland_scanner = os.path.abspath(pkg_config('wayland-scanner', '--variable=wayland_scanner')[0])
scanner_version = tuple(map(int, pkg_config('wayland-scanner', '--modversion')[0].strip().split('.')))
ans.wayland_scanner_code = 'private-code' if scanner_version >= (1, 14, 91) else 'code'
ans.wayland_protocols = tuple(sinfo[module]['protocols'])
for p in ans.wayland_protocols:
ans.sources.append(wayland_protocol_file_name(p))
ans.all_headers.append(wayland_protocol_file_name(p, 'h'))
for dep in 'wayland-client wayland-cursor xkbcommon dbus-1'.split():
ans.cflags.extend(pkg_config(dep, '--cflags-only-I'))
ans.ldpaths.extend(pkg_config(dep, '--libs'))
has_memfd_create = test_compile(env.cc, '-Werror', src='''#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>
int main(void) {
return syscall(__NR_memfd_create, "test", 0);
}''')
if has_memfd_create:
ans.cppflags.append('-DHAS_MEMFD_CREATE')
return ans
def build_wayland_protocols(
env: Env,
parallel_run: Callable[[List[Command]], None],
emphasis: Callable[[str], str],
newer: Callable[..., bool],
dest_dir: str
) -> None:
items = []
for protocol in env.wayland_protocols:
if '/' in protocol:
src = os.path.join(env.wayland_packagedir, protocol)
if not os.path.exists(src):
raise SystemExit(f'The wayland-protocols package on your system is missing the {protocol} protocol definition file')
else:
src = os.path.join(glfw_base, protocol)
if not os.path.exists(src):
raise SystemExit(f'The local Wayland protocol {protocol} is missing from kitty sources')
for ext in 'hc':
dest = wayland_protocol_file_name(src, ext)
dest = os.path.join(dest_dir, dest)
if newer(dest, src):
q = 'client-header' if ext == 'h' else env.wayland_scanner_code
items.append(Command(
f'Generating {emphasis(os.path.basename(dest))} ...',
[env.wayland_scanner, q, src, dest], lambda: True))
if items:
parallel_run(items)
def compile_glfw(compilation_database: CompilationDatabase, build_dsym: bool = False) -> None:
modules = 'cocoa' if is_macos else 'x11 wayland'
for module in modules.split():
try:
genv = glfw_init_env(env, pkg_config, pkg_version, at_least_version, test_compile, module)
genv = glfw.init_env(env, pkg_config, pkg_version, at_least_version, test_compile, module)
except SystemExit as err:
if module != 'wayland':
raise
@@ -1269,7 +1053,7 @@ def compile_glfw(compilation_database: CompilationDatabase, build_dsym: bool = F
all_headers = [os.path.join('glfw', x) for x in genv.all_headers]
if module == 'wayland':
try:
build_wayland_protocols(genv, parallel_run, emphasis, newer, 'glfw')
glfw.build_wayland_protocols(genv, parallel_run, emphasis, newer, 'glfw')
except SystemExit as err:
print(err, file=sys.stderr)
print(error('Disabling building of wayland backend'), file=sys.stderr)
@@ -1398,7 +1182,6 @@ def build_uniforms_header(skip_generation: bool = False) -> str:
a(f' ans->{n} = get_uniform_location(program, "{n}");')
a('}')
a('')
# }]]]))
src = '\n'.join(lines)
try:
with open(dest) as f:
@@ -1421,24 +1204,6 @@ def wrapped_kittens() -> str:
raise Exception('Failed to read wrapped kittens from kitty wrapper script')
def build_shaders(args: Options, kitty_exe: str, for_freeze: bool) -> None:
if args.skip_code_generation:
print('Skipping building of shaders due to command line option', flush=True)
return
env = os.environ.copy()
env['ASAN_OPTIONS'] = 'detect_leaks=0'
cp = subprocess.run([
kitty_exe, '+launch', os.path.join(src_base, 'kitty/shaders/slang.py'), 'build/shaders', 'shaders',
], env=env)
if cp.returncode != 0:
if os.environ.get('CI') == 'true' and cp.returncode < 0 and shutil.which('coredumpctl'):
subprocess.run(['sh', '-c', 'echo bt | coredumpctl debug'])
raise SystemExit(f'Generating shaders failed with exit code: {cp.returncode}')
if for_freeze:
libdir = os.path.join(os.path.dirname(kitty_exe), '..', 'Resources' if is_macos else 'lib', 'kitty')
shutil.copytree('shaders', os.path.join(libdir, 'shaders'), dirs_exist_ok=True)
def build(args: Options, native_optimizations: bool = True, call_init: bool = True) -> None:
if call_init:
init_env_from_args(args, native_optimizations)
@@ -1515,8 +1280,6 @@ def build_static_kittens(
raise SystemExit(f'The version of go on this system ({current_go_version}) is too old. go >= {required_go_version} is needed')
if not for_platform:
update_go_generated_files(args, os.path.join(launcher_dir, appname))
build_shaders(args, os.path.join(launcher_dir, appname), for_freeze)
if args.skip_building_kitten:
print('Skipping building of the kitten binary because of a command line option. Build is incomplete', file=sys.stderr)
return ''
@@ -2114,7 +1877,7 @@ def package(args: Options, bundle_type: str, do_build_all: bool = True) -> None:
shutil.copy2('logo/beam-cursor@2x.png', os.path.join(libdir, 'logo'))
shutil.copytree('shell-integration', os.path.join(libdir, 'shell-integration'), dirs_exist_ok=True)
shutil.copytree('fonts', os.path.join(libdir, 'fonts'), dirs_exist_ok=True)
allowed_extensions = frozenset('py slang glsl so'.split())
allowed_extensions = frozenset('py glsl so'.split())
def src_ignore(parent: str, entries: Iterable[str]) -> List[str]:
return [
@@ -2161,10 +1924,8 @@ def package(args: Options, bundle_type: str, do_build_all: bool = True) -> None:
for f_ in files:
path = os.path.join(root, f_)
os.chmod(path, 0o755 if should_be_executable(path) else 0o644)
if not for_freeze:
if not bundle_type.startswith('macos-'):
build_static_kittens(args, launcher_dir=launcher_dir)
shutil.copytree('shaders', os.path.join(libdir, 'shaders'), dirs_exist_ok=True)
if not for_freeze and not bundle_type.startswith('macos-'):
build_static_kittens(args, launcher_dir=launcher_dir)
if not is_macos and not is_windows:
create_linux_bundle_gunk(ddir, args)
@@ -2196,7 +1957,7 @@ def clean(for_cross_compile: bool = False) -> None:
'linux-package', 'kitty.app', 'asan-launcher',
'kitty-profile') # no fonts as that is not generated by build
if not for_cross_compile:
safe_remove('docs/generated', 'shaders')
safe_remove('docs/generated')
clean_launcher_dir('kitty/launcher')
def excluded(root: str, d: str) -> bool:
@@ -2562,14 +2323,12 @@ def do_build(args: Options) -> None:
elif args.action == 'export-ci-bundles':
cmd = [sys.executable, '../bypy', 'export', 'download.calibre-ebook.com:/srv/download/ci/kitty']
subprocess.check_call(cmd + ['linux'])
subprocess.check_call(cmd + ['linux', '--arch=arm64'])
subprocess.check_call(cmd + ['macos'])
elif args.action == 'build-static-binaries':
build_static_binaries(args, launcher_dir)
def main() -> None:
check_version_info()
global verbose, build_dir
if len(sys.argv) > 1 and sys.argv[1] == 'build-dep':
return build_dep()

View File

@@ -15,7 +15,6 @@ in
simde
go
matplotlib
shader-slang
]
++ optionals stdenv.isDarwin [
libpng

View File

@@ -29,7 +29,7 @@ func encode(x string) string {
type OnDataCallback = func(data_type string, data []byte) error
func edit_loop(data_to_send string, kill_if_signaled bool, on_data OnDataCallback) (exit_code int, err error) {
func edit_loop(data_to_send string, kill_if_signaled bool, on_data OnDataCallback) (err error) {
lp, err := loop.New(loop.NoAlternateScreen, loop.NoRestoreColors, loop.NoMouseTracking)
if err != nil {
return
@@ -52,18 +52,7 @@ func edit_loop(data_to_send string, kill_if_signaled bool, on_data OnDataCallbac
if line == "KITTY_DATA_END" {
lp.QueueWriteString(update_type + "\r\n")
if update_type == "DONE" {
// Parse the editor exit code from the data payload.
// Decoding errors are intentionally ignored: if the data is
// missing or malformed (e.g. from an older kitty server),
// we fall back to exit_code=0 for backward compatibility.
if data.Len() > 0 {
if b, err2 := base64.StdEncoding.DecodeString(data.String()); err2 == nil {
if n, err3 := strconv.Atoi(strings.TrimSpace(string(b))); err3 == nil {
exit_code = n
}
}
}
lp.Quit(exit_code)
lp.Quit(0)
return nil
}
b, err := base64.StdEncoding.DecodeString(data.String())
@@ -161,7 +150,7 @@ func edit_loop(data_to_send string, kill_if_signaled bool, on_data OnDataCallbac
return
}
if canceled {
return 1, tui.Canceled
return tui.Canceled
}
ds := lp.DeathSignalName()
@@ -171,29 +160,29 @@ func edit_loop(data_to_send string, kill_if_signaled bool, on_data OnDataCallbac
lp.KillIfSignalled()
return
}
return 1, &tui.KilledBySignal{Msg: fmt.Sprint("Killed by signal: ", ds), SignalName: ds}
return &tui.KilledBySignal{Msg: fmt.Sprint("Killed by signal: ", ds), SignalName: ds}
}
return
}
func edit_in_kitty(path string, opts *Options) (exit_code int, err error) {
func edit_in_kitty(path string, opts *Options) (err error) {
read_file, err := os.Open(path)
if err != nil {
return 1, fmt.Errorf("Failed to open %s for reading with error: %w", path, err)
return fmt.Errorf("Failed to open %s for reading with error: %w", path, err)
}
defer read_file.Close()
var s unix.Stat_t
err = unix.Fstat(int(read_file.Fd()), &s)
if err != nil {
return 1, fmt.Errorf("Failed to stat %s with error: %w", path, err)
return fmt.Errorf("Failed to stat %s with error: %w", path, err)
}
if s.Size > int64(opts.MaxFileSize)*1024*1024 {
return 1, fmt.Errorf("File size %s is too large for performant editing", humanize.Bytes(uint64(s.Size)))
return fmt.Errorf("File size %s is too large for performant editing", humanize.Bytes(uint64(s.Size)))
}
file_data, err := io.ReadAll(read_file)
if err != nil {
return 1, fmt.Errorf("Failed to read from %s with error: %w", path, err)
return fmt.Errorf("Failed to read from %s with error: %w", path, err)
}
read_file.Close()
data := strings.Builder{}
@@ -210,11 +199,11 @@ func edit_in_kitty(path string, opts *Options) (exit_code int, err error) {
add_encoded := func(key, val string) { add(key, encode(val)) }
if unix.Access(path, unix.R_OK|unix.W_OK) != nil {
return 1, fmt.Errorf("%s is not readable and writeable", path)
return fmt.Errorf("%s is not readable and writeable", path)
}
cwd, err := os.Getwd()
if err != nil {
return 1, fmt.Errorf("Failed to get the current working directory with error: %w", err)
return fmt.Errorf("Failed to get the current working directory with error: %w", err)
}
add_encoded("cwd", cwd)
for _, arg := range os.Args[2:] {
@@ -230,12 +219,12 @@ func edit_in_kitty(path string, opts *Options) (exit_code int, err error) {
}
return
}
exit_code, err = edit_loop(data.String(), true, write_data)
err = edit_loop(data.String(), true, write_data)
if err != nil {
if err == tui.Canceled {
return 1, err
return err
}
return 1, fmt.Errorf("Failed to receive edited file back from terminal with error: %w", err)
return fmt.Errorf("Failed to receive edited file back from terminal with error: %w", err)
}
return
}
@@ -276,7 +265,8 @@ func EntryPoint(parent *cli.Command) *cli.Command {
if err != nil {
return 1, err
}
return edit_in_kitty(file_path, &opts)
err = edit_in_kitty(file_path, &opts)
return 0, err
},
})
AddCloneSafeOpts(sc)

View File

@@ -4,7 +4,6 @@ package tool
import (
"bufio"
"errors"
"fmt"
"os"
"os/exec"
@@ -121,13 +120,7 @@ func confirm_and_run_shebang(args []string, confirm_policy ConfirmPolicy) (rc in
editor.Stdin = os.Stdin
editor.Stdout = os.Stdout
editor.Stderr = os.Stderr
if err = editor.Run(); err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return exitErr.ExitCode(), nil
}
return 1, err
}
editor.Run()
return confirm_and_run_shebang(args, ConfirmIfNeeded)
case "y":
}