Compare commits

..

7 Commits

Author SHA1 Message Date
Kovid Goyal
c1d507dbe8 Merge branch 'dependabot/github_actions/actions-77b068c286' of https://github.com/kovidgoyal/kitty 2026-06-29 11:51:19 +05:30
Kovid Goyal
2b38afabce Merge branch 'dependabot/go_modules/all-go-deps-9c5f66ad81' of https://github.com/kovidgoyal/kitty 2026-06-29 11:51:05 +05:30
dependabot[bot]
2ead860607 Bump the actions group with 4 updates
Bumps the actions group with 4 updates: [actions/checkout](https://github.com/actions/checkout), [actions/setup-python](https://github.com/actions/setup-python), [actions/setup-go](https://github.com/actions/setup-go) and [actions/cache](https://github.com/actions/cache).


Updates `actions/checkout` from 6.0.3 to 7.0.0
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6.0.3...v7.0.0)

Updates `actions/setup-python` from 6 to 6.2.0
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v6.2.0)

Updates `actions/setup-go` from 6 to 6.4.0
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v6...v6.4.0)

Updates `actions/cache` from 5 to 5.0.5
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v5...v5.0.5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/setup-python
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: actions/setup-go
  dependency-version: 6.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: actions/cache
  dependency-version: 5.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-29 03:44:56 +00:00
