mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-02 20:53:37 +02:00
Compare commits
1 Commits
slang
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a80bab2588 |
57
.github/workflows/ci.py
vendored
57
.github/workflows/ci.py
vendored
@@ -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'])
|
||||
|
||||
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
|
||||
97
.github/workflows/macos_crash_report.py
vendored
97
.github/workflows/macos_crash_report.py
vendored
@@ -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
1
.gitignore
vendored
@@ -13,7 +13,6 @@
|
||||
/tags
|
||||
/build/
|
||||
/fonts/
|
||||
/shaders/
|
||||
/linux-package/
|
||||
/kitty.app/
|
||||
/glad/out/
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
221
glfw/glfw.py
221
glfw/glfw.py
@@ -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
4
go.mod
@@ -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
8
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)])
|
||||
|
||||
|
||||
@@ -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
267
setup.py
@@ -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()
|
||||
|
||||
@@ -15,7 +15,6 @@ in
|
||||
simde
|
||||
go
|
||||
matplotlib
|
||||
shader-slang
|
||||
]
|
||||
++ optionals stdenv.isDarwin [
|
||||
libpng
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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":
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user