dependabot[bot]
691a971a70 Bump github.com/alecthomas/chroma/v2 in the all-go-deps group
Bumps the all-go-deps group with 1 update: [github.com/alecthomas/chroma/v2](https://github.com/alecthomas/chroma).


Updates `github.com/alecthomas/chroma/v2` from 2.26.1 to 2.27.0
- [Release notes](https://github.com/alecthomas/chroma/releases)
- [Commits](https://github.com/alecthomas/chroma/compare/v2.26.1...v2.27.0)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/chroma/v2
  dependency-version: 2.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-go-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-29 03:42:55 +00:00
Kovid Goyal
9cd2c60064 Clean previous PR 2026-06-29 06:17:08 +05:30
Kovid Goyal
4e0f7faef8 Merge branch 'fix-erase-last-command-multiline-prompt' of https://github.com/slayerlab/kitty 2026-06-29 06:13:36 +05:30
amoena
207b22aa80 fix(erase_last_command): erase the most recent command even empty output
erase_last_command selected the region to erase via find_cmd_output(..., -1),
which anchors on OUTPUT_START (OSC, 133;C). Commands that produce no output
(an empty Enter, a comment, cd, export, etc. -- never emit 133;C, so they were
skipped and an older command-with-output was erased instead. "Erase the last
command" therefore did not erase the last command whenever the most recent ones
has no output.

Select the region by prompt marks instead: erase the prompt block immediately
above the current (live) prompt, whatever it contains. Every submittd command
is now one unit, removed newest-first, one prompt block per invocation.

This also fixes two latent defects in the previous implementation:

* The on-screen deletion was anchored at `cursor->y - count`, which
  assumes the region ends exactly one row above the cursor.
  Multi-line prompts and skipped rows broke that assumption and left
  residual lines. Anchor at the top of the region instead.

* When part of the erased region was in the scrollback, the lines
  were removed from the history buffer but no redraw was signalled,
  so the deletion of the off-screen lines only became visible after
  the next scroll event recomputed the history viewport. Clamp
  scrolled_by to the new history length and call dirty_scroll()
  after shrinking the buffer.

include_prompt is retained for API compatibility but is now a no-op: the
unit erased is always the whole prompt block.
2026-06-28 11:17:01 -03:00
43 changed files with 364 additions and 2731 deletions

View File

@@ -4,10 +4,8 @@
import glob
import io
import json
import lzma
import os
import platform
import shlex
import shutil
import subprocess
@@ -24,7 +22,6 @@ 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 +101,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 +118,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 +129,14 @@ 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:
cmd += ' --debug --sanitize'
set_slangc()
run(cmd)
@@ -184,15 +146,12 @@ def test_kitty() -> None:
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 +166,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 +282,11 @@ def main() -> None:
if action == 'build':
build_kitty()
elif action == 'package':
build_kitty()
test_kitty()
package_kitty()
elif action == 'test':
test_kitty()
elif action == 'test':
test_kitty()
elif action == 'govulncheck':
subprocess.check_call(['go', 'install', 'golang.org/x/vuln/cmd/govulncheck@latest'])
subprocess.check_call(['govulncheck', '-mode=binary', 'kitty/launcher/kitten'])

View File

@@ -45,18 +45,18 @@ jobs:
steps:
- name: Checkout source code
uses: actions/checkout@v6.0.3
uses: actions/checkout@v7.0.0
with:
fetch-depth: 10
persist-credentials: false
- name: Set up Python ${{ matrix.pyver }}
uses: actions/setup-python@v6
uses: actions/setup-python@v6.2.0
with:
python-version: ${{ matrix.pyver }}
- name: Install Go
uses: actions/setup-go@v6
uses: actions/setup-go@v6.4.0
with:
go-version-file: go.mod
@@ -73,7 +73,7 @@ jobs:
CFLAGS: -funsigned-char
steps:
- name: Checkout source code
uses: actions/checkout@v6.0.3
uses: actions/checkout@v7.0.0
with:
fetch-depth: 0 # needed for :commit: docs role
persist-credentials: false
@@ -85,18 +85,18 @@ jobs:
run: if grep -Inr ':code:`\s' kitty kitty_tests kittens docs *.py *.asciidoc *.rst *.go .gitattributes .gitignore; then echo Space at code block start found, aborting.; exit 1; fi
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v6.2.0
with:
python-version: "3.14"
- name: Install Go
uses: actions/setup-go@v6
uses: actions/setup-go@v6.4.0
with:
go-version-file: go.mod
cache: false
- name: Cache Go build artifacts separately
uses: actions/cache@v5
uses: actions/cache@v5.0.5
with:
path: |
~/.cache/go-build
@@ -117,12 +117,18 @@ 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 docs
run: make FAIL_WARN=1 docs
- name: Build static kittens
run: python setup.py build-static-binaries
@@ -137,13 +143,13 @@ jobs:
KITTY_SANITIZE: 1
steps:
- name: Checkout source code
uses: actions/checkout@v6.0.3
uses: actions/checkout@v7.0.0
with:
fetch-depth: 10
persist-credentials: false
- name: Install Go
uses: actions/setup-go@v6
uses: actions/setup-go@v6.4.0
with:
go-version-file: go.mod
@@ -158,24 +164,33 @@ jobs:
runs-on: macos-latest
steps:
- name: Checkout source code
uses: actions/checkout@v6.0.3
uses: actions/checkout@v7.0.0
with:
fetch-depth: 0 # needed for :commit: docs role
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v6.2.0
with:
python-version: "3.14"
- name: Install Go
uses: actions/setup-go@v6
uses: actions/setup-go@v6.4.0
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
@@ -187,7 +202,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v6.0.3
uses: actions/checkout@v7.0.0
with:
fetch-depth: 10
persist-credentials: false
@@ -196,7 +211,7 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y curl xz-utils build-essential git pkg-config libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl1-mesa-dev libxkbcommon-x11-dev libfontconfig-dev libx11-xcb-dev libdbus-1-dev
- name: Install Go
uses: actions/setup-go@v6
uses: actions/setup-go@v6.4.0
with:
go-version-file: go.mod

View File

@@ -39,7 +39,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6.0.3
uses: actions/checkout@v7.0.0
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -48,7 +48,7 @@ jobs:
- name: Install Go
if: matrix.language == 'c' || matrix.language == 'go'
uses: actions/setup-go@v6
uses: actions/setup-go@v6.4.0
with:
go-version-file: go.mod

View File

@@ -22,13 +22,13 @@ jobs:
KITTY_BUNDLE: 1
steps:
- name: Checkout source code
uses: actions/checkout@v6.0.3
uses: actions/checkout@v7.0.0
with:
fetch-depth: 10
persist-credentials: false
- name: Checkout bypy
uses: actions/checkout@v6.0.3
uses: actions/checkout@v7.0.0
with:
fetch-depth: 1
persist-credentials: false

View File

@@ -393,7 +393,6 @@ func build(args []string) {
}
root := root_dir()
os.Setenv("DEVELOP_ROOT", root)
os.Setenv("SLANGC", filepath.Join(root, "bin", "slangc"))
prepend("PKG_CONFIG_PATH", filepath.Join(root, "lib", "pkgconfig"))
if runtime.GOOS == "darwin" {
os.Setenv("PKGCONFIG_EXE", filepath.Join(root, "bin", "pkg-config"))

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ import sys
import tempfile
import zipfile
from bypy.constants import BIN, LIBDIR, PREFIX, PYTHON, SW, python_major_minor_version
from bypy.constants import PREFIX, PYTHON, SW, python_major_minor_version
from bypy.freeze import extract_extension_modules, freeze_python, path_to_freeze_dir
from bypy.macos_sign import codesign, create_entitlements_file, make_certificate_useable, notarize_app, verify_signature
from bypy.utils import current_dir, mkdtemp, py_compile, run_shell, timeit, walk
@@ -301,7 +301,7 @@ class Freeze(object):
self.fix_dependencies_in_lib(join(self.frameworks_dir, basename(path)))
@flush
def add_misc_libraries(self) -> None:
def add_misc_libraries(self):
for x in (
'sqlite3.0',
'z.1',
@@ -319,22 +319,6 @@ class Freeze(object):
dest = join(self.frameworks_dir, x)
self.set_id(dest, f'{self.FID}/{x}')
self.fix_dependencies_in_lib(dest)
# Copy slang
x = 'libslang-compiler.0.0.0.0.dylib'
shutil.copy2(os.path.join(LIBDIR, x), self.frameworks_dir)
os.symlink(x, os.path.join(self.frameworks_dir, 'libslang-compiler.dylib'))
dest = join(self.frameworks_dir, x)
self.set_id(dest, f'{self.FID}/{x}')
self.fix_dependencies_in_lib(dest)
for x in ('glsl-module', 'glslang'):
x = f'libslang-{x}-0.0.0.dylib'
shutil.copy2(os.path.join(LIBDIR, x), self.frameworks_dir)
dest = join(self.frameworks_dir, x)
self.set_id(dest, f'{self.FID}/{x}')
dest = os.path.join(self.contents_dir, 'MacOS')
shutil.copy2(os.path.join(BIN, 'slangc'), dest)
dest = os.path.join(dest, 'slangc')
self.fix_dependencies_in_lib(dest)
@flush
def add_package_dir(self, x, dest=None):

View File

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

View File

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

View File

@@ -196,6 +196,8 @@ Detailed list of changes
- ``edit-in-kitty``: Return exit code from underlying editor process on exit (:iss:`10198`)
- Make erasing last command robust against commands with no output and commands in the scrollback (:pull:`10201`)
0.47.4 [2026-06-15]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

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

4
go.mod
View File

@@ -6,7 +6,7 @@ toolchain go1.26.4
require (
github.com/ALTree/bigfloat v0.2.0
github.com/alecthomas/chroma/v2 v2.26.1
github.com/alecthomas/chroma/v2 v2.27.0
github.com/bmatcuk/doublestar/v4 v4.10.0
github.com/dlclark/regexp2 v1.12.0
github.com/ebitengine/purego v0.10.1
@@ -39,7 +39,7 @@ require (
// replace github.com/kovidgoyal/imaging => ../imaging
require (
github.com/dlclark/regexp2/v2 v2.1.1 // indirect
github.com/dlclark/regexp2/v2 v2.2.1 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect

8
go.sum
View File

@@ -2,8 +2,8 @@ github.com/ALTree/bigfloat v0.2.0 h1:AwNzawrpFuw55/YDVlcPw0F0cmmXrmngBHhVrvdXPvM
github.com/ALTree/bigfloat v0.2.0/go.mod h1:+NaH2gLeY6RPBPPQf4aRotPPStg+eXc8f9ZaE4vRfD4=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.26.1 h1:2X21EdxGZNv5GF9mG5u+uzc02GCFyGxbcBm3Grd9A78=
github.com/alecthomas/chroma/v2 v2.26.1/go.mod h1:lxhRRa9H4hPmRLOOdYga4zkQIQjq3dtrrdwQeCfu78Y=
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
@@ -12,8 +12,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.1.1 h1:LCUGyd9Wf+r+VVOl8Ny38JTpWJcAsdVnCIuhhtthmKw=
github.com/dlclark/regexp2/v2 v2.1.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/emmansun/base64 v0.9.0 h1:92dLrE7iro6g/yWuPsd7M9TzJpe9fEeqKH0H7MApDtE=

View File

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

View File

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

View File

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

View File

@@ -1261,7 +1261,7 @@ class Screen:
def test_commit_write_buffer(self, inp: memoryview, output: memoryview) -> int: ...
def test_parse_written_data(self, dump_callback: None = None) -> None: ...
def hyperlink_for_id(self, hyperlink_id: int) -> str: ...
def erase_last_command(self, include_prompt: bool = True) -> bool: ...
def erase_last_command(self) -> bool: ...
def set_progress(self, state: int, percent: int) -> None: ...
def mark_potential_url_drag(self) -> bool: ...

View File

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

View File

@@ -4599,27 +4599,47 @@ find_cmd_output(Screen *self, OutputOffset *oo, index_type start_screen_y, unsig
}
static PyObject*
erase_last_command(Screen *self, PyObject *args) {
int include_prompt = 1;
if (!PyArg_ParseTuple(args, "|p", &include_prompt)) return NULL;
OutputOffset oo = {.screen=self};
if (self->linebuf != self->main_linebuf || !find_cmd_output(self, &oo, self->cursor->y + self->scrolled_by, self->scrolled_by, -1, false)) Py_RETURN_FALSE;
if (include_prompt) {
int y = oo.start - 1; Line *line;
while ((line = checked_range_line(self, y))) {
oo.start--; oo.num_lines++; y--;
if (line->attrs.prompt_kind == PROMPT_START) break;
}
erase_last_command(Screen *self, PyObject *args UNUSED) {
if (self->linebuf != self->main_linebuf) Py_RETURN_FALSE;
Line *line;
// Erase the most recent command: the prompt block immediately above the
// current (live) prompt, regardless of whether that command produced any
// output. Commands without output (an empty Enter, a comment, cd, export,
// ...) emit no OSC 133;C, so a search anchored on OUTPUT_START would skip
// them and erase an older command instead. Selecting by prompt marks makes
// every submitted command one unit, removed newest-first.
int y = self->cursor->y + self->scrolled_by;
bool found = false;
for (; (line = checked_range_line(self, y)); y--) {
if (line->attrs.prompt_kind == PROMPT_START) { found = true; break; }
}
index_type num_lines_to_erase_in_screen = oo.start >= 0 ? oo.num_lines : oo.num_lines + oo.start;
if (!found) Py_RETURN_FALSE;
const int live_prompt_start = y;
found = false;
for (y = live_prompt_start - 1; (line = checked_range_line(self, y)); y--) {
if (line->attrs.prompt_kind == PROMPT_START) { found = true; break; }
}
if (!found) Py_RETURN_FALSE; // nothing above the current prompt to erase
const int start = y;
const index_type num_lines = (index_type)(live_prompt_start - start);
index_type num_lines_to_erase_in_screen = start >= 0 ? num_lines : num_lines + start;
num_lines_to_erase_in_screen = MIN(self->cursor->y, num_lines_to_erase_in_screen);
if (num_lines_to_erase_in_screen) {
screen_delete_lines_impl(self, self->cursor->y - num_lines_to_erase_in_screen, num_lines_to_erase_in_screen, 0, self->lines - 1);
// Anchor the on-screen deletion at the top of the region, not at
// cursor->y - count: a multi-line prompt or rows skipped above would
// otherwise shift the deletion and leave residual lines on screen.
index_type screen_erase_start = start >= 0 ? (index_type)start : 0;
screen_delete_lines_impl(self, screen_erase_start, num_lines_to_erase_in_screen, 0, self->lines - 1);
self->cursor->y -= num_lines_to_erase_in_screen;
}
if (oo.num_lines > num_lines_to_erase_in_screen) {
index_type num_of_lines_to_erase_from_history = oo.num_lines - num_lines_to_erase_in_screen;
historybuf_delete_newest_lines(self->historybuf, num_of_lines_to_erase_from_history);
if (num_lines > num_lines_to_erase_in_screen) {
historybuf_delete_newest_lines(self->historybuf, num_lines - num_lines_to_erase_in_screen);
// The scrollback shrank: clamp the scroll position and signal a redraw,
// otherwise the deletion of off-screen lines is not reflected until the
// next scroll event forces the history viewport to be recomputed.
self->scrolled_by = MIN(self->scrolled_by, self->historybuf->count);
self->is_dirty = true;
dirty_scroll(self);
}
Py_RETURN_TRUE;
}
@@ -6221,7 +6241,7 @@ static PyMethodDef methods[] = {
MND(draw, METH_O)
MND(apply_sgr, METH_O)
MND(cursor_position, METH_VARARGS)
MND(erase_last_command, METH_VARARGS)
MND(erase_last_command, METH_NOARGS)
MND(set_window_char, METH_VARARGS)
MND(set_progress, METH_VARARGS)
MND(set_mode, METH_VARARGS)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)

View File

@@ -1627,8 +1627,8 @@ class TestScreen(BaseTest):
s.draw('before\r\n')
draw_prompt('p1'), draw_output(2), mark_prompt(), s.draw('partial')
x = s.cursor.x
s.erase_last_command(False)
self.ae('before\n$ p1\npartial', at().rstrip())
s.erase_last_command()
self.ae('before\npartial', at().rstrip())
for scroll in (8, 9, 10):
s.reset()
s.draw('before'), s.carriage_return(), s.linefeed()
@@ -1641,6 +1641,33 @@ class TestScreen(BaseTest):
s.erase_last_command()
self.ae(at().rstrip(), ' a b\npartial')
# the most recent command is erased even when it produced no output (an
# empty Enter, a comment, cd, ...): such commands emit no OSC 133;C and
# must not be skipped in favour of an older command-with-output.
s = self.create_screen(cols=10, lines=12, scrollback=30)
s.draw('before\r\n')
draw_prompt('p1'), draw_output(2), draw_prompt('# note'), mark_prompt(), s.draw('partial')
s.erase_last_command()
self.ae('before\n$ p1\n0\n1\npartial', at().rstrip()) # the output-less command goes first
s.erase_last_command()
self.ae('before\npartial', at().rstrip()) # then the command with output
# consecutive output-less commands are removed newest-first, one per call
s.reset()
s.draw('before\r\n')
draw_prompt('p1'), draw_output(1), draw_prompt('e1'), draw_prompt('e2'), mark_prompt(), s.draw('partial')
s.erase_last_command()
self.ae('before\n$ p1\n0\n$ e1\npartial', at().rstrip())
s.erase_last_command()
self.ae('before\n$ p1\n0\npartial', at().rstrip())
s.erase_last_command()
self.ae('before\npartial', at().rstrip())
# multi-line live prompt: the command region is erased with no residual
s.reset()
s.draw('before\r\n')
draw_prompt('p1'), draw_output(9), mark_prompt(), s.draw('l1'), s.carriage_return(), s.index(), s.draw('partial')
s.erase_last_command()
self.ae('before\nl1\npartial', at().rstrip())
def test_pointer_shapes(self):
from kitty.window import set_pointer_shape
s = self.create_screen()

View File

@@ -1,55 +0,0 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2026, Kovid Goyal <kovid at kovidgoyal.net>
import os
import tempfile
from unittest.mock import patch
from kitty.options.types import defaults
from kitty.shaders.slang import specialize_cache, specialize_cell_shader
from . import BaseTest
class TestShaders(BaseTest):
def test_specialize_cell_shader(self):
specialize_cache.clear()
# No action when opts are the same as defaults
self.assertEqual(specialize_cell_shader(opts=defaults), {})
# Mock out compilation to avoid needing slangc
call_count = [0]
def fake_specialize_shaders_to(sources, dest_dir):
call_count[0] += 1
with open(os.path.join(dest_dir, 'cell.glsl'), 'wb') as f:
f.write(b'fake shader ' + str(call_count[0]).encode())
def fake_ensure_cache_dir(path):
os.makedirs(path, exist_ok=True)
with patch('kitty.shaders.slang.specialize_shaders_to', fake_specialize_shaders_to), \
patch('kitty.shaders.slang.ensure_cache_dir', fake_ensure_cache_dir):
# Changing options produces non-empty shaders
opts1 = defaults._replace(text_composition_strategy='legacy')
with tempfile.TemporaryDirectory() as dir1:
result1 = specialize_cell_shader(create_cache_dir=lambda: dir1, opts=opts1)
self.assertNotEqual(result1, {})
# Changing to different options produces different shaders
opts2 = defaults._replace(text_fg_override_threshold=(10.0, '%'))
with tempfile.TemporaryDirectory() as dir2:
result2 = specialize_cell_shader(create_cache_dir=lambda: dir2, opts=opts2)
self.assertNotEqual(result2, {})
self.assertNotEqual(result1, result2)
# Calling twice with the same options returns the same object
opts3 = defaults._replace(text_composition_strategy='legacy')
with tempfile.TemporaryDirectory() as dir3:
get_dir3 = lambda: dir3
result3a = specialize_cell_shader(create_cache_dir=get_dir3, opts=opts3)
result3b = specialize_cell_shader(create_cache_dir=get_dir3, opts=opts3)
self.assertIs(result3a, result3b)

View File

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

266
setup.py
View File

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

View File

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