Compare commits

...

145 Commits
seg ... var

Author SHA1 Message Date
Kovid Goyal
656d2179c9 ... 2024-06-22 11:33:22 +05:30
Kovid Goyal
da582b5622 ... 2024-06-22 11:20:54 +05:30
Kovid Goyal
ba4292e912 More choose-fonts documentation 2024-06-22 11:20:54 +05:30
Kovid Goyal
785726d21d Sort style names by variant axis value when available 2024-06-22 11:20:54 +05:30
Kovid Goyal
e3a155266e Fix marking of current style in list 2024-06-22 11:20:54 +05:30
Kovid Goyal
e3eb179be2 Fix matching against style names in presence of elision 2024-06-22 11:20:54 +05:30
Kovid Goyal
e88ae3397f Start documenting the choose fonts kitten 2024-06-22 11:20:54 +05:30
Kovid Goyal
8e0ef0c430 Fix spec generation for auto setting 2024-06-22 11:20:54 +05:30
Kovid Goyal
02df66733e Fix changing styles discarding features 2024-06-22 11:20:54 +05:30
Kovid Goyal
4a038ea581 Preserve auto setting when re-running choose-fonts on already selected family 2024-06-22 11:20:54 +05:30
Kovid Goyal
e41d57dffd Output features in spec_from_face 2024-06-22 11:20:54 +05:30
Kovid Goyal
b63be88bac ... 2024-06-22 11:20:54 +05:30
Kovid Goyal
8bfff51d23 Automatically propagate features from regular face to the other faces when they are set to auto 2024-06-22 11:20:54 +05:30
Kovid Goyal
72268539ef Fix sorting of features in UI 2024-06-22 11:20:54 +05:30
Kovid Goyal
5b83a33888 Setting of index features now basically works 2024-06-22 11:20:54 +05:30
Kovid Goyal
daaec1b47f ... 2024-06-22 11:20:53 +05:30
Kovid Goyal
bc56fce38d Add support for font features when rendering sample text 2024-06-22 11:20:53 +05:30
Kovid Goyal
788b3dc4b2 ... 2024-06-22 11:20:53 +05:30
Kovid Goyal
f4e22ebe3c Implement toggling of boolean features 2024-06-22 11:20:53 +05:30
Kovid Goyal
349c32f60e Work on UI for features 2024-06-22 11:20:53 +05:30
Kovid Goyal
3f919db0c7 Fix preview rendering when height of previewed font greater than cell height 2024-06-22 11:20:53 +05:30
Kovid Goyal
c4dad85f99 test render function to develop the sample renderer 2024-06-22 11:20:53 +05:30
Kovid Goyal
a164c73389 ... 2024-06-22 11:20:53 +05:30
Kovid Goyal
3e430e1a70 Render font feature list in UI 2024-06-22 11:20:53 +05:30
Kovid Goyal
12314cc33f Add tests to validate feature-from-spec 2024-06-22 11:20:53 +05:30
Kovid Goyal
c3bba2e926 work on passing font features via font specs 2024-06-22 11:20:51 +05:30
Kovid Goyal
a4f67b7424 Get feature human readable names 2024-06-22 11:16:42 +05:30
Kovid Goyal
b217c9acde List of all known OpenType font features 2024-06-22 11:16:42 +05:30
Kovid Goyal
23e777ea9e Code to read features from GSUB/GPOS tables 2024-06-22 11:16:42 +05:30
Kovid Goyal
ecb106e92c revert simde bump 2024-06-22 11:16:42 +05:30
Kovid Goyal
23deaae5e7 more tests 2024-06-22 11:16:42 +05:30
Kovid Goyal
bc6230d90c Centralize FontSpec related code 2024-06-22 11:16:42 +05:30
Kovid Goyal
03f35812eb Fix O(n^2) algorithm 2024-06-22 11:16:42 +05:30
Kovid Goyal
7c0007c1bd Ensure bold face is at least as heavy as regular face when auto selecting 2024-06-22 11:16:42 +05:30
Kovid Goyal
d2d2f6c503 Improve auto selection of variable faces 2024-06-22 11:16:42 +05:30
Kovid Goyal
69fb2e4231 Handle variable fonts like cascadia code that dont have a postfix variation prefix name for some of their faces 2024-06-22 11:16:42 +05:30
Kovid Goyal
94d056ed4f Wire up applying of font config 2024-06-22 11:16:41 +05:30
Kovid Goyal
726f62b948 Refactor config patching code to make it re-useable 2024-06-22 11:16:41 +05:30
Kovid Goyal
7e15839141 More work on choose_fonts 2024-06-22 11:16:41 +05:30
Kovid Goyal
13a6ff25a2 Render preview synchronously to avoid flashing 2024-06-22 11:16:41 +05:30
Kovid Goyal
e050557db7 Get axis clicking working 2024-06-22 11:16:41 +05:30
Kovid Goyal
3e2b3a89ce more work on axis fine tuning 2024-06-22 11:16:41 +05:30
Kovid Goyal
81c30cc5fa Render variable axes 2024-06-22 11:16:41 +05:30
Kovid Goyal
73a6668b17 Generalize code to get variable spec 2024-06-22 11:16:41 +05:30
Kovid Goyal
f8e2dc1eca More work on face fine tune UI 2024-06-22 11:16:41 +05:30
Kovid Goyal
32b8077c89 Make debug printing in backend.py more convenient 2024-06-22 11:16:41 +05:30
Kovid Goyal
b90fede2c1 Fix medium face selection when more than family specified 2024-06-22 11:16:41 +05:30
Kovid Goyal
87d1a97486 Dont log an error when the default famil "monospace" is not found 2024-06-22 11:16:41 +05:30
Kovid Goyal
98450a0605 More work on face fine tuning 2024-06-22 11:16:41 +05:30
Kovid Goyal
29377db94c CoreText: When finding medium face for a family prefer variable font if available 2024-06-22 11:16:41 +05:30
Kovid Goyal
8d8c2d7170 Skip test o older freetype 2024-06-22 11:16:41 +05:30
Kovid Goyal
5be4f7b566 Add a test for dejavu sans mono 2024-06-22 11:16:41 +05:30
Kovid Goyal
ce74706c95 Install needed fonts in CI 2024-06-22 11:16:40 +05:30
Kovid Goyal
8f8da2d2c6 IBM Plex Mono workaround is needed only under fontconfig 2024-06-22 11:16:40 +05:30
Kovid Goyal
540e11fa01 DRYer 2024-06-22 11:16:40 +05:30
Kovid Goyal
a230eb87a1 Get font selection for the cascadia code variable fonts working 2024-06-22 11:16:40 +05:30
Kovid Goyal
4ce7da044f Better scoring for malformed fonts with weird weight ranges 2024-06-22 11:16:40 +05:30
Kovid Goyal
ceccacafa8 Refactor scoring 2024-06-22 11:16:40 +05:30
Kovid Goyal
df80da7cb5 Add more font selection tests 2024-06-22 11:16:40 +05:30
Kovid Goyal
08bffde4ad CoreText: Fix selection of font file with multi-file variant font
Also prefer semibold and use similar scorer as fontconfig
2024-06-22 11:16:40 +05:30
Kovid Goyal
983bf134a1 fontconfig: Prefer semi-bold as bold weight even for system selection 2024-06-22 11:16:40 +05:30
Kovid Goyal
e25c61f781 fontconfig: Lift axes spec to named style 2024-06-22 11:16:40 +05:30
Kovid Goyal
d6026c377b Test for font selection 2024-06-22 11:16:40 +05:30
Kovid Goyal
a766a95de6 ... 2024-06-22 11:16:40 +05:30
Kovid Goyal
36d906d4f5 DRYer 2024-06-22 11:16:40 +05:30
Kovid Goyal
881b72fea7 Fix medium face selection via fontconfig when family has only variable font files 2024-06-22 11:16:40 +05:30
Kovid Goyal
d6a7d4a8a1 Fix face sample rendering 2024-06-22 11:16:40 +05:30
Kovid Goyal
db092eb88d Work on face panel 2024-06-22 11:16:40 +05:30
Kovid Goyal
33df5355a6 Start work on face panel 2024-06-22 11:16:39 +05:30
Kovid Goyal
99e279cbe9 ... 2024-06-22 11:16:39 +05:30
Kovid Goyal
82ffb376b2 Use PSname in faces preview panel 2024-06-22 11:16:39 +05:30
Kovid Goyal
fc72f4961e Transmit metadata about rendered samples 2024-06-22 11:16:39 +05:30
Kovid Goyal
880078cab8 Get rendering of faces panel working 2024-06-22 11:16:39 +05:30
Kovid Goyal
88058c9075 Work on faces panel 2024-06-22 11:16:39 +05:30
Kovid Goyal
ecdea9d4d3 Start work on faces panel 2024-06-22 11:16:39 +05:30
Kovid Goyal
5ace209024 ... 2024-06-22 11:16:39 +05:30
Kovid Goyal
b68aac9f28 Forgot to initialize canvas when rendering sample text onto it 2024-06-22 11:16:39 +05:30
Kovid Goyal
71bbf4ecb9 Fix graphics being freed instead of deleted in draw_screen() 2024-06-22 11:16:39 +05:30
Kovid Goyal
1590462038 Get preview to basically display 2024-06-22 11:16:39 +05:30
Kovid Goyal
1735404a05 Move listing code into its own file 2024-06-22 11:16:39 +05:30
Kovid Goyal
3c18d20d1b Code to get specs from options 2024-06-22 11:16:39 +05:30
Kovid Goyal
e51a1362ca Handle default values not present in variation data under CoreText 2024-06-22 11:16:39 +05:30
Kovid Goyal
5cea5cce81 Use the cache for getting variable data for faces 2024-06-22 11:16:38 +05:30
Kovid Goyal
bb722ed6dc Function to get the named style used by a variable font instance 2024-06-22 11:16:38 +05:30
Kovid Goyal
78e81ec589 Get variable font selection working on coretext 2024-06-22 11:16:38 +05:30
Kovid Goyal
80721e1e63 Fix build on older fontconfig 2024-06-22 11:16:38 +05:30
Kovid Goyal
7186b862f2 Cleanup repr for fontconfig faces 2024-06-22 11:16:38 +05:30
Kovid Goyal
e4d82074ff Implement spec based selection for variable fonts 2024-06-22 11:16:38 +05:30
Kovid Goyal
804ad62a33 Start work on displaying font sampler images 2024-06-22 11:16:38 +05:30
Kovid Goyal
4431cff7fa Use KITTY_PID to find kitty exe when possible 2024-06-22 11:16:38 +05:30
Kovid Goyal
78b77b0d01 Fix crash on CoreText for very large font sizes 2024-06-22 11:16:38 +05:30
Kovid Goyal
2c2fbd4445 Implement rendering of sample text 2024-06-22 11:16:38 +05:30
Kovid Goyal
333e94622e Move the query_terminal implementation to Go 2024-06-22 11:16:38 +05:30
Kovid Goyal
41869d88c2 Work on rendering sample text for a font 2024-06-22 11:16:38 +05:30
Kovid Goyal
65b790df40 Also get the current fg/bg colors to render text with 2024-06-22 11:16:38 +05:30
Kovid Goyal
5d74d210ee Query font size and DPI from terminal 2024-06-22 11:16:38 +05:30
Kovid Goyal
431fc98659 Get query terminal working again
Also return current OS Window's font size
2024-06-22 11:16:38 +05:30
Kovid Goyal
21d3759f62 Report when a family has variable fonts 2024-06-22 11:16:38 +05:30
Kovid Goyal
24dea14795 Fix off by one in hyperlink extent 2024-06-22 11:16:37 +05:30
Kovid Goyal
94628dca21 Use correct pointer shape for hyperlinks 2024-06-22 11:16:37 +05:30
Kovid Goyal
d6016f4246 Get clicking on family names functional 2024-06-22 11:16:37 +05:30
Kovid Goyal
688736c4cf ... 2024-06-22 11:16:37 +05:30
Kovid Goyal
1ce31a339e Make kitty +list-fonts a wrapper around choose font 2024-06-22 11:16:37 +05:30
Kovid Goyal
d86da0616e Wire up the backend 2024-06-22 11:16:37 +05:30
Kovid Goyal
926dfd7ba1 Replace list_fonts with choose-fonts kitten 2024-06-22 11:16:37 +05:30
Kovid Goyal
e1b367e1b3 Use stdlib maps/slices 2024-06-22 11:16:37 +05:30
Kovid Goyal
4998fe66b9 Use RenderLines.InRectangle 2024-06-22 11:16:37 +05:30
Kovid Goyal
7f965eba5f Infrastructure for simple internal hyperlink handling 2024-06-22 11:16:37 +05:30
Kovid Goyal
0b743464fb Work on supporting mouse interactions via simple hyperlinks 2024-06-22 11:16:37 +05:30
Kovid Goyal
ffed63a048 Display all styles from STAT table 2024-06-22 11:16:37 +05:30
Kovid Goyal
96c17b0a67 Work on getting styles from STAT table data 2024-06-22 11:16:37 +05:30
Kovid Goyal
8a0b562f4f Work on listing available styles for a family 2024-06-22 11:16:37 +05:30
Kovid Goyal
198aec84c2 Load font variable data on demand 2024-06-22 11:16:37 +05:30
Kovid Goyal
217ded7b3f ... 2024-06-22 11:16:37 +05:30
Kovid Goyal
7522675553 dont use a thread for I/O with kitten 2024-06-22 11:16:36 +05:30
Kovid Goyal
8a8158f287 get multiple JSON messages working 2024-06-22 11:16:36 +05:30
Kovid Goyal
1646c297b3 List families asynchronously 2024-06-22 11:16:36 +05:30
Kovid Goyal
de9d9fd157 Wire up arrow keys for moving in family list 2024-06-22 11:16:36 +05:30
Kovid Goyal
c2e0ecef13 Wire up searching 2024-06-22 11:16:36 +05:30
Kovid Goyal
6e55949094 Start work on list-fonts kitten 2024-06-22 11:16:36 +05:30
Kovid Goyal
21f824e825 Code to read the STAT OpenType table 2024-06-22 11:16:36 +05:30
Kovid Goyal
086185e409 Needed for typing.NotRequired 2024-06-22 11:16:36 +05:30
Kovid Goyal
e3e7bded06 Refactor font selection code to share more between fontconfig and CoreText 2024-06-22 11:16:36 +05:30
Kovid Goyal
7e232b33be Make CoreText signatures for some font finding methods the same as their equivalents in fontconfig 2024-06-22 11:16:36 +05:30
Kovid Goyal
53be33f14d DRYer 2024-06-22 11:16:36 +05:30
Kovid Goyal
54b50858ec Implement basic support for selecting font variations in fontconfig 2024-06-22 11:16:36 +05:30
Kovid Goyal
46d75ea48f Wire up parsing of font specs 2024-06-22 11:16:34 +05:30
Kovid Goyal
bf73720805 Output resolved fonts in debug config 2024-06-22 11:13:23 +05:30
Kovid Goyal
eb580ed91b Implement parsing of fvar table
Cant rely on CoreText for this as it has incomplete and buggy APIs
2024-06-22 11:13:22 +05:30
Kovid Goyal
7f8ce387be Implement postscript variation name prefix for CoreText as well 2024-06-22 11:13:22 +05:30
Kovid Goyal
8ddd20770b Show the postscript variations name prefix when listing fonts 2024-06-22 11:13:22 +05:30
Kovid Goyal
53ea650c27 Use the full name from CoreText when available 2024-06-22 11:13:22 +05:30
Kovid Goyal
3b84498af9 Prune listings of variable fonts
Show only one entry per style per variable font as identified by path
2024-06-22 11:13:22 +05:30
Kovid Goyal
fa52066156 Ensure fontconfig pattern dict has all keys with default values 2024-06-22 11:13:22 +05:30
Kovid Goyal
e2e33cc11c Cleanup resource management
Also add slant to CTFont descriptor
2024-06-22 11:13:22 +05:30
Kovid Goyal
71a8801a68 Output psname for variation font 2024-06-22 11:13:22 +05:30
Kovid Goyal
e54e17acf8 Get variable font data from CoreText 2024-06-22 11:13:22 +05:30
Kovid Goyal
b57cb95522 Start work on listing variable fonts under macOS 2024-06-22 11:13:22 +05:30
Kovid Goyal
12fd1472f5 Fix building on older FreeType 2024-06-22 11:13:22 +05:30
Kovid Goyal
5a86960acd Add information about variable fonts to list-fonts output 2024-06-22 11:13:22 +05:30
Kovid Goyal
dbbc0ea9d4 Code to get variable data from freetype to python 2024-06-22 11:13:22 +05:30
Kovid Goyal
39f1ff6ead Work on list variable fonts on Linux 2024-06-22 11:13:22 +05:30
Kovid Goyal
2d088889f7 Allow fc-list to return variable fonts 2024-06-22 11:13:22 +05:30
Kovid Goyal
48a0a7fe82 Drop support for 32-bit x86 prebuilt binaries
SIMDe 0.8.2 doesnt build on 32 bit and while that will likely be fixed
eventually, 32bit isn't tested in CI and generally speaking there isn't
much use for this platform anymore. I dont know of any 32-bit computers
in common use these days.

As such the overhead of maintaining these is not worth it for me.
kitty itself remains buildable on 32-bit though no guarantees for how
long that will last. kitten remains available on 32bit.
2024-06-22 11:13:22 +05:30
Kovid Goyal
fbeead1661 bump simde to 0.8.2 2024-06-22 11:13:21 +05:30
98 changed files with 6563 additions and 781 deletions

View File

@@ -14,12 +14,13 @@ import time
from urllib.request import urlopen
BUNDLE_URL = 'https://download.calibre-ebook.com/ci/kitty/{}-64.tar.xz'
FONTS_URL = 'https://download.calibre-ebook.com/ci/fonts.tar.xz'
is_bundle = os.environ.get('KITTY_BUNDLE') == '1'
is_macos = 'darwin' in sys.platform.lower()
SW = None
SW = ''
def do_print_crash_reports():
def do_print_crash_reports() -> None:
print('Printing available crash reports...')
if is_macos:
end_time = time.monotonic() + 90
@@ -38,9 +39,9 @@ def do_print_crash_reports():
print(flush=True)
def run(*a, print_crash_reports=False):
def run(*a: str, print_crash_reports: bool = False) -> None:
if len(a) == 1:
a = shlex.split(a[0])
a = tuple(shlex.split(a[0]))
cmd = ' '.join(map(shlex.quote, a))
print(cmd)
sys.stdout.flush()
@@ -59,7 +60,16 @@ def run(*a, print_crash_reports=False):
raise SystemExit(f'The following process failed with exit code: {ret}:\n{cmd}')
def install_deps():
def install_fonts() -> None:
with urlopen(FONTS_URL) as f:
data = f.read()
fonts_dir = os.path.expanduser('~/Library/Fonts' if is_macos else '~/.local/share/fonts')
os.makedirs(fonts_dir, exist_ok=True)
with tarfile.open(fileobj=io.BytesIO(data), mode='r:xz') as tf:
tf.extractall(fonts_dir)
def install_deps() -> None:
print('Installing kitty dependencies...')
sys.stdout.flush()
if is_macos:
@@ -86,9 +96,10 @@ def install_deps():
if sys.version_info[:2] < (3, 7):
cmd += ' importlib-resources dataclasses'
run(cmd)
install_fonts()
def build_kitty():
def build_kitty() -> None:
python = shutil.which('python3') if is_bundle else sys.executable
cmd = f'{python} setup.py build --verbose'
if is_macos:
@@ -98,14 +109,14 @@ def build_kitty():
run(cmd)
def test_kitty():
def test_kitty() -> None:
if is_macos:
run('ulimit -c unlimited')
run('sudo chmod -R 777 /cores')
run('./test.py', print_crash_reports=True)
def package_kitty():
def package_kitty() -> None:
python = 'python3' if is_macos else 'python'
run(f'{python} setup.py linux-package --update-check-interval=0 --verbose')
if is_macos:
@@ -113,14 +124,14 @@ def package_kitty():
run('kitty.app/Contents/MacOS/kitty +runpy "from kitty.constants import *; print(kitty_exe())"')
def replace_in_file(path, src, dest):
def replace_in_file(path: str, src: str, dest: str) -> None:
with open(path, 'r+') as f:
n = f.read().replace(src, dest)
f.seek(0), f.truncate()
f.write(n)
def setup_bundle_env():
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['PKG_CONFIG_PATH'] = os.path.join(SW, 'lib', 'pkgconfig')
@@ -132,7 +143,7 @@ def setup_bundle_env():
os.environ['PATH'] = '{}:{}'.format(os.path.join(SW, 'bin'), os.environ['PATH'])
def install_bundle():
def install_bundle() -> None:
cwd = os.getcwd()
os.makedirs(SW)
os.chdir(SW)
@@ -152,7 +163,7 @@ def install_bundle():
os.chdir(cwd)
def main():
def main() -> None:
if is_bundle:
setup_bundle_env()
else:
@@ -168,9 +179,9 @@ def main():
elif action == 'test':
test_kitty()
elif action == 'gofmt':
q = subprocess.check_output('gofmt -s -l tools'.split())
q = subprocess.check_output('gofmt -s -l tools'.split()).decode()
if q.strip():
q = '\n'.join(filter(lambda x: not x.rstrip().endswith('_generated.go'), q.decode().strip().splitlines())).strip()
q = '\n'.join(filter(lambda x: not x.rstrip().endswith('_generated.go'), q.strip().splitlines())).strip()
if q:
raise SystemExit(q)
else:

View File

@@ -27,7 +27,7 @@ jobs:
sanitize: 0
- python: b
pyver: "3.11"
pyver: "3.10"
sanitize: 1
- python: c
@@ -82,7 +82,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.11"
- name: Install Go
uses: actions/setup-go@v4

View File

@@ -270,7 +270,7 @@
{
"name": "simde",
"comment": "Cannot update till gcc in the build VM is updated as simde 8 requires newer gcc",
"comment": "Cannot update till gcc in the build VM is updated as simde 0.8 requires newer gcc",
"unix": {
"filename": "simde-amalgamated-0.7.6.tar.xz",
"hash": "sha256:703eac1f2af7de1f7e4aea2286130b98e1addcc0559426e78304c92e2b4eb5e1",

View File

@@ -49,9 +49,7 @@ detect_os() {
amd64|x86_64) arch="x86_64";;
aarch64*) arch="arm64";;
armv8*) arch="arm64";;
i386) arch="i686";;
i686) arch="i686";;
*) die "Unknown CPU architecture $(command uname -m)";;
*) die "kitty binaries not available for architecture $(command uname -m)";;
esac
;;
*) die "kitty binaries are not available for $(command uname)"

View File

@@ -0,0 +1,51 @@
Changing kitty fonts
========================
.. only:: man
Overview
--------------
Terminal aficionados spend all day staring at text, as such, getting text
rendering just right is very important. kitty has extremely powerful facilities
for fine-tuning text rendering. It supports `OpenType features
<https://en.wikipedia.org/wiki/List_of_typographic_features>`__ to select
alternate glyph shapes, and `Variable fonts
<https://en.wikipedia.org/wiki/Variable_font>`__ to control the weight or
spacing of a font precisely. You can also :opt:`select which font is used to
render particular unicode codepoints <symbol_map>` and you can :opt:`modify
font metrics <modify_font>` and even :opt:`adjust the gamma curves
<text_composition_strategy>` used for rendering text onto the background color.
The first step is to select the font faces kitty will use for rendering
regular, bold and italic text. kitty comes with a convenient UI for choosing fonts,
in the form of the *choose-fonts* kitten. Simply run::
kitten choose-fonts
and follow the on screen prompts.
First, choose the family you want, the list of families can be easily filtered by
typing a few letters from the family name you are looking for. The family
selection screen shows you a preview of how the family looks.
.. image:: ../screenshots/family-selection.png
:alt: Choosing a family with the choose fonts kitten
:width: 600
Once you select a family by pressing the :kbd:`Enter` key, you
are shown previews of what the regular, bold and italic faces look like
for that family. You can choose to fine tune any of the faces. Start with
fine-tuning the regular face by pressing the :kbd:`R` key. The other styles
will be automatically adjusted based on what you select for the regular face.
.. image:: ../screenshots/font-fine-tune.png
:alt: Fine tune a font by choosing a precise weight and features
:width: 600
You can choose a specific style or font feature by clicking on it. A precise
value for any variable axes can be selected using the slider, in the screenshot
above the font supports precise weight adjustment. If you are lucky the font
designer has included descriptive names for font features, which will be
displayed, if not, consult the documentation of the font to see what each feature does.

View File

@@ -17,8 +17,9 @@ slow, since it requires a roundtrip to the terminal emulator and back.
If you want to do some of the same querying in your terminal program without
depending on the kitten, you can do so, by processing the same escape codes.
Search `this page <https://invisible-island.net/xterm/ctlseqs/ctlseqs.html>`__
for *XTGETTCAP* to see the syntax for the escape code and read the source of
this kitten to find the values of the keys for the various queries.
for *XTGETTCAP* to see the syntax for the escape code. The kitty specific keys
are all documented below, when sent via escape code they must be prefixed with
``kitty-query-``.
.. include:: ../generated/cli-kitten-query_terminal.rst

View File

@@ -11,6 +11,7 @@ Extend with kittens
kittens/diff
kittens/unicode_input
kittens/themes
kittens/choose-fonts
kittens/hints
kittens/remote_file
kittens/hyperlinked_grep
@@ -41,6 +42,10 @@ Some prominent kittens:
Preview and quick switch between over three hundred color themes.
:doc:`Fonts <kittens/choose-fonts>`
Preview, fine-tune and quick switch the fonts used by kitty.
:doc:`Hints <kittens/hints>`
Select and open/paste/insert arbitrary text snippets such as URLs,
filenames, words, lines, etc. from the terminal screen.

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

View File

@@ -570,10 +570,12 @@ def load_ref_map() -> Dict[str, Dict[str, str]]:
def generate_constants() -> str:
from kittens.hints.main import DEFAULT_REGEX
from kittens.query_terminal.main import all_queries
from kitty.config import option_names_for_completion
from kitty.fast_data_types import FILE_TRANSFER_CODE
from kitty.options.utils import allowed_shell_integration_values
from kitty.options.utils import allowed_shell_integration_values, url_style_map
del sys.modules['kittens.hints.main']
del sys.modules['kittens.query_terminal.main']
ref_map = load_ref_map()
with open('kitty/data-types.h') as dt:
m = re.search(r'^#define IMAGE_PLACEHOLDER_CHAR (\S+)', dt.read(), flags=re.M)
@@ -582,6 +584,8 @@ def generate_constants() -> str:
dp = ", ".join(map(lambda x: f'"{serialize_as_go_string(x)}"', kc.default_pager_for_help))
url_prefixes = ','.join(f'"{x}"' for x in Options.url_prefixes)
option_names = '`' + '\n'.join(option_names_for_completion()) + '`'
url_style = {v:k for k, v in url_style_map.items()}[Options.url_style]
query_names = ', '.join(f'"{name}"' for name in all_queries)
return f'''\
package kitty
@@ -600,6 +604,8 @@ var IsStandaloneBuild string = ""
const HandleTermiosSignals = {Mode.HANDLE_TERMIOS_SIGNALS.value[0]}
const HintsDefaultRegex = `{DEFAULT_REGEX}`
const DefaultTermName = `{Options.term}`
const DefaultUrlStyle = `{url_style}`
const DefaultUrlColor = `{Options.url_color.as_sharp}`
var Version VersionType = VersionType{{Major: {kc.version.major}, Minor: {kc.version.minor}, Patch: {kc.version.patch},}}
var DefaultPager []string = []string{{ {dp} }}
var FunctionalKeyNameAliases = map[string]string{serialize_go_dict(functional_key_name_aliases)}
@@ -608,6 +614,7 @@ var ConfigModMap = map[string]uint16{serialize_go_dict(config_mod_map)}
var RefMap = map[string]string{serialize_go_dict(ref_map['ref'])}
var DocTitleMap = map[string]string{serialize_go_dict(ref_map['doc'])}
var AllowedShellIntegrationValues = []string{{ {str(sorted(allowed_shell_integration_values))[1:-1].replace("'", '"')} }}
var QueryNames = []string{{ {query_names} }}
var KittyConfigDefaults = struct {{
Term, Shell_integration, Select_by_word_characters, Url_excluded_characters, Shell string
Wheel_scroll_multiplier int

View File

View File

@@ -0,0 +1,165 @@
package choose_fonts
import (
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"strings"
"sync"
"time"
"kitty/tools/utils"
)
var _ = fmt.Print
type kitty_font_backend_type struct {
from io.ReadCloser
to io.WriteCloser
json_decoder *json.Decoder
cmd *exec.Cmd
stderr strings.Builder
lock sync.Mutex
r io.ReadCloser
w io.WriteCloser
wait_for_exit chan error
started, exited, failed bool
timeout time.Duration
}
func (k *kitty_font_backend_type) start() (err error) {
exe := utils.KittyExe()
if exe == "" {
exe = utils.Which("kitty")
}
if exe == "" {
return fmt.Errorf("Failed to find the kitty executable, this kitten requires the kitty executable to be present. You can use the environment variable KITTY_PATH_TO_KITTY_EXE to specify the path to the kitty executable")
}
k.cmd = exec.Command(exe, "+runpy", "from kittens.choose_fonts.backend import main; main()")
k.cmd.Stderr = &k.stderr
if k.r, k.to, err = os.Pipe(); err != nil {
return err
}
k.cmd.Stdin = k.r
if k.from, k.w, err = os.Pipe(); err != nil {
return err
}
k.cmd.Stdout = k.w
k.json_decoder = json.NewDecoder(k.from)
if err = k.cmd.Start(); err != nil {
return err
}
k.started = true
k.timeout = 60 * time.Second
k.wait_for_exit = make(chan error)
go func() {
k.wait_for_exit <- k.cmd.Wait()
}()
return
}
var kitty_font_backend kitty_font_backend_type
func (k *kitty_font_backend_type) send(v any) error {
data, err := json.Marshal(v)
if err != nil {
return fmt.Errorf("Could not encode message to kitty with error: %w", err)
}
c := make(chan error)
go func() {
if _, err = k.to.Write(data); err != nil {
c <- fmt.Errorf("Failed to send message to kitty with I/O error: %w", err)
return
}
if _, err = k.to.Write([]byte{'\n'}); err != nil {
c <- fmt.Errorf("Failed to send message to kitty with I/O error: %w", err)
return
}
c <- nil
}()
select {
case err := <-c:
return err
case <-time.After(k.timeout):
return fmt.Errorf("Timed out waiting to write to kitty font backend after %v", k.timeout)
case err := <-k.wait_for_exit:
k.exited = true
if err == nil {
err = fmt.Errorf("kitty font backend exited with no error while waiting for a response from it")
} else {
k.failed = true
}
return err
}
}
func (k *kitty_font_backend_type) query(action string, cmd map[string]any, result any) error {
k.lock.Lock()
defer k.lock.Unlock()
if cmd == nil {
cmd = make(map[string]any)
}
cmd["action"] = action
if err := k.send(cmd); err != nil {
return err
}
c := make(chan error)
go func() {
if err := k.json_decoder.Decode(result); err != nil {
c <- fmt.Errorf("Failed to decode JSON from kitty with error: %w", err)
}
c <- nil
}()
select {
case err := <-c:
return err
case <-time.After(k.timeout):
return fmt.Errorf("Timed out waiting for response from kitty font backend after %v", k.timeout)
case err := <-k.wait_for_exit:
k.exited = true
if err == nil {
err = fmt.Errorf("kitty font backed exited with no error while waiting for a response from it")
} else {
k.failed = true
}
return err
}
}
func (k *kitty_font_backend_type) release() (err error) {
if k.r != nil {
k.r.Close()
k.r = nil
}
if k.to != nil {
k.to.Close()
k.to = nil
}
if k.w != nil {
k.w.Close()
k.w = nil
}
if k.from != nil {
k.from.Close()
k.from = nil
}
if k.started && !k.exited {
timeout := 2 * time.Second
select {
case err = <-k.wait_for_exit:
k.exited = true
if err != nil {
k.failed = true
}
case <-time.After(timeout):
k.failed = true
err = fmt.Errorf("Timed out waiting for kitty font backend to exit for %v", timeout)
}
}
os.Stderr.WriteString(k.stderr.String())
return
}

View File

@@ -0,0 +1,256 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
import json
import os
import string
import sys
import tempfile
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Tuple, TypedDict
from kitty.cli import create_default_opts
from kitty.conf.utils import to_color
from kitty.constants import kitten_exe
from kitty.fonts import Descriptor
from kitty.fonts.common import (
face_from_descriptor,
get_axis_map,
get_font_files,
get_named_style,
get_variable_data_for_descriptor,
get_variable_data_for_face,
is_variable,
spec_for_face,
)
from kitty.fonts.features import Type, known_features
from kitty.fonts.list import create_family_groups
from kitty.fonts.render import display_bitmap
from kitty.options.types import Options
from kitty.options.utils import parse_font_spec
from kitty.typing import NotRequired
from kitty.utils import screen_size_function
if TYPE_CHECKING:
from kitty.fast_data_types import FeatureData
def setup_debug_print() -> bool:
if 'KITTY_STDIO_FORWARDED' in os.environ:
try:
fd = int(os.environ['KITTY_STDIO_FORWARDED'])
except Exception:
return False
try:
sys.stdout = open(fd, 'w', closefd=False)
return True
except OSError:
return False
return False
def send_to_kitten(x: Any) -> None:
try:
sys.__stdout__.buffer.write(json.dumps(x).encode())
sys.__stdout__.buffer.write(b'\n')
sys.__stdout__.buffer.flush()
except BrokenPipeError:
raise SystemExit('Pipe to kitten was broken while sending data to it')
class TextStyle(TypedDict):
font_size: float
dpi_x: float
dpi_y: float
foreground: str
background: str
OptNames = Literal['font_family', 'bold_font', 'italic_font', 'bold_italic_font']
FamilyKey = Tuple[OptNames, ...]
def opts_from_cmd(cmd: Dict[str, Any]) -> Tuple[Options, FamilyKey, float, float]:
opts = Options()
ts: TextStyle = cmd['text_style']
opts.font_size = ts['font_size']
opts.foreground = to_color(ts['foreground'])
opts.background = to_color(ts['background'])
family_key = []
def d(k: OptNames) -> None:
if k in cmd:
setattr(opts, k, parse_font_spec(cmd[k]))
family_key.append(k)
d('font_family')
d('bold_font')
d('italic_font')
d('bold_italic_font')
return opts, tuple(family_key), ts['dpi_x'], ts['dpi_y']
BaseKey = Tuple[str, int, int]
FaceKey = Tuple[str, BaseKey]
RenderedSample = Tuple[bytes, Dict[str, Any]]
RenderedSampleTransmit = Dict[str, Any]
SAMPLE_TEXT = string.ascii_lowercase + ' ' + string.digits + ' ' + string.ascii_uppercase + ' ' + string.punctuation
class FD(TypedDict):
is_index: bool
name: NotRequired[str]
tooltip: NotRequired[str]
sample: NotRequired[str]
params: NotRequired[Tuple[str, ...]]
def get_features(features: Dict[str, Optional['FeatureData']]) -> Dict[str, FD]:
ans = {}
for tag, data in features.items():
kf = known_features.get(tag)
if kf is None or kf.type is Type.hidden:
continue
fd: FD = {'is_index': kf.type is Type.index}
ans[tag] = fd
if data is not None:
if n := data.get('name'):
fd['name'] = n
if n := data.get('tooltip'):
fd['tooltip'] = n
if n := data.get('sample'):
fd['sample'] = n
if p := data.get('params'):
fd['params'] = p
return ans
def render_face_sample(font: Descriptor, opts: Options, dpi_x: float, dpi_y: float, width: int, height: int) -> RenderedSample:
face = face_from_descriptor(font, opts.font_size, dpi_x, dpi_y)
face.set_size(opts.font_size, dpi_x, dpi_y)
metadata = {
'variable_data': get_variable_data_for_face(face),
'style': font['style'],
'psname': face.postscript_name(),
'features': get_features(face.get_features()),
'applied_features': face.applied_features(),
'spec': spec_for_face(font['family'], face).as_setting,
'cell_width': 0, 'cell_height': 0, 'canvas_height': 0, 'canvas_width': width,
}
if is_variable(font):
ns = get_named_style(face)
if ns:
metadata['variable_named_style'] = ns
metadata['variable_axis_map'] = get_axis_map(face)
bitmap, cell_width, cell_height = face.render_sample_text(SAMPLE_TEXT, width, height, opts.foreground.rgb)
metadata['cell_width'] = cell_width
metadata['cell_height'] = cell_height
metadata['canvas_height'] = len(bitmap) // (4 *width)
return bitmap, metadata
def render_family_sample(
opts: Options, family_key: FamilyKey, dpi_x: float, dpi_y: float, width: int, height: int, output_dir: str,
cache: Dict[FaceKey, RenderedSampleTransmit]
) -> Dict[str, RenderedSampleTransmit]:
base_key: BaseKey = opts.font_family.created_from_string, width, height
ans: Dict[str, RenderedSampleTransmit] = {}
font_files = get_font_files(opts)
for x in family_key:
key: FaceKey = x + ': ' + str(getattr(opts, x)), base_key
if x == 'font_family':
desc = font_files['medium']
elif x == 'bold_font':
desc = font_files['bold']
elif x == 'italic_font':
desc = font_files['italic']
elif x == 'bold_italic_font':
desc = font_files['bi']
cached = cache.get(key)
if cached is not None:
ans[x] = cached
else:
with tempfile.NamedTemporaryFile(delete=False, suffix='.rgba', dir=output_dir) as tf:
bitmap, metadata = render_face_sample(desc, opts, dpi_x, dpi_y, width, height)
tf.write(bitmap)
metadata['path'] = tf.name
cache[key] = ans[x] = metadata
return ans
ResolvedFace = Dict[Literal['family', 'spec', 'setting'], str]
def spec_for_descriptor(d: Descriptor, font_size: float) -> str:
face = face_from_descriptor(d, font_size, 288, 288)
return spec_for_face(d['family'], face).as_setting
def resolved_faces(opts: Options) -> Dict[OptNames, ResolvedFace]:
font_files = get_font_files(opts)
ans: Dict[OptNames, ResolvedFace] = {}
def d(key: Literal['medium', 'bold', 'italic', 'bi'], opt_name: OptNames) -> None:
descriptor = font_files[key]
ans[opt_name] = {
'family': descriptor['family'], 'spec': spec_for_descriptor(descriptor, opts.font_size),
'setting': getattr(opts, opt_name).created_from_string
}
d('medium', 'font_family')
d('bold', 'bold_font')
d('italic', 'italic_font')
d('bi', 'bold_italic_font')
return ans
def main() -> None:
setup_debug_print()
cache: Dict[FaceKey, RenderedSampleTransmit] = {}
for line in sys.stdin.buffer:
cmd = json.loads(line)
action = cmd.get('action', '')
if action == 'list_monospaced_fonts':
opts = create_default_opts()
send_to_kitten({'fonts': create_family_groups(), 'resolved_faces': resolved_faces(opts)})
elif action == 'read_variable_data':
ans = []
for descriptor in cmd['descriptors']:
ans.append(get_variable_data_for_descriptor(descriptor))
send_to_kitten(ans)
elif action == 'render_family_samples':
opts, family_key, dpi_x, dpi_y = opts_from_cmd(cmd)
send_to_kitten(render_family_sample(opts, family_key, dpi_x, dpi_y, cmd['width'], cmd['height'], cmd['output_dir'], cache))
else:
raise SystemExit(f'Unknown action: {action}')
def query_kitty() -> Dict[str, str]:
import subprocess
ans = {}
for line in subprocess.check_output([kitten_exe(), 'query-terminal']).decode().splitlines():
k, sep, v = line.partition(':')
if sep == ':':
ans[k] = v.strip()
return ans
def showcase(family: str = 'family="Fira Code"') -> None:
q = query_kitty()
opts = Options()
opts.foreground = to_color(q['foreground'])
opts.background = to_color(q['background'])
opts.font_size = float(q['font_size'])
opts.font_family = parse_font_spec(family)
font_files = get_font_files(opts)
desc = font_files['medium']
ss = screen_size_function()()
width = ss.cell_width * ss.cols
height = 5 * ss.cell_height
bitmap, m = render_face_sample(desc, opts, float(q['dpi_x']), float(q['dpi_y']), width, height)
display_bitmap(bitmap, m['canvas_width'], m['canvas_height'])
def test_render(spec: str = 'family="Fira Code"', width: int = 1560, height: int = 116, font_size: float = 12, dpi: float = 288) -> None:
opts = Options()
opts.font_family = parse_font_spec(spec)
opts.font_size = font_size
opts.foreground = to_color('white')
desc = get_font_files(opts)['medium']
bitmap, m = render_face_sample(desc, opts, float(dpi), float(dpi), width, height)
display_bitmap(bitmap, m['canvas_width'], m['canvas_height'])

View File

@@ -0,0 +1,489 @@
package choose_fonts
import (
"fmt"
"maps"
"math"
"slices"
"strconv"
"strings"
"sync"
"kitty/tools/tui"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/wcswidth"
)
var _ = fmt.Print
type face_panel struct {
handler *handler
family, which string
settings faces_settings
current_preview *RenderedSampleTransmit
current_preview_key faces_preview_key
preview_cache map[faces_preview_key]map[string]RenderedSampleTransmit
preview_cache_mutex sync.Mutex
}
// Create a new FontSpec that keeps features and axis values and named styles
// same as the current setting. Names are all reset apart from style name.
func (self *face_panel) new_font_spec() (*FontSpec, error) {
fs, err := NewFontSpec(self.get(), self.current_preview.Features)
if err != nil {
return nil, err
}
if fs.system.val == "auto" {
if fs, err = NewFontSpec(self.current_preview.Spec, self.current_preview.Features); err != nil {
return nil, err
}
}
// reset these selectors as we will be using some style/axis based selector instead
fs.family = settable_string{self.family, true}
fs.postscript_name = settable_string{}
fs.full_name = settable_string{}
if len(self.current_preview.Variable_data.Axes) > 0 {
fs.variable_name = settable_string{self.current_preview.Variable_data.Variations_postscript_name_prefix, true}
} else {
fs.variable_name = settable_string{}
}
return &fs, nil
}
func (self *face_panel) set_variable_spec(named_style string, axis_overrides map[string]float64) error {
fs, err := self.new_font_spec()
if err != nil {
return err
}
if axis_overrides != nil {
axis_values := self.current_preview.current_axis_values()
maps.Copy(axis_values, axis_overrides)
fs.axes = axis_values
fs.style = settable_string{"", false}
} else if named_style != "" {
fs.style = settable_string{named_style, true}
fs.axes = nil
}
self.set(fs.String())
return nil
}
func (self *face_panel) set_style(named_style string) error {
fs, err := self.new_font_spec()
if err != nil {
return err
}
fs.style = settable_string{named_style, true}
self.set(fs.String())
return nil
}
func (self *face_panel) render_lines(start_y int, lines ...string) (y int) {
sz, _ := self.handler.lp.ScreenSize()
_, y, str := self.handler.render_lines.InRectangle(lines, 0, start_y, int(sz.WidthCells), int(sz.HeightCells)-y, &self.handler.mouse_state, self.on_click)
self.handler.lp.QueueWriteString(str)
return
}
const current_val_style = "fg=cyan bold"
const control_name_style = "fg=yellow bright bold"
func (self *face_panel) draw_axis(sz loop.ScreenSize, y int, ax VariableAxis, axis_value float64) int {
lp := self.handler.lp
buf := strings.Builder{}
buf.WriteString(fmt.Sprintf("%s: ", lp.SprintStyled(control_name_style, utils.IfElse(ax.Strid != "", ax.Strid, ax.Tag))))
num_of_cells := int(sz.WidthCells) - wcswidth.Stringwidth(buf.String())
if num_of_cells < 5 {
return y
}
frac := (min(axis_value, ax.Maximum) - ax.Minimum) / (ax.Maximum - ax.Minimum)
current_cell := int(math.Floor(frac * float64(num_of_cells-1)))
for i := 0; i < num_of_cells; i++ {
buf.WriteString(utils.IfElse(i == current_cell, lp.SprintStyled(current_val_style, ``),
tui.InternalHyperlink("•", fmt.Sprintf("axis:%d/%d:%s", i, num_of_cells-1, ax.Tag))))
}
return self.render_lines(y, buf.String())
}
func is_current_named_style(style_group_name, style_name string, vd VariableData, ns NamedStyle) bool {
for _, dax := range vd.Design_axes {
if dax.Name == style_group_name {
if val, found := ns.Axis_values[dax.Tag]; found {
for _, v := range dax.Values {
if v.Value == val {
return v.Name == style_name
}
}
}
break
}
}
return false
}
func (self *face_panel) draw_variable_fine_tune(sz loop.ScreenSize, start_y int, preview RenderedSampleTransmit) (y int, err error) {
s := styles_for_variable_data(preview.Variable_data)
lines := []string{}
lp := self.handler.lp
for _, sg := range s.style_groups {
if len(sg.styles) < 2 {
continue
}
formatted := make([]string, len(sg.styles))
for i, style_name := range sg.styles {
if is_current_named_style(sg.name, style_name, preview.Variable_data, preview.Variable_named_style) {
formatted[i] = self.handler.lp.SprintStyled(current_val_style, style_name)
} else {
formatted[i] = tui.InternalHyperlink(style_name, "variable_style:"+style_name)
}
}
line := lp.SprintStyled(control_name_style, sg.name) + ": " + strings.Join(formatted, ", ")
lines = append(lines, line)
}
y = self.render_lines(start_y, lines...)
sub_title := "Fine tune the appearance by clicking in the variable axes below:"
axis_values := self.current_preview.current_axis_values()
for _, ax := range self.current_preview.Variable_data.Axes {
if ax.Hidden {
continue
}
if sub_title != "" {
y = self.render_lines(y+1, sub_title, "")
sub_title = ``
}
y = self.draw_axis(sz, y, ax, axis_values[ax.Tag])
}
return y, nil
}
func (self *face_panel) draw_family_style_select(_ loop.ScreenSize, start_y int, preview RenderedSampleTransmit) (y int, err error) {
lp := self.handler.lp
s := styles_in_family(self.family, self.handler.listing.fonts[self.family])
lines := []string{}
for _, sg := range s.style_groups {
formatted := make([]string, len(sg.styles))
for i, style_name := range sg.styles {
if style_name == preview.Style {
formatted[i] = lp.SprintStyled(current_val_style, style_name)
} else {
formatted[i] = tui.InternalHyperlink(style_name, "style:"+style_name)
}
}
line := lp.SprintStyled(control_name_style, sg.name) + ": " + strings.Join(formatted, ", ")
lines = append(lines, line)
}
y = self.render_lines(start_y, lines...)
return y, nil
}
func (self *face_panel) draw_font_features(_ loop.ScreenSize, start_y int, preview RenderedSampleTransmit) (y int, err error) {
lp := self.handler.lp
y = start_y
if len(preview.Features) == 0 {
return
}
formatted := make([]string, 0, len(preview.Features))
sort_keys := make(map[string]string)
for feat_tag, data := range preview.Features {
var text, sort_key string
if preview.Applied_features[feat_tag] != "" {
text = preview.Applied_features[feat_tag]
sort_key = text
if sort_key[0] == '-' || sort_key[1] == '+' {
sort_key = sort_key[1:]
}
text = strings.Replace(text, "+", lp.SprintStyled("fg=green", "+"), 1)
text = strings.Replace(text, "-", lp.SprintStyled("fg=red", "-"), 1)
text = strings.Replace(text, "=", lp.SprintStyled("fg=cyan", "="), 1)
if data.Name != "" {
text = data.Name + ": " + text
sort_key = data.Name
}
} else {
if data.Name != "" {
text = data.Name
sort_key = data.Name + ": " + text
} else {
text = feat_tag
sort_key = text
}
text = lp.SprintStyled("dim", text)
}
f := tui.InternalHyperlink(text, "feature:"+feat_tag)
sort_keys[f] = strings.ToLower(sort_key)
formatted = append(formatted, f)
}
utils.StableSortWithKey(formatted, func(a string) string { return sort_keys[a] })
line := lp.SprintStyled(control_name_style, `Features`) + ": " + strings.Join(formatted, ", ")
y = self.render_lines(start_y, ``, line)
return
}
func (self *handler) draw_preview_header(x int) {
sz, _ := self.lp.ScreenSize()
width := int(sz.WidthCells) - x
p := center_string(self.lp.SprintStyled("italic", " preview "), width, "─")
self.lp.QueueWriteString(self.lp.SprintStyled("dim", p))
}
func (self *face_panel) render_preview(key faces_preview_key) {
var r map[string]RenderedSampleTransmit
s := key.settings
self.handler.set_worker_error(kitty_font_backend.query("render_family_samples", map[string]any{
"text_style": self.handler.text_style, "font_family": s.font_family,
"bold_font": s.bold_font, "italic_font": s.italic_font, "bold_italic_font": s.bold_italic_font,
"width": key.width, "height": key.height, "output_dir": self.handler.temp_dir,
}, &r))
self.preview_cache_mutex.Lock()
defer self.preview_cache_mutex.Unlock()
self.preview_cache[key] = r
}
func (self *face_panel) draw_screen() (err error) {
lp := self.handler.lp
lp.SetCursorVisible(false)
sz, _ := lp.ScreenSize()
styled := lp.SprintStyled
wt := "Regular"
switch self.which {
case "bold_font":
wt = "Bold"
case "italic_font":
wt = "Italic"
case "bold_italic_font":
wt = "Bold-Italic font"
}
lp.QueueWriteString(self.handler.format_title(fmt.Sprintf("%s: %s face", self.family, wt), 0))
lines := []string{
fmt.Sprintf("Press %s to accept any changes or %s to cancel. Click on a style name below to switch to it.", styled("fg=green", "Enter"), styled("fg=red", "Esc")), "",
fmt.Sprintf("Current setting: %s", self.get()), "",
}
y := self.render_lines(2, lines...)
num_lines_per_font := (int(sz.HeightCells) - y - 1) - 2
num_lines := max(1, num_lines_per_font)
key := faces_preview_key{settings: self.settings, width: int(sz.WidthCells * sz.CellWidth), height: int(sz.CellHeight) * num_lines}
self.current_preview_key = key
self.preview_cache_mutex.Lock()
defer self.preview_cache_mutex.Unlock()
previews, found := self.preview_cache[key]
if !found {
self.preview_cache[key] = make(map[string]RenderedSampleTransmit)
go func() {
self.render_preview(key)
self.handler.lp.WakeupMainThread()
}()
return
}
if len(previews) < 4 {
return
}
preview := previews[self.which]
self.current_preview = &preview
if len(preview.Variable_data.Axes) > 0 {
y, err = self.draw_variable_fine_tune(sz, y, preview)
} else {
y, err = self.draw_family_style_select(sz, y, preview)
}
if err != nil {
return err
}
if y, err = self.draw_font_features(sz, y, preview); err != nil {
return err
}
num_lines = int(math.Ceil(float64(preview.Canvas_height) / float64(sz.CellHeight)))
if int(sz.HeightCells)-y >= num_lines+2 {
y++
lp.MoveCursorTo(1, y+1)
self.handler.draw_preview_header(0)
y++
lp.MoveCursorTo(1, y+1)
self.handler.graphics_manager.display_image(0, preview.Path, preview.Canvas_width, preview.Canvas_height)
}
return
}
func (self *face_panel) initialize(h *handler) (err error) {
self.handler = h
self.preview_cache = make(map[faces_preview_key]map[string]RenderedSampleTransmit)
return
}
func (self *face_panel) on_wakeup() error {
return self.handler.draw_screen()
}
func (self *face_panel) get() string {
switch self.which {
case "font_family":
return self.settings.font_family
case "bold_font":
return self.settings.bold_font
case "italic_font":
return self.settings.italic_font
case "bold_italic_font":
return self.settings.bold_italic_font
}
panic(fmt.Sprintf("Unknown self.which value: %s", self.which))
}
func (self *face_panel) set(setting string) {
switch self.which {
case "font_family":
self.settings.font_family = setting
case "bold_font":
self.settings.bold_font = setting
case "italic_font":
self.settings.italic_font = setting
case "bold_italic_font":
self.settings.bold_italic_font = setting
}
}
func (self *face_panel) update_feature_in_setting(pff ParsedFontFeature) error {
fs, err := self.new_font_spec()
if err != nil {
return err
}
found := false
for _, f := range fs.features {
if f.tag == pff.tag {
f.val = pff.val
found = true
break
}
}
if !found {
fs.features = append(fs.features, &pff)
}
self.set(fs.String())
return nil
}
func (self *face_panel) remove_feature_in_setting(tag string) error {
fs, err := self.new_font_spec()
if err != nil {
return err
}
if len(fs.features) > 0 {
fs.features = slices.DeleteFunc(fs.features, func(x *ParsedFontFeature) bool {
return x.tag == tag
})
}
self.set(fs.String())
return nil
}
func (self *face_panel) change_feature_value(tag string, val uint, remove bool) error {
if remove {
return self.remove_feature_in_setting(tag)
}
pff := ParsedFontFeature{tag: tag, val: val}
return self.update_feature_in_setting(pff)
}
func (self *face_panel) handle_click_on_feature(feat_tag string) error {
d := self.current_preview.Features[feat_tag]
if d.Is_index {
var current_val uint
for q, serialized := range self.current_preview.Applied_features {
if q == feat_tag && serialized != "" {
if _, num, found := strings.Cut(serialized, "="); found {
if v, err := strconv.ParseUint(num, 10, 0); err == nil {
current_val = uint(v)
}
} else {
current_val = utils.IfElse(serialized[0] == '-', uint(0), uint(1))
}
return self.handler.if_pane.on_enter(self.family, self.which, self.settings, feat_tag, d, current_val)
}
}
return self.handler.if_pane.on_enter(self.family, self.which, self.settings, feat_tag, d, current_val)
} else {
for q, serialized := range self.current_preview.Applied_features {
if q == feat_tag && serialized != "" {
if serialized[0] == '-' {
return self.remove_feature_in_setting(feat_tag)
}
return self.update_feature_in_setting(ParsedFontFeature{tag: feat_tag, is_bool: true, val: 0})
}
}
return self.update_feature_in_setting(ParsedFontFeature{tag: feat_tag, is_bool: true, val: 1})
}
}
func (self *face_panel) on_click(id string) (err error) {
scheme, val, _ := strings.Cut(id, ":")
switch scheme {
case "style":
if err = self.set_style(val); err != nil {
return err
}
case "variable_style":
if err = self.set_variable_spec(val, nil); err != nil {
return err
}
case "feature":
if err = self.handle_click_on_feature(val); err != nil {
return err
}
case "axis":
p, tag, _ := strings.Cut(val, ":")
num, den, _ := strings.Cut(p, "/")
n, _ := strconv.Atoi(num)
d, _ := strconv.Atoi(den)
frac := float64(n) / float64(d)
for _, ax := range self.current_preview.Variable_data.Axes {
if ax.Tag == tag {
axval := ax.Minimum + (ax.Maximum-ax.Minimum)*frac
if err = self.set_variable_spec("", map[string]float64{tag: axval}); err != nil {
return err
}
break
}
}
}
// Render preview synchronously to void flashing
key := self.current_preview_key
key.settings = self.settings
self.preview_cache_mutex.Lock()
previews := self.preview_cache[key]
self.preview_cache_mutex.Unlock()
if len(previews) < 4 {
self.render_preview(key)
}
return self.handler.draw_screen()
}
func (self *face_panel) on_key_event(event *loop.KeyEvent) (err error) {
if event.MatchesPressOrRepeat("esc") {
event.Handled = true
self.handler.current_pane = &self.handler.faces
return self.handler.draw_screen()
} else if event.MatchesPressOrRepeat("enter") {
event.Handled = true
self.handler.current_pane = &self.handler.faces
self.handler.faces.settings = self.settings
return self.handler.draw_screen()
}
return
}
func (self *face_panel) on_text(text string, from_key_event bool, in_bracketed_paste bool) (err error) {
return
}
func (self *face_panel) on_enter(family, which string, settings faces_settings) error {
self.family = family
self.settings = settings
self.which = which
self.handler.current_pane = self
return self.handler.draw_screen()
}

View File

@@ -0,0 +1,161 @@
package choose_fonts
import (
"fmt"
"math"
"sync"
"kitty/tools/tui/loop"
"kitty/tools/utils"
)
var _ = fmt.Print
type faces_settings struct {
font_family, bold_font, italic_font, bold_italic_font string
}
type faces_preview_key struct {
settings faces_settings
width, height int
}
type faces struct {
handler *handler
family string
settings faces_settings
preview_cache map[faces_preview_key]map[string]RenderedSampleTransmit
preview_cache_mutex sync.Mutex
}
const highlight_key_style = "fg=magenta bold"
func (self *faces) draw_screen() (err error) {
lp := self.handler.lp
lp.SetCursorVisible(false)
sz, _ := lp.ScreenSize()
styled := lp.SprintStyled
lp.QueueWriteString(self.handler.format_title(self.family, 0))
lines := []string{
fmt.Sprintf("Press %s to select this font, %s to go back to the font list or any of the %s keys below to fine-tune the appearance of the individual font styles.", styled("fg=green", "Enter"), styled("fg=red", "Esc"), styled(highlight_key_style, "highlighted")), "",
}
_, y, str := self.handler.render_lines.InRectangle(lines, 0, 2, int(sz.WidthCells), int(sz.HeightCells), &self.handler.mouse_state, self.on_click)
lp.QueueWriteString(str)
num_lines_per_font := ((int(sz.HeightCells) - y - 1) / 4) - 2
num_lines := max(1, num_lines_per_font)
key := faces_preview_key{settings: self.settings, width: int(sz.WidthCells * sz.CellWidth), height: int(sz.CellHeight) * num_lines}
self.preview_cache_mutex.Lock()
defer self.preview_cache_mutex.Unlock()
previews, found := self.preview_cache[key]
if !found {
self.preview_cache[key] = make(map[string]RenderedSampleTransmit)
go func() {
var r map[string]RenderedSampleTransmit
s := key.settings
self.handler.set_worker_error(kitty_font_backend.query("render_family_samples", map[string]any{
"text_style": self.handler.text_style, "font_family": s.font_family,
"bold_font": s.bold_font, "italic_font": s.italic_font, "bold_italic_font": s.bold_italic_font,
"width": key.width, "height": key.height, "output_dir": self.handler.temp_dir,
}, &r))
self.preview_cache_mutex.Lock()
defer self.preview_cache_mutex.Unlock()
self.preview_cache[key] = r
self.handler.lp.WakeupMainThread()
}()
return
}
if len(previews) < 4 {
return
}
slot := 0
d := func(setting, title string) {
r := previews[setting]
num_lines := int(math.Ceil(float64(r.Canvas_height) / float64(sz.CellHeight)))
if int(sz.HeightCells)-y < num_lines+1 {
return
}
lp.MoveCursorTo(1, y+1)
_, y, str = self.handler.render_lines.InRectangle([]string{title + ": " + previews[setting].Psname}, 0, y, int(sz.WidthCells), int(sz.HeightCells), &self.handler.mouse_state, self.on_click)
lp.QueueWriteString(str)
if y+num_lines < int(sz.HeightCells) {
lp.MoveCursorTo(1, y+1)
self.handler.graphics_manager.display_image(slot, r.Path, r.Canvas_width, r.Canvas_height)
slot++
y += num_lines + 1
}
}
d(`font_family`, styled(highlight_key_style, "R")+`egular`)
d(`bold_font`, styled(highlight_key_style, "B")+`old`)
d(`italic_font`, styled(highlight_key_style, "I")+`talic`)
d(`bold_italic_font`, "B"+styled(highlight_key_style, "o")+`ld-Italic`)
return
}
func (self *faces) initialize(h *handler) (err error) {
self.handler = h
self.preview_cache = make(map[faces_preview_key]map[string]RenderedSampleTransmit)
return
}
func (self *faces) on_wakeup() error {
return self.handler.draw_screen()
}
func (self *faces) on_click(id string) (err error) {
return
}
func (self *faces) on_key_event(event *loop.KeyEvent) (err error) {
if event.MatchesPressOrRepeat("esc") {
event.Handled = true
self.handler.current_pane = &self.handler.listing
return self.handler.draw_screen()
}
if event.MatchesPressOrRepeat("enter") {
event.Handled = true
return self.handler.final_pane.on_enter(self.family, self.settings)
}
return
}
func (self *faces) on_text(text string, from_key_event bool, in_bracketed_paste bool) (err error) {
if from_key_event {
which := ""
switch text {
case "r", "R":
which = "font_family"
case "b", "B":
which = "bold_font"
case "i", "I":
which = "italic_font"
case "o", "O":
which = "bold_italic_font"
}
if which != "" {
return self.handler.face_pane.on_enter(self.family, which, self.settings)
}
}
return
}
func (self *faces) on_enter(family string) error {
if family != "" {
self.family = family
r := self.handler.listing.resolved_faces_from_kitty_conf
d := func(conf ResolvedFace, setting *string, defval string) {
s := utils.IfElse(conf.Setting == "auto", "auto", conf.Spec)
*setting = utils.IfElse(family == conf.Family, s, defval)
}
d(r.Font_family, &self.settings.font_family, fmt.Sprintf(`family="%s"`, family))
d(r.Bold_font, &self.settings.bold_font, "auto")
d(r.Italic_font, &self.settings.italic_font, "auto")
d(r.Bold_italic_font, &self.settings.bold_italic_font, "auto")
}
self.handler.current_pane = self
return self.handler.draw_screen()
}

View File

@@ -0,0 +1,161 @@
package choose_fonts
import (
"fmt"
"kitty/tools/tui"
"kitty/tools/tui/subseq"
"kitty/tools/utils"
"kitty/tools/wcswidth"
)
var _ = fmt.Print
type FamilyList struct {
families, all_families []string
current_search string
display_strings []string
widths []int
max_width, current_idx int
}
func (self *FamilyList) Len() int {
return len(self.families)
}
func (self *FamilyList) Select(family string) bool {
for idx, q := range self.families {
if q == family {
self.current_idx = idx
return true
}
}
return false
}
func (self *FamilyList) Next(delta int, allow_wrapping bool) bool {
l := func() int { return self.Len() }
if l() == 0 {
return false
}
idx := self.current_idx + delta
if !allow_wrapping && (idx < 0 || idx > l()) {
return false
}
for idx < 0 {
idx += l()
}
self.current_idx = idx % l()
return true
}
func limit_lengths(text string) string {
t, _ := wcswidth.TruncateToVisualLengthWithWidth(text, 31)
if len(t) >= len(text) {
return text
}
return t + "…"
}
func match(expression string, items []string) []*subseq.Match {
matches := subseq.ScoreItems(expression, items, subseq.Options{Level1: " "})
matches = utils.StableSort(matches, func(a, b *subseq.Match) int {
if b.Score < a.Score {
return -1
}
if b.Score > a.Score {
return 1
}
return 0
})
return matches
}
const (
MARK_BEFORE = "\033[33m"
MARK_AFTER = "\033[39m"
)
func apply_search(families []string, expression string, marks ...string) (matched_families []string, display_strings []string) {
mark_before, mark_after := MARK_BEFORE, MARK_AFTER
if len(marks) == 2 {
mark_before, mark_after = marks[0], marks[1]
}
results := utils.Filter(match(expression, families), func(x *subseq.Match) bool { return x.Score > 0 })
matched_families = make([]string, 0, len(results))
display_strings = make([]string, 0, len(results))
for _, m := range results {
text := m.Text
positions := m.Positions
for i := len(positions) - 1; i >= 0; i-- {
p := positions[i]
text = text[:p] + mark_before + text[p:p+1] + mark_after + text[p+1:]
}
display_strings = append(display_strings, text)
matched_families = append(matched_families, m.Text)
}
return
}
func make_family_names_clickable(family string) string {
id := wcswidth.StripEscapeCodes(family)
return tui.InternalHyperlink(family, "family-chosen:"+id)
}
func (self *FamilyList) UpdateFamilies(families []string) {
self.families, self.all_families = families, families
if self.current_search != "" {
self.families, self.display_strings = apply_search(self.all_families, self.current_search)
self.display_strings = utils.Map(limit_lengths, self.display_strings)
} else {
self.display_strings = utils.Map(limit_lengths, families)
}
self.display_strings = utils.Map(make_family_names_clickable, self.display_strings)
self.widths = utils.Map(wcswidth.Stringwidth, self.display_strings)
self.max_width = utils.Max(0, self.widths...)
self.current_idx = 0
}
func (self *FamilyList) UpdateSearch(query string) bool {
if query == self.current_search || len(self.all_families) == 0 {
return false
}
self.current_search = query
self.UpdateFamilies(self.all_families)
return true
}
type Line struct {
text string
width int
is_current bool
}
func (self *FamilyList) Lines(num_rows int) []Line {
if num_rows < 1 {
return nil
}
ans := make([]Line, 0, len(self.display_strings))
before_num := utils.Min(self.current_idx, num_rows-1)
start := self.current_idx - before_num
for i := start; i < utils.Min(start+num_rows, len(self.display_strings)); i++ {
ans = append(ans, Line{self.display_strings[i], self.widths[i], i == self.current_idx})
}
return ans
}
func (self *FamilyList) SelectFamily(family string) bool {
for i, f := range self.families {
if f == family {
self.current_idx = i
return true
}
}
return false
}
func (self *FamilyList) CurrentFamily() string {
if self.current_idx >= 0 && self.current_idx < len(self.families) {
return self.families[self.current_idx]
}
return ""
}

View File

@@ -0,0 +1,118 @@
package choose_fonts
import (
"fmt"
"path/filepath"
"strings"
"kitty/tools/config"
"kitty/tools/tui/loop"
"kitty/tools/utils"
)
var _ = fmt.Print
type final_pane struct {
handler *handler
settings faces_settings
family string
lp *loop.Loop
}
func (self *final_pane) render_lines(start_y int, lines ...string) (y int) {
sz, _ := self.handler.lp.ScreenSize()
_, y, str := self.handler.render_lines.InRectangle(lines, 0, start_y, int(sz.WidthCells), int(sz.HeightCells)-y, &self.handler.mouse_state, self.on_click)
self.handler.lp.QueueWriteString(str)
return
}
func (self *final_pane) draw_screen() (err error) {
s := self.lp.SprintStyled
h := func(x string) string { return s(highlight_key_style, x) }
self.render_lines(0,
fmt.Sprintf("You have chosen the %s family", s(current_val_style, self.family)),
"",
"What would you like to do?",
"",
fmt.Sprintf("%s to modify %s and use the new fonts", h("Enter"), s("italic", `kitty.conf`)),
"",
fmt.Sprintf("%s to abort and return to font selection", h("Esc")),
"",
fmt.Sprintf("%s to write the new font settings to %s", h("s"), s("italic", `STDOUT`)),
"",
fmt.Sprintf("%s to quit", h("Ctrl+c")),
)
return
}
func (self *final_pane) initialize(h *handler) (err error) {
self.handler = h
self.lp = h.lp
return
}
func (self *final_pane) on_wakeup() error {
return self.handler.draw_screen()
}
func (self *final_pane) on_click(id string) (err error) {
return
}
func (self faces_settings) serialized() string {
return strings.Join([]string{
"font_family " + self.font_family,
"bold_font " + self.bold_font,
"italic_font " + self.italic_font,
"bold_italic_font " + self.bold_italic_font,
}, "\n")
}
func (self *final_pane) on_key_event(event *loop.KeyEvent) (err error) {
if event.MatchesPressOrRepeat("esc") {
event.Handled = true
self.handler.current_pane = &self.handler.faces
return self.handler.draw_screen()
}
if event.MatchesPressOrRepeat("enter") {
event.Handled = true
patcher := config.Patcher{Write_backup: true}
path := filepath.Join(utils.ConfigDir(), "kitty.conf")
updated, err := patcher.Patch(path, "KITTY_FONTS", self.settings.serialized(), "font_family", "bold_font", "italic_font", "bold_italic_font")
if err != nil {
return err
}
if updated {
switch self.handler.opts.Reload_in {
case "parent":
config.ReloadConfigInKitty(true)
case "all":
config.ReloadConfigInKitty(false)
}
}
self.lp.Quit(0)
return nil
}
return
}
func (self *final_pane) on_text(text string, from_key_event bool, in_bracketed_paste bool) (err error) {
if from_key_event {
switch text {
case "s", "S":
output_on_exit = self.settings.serialized() + "\n"
self.lp.Quit(0)
return
}
}
return
}
func (self *final_pane) on_enter(family string, settings faces_settings) error {
self.settings = settings
self.family = family
self.handler.current_pane = self
return self.handler.draw_screen()
}

View File

@@ -0,0 +1,106 @@
package choose_fonts
import (
"fmt"
"strings"
"kitty/tools/tui/graphics"
"kitty/tools/tui/loop"
"kitty/tools/utils"
)
var _ = fmt.Print
type image struct {
id, image_number uint32
current_file string
}
func (i image) new_graphics_command() *graphics.GraphicsCommand {
gc := &graphics.GraphicsCommand{}
if i.id > 0 {
gc.SetImageId(i.id)
} else {
gc.SetImageNumber(i.image_number)
}
return gc
}
type graphics_manager struct {
main, bold, italic, bi, extra image
lp *loop.Loop
images [5]*image
}
func (g *graphics_manager) initialize(lp *loop.Loop) {
g.images = [5]*image{&g.main, &g.bold, &g.italic, &g.bi, &g.extra}
g.lp = lp
payload := []byte("123")
buf := strings.Builder{}
gc := &graphics.GraphicsCommand{}
gc.SetImageNumber(7891230).SetTransmission(graphics.GRT_transmission_direct).SetDataWidth(1).SetDataHeight(1).SetFormat(
graphics.GRT_format_rgb).SetDataSize(uint64(len(payload)))
d := func() uint32 {
im := gc.ImageNumber()
im++
gc.SetImageNumber(im)
_ = gc.WriteWithPayloadTo(&buf, payload)
return im
}
for _, img := range g.images {
img.image_number = d()
}
lp.QueueWriteString(buf.String())
}
func (g *graphics_manager) clear_placements() {
buf := strings.Builder{}
for _, img := range g.images {
if img.current_file == "" {
continue
}
gc := img.new_graphics_command()
gc.SetAction(graphics.GRT_action_delete)
gc.SetDelete(utils.IfElse(img.id > 0, graphics.GRT_delete_by_id, graphics.GRT_delete_by_number))
gc.WriteWithPayloadTo(&buf, nil)
}
g.lp.QueueWriteString(buf.String())
}
func (g *graphics_manager) display_image(slot int, path string, img_width, img_height int) {
img := g.images[slot]
if img.current_file != path {
gc := img.new_graphics_command()
gc.SetAction(graphics.GRT_action_transmit).SetDataWidth(uint64(img_width)).SetDataHeight(uint64(img_height)).SetTransmission(graphics.GRT_transmission_file)
gc.WriteWithPayloadToLoop(g.lp, []byte(path))
img.current_file = path
}
gc := img.new_graphics_command()
gc.SetAction(graphics.GRT_action_display).SetCursorMovement(graphics.GRT_cursor_static)
gc.WriteWithPayloadToLoop(g.lp, nil)
}
func (g *graphics_manager) on_response(gc *graphics.GraphicsCommand) (err error) {
if gc.ResponseMessage() != "OK" {
return fmt.Errorf("Failed to load image with error: %s", gc.ResponseMessage())
}
for _, img := range g.images {
if img.image_number == gc.ImageNumber() {
img.id = gc.ImageId()
break
}
}
return
}
func (g *graphics_manager) finalize() {
buf := strings.Builder{}
for _, img := range g.images {
gc := img.new_graphics_command()
gc.SetAction(graphics.GRT_action_delete)
gc.SetDelete(utils.IfElse(img.id > 0, graphics.GRT_free_by_id, graphics.GRT_free_by_number))
gc.WriteWithPayloadTo(&buf, nil)
}
g.lp.QueueWriteString(buf.String())
}

View File

@@ -0,0 +1,149 @@
package choose_fonts
import (
"fmt"
"strconv"
"strings"
"kitty/tools/tui"
"kitty/tools/tui/loop"
"kitty/tools/tui/readline"
"kitty/tools/utils"
)
var _ = fmt.Print
type if_panel struct {
handler *handler
rl *readline.Readline
family, which, feat_tag string
settings faces_settings
feature_data FeatureData
current_val uint
}
func (self *if_panel) render_lines(start_y int, lines ...string) (y int) {
sz, _ := self.handler.lp.ScreenSize()
_, y, str := self.handler.render_lines.InRectangle(lines, 0, start_y, int(sz.WidthCells), int(sz.HeightCells)-y, &self.handler.mouse_state, self.on_click)
self.handler.lp.QueueWriteString(str)
return
}
func (self *if_panel) draw_screen() (err error) {
lp := self.handler.lp
feat_name := utils.IfElse(self.feature_data.Name == "", self.feat_tag, self.feature_data.Name)
lp.QueueWriteString(self.handler.format_title("Edit "+feat_name, 0))
lines := []string{
fmt.Sprintf("Enter a value for the '%s' feature of the %s font. Values are non-negative integers. Leaving it blank will cause the feature value to be not set, i.e. take its default value.", feat_name, self.family),
}
if self.feature_data.Tooltip != "" {
lines = append(lines, "")
lines = append(lines, self.feature_data.Tooltip)
}
if len(self.feature_data.Params) > 0 {
lines = append(lines, "")
lines = append(lines, "You can also click on any of the feature names below to choose the corresponding value.")
} else {
lines = append(lines, "")
lines = append(lines, "Consult the documentation for this font to find out what values are valid for this feature.")
}
lines = append(lines, "")
cursor_y := self.render_lines(2, lines...)
if len(self.feature_data.Params) > 0 {
lp.MoveCursorTo(1, cursor_y+3)
num := 1
strings.Join(utils.Map(func(x string) string {
ans := tui.InternalHyperlink(x, fmt.Sprintf("fval:%d", num))
num++
return ans
}, self.feature_data.Params), ", ")
}
lp.MoveCursorTo(1, cursor_y+1)
lp.ClearToEndOfLine()
self.rl.RedrawNonAtomic()
lp.SetCursorVisible(true)
return
}
func (self *if_panel) initialize(h *handler) (err error) {
self.handler = h
self.rl = readline.New(h.lp, readline.RlInit{DontMarkPrompts: true, Prompt: "Value: "})
return
}
func (self *if_panel) on_wakeup() error {
return self.handler.draw_screen()
}
func (self *if_panel) on_click(id string) (err error) {
scheme, val, _ := strings.Cut(id, ":")
if scheme != "fval" {
return
}
v, _ := strconv.ParseUint(val, 10, 64)
if err = self.handler.face_pane.change_feature_value(self.feat_tag, uint(v), false); err != nil {
return err
}
self.handler.current_pane = &self.handler.face_pane
return self.handler.draw_screen()
}
func (self *if_panel) on_key_event(event *loop.KeyEvent) (err error) {
if event.MatchesPressOrRepeat("esc") {
event.Handled = true
self.handler.current_pane = &self.handler.face_pane
return self.handler.draw_screen()
}
if event.MatchesPressOrRepeat("enter") {
event.Handled = true
text := strings.TrimSpace(self.rl.AllText())
remove := false
var val uint64
if text == "" {
remove = true
} else {
val, err = strconv.ParseUint(text, 10, 0)
}
if err != nil {
self.rl.ResetText()
self.handler.lp.Beep()
} else {
if err = self.handler.face_pane.change_feature_value(self.feat_tag, uint(val), remove); err != nil {
return err
}
self.handler.current_pane = &self.handler.face_pane
}
return self.handler.draw_screen()
}
if err = self.rl.OnKeyEvent(event); err != nil {
if err == readline.ErrAcceptInput {
return nil
}
return err
}
return self.draw_screen()
}
func (self *if_panel) on_text(text string, from_key_event bool, in_bracketed_paste bool) (err error) {
if err = self.rl.OnText(text, from_key_event, in_bracketed_paste); err != nil {
return err
}
return self.draw_screen()
}
func (self *if_panel) on_enter(family, which string, settings faces_settings, feat_tag string, fd FeatureData, current_val uint) error {
self.family = family
self.feat_tag = feat_tag
self.settings = settings
self.which = which
self.handler.current_pane = self
self.feature_data = fd
self.current_val = current_val
self.rl.ResetText()
if self.current_val > 0 {
self.rl.SetText(strconv.Itoa(int(self.current_val)))
}
return self.handler.draw_screen()
}

View File

@@ -0,0 +1,319 @@
package choose_fonts
import (
"fmt"
"strings"
"sync"
"kitty/tools/tui/loop"
"kitty/tools/tui/readline"
"kitty/tools/utils"
"kitty/tools/utils/style"
"kitty/tools/wcswidth"
)
var _ = fmt.Print
type preview_cache_key struct {
family string
width, height int
}
type preview_cache_value struct {
path string
width, height int
}
type FontList struct {
rl *readline.Readline
family_list FamilyList
fonts map[string][]ListedFont
family_list_updated bool
resolved_faces_from_kitty_conf ResolvedFaces
handler *handler
variable_data_requested_for *utils.Set[string]
preview_cache map[preview_cache_key]preview_cache_value
preview_cache_mutex sync.Mutex
}
func (self *FontList) initialize(h *handler) error {
self.handler = h
self.preview_cache = make(map[preview_cache_key]preview_cache_value)
self.rl = readline.New(h.lp, readline.RlInit{DontMarkPrompts: true, Prompt: "Family: "})
self.variable_data_requested_for = utils.NewSet[string](256)
return nil
}
func (self *FontList) draw_search_bar() {
lp := self.handler.lp
lp.SetCursorVisible(true)
lp.SetCursorShape(loop.BAR_CURSOR, true)
sz, err := lp.ScreenSize()
if err != nil {
return
}
lp.MoveCursorTo(1, int(sz.HeightCells))
lp.ClearToEndOfLine()
self.rl.RedrawNonAtomic()
}
const SEPARATOR = "║"
func center_string(x string, width int, filler ...string) string {
space := " "
if len(filler) > 0 {
space = filler[0]
}
l := wcswidth.Stringwidth(x)
spaces := int(float64(width-l) / 2)
space = strings.Repeat(space, utils.Max(0, spaces))
return space + x + space
}
func (self *handler) format_title(title string, start_x int) string {
sz, _ := self.lp.ScreenSize()
return self.lp.SprintStyled("fg=green bold", center_string(title, int(sz.WidthCells)-start_x))
}
func (self *FontList) draw_family_summary(start_x int, sz loop.ScreenSize) (err error) {
lp := self.handler.lp
family := self.family_list.CurrentFamily()
if family == "" || int(sz.WidthCells) < start_x+2 {
return nil
}
lines := []string{self.handler.format_title(family, start_x), ""}
width := int(sz.WidthCells) - start_x - 1
add_line := func(x string) {
lines = append(lines, style.WrapTextAsLines(x, width, style.WrapOptions{})...)
}
fonts := self.fonts[family]
if len(fonts) == 0 {
return fmt.Errorf("The family: %s has no fonts", family)
}
if has_variable_data_for_font(fonts[0]) {
s := styles_in_family(family, fonts)
for _, sg := range s.style_groups {
styles := lp.SprintStyled(control_name_style, sg.name) + ": " + strings.Join(sg.styles, ", ")
add_line(styles)
add_line("")
}
if s.has_variable_faces {
add_line(fmt.Sprintf("This font is %s allowing for finer style control", lp.SprintStyled("fg=magenta", "variable")))
}
add_line(fmt.Sprintf("Press the %s key to choose this family", lp.SprintStyled("fg=yellow", "Enter")))
} else {
lines = append(lines, "Reading font data, please wait…")
key := fonts[0].cache_key()
if !self.variable_data_requested_for.Has(key) {
self.variable_data_requested_for.Add(key)
go func() {
self.handler.set_worker_error(ensure_variable_data_for_fonts(fonts...))
lp.WakeupMainThread()
}()
}
}
y := 0
for _, line := range lines {
if y >= int(sz.HeightCells)-1 {
break
}
lp.MoveCursorTo(start_x+1, y+1)
lp.QueueWriteString(line)
y++
}
if self.handler.text_style.Background != "" {
return self.draw_preview(start_x, y, sz)
}
return
}
func (self *FontList) draw_preview(x, y int, sz loop.ScreenSize) (err error) {
width_cells, height_cells := int(sz.WidthCells)-x, int(sz.HeightCells)-y
if height_cells < 3 {
return
}
y++
self.handler.lp.MoveCursorTo(x+1, y+1)
self.handler.draw_preview_header(x)
y++
height_cells -= 2
self.handler.lp.MoveCursorTo(x+1, y+1)
key := preview_cache_key{
family: self.family_list.CurrentFamily(), width: int(sz.CellWidth) * width_cells, height: int(sz.CellHeight) * height_cells,
}
if key.family == "" {
return
}
self.preview_cache_mutex.Lock()
defer self.preview_cache_mutex.Unlock()
cc := self.preview_cache[key]
switch cc.path {
case "":
self.preview_cache[key] = preview_cache_value{path: "requested"}
go func() {
var r map[string]RenderedSampleTransmit
self.handler.set_worker_error(kitty_font_backend.query("render_family_samples", map[string]any{
"text_style": self.handler.text_style, "font_family": key.family, "width": key.width, "height": key.height,
"output_dir": self.handler.temp_dir,
}, &r))
self.preview_cache_mutex.Lock()
defer self.preview_cache_mutex.Unlock()
self.preview_cache[key] = preview_cache_value{path: r["font_family"].Path, width: r["font_family"].Canvas_width, height: r["font_family"].Canvas_height}
self.handler.lp.WakeupMainThread()
}()
return
case "requested":
return
}
self.handler.graphics_manager.display_image(0, cc.path, cc.width, cc.height)
return
}
func (self *FontList) on_wakeup() error {
if !self.family_list_updated {
self.family_list_updated = true
self.family_list.UpdateFamilies(utils.StableSortWithKey(utils.Keys(self.fonts), strings.ToLower))
self.family_list.SelectFamily(self.resolved_faces_from_kitty_conf.Font_family.Family)
}
return self.handler.draw_screen()
}
func (self *FontList) draw_screen() (err error) {
lp := self.handler.lp
sz, err := lp.ScreenSize()
if err != nil {
return err
}
num_rows := max(0, int(sz.HeightCells)-1)
mw := self.family_list.max_width + 1
green_fg, _, _ := strings.Cut(lp.SprintStyled("fg=green", "|"), "|")
lines := make([]string, 0, num_rows)
for _, l := range self.family_list.Lines(num_rows) {
line := l.text
if l.is_current {
line = strings.ReplaceAll(line, MARK_AFTER, green_fg)
line = lp.SprintStyled("fg=green", ">") + lp.SprintStyled("fg=green bold", line)
} else {
line = " " + line
}
lines = append(lines, line)
}
_, _, str := self.handler.render_lines.InRectangle(lines, 0, 0, 0, num_rows, &self.handler.mouse_state, self.on_click)
lp.QueueWriteString(str)
seps := strings.Repeat(SEPARATOR, num_rows)
seps = strings.TrimSpace(seps)
_, _, str = self.handler.render_lines.InRectangle(strings.Split(seps, ""), mw+1, 0, 0, num_rows, &self.handler.mouse_state)
lp.QueueWriteString(str)
if self.family_list.Len() > 0 {
if err = self.draw_family_summary(mw+3, sz); err != nil {
return err
}
}
self.draw_search_bar()
return
}
func (self *FontList) on_click(id string) error {
which, data, found := strings.Cut(id, ":")
if !found {
return fmt.Errorf("Not a valid click id: %s", id)
}
switch which {
case "family-chosen":
if self.handler.state == LISTING_FAMILIES {
if self.family_list.Select(data) {
self.handler.draw_screen()
} else {
self.handler.lp.Beep()
}
}
}
return nil
}
func (self *FontList) update_family_search() {
text := self.rl.AllText()
if self.family_list.UpdateSearch(text) {
self.handler.draw_screen()
} else {
self.draw_search_bar()
}
}
func (self *FontList) next(delta int, allow_wrapping bool) error {
if self.family_list.Next(delta, allow_wrapping) {
return self.handler.draw_screen()
}
self.handler.lp.Beep()
return nil
}
func (self *FontList) on_key_event(event *loop.KeyEvent) (err error) {
if event.MatchesPressOrRepeat("enter") {
event.Handled = true
if family := self.family_list.CurrentFamily(); family != "" {
return self.handler.faces.on_enter(family)
}
self.handler.lp.Beep()
return
}
if event.MatchesPressOrRepeat("esc") {
event.Handled = true
if self.rl.AllText() != "" {
self.rl.ResetText()
self.update_family_search()
self.handler.draw_screen()
} else {
return fmt.Errorf("canceled by user")
}
return
}
ev := event
if ev.MatchesPressOrRepeat("down") {
ev.Handled = true
return self.next(1, true)
}
if ev.MatchesPressOrRepeat("up") {
ev.Handled = true
return self.next(-1, true)
}
if ev.MatchesPressOrRepeat("page_down") {
ev.Handled = true
sz, err := self.handler.lp.ScreenSize()
if err == nil {
err = self.next(int(sz.HeightCells)-3, false)
}
return err
}
if ev.MatchesPressOrRepeat("page_up") {
ev.Handled = true
sz, err := self.handler.lp.ScreenSize()
if err == nil {
err = self.next(3-int(sz.HeightCells), false)
}
return err
}
if err = self.rl.OnKeyEvent(event); err != nil {
if err == readline.ErrAcceptInput {
return nil
}
return err
}
if event.Handled {
self.update_family_search()
}
self.draw_search_bar()
return
}
func (self *FontList) on_text(text string, from_key_event bool, in_bracketed_paste bool) (err error) {
if err = self.rl.OnText(text, from_key_event, in_bracketed_paste); err != nil {
return err
}
self.update_family_search()
return
}

View File

@@ -0,0 +1,99 @@
package choose_fonts
import (
"fmt"
"os"
"kitty/tools/cli"
"kitty/tools/tty"
"kitty/tools/tui/loop"
)
var _ = fmt.Print
var debugprintln = tty.DebugPrintln
var output_on_exit string
func main(opts *Options) (rc int, err error) {
if err = kitty_font_backend.start(); err != nil {
return 1, err
}
defer func() {
if werr := kitty_font_backend.release(); werr != nil {
if err == nil {
err = werr
}
if rc == 0 {
rc = 1
}
}
}()
lp, err := loop.New()
if err != nil {
return 1, err
}
lp.MouseTrackingMode(loop.FULL_MOUSE_TRACKING)
h := &handler{lp: lp, opts: opts}
lp.OnInitialize = func() (string, error) {
lp.AllowLineWrapping(false)
lp.SetWindowTitle(`Choose a font for kitty`)
return "", h.initialize()
}
lp.OnWakeup = h.on_wakeup
lp.OnEscapeCode = h.on_escape_code
lp.OnFinalize = func() string {
h.finalize()
lp.SetCursorVisible(true)
return ``
}
lp.OnMouseEvent = h.on_mouse_event
lp.OnResize = func(_, _ loop.ScreenSize) error {
return h.draw_screen()
}
lp.OnKeyEvent = h.on_key_event
lp.OnText = h.on_text
err = lp.Run()
if err != nil {
return 1, err
}
ds := lp.DeathSignalName()
if ds != "" {
fmt.Println("Killed by signal: ", ds)
lp.KillIfSignalled()
return 1, nil
}
if output_on_exit != "" {
os.Stdout.WriteString(output_on_exit)
}
return lp.ExitCode(), nil
}
type Options struct {
Reload_in string
}
func EntryPoint(root *cli.Command) {
ans := root.AddSubCommand(&cli.Command{
Name: "choose-fonts",
ShortDescription: "Choose the fonts used in kitty",
Run: func(cmd *cli.Command, args []string) (rc int, err error) {
opts := Options{}
if err = cmd.GetOptionValues(&opts); err != nil {
return 1, err
}
return main(&opts)
},
})
ans.Add(cli.OptionSpec{
Name: "--reload-in",
Dest: "Reload_in",
Type: "choices",
Choices: "parent, all, none",
Default: "parent",
Help: `By default, this kitten will signal only the parent kitty instance it is
running in to reload its config, after making changes. Use this option to
instead either not reload the config at all or in all running kitty instances.`,
})
clone := root.AddClone(ans.Group, ans)
clone.Hidden = false
clone.Name = "choose_fonts"
}

View File

View File

@@ -0,0 +1,113 @@
package choose_fonts
import (
"fmt"
"kitty/tools/utils"
"slices"
"strings"
)
var _ = fmt.Print
type style_group struct {
name string
ordering int
styles []string
style_sort_map map[string]string
}
type family_style_data struct {
style_groups []style_group
has_variable_faces bool
has_style_attribute_data bool
}
func styles_with_attribute_data(ans *family_style_data, items ...VariableData) {
groups := make(map[string]*style_group)
seen_map := make(map[string]map[string]string)
get := func(key string, ordering int) *style_group {
sg := groups[key]
seen := seen_map[key]
if sg == nil {
ans.style_groups = append(ans.style_groups, style_group{name: key, ordering: ordering, styles: make([]string, 0)})
sg = &ans.style_groups[len(ans.style_groups)-1]
groups[key] = sg
sg.style_sort_map = make(map[string]string)
seen = make(map[string]string)
seen_map[key] = seen
}
return sg
}
has := func(n string, m map[string]string) bool {
_, found := m[n]
return found
}
for _, vd := range items {
for _, ax := range vd.Design_axes {
if ax.Name == "" {
continue
}
sg := get(ax.Name, ax.Ordering)
for _, v := range ax.Values {
if v.Name != "" && !has(v.Name, sg.style_sort_map) {
sort_key := fmt.Sprintf("%09d:%s", int(v.Value*10000), strings.ToLower(v.Name))
sg.style_sort_map[v.Name] = sort_key
sg.styles = append(sg.styles, v.Name)
}
}
}
for _, ma := range vd.Multi_axis_styles {
sg := get("Styles", 0)
if ma.Name != "" && !has(ma.Name, sg.style_sort_map) {
sg.style_sort_map[ma.Name] = strings.ToLower(ma.Name)
sg.styles = append(sg.styles, ma.Name)
}
}
}
ans.style_groups = utils.StableSortWithKey(ans.style_groups, func(sg style_group) int { return sg.ordering })
for _, sg := range ans.style_groups {
sg.styles = utils.StableSortWithKey(sg.styles, func(s string) string { return sg.style_sort_map[s] })
}
}
func styles_for_variable_data(vd VariableData) (ans *family_style_data) {
ans = &family_style_data{style_groups: make([]style_group, 0)}
styles_with_attribute_data(ans, vd)
return
}
func styles_in_family(family string, fonts []ListedFont) (ans *family_style_data) {
_ = family
ans = &family_style_data{style_groups: make([]style_group, 0)}
vds := make([]VariableData, len(fonts))
for i, f := range fonts {
vds[i] = variable_data_for(f)
}
for _, vd := range vds {
if len(vd.Design_axes) > 0 {
ans.has_style_attribute_data = true
}
if len(vd.Axes) > 0 {
ans.has_variable_faces = true
}
}
if ans.has_style_attribute_data {
styles_with_attribute_data(ans, vds...)
} else {
ans.style_groups = append(ans.style_groups, style_group{name: "Styles", styles: make([]string, 0)})
sg := &ans.style_groups[0]
seen := utils.NewSet[string]()
for _, f := range fonts {
if f.Style != "" && !seen.Has(f.Style) {
seen.Add(f.Style)
sg.styles = append(sg.styles, f.Style)
}
}
}
for _, sg := range ans.style_groups {
slices.Sort(sg.styles)
}
return
}

View File

@@ -0,0 +1,333 @@
package choose_fonts
import (
"fmt"
"maps"
"strconv"
"strings"
"sync"
"kitty/tools/utils"
"kitty/tools/utils/shlex"
)
var _ = fmt.Print
type VariableAxis struct {
Minimum float64 `json:"minimum"`
Maximum float64 `json:"maximum"`
Default float64 `json:"default"`
Hidden bool `json:"hidden"`
Tag string `json:"tag"`
Strid string `json:"strid"`
}
type NamedStyle struct {
Axis_values map[string]float64 `json:"axis_values"`
Name string `json:"name"`
Postscript_name string `json:"psname"`
}
type DesignAxisValue struct {
Format int `json:"format"`
Flags int `json:"flags"`
Name string `json:"name"`
Value float64 `json:"value"`
Minimum float64 `json:"minimum"`
Maximum float64 `json:"maximum"`
Linked_value float64 `json:"linked_value"`
}
type DesignAxis struct {
Tag string `json:"tag"`
Name string `json:"name"`
Ordering int `json:"ordering"`
Values []DesignAxisValue `json:"values"`
}
type AxisValue struct {
Design_index int `json:"design_index"`
Value float64 `json:"value"`
}
type MultiAxisStyle struct {
Flags int `json:"flags"`
Name string `json:"name"`
Values []AxisValue `json:"values"`
}
type ListedFont struct {
Family string `json:"family"`
Style string `json:"style"`
Fullname string `json:"full_name"`
Postscript_name string `json:"postscript_name"`
Is_monospace bool `json:"is_monospace"`
Is_variable bool `json:"is_variable"`
Descriptor map[string]any `json:"descriptor"`
}
type VariableData struct {
Axes []VariableAxis `json:"axes"`
Named_styles []NamedStyle `json:"named_styles"`
Variations_postscript_name_prefix string `json:"variations_postscript_name_prefix"`
Elided_fallback_name string `json:"elided_fallback_name"`
Design_axes []DesignAxis `json:"design_axes"`
Multi_axis_styles []MultiAxisStyle `json:"multi_axis_styles"`
}
type ResolvedFace struct {
Family string `json:"family"`
Spec string `json:"spec"`
Setting string `json:"setting"`
}
type ResolvedFaces struct {
Font_family ResolvedFace `json:"font_family"`
Bold_font ResolvedFace `json:"bold_font"`
Italic_font ResolvedFace `json:"italic_font"`
Bold_italic_font ResolvedFace `json:"bold_italic_font"`
}
type ListResult struct {
Fonts map[string][]ListedFont `json:"fonts"`
Resolved_faces ResolvedFaces `json:"resolved_faces"`
}
type FeatureData struct {
Is_index bool `json:"is_index"`
Name string `json:"name"`
Tooltip string `json:"tooltip"`
Sample string `json:"sample"`
Params []string `json:"params"`
}
type RenderedSampleTransmit struct {
Path string `json:"path"`
Variable_data VariableData `json:"variable_data"`
Style string `json:"style"`
Psname string `json:"psname"`
Spec string `json:"spec"`
Features map[string]FeatureData `json:"features"`
Applied_features map[string]string `json:"applied_features"`
Variable_named_style NamedStyle `json:"variable_named_style"`
Variable_axis_map map[string]float64 `json:"variable_axis_map"`
Cell_width int `json:"cell_width"`
Cell_height int `json:"cell_height"`
Canvas_width int `json:"canvas_width"`
Canvas_height int `json:"canvas_height"`
}
func (self RenderedSampleTransmit) default_axis_values() (ans map[string]float64) {
ans = make(map[string]float64)
for _, ax := range self.Variable_data.Axes {
ans[ax.Tag] = ax.Default
}
return
}
func (self RenderedSampleTransmit) current_axis_values() (ans map[string]float64) {
ans = make(map[string]float64, len(self.Variable_data.Axes))
for _, ax := range self.Variable_data.Axes {
ans[ax.Tag] = ax.Default
}
if self.Variable_named_style.Name != "" {
maps.Copy(ans, self.Variable_named_style.Axis_values)
} else {
maps.Copy(ans, self.Variable_axis_map)
}
return
}
var variable_data_cache map[string]VariableData
var variable_data_cache_mutex sync.Mutex
func (f ListedFont) cache_key() string {
key := f.Postscript_name
if key == "" {
key = "path:" + f.Descriptor["path"].(string)
} else {
key = "psname:" + key
}
return key
}
func ensure_variable_data_for_fonts(fonts ...ListedFont) error {
descriptors := make([]map[string]any, 0, len(fonts))
keys := make([]string, 0, len(fonts))
variable_data_cache_mutex.Lock()
for _, f := range fonts {
key := f.cache_key()
if _, found := variable_data_cache[key]; !found {
descriptors = append(descriptors, f.Descriptor)
keys = append(keys, key)
}
}
variable_data_cache_mutex.Unlock()
var data []VariableData
if err := kitty_font_backend.query("read_variable_data", map[string]any{"descriptors": descriptors}, &data); err != nil {
return err
}
variable_data_cache_mutex.Lock()
for i, key := range keys {
variable_data_cache[key] = data[i]
}
variable_data_cache_mutex.Unlock()
return nil
}
func initialize_variable_data_cache() {
variable_data_cache = make(map[string]VariableData)
}
func _cached_vd(key string) (ans VariableData, found bool) {
variable_data_cache_mutex.Lock()
defer variable_data_cache_mutex.Unlock()
ans, found = variable_data_cache[key]
return
}
func variable_data_for(f ListedFont) VariableData {
key := f.cache_key()
ans, found := _cached_vd(key)
if found {
return ans
}
if err := ensure_variable_data_for_fonts(f); err != nil {
panic(err)
}
ans, found = _cached_vd(key)
return ans
}
func has_variable_data_for_font(font ListedFont) bool {
_, found := _cached_vd(font.cache_key())
return found
}
type ParsedFontFeature struct {
tag string
val uint
is_bool bool
}
func (self ParsedFontFeature) String() string {
if self.is_bool {
return utils.IfElse(self.val == 0, "-", "+") + self.tag
}
return fmt.Sprintf("%s=%d", self.tag, self.val)
}
type settable_string struct {
val string
is_set bool
}
type FontSpec struct {
family, style, postscript_name, full_name, system, variable_name settable_string
axes map[string]float64
features []*ParsedFontFeature
}
func (self FontSpec) String() string {
if self.system.val != "" {
return self.system.val
}
ans := strings.Builder{}
a := func(k string, v settable_string) {
if v.is_set {
ans.WriteString(fmt.Sprintf(" %s=%s", k, shlex.Quote(v.val)))
}
}
a(`family`, self.family)
a(`style`, self.style)
a(`postscript_name`, self.postscript_name)
a(`full_name`, self.full_name)
a(`variable_name`, self.variable_name)
for name, val := range self.axes {
a(name, settable_string{strconv.FormatFloat(val, 'f', -1, 64), true})
}
if len(self.features) > 0 {
buf := strings.Builder{}
for _, f := range self.features {
buf.WriteString(f.String())
buf.WriteString(" ")
}
a(`features`, settable_string{strings.TrimSpace(buf.String()), true})
}
return strings.TrimSpace(ans.String())
}
func NewParsedFontFeature(x string, features map[string]FeatureData) (ans ParsedFontFeature, err error) {
if x != "" {
if x[0] == '+' || x[0] == '-' {
return ParsedFontFeature{x[1:], utils.IfElse(x[0] == '+', uint(1), uint(0)), true}, nil
} else {
tag, val, found := strings.Cut(x, "=")
fd, defn_found := features[tag]
if defn_found && !fd.Is_index {
return ParsedFontFeature{tag, 1, true}, nil
}
pff := ParsedFontFeature{tag: tag}
if found {
v, err := strconv.ParseUint(val, 10, 0)
if err != nil {
return ans, err
}
pff.val = uint(v)
}
return pff, nil
}
}
return
}
func NewFontSpec(spec string, features map[string]FeatureData) (ans FontSpec, err error) {
if spec == "" || spec == "auto" {
ans.system = settable_string{"auto", true}
return
}
parts, err := shlex.Split(spec)
if err != nil {
return
}
if !strings.Contains(parts[0], "=") {
ans.system = settable_string{spec, true}
return
}
for _, item := range parts {
k, v, found := strings.Cut(item, "=")
if !found {
return ans, fmt.Errorf(fmt.Sprintf("The font specification %s is invalid as %s does not contain an =", spec, item))
}
switch k {
case "family":
ans.family = settable_string{v, true}
case "style":
ans.style = settable_string{v, true}
case "full_name":
ans.full_name = settable_string{v, true}
case "postscript_name":
ans.postscript_name = settable_string{v, true}
case "variable_name":
ans.variable_name = settable_string{v, true}
case "features":
for _, x := range utils.NewSeparatorScanner(v, " ").Split(v) {
pff, err := NewParsedFontFeature(x, features)
if err != nil {
return ans, err
}
ans.features = append(ans.features, &pff)
}
default:
if ans.axes == nil {
ans.axes = make(map[string]float64)
}
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return ans, err
}
ans.axes[k] = f
}
}
return
}

225
kittens/choose_fonts/ui.go Normal file
View File

@@ -0,0 +1,225 @@
package choose_fonts
import (
"fmt"
"os"
"strconv"
"sync"
"kitty/tools/tui"
"kitty/tools/tui/graphics"
"kitty/tools/tui/loop"
"kitty/tools/utils"
)
var _ = fmt.Print
type State int
const (
SCANNING_FAMILIES State = iota
LISTING_FAMILIES
CHOOSING_FACES
)
type TextStyle struct {
Font_sz float64 `json:"font_size"`
Dpi_x float64 `json:"dpi_x"`
Dpi_y float64 `json:"dpi_y"`
Foreground string `json:"foreground"`
Background string `json:"background"`
}
type pane interface {
initialize(*handler) error
draw_screen() error
on_wakeup() error
on_key_event(event *loop.KeyEvent) error
on_text(text string, from_key_event bool, in_bracketed_paste bool) error
on_click(id string) error
}
type handler struct {
opts *Options
lp *loop.Loop
state State
err_mutex sync.Mutex
err_in_worker_thread error
mouse_state tui.MouseState
render_count uint
render_lines tui.RenderLines
text_style TextStyle
graphics_manager graphics_manager
temp_dir string
listing FontList
faces faces
face_pane face_panel
if_pane if_panel
final_pane final_pane
panes []pane
current_pane pane
}
func (h *handler) set_worker_error(err error) {
h.err_mutex.Lock()
defer h.err_mutex.Unlock()
h.err_in_worker_thread = err
}
func (h *handler) get_worker_error() error {
h.err_mutex.Lock()
defer h.err_mutex.Unlock()
return h.err_in_worker_thread
}
// Events {{{
func (h *handler) initialize() (err error) {
h.lp.SetCursorVisible(false)
h.lp.OnQueryResponse = h.on_query_response
h.lp.QueryTerminal("font_size", "dpi_x", "dpi_y", "foreground", "background")
h.panes = []pane{&h.listing, &h.faces, &h.face_pane, &h.if_pane, &h.final_pane}
for _, pane := range h.panes {
if err = pane.initialize(h); err != nil {
return err
}
}
// dont use /tmp as it may be mounted in RAM, Le Sigh
if h.temp_dir, err = os.MkdirTemp(utils.CacheDir(), "kitten-choose-fonts-*"); err != nil {
return
}
initialize_variable_data_cache()
h.graphics_manager.initialize(h.lp)
go func() {
var r ListResult
h.set_worker_error(kitty_font_backend.query("list_monospaced_fonts", nil, &r))
h.listing.fonts = r.Fonts
h.listing.resolved_faces_from_kitty_conf = r.Resolved_faces
h.lp.WakeupMainThread()
}()
h.draw_screen()
return
}
func (h *handler) finalize() {
if h.temp_dir != "" {
os.RemoveAll(h.temp_dir)
h.temp_dir = ""
}
h.lp.SetCursorVisible(true)
h.lp.SetCursorShape(loop.BLOCK_CURSOR, true)
h.graphics_manager.finalize()
}
func (h *handler) on_query_response(key, val string, valid bool) error {
if !valid {
return fmt.Errorf("Terminal does not support querying the: %s", key)
}
set_float := func(k, v string, dest *float64) error {
if fs, err := strconv.ParseFloat(v, 64); err == nil {
*dest = fs
} else {
return fmt.Errorf("Invalid response from terminal to %s query: %#v", k, v)
}
return nil
}
switch key {
case "font_size":
if err := set_float(key, val, &h.text_style.Font_sz); err != nil {
return err
}
case "dpi_x":
if err := set_float(key, val, &h.text_style.Dpi_x); err != nil {
return err
}
case "dpi_y":
if err := set_float(key, val, &h.text_style.Dpi_y); err != nil {
return err
}
case "foreground":
h.text_style.Foreground = val
case "background":
h.text_style.Background = val
return h.draw_screen()
}
return nil
}
func (h *handler) draw_screen() (err error) {
h.render_count++
h.lp.StartAtomicUpdate()
defer func() {
h.mouse_state.UpdateHoveredIds()
h.mouse_state.ApplyHoverStyles(h.lp)
h.lp.EndAtomicUpdate()
}()
h.graphics_manager.clear_placements()
h.lp.ClearScreenButNotGraphics()
h.lp.AllowLineWrapping(false)
h.mouse_state.ClearCellRegions()
if h.current_pane == nil {
h.lp.Println("Scanning system for fonts, please wait...")
} else {
return h.current_pane.draw_screen()
}
return
}
func (h *handler) on_wakeup() (err error) {
if err = h.get_worker_error(); err != nil {
return
}
if h.current_pane == nil {
h.current_pane = &h.listing
}
return h.listing.on_wakeup()
}
func (h *handler) on_mouse_event(event *loop.MouseEvent) (err error) {
rc := h.render_count
redraw_needed := false
if h.mouse_state.UpdateState(event) {
redraw_needed = true
}
if event.Event_type == loop.MOUSE_CLICK && event.Buttons&loop.LEFT_MOUSE_BUTTON != 0 {
if err = h.mouse_state.ClickHoveredRegions(); err != nil {
return
}
}
if redraw_needed && rc == h.render_count {
err = h.draw_screen()
}
return
}
func (h *handler) on_key_event(event *loop.KeyEvent) (err error) {
if event.MatchesPressOrRepeat("ctrl+c") {
event.Handled = true
return fmt.Errorf("canceled by user")
}
if h.current_pane != nil {
err = h.current_pane.on_key_event(event)
}
return
}
func (h *handler) on_text(text string, from_key_event bool, in_bracketed_paste bool) (err error) {
if h.current_pane != nil {
err = h.current_pane.on_text(text, from_key_event, in_bracketed_paste)
}
return
}
func (h *handler) on_escape_code(etype loop.EscapeCodeType, payload []byte) error {
switch etype {
case loop.APC:
gc := graphics.GraphicsCommandFromAPC(payload)
if gc != nil {
return h.graphics_manager.on_response(gc)
}
}
return nil
}
// }}}

View File

@@ -10,6 +10,7 @@ import (
"io"
"os"
"path/filepath"
"slices"
"strings"
"sync"
@@ -17,9 +18,6 @@ import (
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/utils/images"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
var _ = fmt.Print
@@ -365,7 +363,7 @@ func run_get_loop(opts *Options, args []string) (err error) {
}
}
if len(requested_mimes) > 0 {
lp.QueueWriteString(encode(basic_metadata, strings.Join(maps.Keys(requested_mimes), " ")))
lp.QueueWriteString(encode(basic_metadata, strings.Join(utils.Keys(requested_mimes), " ")))
} else {
lp.Quit(0)
}

View File

@@ -5,6 +5,7 @@ package diff
import (
"fmt"
"regexp"
"slices"
"strings"
"sync"
@@ -12,8 +13,6 @@ import (
"kitty/tools/utils"
"kitty/tools/utils/images"
"kitty/tools/wcswidth"
"golang.org/x/exp/slices"
)
var _ = fmt.Print

View File

@@ -10,6 +10,7 @@ import (
"os/exec"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
"sync"
@@ -18,7 +19,6 @@ import (
"github.com/dlclark/regexp2"
"github.com/seancfoley/ipaddress-go/ipaddr"
"golang.org/x/exp/slices"
"kitty"
"kitty/tools/config"

View File

@@ -0,0 +1,82 @@
package query_terminal
import (
"bytes"
"fmt"
"kitty"
"os"
"slices"
"strings"
"time"
"kitty/tools/cli"
"kitty/tools/tui/loop"
)
var _ = fmt.Print
func main(cmd *cli.Command, opts *Options, args []string) (rc int, err error) {
queries := kitty.QueryNames
if len(args) > 0 && !slices.Contains(args, "all") {
queries = make([]string, len(args))
for i, x := range args {
if !slices.Contains(kitty.QueryNames, x) {
return 1, fmt.Errorf("Unknown query: %s", x)
}
queries[i] = x
}
}
lp, err := loop.New(loop.NoAlternateScreen, loop.NoKeyboardStateChange, loop.NoMouseTracking, loop.NoRestoreColors)
if err != nil {
return 1, err
}
timed_out := false
lp.OnInitialize = func() (string, error) {
lp.QueryTerminal(queries...)
lp.QueueWriteString("\x1b[c")
_, err := lp.AddTimer(time.Duration(opts.WaitFor*float64(time.Second)), false, func(timer_id loop.IdType) error {
timed_out = true
lp.Quit(1)
return nil
})
return "", err
}
buf := strings.Builder{}
lp.OnQueryResponse = func(key, val string, found bool) error {
if found {
fmt.Fprintf(&buf, "%s: %s\n", key, val)
} else {
fmt.Fprintf(&buf, "%s:\n", key)
}
return nil
}
lp.OnEscapeCode = func(typ loop.EscapeCodeType, data []byte) error {
if typ == loop.CSI && bytes.HasSuffix(data, []byte{'c'}) {
lp.Quit(0)
}
return nil
}
err = lp.Run()
rc = lp.ExitCode()
if err != nil {
return 1, err
}
ds := lp.DeathSignalName()
if ds != "" {
fmt.Println("Killed by signal: ", ds)
lp.KillIfSignalled()
return
}
os.Stdout.WriteString(buf.String())
if timed_out {
return 1, fmt.Errorf("timed out waiting for response from terminal")
}
return
}
func EntryPoint(parent *cli.Command) {
create_cmd(parent, main)
}

View File

@@ -5,14 +5,11 @@ import re
import sys
from binascii import hexlify, unhexlify
from contextlib import suppress
from typing import Dict, Iterable, List, Optional, Type
from typing import Dict, Optional, Type
from kitty.cli import parse_args
from kitty.cli_stub import QueryTerminalCLIOptions
from kitty.constants import appname, str_version
from kitty.options.types import Options
from kitty.terminfo import names
from kitty.utils import TTYIO
class Query:
@@ -50,7 +47,7 @@ class Query:
return self.ans
@staticmethod
def get_result(opts: Options) -> str:
def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
raise NotImplementedError()
@@ -69,7 +66,7 @@ class TerminalName(Query):
help_text: str = f'Terminal name (e.g. :code:`{names[0]}`)'
@staticmethod
def get_result(opts: Options) -> str:
def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
return appname
@@ -79,7 +76,7 @@ class TerminalVersion(Query):
help_text: str = f'Terminal version (e.g. :code:`{str_version}`)'
@staticmethod
def get_result(opts: Options) -> str:
def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
return str_version
@@ -89,7 +86,7 @@ class AllowHyperlinks(Query):
help_text: str = 'The config option :opt:`allow_hyperlinks` in :file:`kitty.conf` for allowing hyperlinks can be :code:`yes`, :code:`no` or :code:`ask`'
@staticmethod
def get_result(opts: Options) -> str:
def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
return 'ask' if opts.allow_hyperlinks == 0b11 else ('yes' if opts.allow_hyperlinks else 'no')
@@ -99,10 +96,10 @@ class FontFamily(Query):
help_text: str = 'The current font\'s PostScript name'
@staticmethod
def get_result(opts: Options) -> str:
def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
from kitty.fast_data_types import current_fonts
cf = current_fonts()
return str(cf['medium'].display_name())
cf = current_fonts(os_window_id)
return cf['medium'].postscript_name()
@query
@@ -111,10 +108,10 @@ class BoldFont(Query):
help_text: str = 'The current bold font\'s PostScript name'
@staticmethod
def get_result(opts: Options) -> str:
def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
from kitty.fast_data_types import current_fonts
cf = current_fonts()
return str(cf['bold'].display_name())
cf = current_fonts(os_window_id)
return cf['bold'].postscript_name()
@query
@@ -123,10 +120,10 @@ class ItalicFont(Query):
help_text: str = 'The current italic font\'s PostScript name'
@staticmethod
def get_result(opts: Options) -> str:
def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
from kitty.fast_data_types import current_fonts
cf = current_fonts()
return str(cf['italic'].display_name())
cf = current_fonts(os_window_id)
return cf['italic'].postscript_name()
@query
@@ -135,20 +132,79 @@ class BiFont(Query):
help_text: str = 'The current bold-italic font\'s PostScript name'
@staticmethod
def get_result(opts: Options) -> str:
def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
from kitty.fast_data_types import current_fonts
cf = current_fonts()
return str(cf['bi'].display_name())
cf = current_fonts(os_window_id)
return cf['bi'].postscript_name()
@query
class FontSize(Query):
name: str = 'font_size'
help_text: str = 'The current overall font size (individual windows can have different per window font sizes)'
help_text: str = 'The current font size in pts'
@staticmethod
def get_result(opts: Options) -> str:
return f'{opts.font_size:g}'
def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
from kitty.fast_data_types import current_fonts
cf = current_fonts(os_window_id)
return f'{cf["font_sz_in_pts"]:g}'
@query
class DpiX(Query):
name: str = 'dpi_x'
help_text: str = 'The current DPI on the x-axis'
@staticmethod
def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
from kitty.fast_data_types import current_fonts
cf = current_fonts(os_window_id)
return f'{cf["logical_dpi_x"]:g}'
@query
class DpiY(Query):
name: str = 'dpi_y'
help_text: str = 'The current DPI on the y-axis'
@staticmethod
def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
from kitty.fast_data_types import current_fonts
cf = current_fonts(os_window_id)
return f'{cf["logical_dpi_y"]:g}'
@query
class Foreground(Query):
name: str = 'foreground'
help_text: str = 'The current foreground color as a 24-bit # color code'
@staticmethod
def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
from kitty.fast_data_types import Color, get_boss
boss = get_boss()
w = boss.window_id_map.get(window_id)
if w is None:
return opts.foreground.as_sharp
col = w.screen.color_profile.default_fg
r, g, b = col >> 16, (col >> 8) & 0xff, col & 0xff
return Color(r, g, b).as_sharp
@query
class Background(Query):
name: str = 'background'
help_text: str = 'The current background color as a 24-bit # color code'
@staticmethod
def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
from kitty.fast_data_types import Color, get_boss
boss = get_boss()
w = boss.window_id_map.get(window_id)
if w is None:
return opts.background.as_sharp
col = w.screen.color_profile.default_bg
r, g, b = col >> 16, (col >> 8) & 0xff, col & 0xff
return Color(r, g, b).as_sharp
@query
@@ -157,41 +213,16 @@ class ClipboardControl(Query):
help_text: str = 'The config option :opt:`clipboard_control` in :file:`kitty.conf` for allowing reads/writes to/from the clipboard'
@staticmethod
def get_result(opts: Options) -> str:
def get_result(opts: Options, window_id: int, os_window_id: int) -> str:
return ' '.join(opts.clipboard_control)
def get_result(name: str) -> Optional[str]:
def get_result(name: str, window_id: int, os_window_id: int) -> Optional[str]:
from kitty.fast_data_types import get_options
q = all_queries.get(name)
if q is None:
return None
return q.get_result(get_options())
def do_queries(queries: Iterable[str], cli_opts: QueryTerminalCLIOptions) -> Dict[str, str]:
actions = tuple(all_queries[x]() for x in queries)
qstring = ''.join(a.query_code() for a in actions)
received = b''
pat = re.compile(rb'\x1b\[\?.+?c')
def more_needed(data: bytes) -> bool:
nonlocal received
received += data
has_da1_response = pat.search(received) is not None
if has_da1_response:
return False
for a in actions:
if a.more_needed(received):
return True
return has_da1_response
with TTYIO() as ttyio:
ttyio.send(qstring)
ttyio.send('\x1b[c') # DA1 query https://vt100.net/docs/vt510-rm/DA1.html
ttyio.recv(more_needed, timeout=cli_opts.wait_for)
return {a.name: a.output_line() for a in actions}
return q.get_result(get_options(), window_id, os_window_id)
def options_spec() -> str:
@@ -230,29 +261,8 @@ Available queries are:
usage = '[query1 query2 ...]'
def main(args: List[str] = sys.argv) -> None:
cli_opts, items_ = parse_args(
args[1:],
options_spec,
usage,
help_text,
f'{appname} +kitten query_terminal',
result_class=QueryTerminalCLIOptions
)
queries: List[str] = list(items_)
if 'all' in queries or not queries:
queries = sorted(all_queries)
else:
extra = frozenset(queries) - frozenset(all_queries)
if extra:
raise SystemExit(f'Unknown queries: {", ".join(extra)}')
for key, val in do_queries(queries, cli_opts).items():
print(f'{key}:', val)
if __name__ == '__main__':
main()
raise SystemExit('Should be run as kitten hints')
elif __name__ == '__doc__':
cd = sys.cli_docs # type: ignore
cd['usage'] = usage

View File

@@ -16,7 +16,7 @@ func csi(csi string) string {
return "CSI " + strings.NewReplacer(":", " : ", ";", " ; ").Replace(csi[:len(csi)-1]) + " " + csi[len(csi)-1:]
}
func run_kitty_loop(opts *Options) (err error) {
func run_kitty_loop(_ *Options) (err error) {
lp, err := loop.New(loop.FullKeyboardProtocol)
if err != nil {
return err

View File

@@ -13,6 +13,7 @@ import (
"io"
"io/fs"
"kitty"
"maps"
"net/url"
"os"
"os/exec"
@@ -21,6 +22,7 @@ import (
"path"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
"time"
@@ -36,8 +38,6 @@ import (
"kitty/tools/utils/shlex"
"kitty/tools/utils/shm"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"golang.org/x/sys/unix"
)
@@ -411,7 +411,7 @@ func prepare_script(script string, replacements map[string]string) string {
if _, found := replacements["EXPORT_HOME_CMD"]; !found {
replacements["EXPORT_HOME_CMD"] = ""
}
keys := maps.Keys(replacements)
keys := utils.Keys(replacements)
for i, key := range keys {
keys[i] = "\\b" + key + "\\b"
}

View File

@@ -43,8 +43,8 @@ func (self *ThemesList) Next(delta int, allow_wrapping bool) bool {
}
func limit_lengths(text string) string {
t, x := wcswidth.TruncateToVisualLengthWithWidth(text, 31)
if x >= len(text) {
t, _ := wcswidth.TruncateToVisualLengthWithWidth(text, 31)
if len(t) >= len(text) {
return text
}
return t + "…"

View File

@@ -5,8 +5,10 @@ package themes
import (
"fmt"
"io"
"maps"
"path/filepath"
"regexp"
"slices"
"strings"
"time"
@@ -16,9 +18,6 @@ import (
"kitty/tools/tui/readline"
"kitty/tools/utils"
"kitty/tools/wcswidth"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
var _ = fmt.Print
@@ -327,6 +326,11 @@ func (self *handler) draw_browsing_screen() {
}
self.lp.MoveCursorHorizontally(mw - l.width)
self.lp.Println(SEPARATOR)
num_rows--
}
for ; num_rows > 0; num_rows-- {
self.lp.MoveCursorHorizontally(mw + 1)
self.lp.Println(SEPARATOR)
}
if self.themes_list != nil && self.themes_list.Len() > 0 {
self.draw_theme_demo()

View File

@@ -11,6 +11,7 @@ import (
"io/fs"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"time"
@@ -25,7 +26,6 @@ import (
"kitty/tools/utils/humanize"
"kitty/tools/wcswidth"
"golang.org/x/exp/slices"
"golang.org/x/sys/unix"
)

View File

@@ -11,6 +11,7 @@ import (
"io/fs"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"syscall"
@@ -18,7 +19,6 @@ import (
"unicode/utf8"
"golang.org/x/exp/constraints"
"golang.org/x/exp/slices"
"kitty"
"kitty/tools/cli/markup"

View File

@@ -8,6 +8,7 @@ import (
"os"
"os/exec"
"path/filepath"
"slices"
"strconv"
"strings"
"unicode"
@@ -21,8 +22,6 @@ import (
"kitty/tools/utils"
"kitty/tools/utils/style"
"kitty/tools/wcswidth"
"golang.org/x/exp/slices"
)
var _ = fmt.Print

View File

@@ -4,6 +4,7 @@ package unicode_input
import (
"fmt"
"slices"
"strconv"
"strings"
@@ -11,8 +12,6 @@ import (
"kitty/tools/utils"
"kitty/tools/utils/style"
"kitty/tools/wcswidth"
"golang.org/x/exp/slices"
)
var _ = fmt.Print

View File

@@ -2655,7 +2655,7 @@ class Boss:
# Update font data
set_scale(opts.box_drawing_scale)
from .fonts.render import set_font_family
set_font_family(opts, debug_font_matching=self.args.debug_font_fallback)
set_font_family(opts)
for os_window_id, tm in self.os_window_map.items():
if tm is not None:
os_window_font_size(os_window_id, opts.font_size, True)

View File

@@ -12,7 +12,7 @@ class CLIOptions:
LaunchCLIOptions = AskCLIOptions = ClipboardCLIOptions = DiffCLIOptions = CLIOptions
HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions
ErrorCLIOptions = UnicodeCLIOptions = RCOptions = RemoteFileCLIOptions = CLIOptions
QueryTerminalCLIOptions = BroadcastCLIOptions = ShowKeyCLIOptions = CLIOptions
BroadcastCLIOptions = ShowKeyCLIOptions = CLIOptions
ThemesCLIOptions = TransferCLIOptions = LoadConfigRCOptions = ActionRCOptions = CLIOptions
@@ -57,9 +57,6 @@ def generate_stub() -> None:
from kittens.icat.main import OPTIONS
do(OPTIONS, 'IcatCLIOptions')
from kittens.query_terminal.main import options_spec
do(options_spec(), 'QueryTerminalCLIOptions')
from kittens.panel.main import OPTIONS
do(OPTIONS(), 'PanelCLIOptions')

View File

@@ -22,6 +22,8 @@
#import <Foundation/NSDictionary.h>
#define debug debug_fonts
static inline void cleanup_cfrelease(void *__p) { CFTypeRef *tp = (CFTypeRef *)__p; CFTypeRef cf = *tp; if (cf) { CFRelease(cf); } }
#define RAII_CoreFoundation(type, name, initializer) __attribute__((cleanup(cleanup_cfrelease))) type name = initializer
typedef struct {
PyObject_HEAD
@@ -30,29 +32,32 @@ typedef struct {
float ascent, descent, leading, underline_position, underline_thickness, point_sz, scaled_point_sz;
CTFontRef ct_font;
hb_font_t *hb_font;
PyObject *family_name, *full_name, *postscript_name, *path;
PyObject *family_name, *full_name, *postscript_name, *path, *name_lookup_table;
FontFeatures font_features;
} CTFace;
PyTypeObject CTFace_Type;
static CTFontRef window_title_font = nil;
static char*
static PyObject*
convert_cfstring(CFStringRef src, int free_src) {
#define SZ 4094
static char buf[SZ+2] = {0};
bool ok = false;
if(!CFStringGetCString(src, buf, SZ, kCFStringEncodingUTF8)) PyErr_SetString(PyExc_ValueError, "Failed to convert CFString");
else ok = true;
if (free_src) CFRelease(src);
return ok ? buf : NULL;
RAII_CoreFoundation(CFStringRef, releaseme, free_src ? src : nil);
(void)releaseme;
if (!src) return PyUnicode_FromString("");
const char *fast = CFStringGetCStringPtr(src, kCFStringEncodingUTF8);
if (fast) return PyUnicode_FromString(fast);
#define SZ 4096
char buf[SZ];
if(!CFStringGetCString(src, buf, SZ, kCFStringEncodingUTF8)) { PyErr_SetString(PyExc_ValueError, "Failed to convert CFString"); return NULL; }
return PyUnicode_FromString(buf);
#undef SZ
}
static void
init_face(CTFace *self, CTFontRef font, FONTS_DATA_HANDLE fg UNUSED) {
init_face(CTFace *self, CTFontRef font) {
if (self->hb_font) hb_font_destroy(self->hb_font);
self->hb_font = NULL;
if (self->ct_font) CFRelease(self->ct_font);
self->ct_font = font;
self->ct_font = font; CFRetain(font);
self->units_per_em = CTFontGetUnitsPerEm(self->ct_font);
self->ascent = CTFontGetAscent(self->ct_font);
self->descent = CTFontGetDescent(self->ct_font);
@@ -62,18 +67,39 @@ init_face(CTFace *self, CTFontRef font, FONTS_DATA_HANDLE fg UNUSED) {
self->scaled_point_sz = CTFontGetSize(self->ct_font);
}
static PyObject*
convert_url_to_filesystem_path(CFURLRef url) {
uint8_t buf[4096];
if (url && CFURLGetFileSystemRepresentation(url, true, buf, sizeof(buf))) return PyUnicode_FromString((const char*)buf);
return PyUnicode_FromString("");
}
static PyObject*
get_path_for_font(CTFontRef font) {
RAII_CoreFoundation(CFURLRef, url, CTFontCopyAttribute(font, kCTFontURLAttribute));
return convert_url_to_filesystem_path(url);
}
static PyObject*
get_path_for_font_descriptor(CTFontDescriptorRef font) {
RAII_CoreFoundation(CFURLRef, url, CTFontDescriptorCopyAttribute(font, kCTFontURLAttribute));
return convert_url_to_filesystem_path(url);
}
static CTFace*
ct_face(CTFontRef font, FONTS_DATA_HANDLE fg) {
ct_face(CTFontRef font, PyObject *features) {
CTFace *self = (CTFace *)CTFace_Type.tp_alloc(&CTFace_Type, 0);
if (self) {
init_face(self, font, fg);
self->family_name = Py_BuildValue("s", convert_cfstring(CTFontCopyFamilyName(self->ct_font), true));
self->full_name = Py_BuildValue("s", convert_cfstring(CTFontCopyFullName(self->ct_font), true));
self->postscript_name = Py_BuildValue("s", convert_cfstring(CTFontCopyPostScriptName(self->ct_font), true));
NSURL *url = (NSURL*)CTFontCopyAttribute(self->ct_font, kCTFontURLAttribute);
self->path = Py_BuildValue("s", [[url path] UTF8String]);
[url release];
init_face(self, font);
self->family_name = convert_cfstring(CTFontCopyFamilyName(self->ct_font), true);
self->full_name = convert_cfstring(CTFontCopyFullName(self->ct_font), true);
self->postscript_name = convert_cfstring(CTFontCopyPostScriptName(self->ct_font), true);
self->path = get_path_for_font(self->ct_font);
if (self->family_name == NULL || self->full_name == NULL || self->postscript_name == NULL || self->path == NULL) { Py_CLEAR(self); }
else {
if (!create_features_for_face(postscript_name_for_face((PyObject*)self), features, &self->font_features)) { Py_CLEAR(self); }
}
}
return self;
}
@@ -84,50 +110,97 @@ dealloc(CTFace* self) {
if (self->ct_font) CFRelease(self->ct_font);
self->hb_font = NULL;
self->ct_font = NULL;
free(self->font_features.features);
Py_CLEAR(self->family_name); Py_CLEAR(self->full_name); Py_CLEAR(self->postscript_name); Py_CLEAR(self->path);
Py_CLEAR(self->name_lookup_table);
Py_TYPE(self)->tp_free((PyObject*)self);
}
static const char*
tag_to_string(uint32_t tag, uint8_t bytes[5]) {
bytes[0] = (tag >> 24) & 0xff;
bytes[1] = (tag >> 16) & 0xff;
bytes[2] = (tag >> 8) & 0xff;
bytes[3] = (tag) & 0xff;
bytes[4] = 0;
return (const char*)bytes;
}
static uint32_t
string_to_tag(const uint8_t *bytes) {
return (((uint32_t)bytes[0]) << 24) | (((uint32_t)bytes[1]) << 16) | (((uint32_t)bytes[2]) << 8) | bytes[3];
}
FontFeatures*
features_for_face(PyObject *s) { return &((CTFace*)s)->font_features; }
static void
add_variation_pair(const void *key_, const void *value_, void *ctx) {
PyObject *ans = ctx;
CFNumberRef key = key_, value = value_;
uint32_t tag; double val;
if (!CFNumberGetValue(key, kCFNumberSInt32Type, &tag)) return;
if (!CFNumberGetValue(value, kCFNumberDoubleType, &val)) return;
uint8_t tag_string[5];
tag_to_string(tag, tag_string);
RAII_PyObject(pyval, PyFloat_FromDouble(val));
if (pyval) PyDict_SetItemString(ans, (const char*)tag_string, pyval);
}
static PyObject*
variation_to_python(CFDictionaryRef v) {
if (!v) { Py_RETURN_NONE; }
RAII_PyObject(ans, PyDict_New());
if (!ans) return NULL;
CFDictionaryApplyFunction(v, add_variation_pair, ans);
if (PyErr_Occurred()) return NULL;
Py_INCREF(ans); return ans;
}
static PyObject*
font_descriptor_to_python(CTFontDescriptorRef descriptor) {
NSURL *url = (NSURL *) CTFontDescriptorCopyAttribute(descriptor, kCTFontURLAttribute);
NSString *psName = (NSString *) CTFontDescriptorCopyAttribute(descriptor, kCTFontNameAttribute);
NSString *family = (NSString *) CTFontDescriptorCopyAttribute(descriptor, kCTFontFamilyNameAttribute);
NSString *style = (NSString *) CTFontDescriptorCopyAttribute(descriptor, kCTFontStyleNameAttribute);
NSDictionary *traits = (NSDictionary *) CTFontDescriptorCopyAttribute(descriptor, kCTFontTraitsAttribute);
unsigned int straits = [traits[(id)kCTFontSymbolicTrait] unsignedIntValue];
float weightVal = [traits[(id)kCTFontWeightTrait] floatValue];
float widthVal = [traits[(id)kCTFontWidthTrait] floatValue];
RAII_PyObject(path, get_path_for_font_descriptor(descriptor));
RAII_PyObject(ps_name, convert_cfstring(CTFontDescriptorCopyAttribute(descriptor, kCTFontNameAttribute), true));
RAII_PyObject(family, convert_cfstring(CTFontDescriptorCopyAttribute(descriptor, kCTFontFamilyNameAttribute), true));
RAII_PyObject(style, convert_cfstring(CTFontDescriptorCopyAttribute(descriptor, kCTFontStyleNameAttribute), true));
RAII_PyObject(display_name, convert_cfstring(CTFontDescriptorCopyAttribute(descriptor, kCTFontDisplayNameAttribute), true));
RAII_CoreFoundation(CFDictionaryRef, traits, CTFontDescriptorCopyAttribute(descriptor, kCTFontTraitsAttribute));
unsigned long symbolic_traits = 0; float weight = 0, width = 0, slant = 0;
#define get_number(d, key, output, type_) { \
CFNumberRef value = (CFNumberRef)CFDictionaryGetValue(d, key); \
if (value) CFNumberGetValue(value, type_, &output); }
get_number(traits, kCTFontSymbolicTrait, symbolic_traits, kCFNumberLongType);
get_number(traits, kCTFontWeightTrait, weight, kCFNumberFloatType);
get_number(traits, kCTFontWidthTrait, width, kCFNumberFloatType);
get_number(traits, kCTFontSlantTrait, slant, kCFNumberFloatType);
RAII_CoreFoundation(CFDictionaryRef, cf_variation, CTFontDescriptorCopyAttribute(descriptor, kCTFontVariationAttribute));
RAII_PyObject(variation, variation_to_python(cf_variation));
if (!variation) return NULL;
#undef get_number
PyObject *ans = Py_BuildValue("{ssssssss sOsOsOsOsOsO sfsfsI}",
"path", [[url path] UTF8String],
"postscript_name", [psName UTF8String],
"family", [family UTF8String],
"style", [style UTF8String],
"bold", (straits & kCTFontBoldTrait) != 0 ? Py_True : Py_False,
"italic", (straits & kCTFontItalicTrait) != 0 ? Py_True : Py_False,
"monospace", (straits & kCTFontMonoSpaceTrait) != 0 ? Py_True : Py_False,
"expanded", (straits & kCTFontExpandedTrait) != 0 ? Py_True : Py_False,
"condensed", (straits & kCTFontCondensedTrait) != 0 ? Py_True : Py_False,
"color_glyphs", (straits & kCTFontColorGlyphsTrait) != 0 ? Py_True : Py_False,
PyObject *ans = Py_BuildValue("{ss sOsOsOsOsO sOsOsOsOsOsOsO sfsfsfsk}",
"descriptor_type", "core_text",
"weight", weightVal,
"width", widthVal,
"traits", straits
"path", path, "postscript_name", ps_name, "family", family, "style", style, "display_name", display_name,
"bold", (symbolic_traits & kCTFontBoldTrait) != 0 ? Py_True : Py_False,
"italic", (symbolic_traits & kCTFontItalicTrait) != 0 ? Py_True : Py_False,
"monospace", (symbolic_traits & kCTFontTraitMonoSpace) != 0 ? Py_True : Py_False,
"expanded", (symbolic_traits & kCTFontExpandedTrait) != 0 ? Py_True : Py_False,
"condensed", (symbolic_traits & kCTFontCondensedTrait) != 0 ? Py_True : Py_False,
"color_glyphs", (symbolic_traits & kCTFontColorGlyphsTrait) != 0 ? Py_True : Py_False,
"variation", variation,
"weight", weight, "width", width, "slant", slant, "traits", symbolic_traits
);
[url release];
[psName release];
[family release];
[style release];
[traits release];
return ans;
}
static CTFontDescriptorRef
font_descriptor_from_python(PyObject *src) {
CTFontSymbolicTraits symbolic_traits = 0;
NSMutableDictionary *attrs = [NSMutableDictionary dictionary];
RAII_CoreFoundation(CFMutableDictionaryRef, ans, CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
PyObject *t = PyDict_GetItemString(src, "traits");
if (t == NULL) {
symbolic_traits = (
@@ -137,19 +210,33 @@ font_descriptor_from_python(PyObject *src) {
} else {
symbolic_traits = PyLong_AsUnsignedLong(t);
}
NSDictionary *traits = @{(id)kCTFontSymbolicTrait:[NSNumber numberWithUnsignedInt:symbolic_traits]};
attrs[(id)kCTFontTraitsAttribute] = traits;
RAII_CoreFoundation(CFNumberRef, cf_symbolic_traits, CFNumberCreate(NULL, kCFNumberSInt32Type, &symbolic_traits));
CFTypeRef keys[] = { kCTFontSymbolicTrait };
CFTypeRef values[] = { cf_symbolic_traits };
RAII_CoreFoundation(CFDictionaryRef, traits, CFDictionaryCreate(NULL, keys, values, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
CFDictionaryAddValue(ans, kCTFontTraitsAttribute, traits);
#define SET(x, attr) \
t = PyDict_GetItemString(src, #x); \
if (t) attrs[(id)attr] = @(PyUnicode_AsUTF8(t));
#define SET(x, attr) if ((t = PyDict_GetItemString(src, #x))) { \
RAII_CoreFoundation(CFStringRef, cs, CFStringCreateWithCString(NULL, PyUnicode_AsUTF8(t), kCFStringEncodingUTF8)); \
CFDictionaryAddValue(ans, attr, cs); }
SET(family, kCTFontFamilyNameAttribute);
SET(style, kCTFontStyleNameAttribute);
SET(postscript_name, kCTFontNameAttribute);
#undef SET
return CTFontDescriptorCreateWithAttributes((CFDictionaryRef) attrs);
if ((t = PyDict_GetItemString(src, "axis_map"))) {
RAII_CoreFoundation(CFMutableDictionaryRef, axis_map, CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
PyObject *key, *value; Py_ssize_t pos = 0;
while (PyDict_Next(t, &pos, &key, &value)) {
double val = PyFloat_AS_DOUBLE(value);
uint32_t tag = string_to_tag((const uint8_t*)PyUnicode_AsUTF8(key));
RAII_CoreFoundation(CFNumberRef, cf_tag, CFNumberCreate(NULL, kCFNumberSInt32Type, &tag));
RAII_CoreFoundation(CFNumberRef, cf_val, CFNumberCreate(NULL, kCFNumberDoubleType, &val));
CFDictionaryAddValue(axis_map, cf_tag, cf_val);
}
CFDictionaryAddValue(ans, kCTFontVariationAttribute, axis_map);
}
return CTFontDescriptorCreateWithAttributes(ans);
}
static CTFontCollectionRef all_fonts_collection_data = NULL;
@@ -161,17 +248,33 @@ all_fonts_collection(void) {
}
static PyObject*
coretext_all_fonts(PyObject UNUSED *_self) {
CFArrayRef matches = CTFontCollectionCreateMatchingFontDescriptors(all_fonts_collection());
coretext_all_fonts(PyObject UNUSED *_self, PyObject *monospaced_only_) {
int monospaced_only = PyObject_IsTrue(monospaced_only_);
RAII_CoreFoundation(CFArrayRef, matches, CTFontCollectionCreateMatchingFontDescriptors(all_fonts_collection()));
const CFIndex count = CFArrayGetCount(matches);
PyObject *ans = PyTuple_New(count), *temp;
if (ans == NULL) { CFRelease(matches); return PyErr_NoMemory(); }
RAII_PyObject(ans, PyTuple_New(count));
if (ans == NULL) return NULL;
PyObject *temp;
Py_ssize_t num = 0;
for (CFIndex i = 0; i < count; i++) {
temp = font_descriptor_to_python((CTFontDescriptorRef) CFArrayGetValueAtIndex(matches, i));
if (temp == NULL) { CFRelease(matches); Py_DECREF(ans); return NULL; }
PyTuple_SET_ITEM(ans, i, temp); temp = NULL;
CTFontDescriptorRef desc = (CTFontDescriptorRef) CFArrayGetValueAtIndex(matches, i);
if (monospaced_only) {
RAII_CoreFoundation(CFDictionaryRef, traits, CTFontDescriptorCopyAttribute(desc, kCTFontTraitsAttribute));
if (traits) {
unsigned long symbolic_traits;
CFNumberRef value = (CFNumberRef)CFDictionaryGetValue(traits, kCTFontSymbolicTrait);
if (value) {
CFNumberGetValue(value, kCFNumberLongType, &symbolic_traits);
if (!(symbolic_traits & kCTFontTraitMonoSpace)) continue;
}
}
}
temp = font_descriptor_to_python(desc);
if (temp == NULL) return NULL;
PyTuple_SET_ITEM(ans, num++, temp); temp = NULL;
}
CFRelease(matches);
if (_PyTuple_Resize(&ans, num) == -1) return NULL;
Py_INCREF(ans);
return ans;
}
@@ -319,7 +422,7 @@ apply_styles_to_fallback_font(CTFontRef original_fallback_font, bool bold, bool
PyObject*
create_fallback_face(PyObject *base_face, CPUCell* cell, bool bold, bool italic, bool emoji_presentation, FONTS_DATA_HANDLE fg) {
CTFace *self = (CTFace*)base_face;
CTFontRef new_font;
RAII_CoreFoundation(CTFontRef, new_font, NULL);
#define search_for_fallback() \
char text[64] = {0}; \
cell_as_utf8_for_fallback(cell, text); \
@@ -337,7 +440,8 @@ create_fallback_face(PyObject *base_face, CPUCell* cell, bool bold, bool italic,
}
else { search_for_fallback(); new_font = apply_styles_to_fallback_font(new_font, bold, italic); }
if (new_font == NULL) return NULL;
PyObject *postscript_name = Py_BuildValue("s", convert_cfstring(CTFontCopyPostScriptName(new_font), true));
RAII_PyObject(postscript_name, convert_cfstring(CTFontCopyPostScriptName(new_font), true));
if (!postscript_name) return NULL;
ssize_t idx = -1;
PyObject *q, *ans = NULL;
while ((q = iter_fallback_faces(fg, &idx))) {
@@ -347,10 +451,7 @@ create_fallback_face(PyObject *base_face, CPUCell* cell, bool bold, bool italic,
break;
}
}
Py_CLEAR(postscript_name);
if (ans == NULL) return (PyObject*)ct_face(new_font, fg);
CFRelease(new_font);
return ans;
return ans ? ans : (PyObject*)ct_face(new_font, NULL);
}
unsigned int
@@ -377,20 +478,38 @@ get_glyph_width(PyObject *s, glyph_index g) {
return (int)ceil(bounds.size.width);
}
static float
_scaled_point_sz(double font_sz_in_pts, double dpi_x, double dpi_y) {
return ((dpi_x + dpi_y) / 144.0) * font_sz_in_pts;
}
static float
scaled_point_sz(FONTS_DATA_HANDLE fg) {
return ((fg->logical_dpi_x + fg->logical_dpi_y) / 144.0) * fg->font_sz_in_pts;
return _scaled_point_sz(fg->font_sz_in_pts, fg->logical_dpi_x, fg->logical_dpi_y);
}
static bool
_set_size_for_face(CTFace *self, bool force, double font_sz_in_pts, double dpi_x, double dpi_y) {
float sz = _scaled_point_sz(font_sz_in_pts, dpi_x, dpi_y);
if (!force && self->scaled_point_sz == sz) return true;
RAII_CoreFoundation(CTFontRef, new_font, CTFontCreateCopyWithAttributes(self->ct_font, sz, NULL, NULL));
if (new_font == NULL) fatal("Out of memory");
init_face(self, new_font);
return true;
}
bool
set_size_for_face(PyObject *s, unsigned int UNUSED desired_height, bool force, FONTS_DATA_HANDLE fg) {
CTFace *self = (CTFace*)s;
float sz = scaled_point_sz(fg);
if (!force && self->scaled_point_sz == sz) return true;
CTFontRef new_font = CTFontCreateCopyWithAttributes(self->ct_font, sz, NULL, NULL);
if (new_font == NULL) fatal("Out of memory");
init_face(self, new_font, fg);
return true;
return _set_size_for_face(self, force, fg->font_sz_in_pts, fg->logical_dpi_x, fg->logical_dpi_y);
}
static PyObject*
set_size(CTFace *self, PyObject *args) {
double font_sz_in_pts, dpi_x, dpi_y;
if (!PyArg_ParseTuple(args, "ddd", &font_sz_in_pts, &dpi_x, &dpi_y)) return NULL;
if (!_set_size_for_face(self, false, font_sz_in_pts, dpi_x, dpi_y)) return NULL;
Py_RETURN_NONE;
}
hb_font_t*
@@ -436,7 +555,7 @@ cell_metrics(PyObject *s, unsigned int* cell_width, unsigned int* cell_height, u
CFAttributedStringReplaceString(test_string, CFRangeMake(0, 0), ts);
CFAttributedStringSetAttribute(test_string, CFRangeMake(0, CFStringGetLength(ts)), kCTFontAttributeName, self->ct_font);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(10, 10, 200, 200));
CGPathAddRect(path, NULL, CGRectMake(10, 10, 200, 8000));
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(test_string);
CFRelease(test_string);
CTFrameRef test_frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
@@ -445,6 +564,7 @@ cell_metrics(PyObject *s, unsigned int* cell_width, unsigned int* cell_height, u
CTFrameGetLineOrigins(test_frame, CFRangeMake(1, 1), &origin2);
CGFloat line_height = origin1.y - origin2.y;
CFArrayRef lines = CTFrameGetLines(test_frame);
if (!CFArrayGetCount(lines)) fatal("Failed to typeset test line to calculate cell metrics");
CTLineRef line = CFArrayGetValueAtIndex(lines, 0);
CGRect bounds = CTLineGetBoundsWithOptions(line, 0);
CGRect bounds_without_leading = CTLineGetBoundsWithOptions(line, kCTLineBoundsExcludeTypographicLeading);
@@ -473,27 +593,38 @@ cell_metrics(PyObject *s, unsigned int* cell_width, unsigned int* cell_height, u
PyObject*
face_from_descriptor(PyObject *descriptor, FONTS_DATA_HANDLE fg) {
CTFontDescriptorRef desc = font_descriptor_from_python(descriptor);
RAII_CoreFoundation(CTFontDescriptorRef, desc, font_descriptor_from_python(descriptor));
if (!desc) return NULL;
CTFontRef font = CTFontCreateWithFontDescriptor(desc, scaled_point_sz(fg), NULL);
CFRelease(desc); desc = NULL;
RAII_CoreFoundation(CTFontRef, font, CTFontCreateWithFontDescriptor(desc, fg ? scaled_point_sz(fg) : 12, NULL));
if (!font) { PyErr_SetString(PyExc_ValueError, "Failed to create CTFont object"); return NULL; }
return (PyObject*) ct_face(font, fg);
return (PyObject*) ct_face(font, PyDict_GetItemString(descriptor, "features"));
}
PyObject*
face_from_path(const char *path, int UNUSED index, FONTS_DATA_HANDLE fg) {
CFStringRef s = CFStringCreateWithCString(NULL, path, kCFStringEncodingUTF8);
CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, s, kCFURLPOSIXPathStyle, false);
CGDataProviderRef dp = CGDataProviderCreateWithURL(url);
CGFontRef cg_font = CGFontCreateWithDataProvider(dp);
CTFontRef ct_font = CTFontCreateWithGraphicsFont(cg_font, 0.0, NULL, NULL);
CFRelease(cg_font); CFRelease(dp); CFRelease(url); CFRelease(s);
return (PyObject*) ct_face(ct_font, fg);
face_from_path(const char *path, int UNUSED index, FONTS_DATA_HANDLE fg UNUSED) {
RAII_CoreFoundation(CFStringRef, s, CFStringCreateWithCString(NULL, path, kCFStringEncodingUTF8));
RAII_CoreFoundation(CFURLRef, url, CFURLCreateWithFileSystemPath(kCFAllocatorDefault, s, kCFURLPOSIXPathStyle, false));
RAII_CoreFoundation(CGDataProviderRef, dp, CGDataProviderCreateWithURL(url));
RAII_CoreFoundation(CGFontRef, cg_font, CGFontCreateWithDataProvider(dp));
RAII_CoreFoundation(CTFontRef, ct_font, CTFontCreateWithGraphicsFont(cg_font, 0.0, NULL, NULL));
return (PyObject*) ct_face(ct_font, NULL);
}
static PyObject*
new(PyTypeObject *type UNUSED, PyObject *args, PyObject *kw) {
const char *path = NULL;
PyObject *descriptor = NULL;
static char *kwds[] = {"descriptor", "path", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kw, "|Os", kwds, &descriptor, &path)) return NULL;
if (descriptor) return face_from_descriptor(descriptor, NULL);
if (path) return face_from_path(path, 0, NULL);
PyErr_SetString(PyExc_TypeError, "Must specify either path or descriptor");
return NULL;
}
PyObject*
specialize_font_descriptor(PyObject *base_descriptor, FONTS_DATA_HANDLE fg UNUSED) {
specialize_font_descriptor(PyObject *base_descriptor, double font_sz_in_pts UNUSED, double dpi_x UNUSED, double dpi_y UNUSED) {
Py_INCREF(base_descriptor);
return base_descriptor;
}
@@ -611,6 +742,71 @@ render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) {
return ans;
}
static void destroy_hb_buffer(hb_buffer_t **x) { if (*x) hb_buffer_destroy(*x); }
static PyObject*
render_sample_text(CTFace *self, PyObject *args) {
unsigned long canvas_width, canvas_height;
unsigned long fg = 0xffffff;
CTFontRef font = self->ct_font;
PyObject *ptext;
if (!PyArg_ParseTuple(args, "Ukk|k", &ptext, &canvas_width, &canvas_height, &fg)) return NULL;
unsigned int cell_width, cell_height, baseline, underline_position, underline_thickness, strikethrough_position, strikethrough_thickness;
cell_metrics((PyObject*)self, &cell_width, &cell_height, &baseline, &underline_position, &underline_thickness, &strikethrough_position, &strikethrough_thickness);
size_t num_chars = PyUnicode_GET_LENGTH(ptext);
int num_chars_per_line = canvas_width / cell_width, num_of_lines = (int)ceil((float)num_chars / (float)num_chars_per_line);
canvas_height = MIN(canvas_height, num_of_lines * cell_height);
RAII_PyObject(pbuf, PyBytes_FromStringAndSize(NULL, sizeof(pixel) * canvas_width * canvas_height));
if (!pbuf) return NULL;
__attribute__((cleanup(destroy_hb_buffer))) hb_buffer_t *hb_buffer = hb_buffer_create();
if (!hb_buffer_pre_allocate(hb_buffer, 4*num_chars)) { PyErr_NoMemory(); return NULL; }
for (size_t n = 0; n < num_chars; n++) {
Py_UCS4 codep = PyUnicode_READ_CHAR(ptext, n);
hb_buffer_add_utf32(hb_buffer, &codep, 1, 0, 1);
}
hb_buffer_guess_segment_properties(hb_buffer);
if (!HB_DIRECTION_IS_HORIZONTAL(hb_buffer_get_direction(hb_buffer))) goto end;
hb_shape(harfbuzz_font_for_face((PyObject*)self), hb_buffer, self->font_features.features, self->font_features.count);
unsigned int len = hb_buffer_get_length(hb_buffer);
hb_glyph_info_t *info = hb_buffer_get_glyph_infos(hb_buffer, NULL);
hb_glyph_position_t *positions = hb_buffer_get_glyph_positions(hb_buffer, NULL);
memset(PyBytes_AS_STRING(pbuf), 0, PyBytes_GET_SIZE(pbuf));
if (cell_width > canvas_width) goto end;
ensure_render_space(canvas_width, canvas_height, len);
float pen_x = 0, pen_y = 0;
unsigned num_glyphs = 0;
CGFloat scale = CTFontGetSize(self->ct_font) / CTFontGetUnitsPerEm(self->ct_font);
for (unsigned int i = 0; i < len; i++) {
float advance = (float)positions[i].x_advance * scale;
if (pen_x + advance > canvas_width) {
pen_y += cell_height;
pen_x = 0;
if (pen_y >= canvas_height) break;
}
double x = pen_x + (double)positions[i].x_offset * scale;
double y = pen_y + (double)positions[i].y_offset * scale;
pen_x += advance;
buffers.positions[i] = CGPointMake(x, -y);
buffers.glyphs[i] = info[i].codepoint;
num_glyphs++;
}
render_glyphs(font, canvas_width, canvas_height, baseline, num_glyphs);
uint8_t r = (fg >> 16) & 0xff, g = (fg >> 8) & 0xff, b = fg & 0xff;
for (
uint8_t *p = (uint8_t*)PyBytes_AS_STRING(pbuf), *s = buffers.render_buf;
p < (uint8_t*)PyBytes_AS_STRING(pbuf) + sizeof(pixel) * canvas_width * canvas_height;
p += 4, s++
) {
p[0] = r; p[1] = g; p[2] = b; p[3] = s[0];
}
end:
return Py_BuildValue("OII", pbuf, cell_width, cell_height);
}
static bool
ensure_ui_font(size_t in_height) {
static size_t for_height = 0;
@@ -728,7 +924,7 @@ do_render(CTFontRef ct_font, unsigned int units_per_em, bool bold, bool italic,
} else {
render_glyphs(ct_font, canvas_width, cell_height, baseline, num_glyphs);
Region src = {.bottom=cell_height, .right=canvas_width}, dest = {.bottom=cell_height, .right=canvas_width};
render_alpha_mask(buffers.render_buf, canvas, &src, &dest, canvas_width, canvas_width);
render_alpha_mask(buffers.render_buf, canvas, &src, &dest, canvas_width, canvas_width, 0xffffff);
}
if (num_cells && (center_glyph || (num_cells == 2 && *was_colored))) {
if (debug_rendering) printf("centering glyphs: center_glyph: %d\n", center_glyph);
@@ -749,6 +945,74 @@ render_glyphs_in_cells(PyObject *s, bool bold, bool italic, hb_glyph_info_t *inf
return do_render(self->ct_font, self->units_per_em, bold, italic, info, hb_positions, num_glyphs, canvas, cell_width, cell_height, num_cells, baseline, was_colored, true, fg, center_glyph);
}
// Font tables {{{
static bool
ensure_name_table(CTFace *self) {
if (self->name_lookup_table) return true;
RAII_CoreFoundation(CFDataRef, cftable, CTFontCopyTable(self->ct_font, kCTFontTableName, kCTFontTableOptionNoOptions));
const uint8_t *table = cftable ? CFDataGetBytePtr(cftable) : NULL;
size_t table_len = cftable ? CFDataGetLength(cftable) : 0;
self->name_lookup_table = read_name_font_table(table, table_len);
return !!self->name_lookup_table;
}
static PyObject*
get_best_name(CTFace *self, PyObject *nameid) {
if (!ensure_name_table(self)) return NULL;
return get_best_name_from_name_table(self->name_lookup_table, nameid);
}
static PyObject*
get_variation(CTFace *self) {
RAII_CoreFoundation(CFDictionaryRef, src, CTFontCopyVariation(self->ct_font));
return variation_to_python(src);
}
static PyObject*
applied_features(CTFace *self, PyObject *a UNUSED) {
return font_features_as_dict(&self->font_features);
}
static PyObject*
get_features(CTFace *self, PyObject *a UNUSED) {
if (!ensure_name_table(self)) return NULL;
RAII_PyObject(output, PyDict_New()); if (!output) return NULL;
RAII_CoreFoundation(CFDataRef, cftable, CTFontCopyTable(self->ct_font, kCTFontTableGSUB, kCTFontTableOptionNoOptions));
const uint8_t *table = cftable ? CFDataGetBytePtr(cftable) : NULL;
size_t table_len = cftable ? CFDataGetLength(cftable) : 0;
if (!read_features_from_font_table(table, table_len, self->name_lookup_table, output)) return NULL;
RAII_CoreFoundation(CFDataRef, cfpostable, CTFontCopyTable(self->ct_font, kCTFontTableGPOS, kCTFontTableOptionNoOptions));
table = cfpostable ? CFDataGetBytePtr(cfpostable) : NULL;
table_len = cfpostable ? CFDataGetLength(cfpostable) : 0;
if (!read_features_from_font_table(table, table_len, self->name_lookup_table, output)) return NULL;
Py_INCREF(output); return output;
}
static PyObject*
get_variable_data(CTFace *self) {
if (!ensure_name_table(self)) return NULL;
RAII_PyObject(output, PyDict_New());
if (!output) return NULL;
RAII_CoreFoundation(CFDataRef, cftable, CTFontCopyTable(self->ct_font, kCTFontTableFvar, kCTFontTableOptionNoOptions));
const uint8_t *table = cftable ? CFDataGetBytePtr(cftable) : NULL;
size_t table_len = cftable ? CFDataGetLength(cftable) : 0;
if (!read_fvar_font_table(table, table_len, self->name_lookup_table, output)) return NULL;
RAII_CoreFoundation(CFDataRef, stable, CTFontCopyTable(self->ct_font, kCTFontTableSTAT, kCTFontTableOptionNoOptions));
table = stable ? CFDataGetBytePtr(stable) : NULL;
table_len = stable ? CFDataGetLength(stable) : 0;
if (!read_STAT_font_table(table, table_len, self->name_lookup_table, output)) return NULL;
Py_INCREF(output); return output;
}
static PyObject*
identify_for_debug(CTFace *self) {
return PyUnicode_FromFormat("%V: %V", self->postscript_name, "[psname]", self->path, "[path]");
}
// }}}
// Boilerplate {{{
@@ -756,12 +1020,26 @@ render_glyphs_in_cells(PyObject *s, bool bold, bool italic, hb_glyph_info_t *inf
static PyObject*
display_name(CTFace *self) {
CFStringRef dn = CTFontCopyDisplayName(self->ct_font);
const char *d = convert_cfstring(dn, true);
return Py_BuildValue("s", d);
return convert_cfstring(dn, true);
}
static PyObject*
postscript_name(CTFace *self) {
return self->postscript_name ? Py_BuildValue("O", self->postscript_name) : PyUnicode_FromString("");
}
static PyMethodDef methods[] = {
METHODB(display_name, METH_NOARGS),
METHODB(postscript_name, METH_NOARGS),
METHODB(get_variable_data, METH_NOARGS),
METHODB(applied_features, METH_NOARGS),
METHODB(get_features, METH_NOARGS),
METHODB(get_variation, METH_NOARGS),
METHODB(identify_for_debug, METH_NOARGS),
METHODB(set_size, METH_VARARGS),
METHODB(render_sample_text, METH_VARARGS),
METHODB(get_best_name, METH_O),
{NULL} /* Sentinel */
};
@@ -786,7 +1064,7 @@ repr(CTFace *self) {
static PyMethodDef module_methods[] = {
METHODB(coretext_all_fonts, METH_NOARGS),
METHODB(coretext_all_fonts, METH_O),
{NULL, NULL, 0, NULL} /* Sentinel */
};
@@ -802,13 +1080,13 @@ static PyMemberDef members[] = {
MEM(family_name, T_OBJECT),
MEM(path, T_OBJECT),
MEM(full_name, T_OBJECT),
MEM(postscript_name, T_OBJECT),
{NULL} /* Sentinel */
};
PyTypeObject CTFace_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "fast_data_types.CTFace",
.tp_new = new,
.tp_basicsize = sizeof(CTFace),
.tp_dealloc = (destructor)dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,

View File

@@ -17,7 +17,7 @@ from kittens.tui.operations import colored, styled
from .child import cmdline_of_pid
from .cli import version
from .constants import extensions_dir, is_macos, is_wayland, kitty_base_dir, kitty_exe, shell_path
from .fast_data_types import Color, SingleKey, num_users, opengl_version_string, wayland_compositor_data
from .fast_data_types import Color, SingleKey, current_fonts, num_users, opengl_version_string, wayland_compositor_data
from .options.types import Options as KittyOpts
from .options.types import defaults
from .options.utils import KeyboardMode, KeyDefinition
@@ -257,6 +257,10 @@ def debug_config(opts: KittyOpts) -> str:
p('Running under:', green(compositor_name()))
p(green('OpenGL:'), opengl_version_string())
p(green('Frozen:'), 'True' if getattr(sys, 'frozen', False) else 'False')
p(green('Fonts:'))
for k, font in current_fonts().items():
if hasattr(font, 'identify_for_debug'):
p(yellow(f' {k}:'), font.identify_for_debug())
p(green('Paths:'))
p(yellow(' kitty:'), os.path.realpath(kitty_exe()))
p(yellow(' base dir:'), kitty_base_dir)

View File

@@ -1,14 +1,14 @@
import termios
from ctypes import Array, c_ubyte
from typing import Any, Callable, Dict, Iterator, List, NewType, Optional, Tuple, TypedDict, Union, overload
from typing import Any, Callable, Dict, Iterator, List, Literal, NewType, Optional, Tuple, TypedDict, Union, overload
from kitty.boss import Boss
from kitty.fonts import FontFeature
from kitty.fonts import VariableData
from kitty.fonts.render import FontObject
from kitty.marks import MarkerFunc
from kitty.options.types import Options
from kitty.types import LayerShellConfig, SignalInfo
from kitty.typing import EdgeLiteral
from kitty.typing import EdgeLiteral, NotRequired
# Constants {{{
GLFW_LAYER_SHELL_NONE: int
@@ -288,6 +288,8 @@ FC_MONO: int = 100
FC_DUAL: int
FC_WEIGHT_REGULAR: int
FC_WEIGHT_BOLD: int
FC_WEIGHT_SEMIBOLD: int
FC_WEIGHT_MEDIUM: int
FC_WIDTH_NORMAL: int
FC_SLANT_ROMAN: int
FC_SLANT_ITALIC: int
@@ -373,6 +375,7 @@ def default_color_table() -> Tuple[int, ...]:
class FontConfigPattern(TypedDict):
descriptor_type: Literal['fontconfig']
path: str
index: int
family: str
@@ -391,12 +394,16 @@ class FontConfigPattern(TypedDict):
scalable: bool
outline: bool
color: bool
variable: bool
named_instance: bool
# The following two are used by C code to get a face from the pattern
named_style: NotRequired[int]
axes: NotRequired[Tuple[float, ...]]
features: NotRequired[Tuple[ParsedFontFeature, ...]]
def fc_list(
spacing: int = -1,
allow_bitmapped_fonts: bool = False
) -> Tuple[FontConfigPattern, ...]:
def fc_list(spacing: int = -1, allow_bitmapped_fonts: bool = False, only_variable: bool = False) -> Tuple[FontConfigPattern, ...]:
pass
@@ -418,9 +425,31 @@ def fc_match_postscript_name(
pass
class FeatureData(TypedDict):
name: NotRequired[str]
tooltip: NotRequired[str]
sample: NotRequired[str]
params: NotRequired[Tuple[str, ...]]
class Face:
path: Optional[str]
def __init__(self, descriptor: Optional[FontConfigPattern] = None, path: str = '', index: int = 0): ...
def get_variable_data(self) -> VariableData: ...
def identify_for_debug(self) -> str: ...
def postscript_name(self) -> str: ...
def set_size(self, sz_in_pts: float, dpi_x: float, dpi_y: float) -> None: ...
def render_sample_text(self, text: str, width: int, height: int, fg_color: int = 0xffffff) -> Tuple[bytes, int, int]: ...
def get_variation(self) -> Optional[Dict[str, float]]: ...
def get_features(self) -> Dict[str, Optional[FeatureData]]: ...
def applied_features(self) -> Dict[str, str]: ...
class CoreTextFont(TypedDict):
descriptor_type: Literal['core_text']
path: str
postscript_name: str
display_name: str
family: str
style: str
bold: bool
@@ -429,15 +458,38 @@ class CoreTextFont(TypedDict):
condensed: bool
color_glyphs: bool
monospace: bool
variation: Optional[Dict[str, float]]
weight: float
width: float
slant: float
traits: int
# The following is used by C code to get a face from the pattern
axis_map: NotRequired[Dict[str, float]]
features: NotRequired[Tuple[ParsedFontFeature, ...]]
def coretext_all_fonts() -> Tuple[CoreTextFont, ...]:
class CTFace:
path: Optional[str]
def __init__(self, descriptor: Optional[CoreTextFont] = None, path: str = ''): ...
def get_variable_data(self) -> VariableData: ...
def identify_for_debug(self) -> str: ...
def postscript_name(self) -> str: ...
def set_size(self, sz_in_pts: float, dpi_x: float, dpi_y: float) -> None: ...
def render_sample_text(self, text: str, width: int, height: int, fg_color: int = 0xffffff) -> Tuple[bytes, int, int]: ...
def get_variation(self) -> Optional[Dict[str, float]]: ...
def get_features(self) -> Dict[str, Optional[FeatureData]]: ...
def applied_features(self) -> Dict[str, str]: ...
def coretext_all_fonts(monospaced_only: bool) -> Tuple[CoreTextFont, ...]:
pass
class ParsedFontFeature:
def __init__(self, s: str): ...
def add_timer(
callback: Callable[[Optional[int]], None],
interval: float,
@@ -556,10 +608,6 @@ def get_options() -> Options:
pass
def parse_font_feature(ff: str) -> bytes:
pass
def glfw_primary_monitor_size() -> Tuple[int, int]:
pass
@@ -703,6 +751,7 @@ class Color:
class ColorProfile:
default_fg: int
default_bg: int
def as_dict(self) -> Dict[str, Optional[int]]:
@@ -888,8 +937,21 @@ def concat_cells(cell_width: int, cell_height: int, is_32_bit: bool, cells: Tupl
pass
def current_fonts() -> Dict[str, Any]:
pass
FontFace = Union[Face, CTFace]
class CurrentFonts(TypedDict):
medium: FontFace
bold: FontFace
italic: FontFace
bi: FontFace
symbol: Tuple[FontFace, ...]
fallback: Tuple[FontFace, ...]
font_sz_in_pts: float
logical_dpi_x: float
logical_dpi_y: float
def current_fonts(os_window_id: int = 0) -> CurrentFonts: ...
def remove_window(os_window_id: int, tab_id: int, window_id: int) -> None:
@@ -1013,7 +1075,6 @@ def set_font_data(
descriptor_for_idx: Callable[[int], Tuple[FontObject, bool, bool]],
bold: int, italic: int, bold_italic: int, num_symbol_fonts: int,
symbol_maps: Tuple[Tuple[int, int, int], ...], font_sz_in_pts: float,
font_feature_settings: Dict[str, Tuple[FontFeature, ...]],
narrow_symbols: Tuple[Tuple[int, int, int], ...],
) -> None:
pass

369
kitty/font-names.c Normal file
View File

@@ -0,0 +1,369 @@
/*
* font-names.c
* Copyright (C) 2024 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "fonts.h"
static PyObject*
decode_name_record(PyObject *namerec) {
#define d(x) PyLong_AsUnsignedLong(PyTuple_GET_ITEM(namerec, x))
unsigned long platform_id = d(0), encoding_id = d(1), language_id = d(2);
#undef d
const char *encoding = "unicode_escape";
if ((platform_id == 3 && encoding_id == 1) || platform_id == 0) encoding = "utf-16-be";
else if (platform_id == 1 && encoding_id == 0 && language_id == 0) encoding = "mac-roman";
PyObject *b = PyTuple_GET_ITEM(namerec, 3);
return PyUnicode_Decode(PyBytes_AS_STRING(b), PyBytes_GET_SIZE(b), encoding, "replace");
}
static bool
namerec_matches(PyObject *namerec, unsigned platform_id, unsigned encoding_id, unsigned language_id) {
#define d(x) PyLong_AsUnsignedLong(PyTuple_GET_ITEM(namerec, x))
return d(0) == platform_id && d(1) == encoding_id && d(2) == language_id;
#undef d
}
static PyObject*
find_matching_namerec(PyObject *namerecs, unsigned platform_id, unsigned encoding_id, unsigned language_id) {
for (Py_ssize_t i = 0; i < PyList_GET_SIZE(namerecs); i++) {
PyObject *namerec = PyList_GET_ITEM(namerecs, i);
if (namerec_matches(namerec, platform_id, encoding_id, language_id)) return decode_name_record(namerec);
}
return NULL;
}
bool
add_font_name_record(PyObject *table, uint16_t platform_id, uint16_t encoding_id, uint16_t language_id, uint16_t name_id, const char *string, uint16_t string_len) {
RAII_PyObject(key, PyLong_FromUnsignedLong((unsigned long)name_id));
if (!key) return false;
RAII_PyObject(list, PyDict_GetItem(table, key));
if (list == NULL) {
list = PyList_New(0);
if (!list) return false;
if (PyDict_SetItem(table, key, list) != 0) return false;
} else Py_INCREF(list);
RAII_PyObject(value, Py_BuildValue("(H H H y#)", platform_id, encoding_id, language_id, string, (Py_ssize_t)string_len));
if (!value) return false;
if (PyList_Append(list, value) != 0) return false;
return true;
}
PyObject*
get_best_name_from_name_table(PyObject *table, PyObject *name_id) {
PyObject *namerecs = PyDict_GetItem(table, name_id);
if (namerecs == NULL) return PyUnicode_FromString("");
if (PyList_GET_SIZE(namerecs) == 1) return decode_name_record(PyList_GET_ITEM(namerecs, 0));
#define d(...) { PyObject *ans = find_matching_namerec(namerecs, __VA_ARGS__); if (ans != NULL || PyErr_Occurred()) return ans; }
d(3, 1, 1033); // Microsoft/Windows/US English
d(1, 0, 0); // Mac/Roman/English
d(0, 6, 0); // Unicode/SMP/*
d(0, 4, 0); // Unicode/SMP/*
d(0, 3, 0); // Unicode/BMP/*
d(0, 2, 0); // Unicode/10646-BMP/*
d(0, 1, 0); // Unicode/1.1/*
#undef d
return PyUnicode_FromString("");
}
static PyObject*
get_best_name(PyObject *table, unsigned long name_id) {
RAII_PyObject(id, PyLong_FromUnsignedLong(name_id));
return get_best_name_from_name_table(table, id);
}
// OpenType tables are big-endian for god knows what reason so need to byteswap
static uint16_t
byteswap(const uint16_t *p) {
const uint8_t *b = (const uint8_t*)p;
return (((uint16_t)b[0]) << 8) | b[1];
}
static uint32_t
byteswap32(const uint32_t *val) {
const uint8_t *p = (const uint8_t*)val;
return (((uint32_t)p[0]) << 24) | (((uint32_t)p[1]) << 16) | (((uint32_t)p[2]) << 8) | p[3];
}
static double
load_fixed(const uint32_t *p_) {
uint32_t p = byteswap32(p_);
static const double denom = 1 << 16;
return ((int32_t)p) / denom;
}
#define next byteswap(p++)
#define next32 load_fixed(p32++)
PyObject*
read_name_font_table(const uint8_t *table, size_t table_len) {
if (!table || table_len < 9 * sizeof(uint16_t)) return PyDict_New();
uint16_t *p = (uint16_t*)table; p++;
uint16_t num_of_name_records = next, storage_offset = next;
const uint8_t *storage = table + storage_offset, *slimit = table + table_len;
if (storage >= slimit) return PyDict_New();
RAII_PyObject(ans, PyDict_New());
for (; num_of_name_records > 0 && p + 6 <= (uint16_t*)slimit; num_of_name_records--) {
uint16_t platform_id = next, encoding_id = next, language_id = next, name_id = next, length = next, offset = next;
const uint8_t *s = storage + offset;
if (s + length <= slimit && !add_font_name_record(
ans, platform_id, encoding_id, language_id, name_id, (const char*)(s), length)) return NULL;
}
Py_INCREF(ans);
return ans;
}
static bool is_digit(char x) { return '0' <= x && x <= '9'; }
static PyObject*
read_cv_feature_table(const uint8_t *table, const uint8_t *limit, PyObject *name_lookup_table) {
RAII_PyObject(ans, PyDict_New()); if (!ans) return NULL;
if (limit - table >= 12) {
uint16_t *p = (uint16_t*)(table + 2);
uint16_t name_id = next, tooltip_id = next, sample_id = next, num_params = next, first_value_id = next;
if (name_id) {
RAII_PyObject(name, get_best_name(name_lookup_table, name_id)); if (!name) return NULL;
if (PyDict_SetItemString(ans, "name", name) != 0) return NULL;
}
if (tooltip_id) {
RAII_PyObject(tooltip, get_best_name(name_lookup_table, tooltip_id)); if (!tooltip) return NULL;
if (PyDict_SetItemString(ans, "tooltip", tooltip) != 0) return NULL;
}
if (sample_id) {
RAII_PyObject(sample, get_best_name(name_lookup_table, sample_id)); if (!sample) return NULL;
if (PyDict_SetItemString(ans, "sample", sample) != 0) return NULL;
}
if (num_params && first_value_id) {
RAII_PyObject(params, PyTuple_New(num_params)); if (!params) return NULL;
for (uint16_t i = 0; i < num_params; i++) {
PyObject *pval = get_best_name(name_lookup_table, first_value_id + i); if (!pval) return NULL;
PyTuple_SET_ITEM(params, i, pval);
}
if (PyDict_SetItemString(ans, "params", params) != 0) return NULL;
}
}
Py_INCREF(ans); return ans;
}
static PyObject*
read_ss_feature_table(const uint8_t *table, const uint8_t *limit, PyObject *name_lookup_table) {
RAII_PyObject(ans, PyDict_New()); if (!ans) return NULL;
if (limit - table < 4) { Py_INCREF(ans); return ans; }
uint16_t *p = (uint16_t*)(table + 2);
uint16_t name_id = next;
if (name_id) {
RAII_PyObject(name, get_best_name(name_lookup_table, name_id)); if (!name) return NULL;
if (PyDict_SetItemString(ans, "name", name) != 0) return NULL;
}
Py_INCREF(ans); return ans;
}
bool
read_features_from_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table, PyObject *output) {
if (table_len < 20) return true;
const uint16_t *p = (uint16_t*)table;
const uint8_t *limit = table + table_len;
uint16_t major_version = next, minor_version = next, script_list_offset = next, feature_list_offset = next;
(void)major_version; (void)minor_version; (void)script_list_offset;
const uint8_t *feature_list_table = table + feature_list_offset;
char tag_buf[5] = {0};
if (feature_list_table + 2 >= limit) return true;
p = (uint16_t*)feature_list_table;
uint16_t feature_count = next;
const uint8_t *pos = (uint8_t*)p;
for (uint16_t i = 0; i < feature_count && pos + 6 <= limit; pos += 6, i++) {
memcpy(tag_buf, pos, 4);
RAII_PyObject(tag, PyUnicode_FromString(tag_buf));
if (!tag) return false;
if (PyDict_Contains(output, tag) == 1) continue;
if (PyDict_SetItem(output, tag, Py_None) != 0) return false;
p = (uint16_t*)(pos + 4); uint16_t offset_to_feature_table = next;
const uint8_t *feature_table = feature_list_table + offset_to_feature_table;
if (feature_table + 2 > limit) continue;
p = (uint16_t*)(feature_table); uint16_t offset_to_feature_params_table = next;
if (tag_buf[0] == 'c' && tag_buf[1] == 'v' && is_digit(tag_buf[2]) && is_digit(tag_buf[3])) {
if (offset_to_feature_params_table) {
RAII_PyObject(cv, read_cv_feature_table(feature_table + offset_to_feature_params_table, limit, name_lookup_table));
if (!cv) return false;
if (PyDict_SetItem(output, tag, cv) != 0) return false;
}
} else if (tag_buf[0] == 's' && tag_buf[1] == 's' && '0' <= tag_buf[2] && tag_buf[2] <= '2' && is_digit(tag_buf[3])) {
if (offset_to_feature_params_table) {
RAII_PyObject(ss, read_ss_feature_table(feature_table + offset_to_feature_params_table, limit, name_lookup_table));
if (!ss) return false;
if (PyDict_SetItem(output, tag, ss) != 0) return false;
}
}
}
return true;
}
bool
read_STAT_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table, PyObject *output) {
RAII_PyObject(design_axes, PyTuple_New(0));
RAII_PyObject(multi_axis_styles, PyTuple_New(0));
if (!design_axes || !multi_axis_styles) return false;
if (table_len < 20) goto ok;
const uint16_t *p = (uint16_t*)table;
uint16_t major_version = next, minor_version = next, size_of_design_axis_entry = next, count_of_design_axis_entries = next;
const uint32_t *p32 = (uint32_t*)p;
uint32_t offset_to_start_of_design_axes_entries = byteswap32(p32++);
p = (uint16_t*)p32;
uint16_t count_of_axis_value_entries = next;
p32 = (uint32_t*)p;
uint32_t offset_to_start_of_axis_value_entries = byteswap32(p32++);
p = (uint16_t*)p32;
uint16_t elided_fallback_name_id = next;
if (major_version == 1 && minor_version < 1) elided_fallback_name_id = 0;
if (PyDict_SetItemString(output, "elided_fallback_name", elided_fallback_name_id ? get_best_name(name_lookup_table, elided_fallback_name_id) : PyUnicode_FromString("")) != 0) return false;
const uint8_t *table_limit = table + table_len;
size_t count = 0;
if (_PyTuple_Resize(&design_axes, count_of_design_axis_entries) == -1) return false;
for (
const uint8_t *pos = table + offset_to_start_of_design_axes_entries;
pos + size_of_design_axis_entry <= table_limit && count < count_of_design_axis_entries;
pos += size_of_design_axis_entry, count++
) {
p = (uint16_t*)(pos + 4);
uint16_t name_id = next, ordering = next;
PyObject *rec = Py_BuildValue("{ss# sN sH sN}", "tag", (char*)pos, 4, "name", get_best_name(name_lookup_table, name_id), "ordering", ordering, "values", PyList_New(0));
if (!rec) return false;
PyTuple_SET_ITEM(design_axes, count, rec);
}
if (_PyTuple_Resize(&design_axes, count) == -1) return false;
count = 0;
const uint8_t *start_of_axis_values_offsets_array = table + offset_to_start_of_axis_value_entries;
Py_ssize_t i = 0;
if (_PyTuple_Resize(&multi_axis_styles, count_of_axis_value_entries) == -1) return false;
for (
const uint8_t *pos = start_of_axis_values_offsets_array;
pos + 2 <= table_limit && count < count_of_axis_value_entries;
pos += 2, count++
) {
p = (uint16_t*)pos;
uint16_t offset = next;
const uint8_t *start_of_axis_values_table = start_of_axis_values_offsets_array + offset;
if (start_of_axis_values_table + 12 > table_limit) continue;
p = (uint16_t*)(start_of_axis_values_table);
uint16_t format = next, axis_index = next, flags = next, value_name_id = next;
p32 = (uint32_t*)p;
#define app(fmt, ...) { \
RAII_PyObject(v, Py_BuildValue("{sH sH sN " fmt "}", \
"format", format, "flags", flags, "name", get_best_name(name_lookup_table, value_name_id), __VA_ARGS__)); \
if (!v) return false; \
PyObject *l = PyDict_GetItemString(PyTuple_GET_ITEM(design_axes, axis_index), "values"); \
if (l && PyList_Append(l, v) != 0) return false; \
}
switch(format) {
case 1: if (p32 + 1 <= (uint32_t*)table_limit && axis_index < PyTuple_GET_SIZE(design_axes)) {
const double value = next32; app("sd", "value", value);
} break;
case 2: if (p32 + 3 <= (uint32_t*)table_limit && axis_index < PyTuple_GET_SIZE(design_axes)) {
const double value = next32, minimum = next32, maximum = next32;
app("sd sd sd", "value", value, "minimum", minimum, "maximum", maximum);
} break;
case 3: if (p32 + 2 <= (uint32_t*)table_limit && axis_index < PyTuple_GET_SIZE(design_axes)) {
const double value = next32, linked_value = next32;
app("sd sd", "value", value, "linked_value", linked_value);
} break;
case 4: if ((uint8_t*)(p) + 6 * axis_index <= table_limit) {
RAII_PyObject(values, PyTuple_New(axis_index));
if (!values) return false;
for (uint16_t n = 0; n < axis_index; n++, p += 3) {
uint16_t actual_axis_index = next;
p32 = (uint32_t*)p; double value = next32;
PyObject *e = Py_BuildValue("{sH sd}", "design_index", actual_axis_index, "value", value);
if (!e) return false;
PyTuple_SET_ITEM(values, n, e);
}
PyObject *e = Py_BuildValue("{sH sN sO}", "flags", flags,
"name", get_best_name(name_lookup_table, value_name_id), "values", values);
if (!e) return false;
PyTuple_SET_ITEM(multi_axis_styles, i++, e);
} break;
}
}
if (_PyTuple_Resize(&multi_axis_styles, i) == -1) return false;
ok:
if (PyDict_SetItemString(output, "design_axes", design_axes) != 0) return false;
if (PyDict_SetItemString(output, "multi_axis_styles", multi_axis_styles) != 0) return false;
return true;
}
bool
read_fvar_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table, PyObject *output) {
RAII_PyObject(named_styles, PyTuple_New(0)); if (!named_styles) return false;
RAII_PyObject(axes, PyTuple_New(0)); if (!axes) return false;
if (!table || table_len < 14 * sizeof(uint16_t)) goto ok;
const uint16_t *p = (uint16_t*)table;
p += 2;
const uint16_t offset_to_start_of_axis_array = next; next;
const uint16_t num_of_axis_records = next, size_of_axis_record = next, num_of_name_records = next, size_of_name_record = next;
const uint16_t size_of_coordinates = num_of_axis_records * sizeof(int32_t);
if (size_of_name_record < size_of_coordinates + 4) {
PyErr_Format(PyExc_ValueError, "size of name record: %u too small", size_of_name_record); return NULL;
}
const bool has_postscript_name = size_of_name_record >= 3 * sizeof(uint16_t) + size_of_coordinates;
uint16_t i = 0;
if (size_of_axis_record < 20) { PyErr_Format(PyExc_ValueError, "size of axis record: %u too small", size_of_axis_record); return NULL; }
if (_PyTuple_Resize(&axes, num_of_axis_records) == -1) return NULL;
for (
const uint8_t *pos = table + offset_to_start_of_axis_array;
pos + size_of_axis_record <= table + table_len && i < num_of_axis_records;
i++, pos += size_of_axis_record
) {
uint32_t *p32 = (uint32_t*)(pos + 4);
const double minimum = next32, def = next32, maximum = next32;
p = (uint16_t*)(pos + 16);
int32_t flags = next, strid = next;
PyObject *axis = Py_BuildValue("{sd sd sd ss# sO sN}",
"minimum", minimum, "maximum", maximum, "default", def, "tag", pos, 4,
"hidden", (flags & 1) ? Py_True : Py_False, "strid", get_best_name(name_lookup_table, strid)
); if (!axis) return NULL;
PyTuple_SET_ITEM(axes, i, axis);
}
if (_PyTuple_Resize(&axes, i) == -1) return NULL;
char tag_buf[5] = {0};
i = 0;
if (_PyTuple_Resize(&named_styles, num_of_name_records) == -1) return NULL;
for (
const uint8_t *pos = table + offset_to_start_of_axis_array + num_of_axis_records * size_of_axis_record;
pos + size_of_name_record <= table + table_len && i < num_of_name_records;
i++, pos += size_of_name_record
) {
p = (uint16_t*)pos;
uint16_t name_id = next, psname_id = 0xffff; next;
const uint32_t *p32 = (uint32_t*)p;
RAII_PyObject(axis_values, PyDict_New());
if (!axis_values) return NULL;
for (uint16_t i = 0; i < num_of_axis_records; i++) {
const uint8_t *t = table + offset_to_start_of_axis_array + i * size_of_axis_record;
memcpy(tag_buf, t, 4);
RAII_PyObject(pval, PyFloat_FromDouble(next32));
if (!pval || PyDict_SetItemString(axis_values, tag_buf, pval) != 0) return NULL;
}
if (has_postscript_name) { p = (uint16_t*)p32; psname_id = next; }
PyObject *ns = Py_BuildValue("{sO sN sN}",
"axis_values", axis_values, "name", get_best_name(name_lookup_table, name_id),
"psname", (psname_id != 0xffff && psname_id ? get_best_name(name_lookup_table, psname_id) : PyUnicode_FromString("")));
if (!ns) return NULL;
PyTuple_SET_ITEM(named_styles, i, ns);
}
if (_PyTuple_Resize(&named_styles, i) == -1) return NULL;
ok:
if (PyDict_SetItemString(output, "variations_postscript_name_prefix", get_best_name(name_lookup_table, 25)) != 0) return false;
if (PyDict_SetItemString(output, "axes", axes) != 0) return false;
if (PyDict_SetItemString(output, "named_styles", named_styles) != 0) return false;
return true;
}
#undef next32
#undef next

View File

@@ -147,45 +147,51 @@ pyspacing(int val) {
#undef S
}
static PyObject*
increment_and_return(PyObject *x) { if (x) Py_INCREF(x); return x; }
static PyObject*
pattern_as_dict(FcPattern *pat) {
PyObject *ans = PyDict_New(), *p = NULL, *list = NULL;
RAII_PyObject(ans, Py_BuildValue("{ss}", "descriptor_type", "fontconfig"));
if (ans == NULL) return NULL;
#define PS(x) PyUnicode_Decode((const char*)x, strlen((const char*)x), "UTF-8", "replace")
#define G(type, get, which, conv, name) { \
#define G(type, get, which, conv, name, default) { \
type out; \
if (get(pat, which, 0, &out) == FcResultMatch) { \
p = conv(out); if (p == NULL) goto exit; \
if (PyDict_SetItemString(ans, #name, p) != 0) goto exit; \
Py_CLEAR(p); \
}}
RAII_PyObject(p, conv(out)); \
if (!p || PyDict_SetItemString(ans, #name, p) != 0) return NULL; \
} else { RAII_PyObject(d, default); if (!d || PyDict_SetItemString(ans, #name, d) != 0) return NULL; } \
}
#define L(type, get, which, conv, name) { \
type out; int n = 0; \
list = PyList_New(0); \
if (!list) goto exit; \
RAII_PyObject(list, PyList_New(0)); \
if (!list) return NULL; \
while (get(pat, which, n++, &out) == FcResultMatch) { \
p = conv(out); if (p == NULL) goto exit; \
if (PyList_Append(list, p) != 0) goto exit; \
Py_CLEAR(p); \
RAII_PyObject(p, conv(out)); \
if (!p || PyList_Append(list, p) != 0) return NULL; \
} \
if (PyDict_SetItemString(ans, #name, list) != 0) goto exit; \
Py_CLEAR(list); \
if (PyDict_SetItemString(ans, #name, list) != 0) return NULL; \
}
#define S(which, key) G(FcChar8*, FcPatternGetString, which, PS, key)
#define S(which, key) G(FcChar8*, FcPatternGetString, which, PS, key, PyUnicode_FromString(""))
#define LS(which, key) L(FcChar8*, FcPatternGetString, which, PS, key)
#define I(which, key) G(int, FcPatternGetInteger, which, PyLong_FromLong, key)
#define B(which, key) G(int, FcPatternGetBool, which, pybool, key)
#define E(which, key, conv) G(int, FcPatternGetInteger, which, conv, key)
#define I(which, key) G(int, FcPatternGetInteger, which, PyLong_FromLong, key, PyLong_FromUnsignedLong(0))
#define B(which, key) G(FcBool, FcPatternGetBool, which, pybool, key, increment_and_return(Py_False))
#define E(which, key, conv) G(int, FcPatternGetInteger, which, conv, key, PyLong_FromUnsignedLong(0))
S(FC_FILE, path);
S(FC_FAMILY, family);
S(FC_STYLE, style);
S(FC_FULLNAME, full_name);
S(FC_POSTSCRIPT_NAME, postscript_name);
LS(FC_FONT_FEATURES, fontfeatures);
B(FC_VARIABLE, variable);
#ifdef FC_NAMED_INSTANCE
B(FC_NAMED_INSTANCE, named_instance);
#else
PyDict_SetItemString(ans, "named_instance", Py_False);
#endif
I(FC_WEIGHT, weight);
I(FC_WIDTH, width)
I(FC_SLANT, slant);
@@ -198,11 +204,8 @@ pattern_as_dict(FcPattern *pat) {
B(FC_OUTLINE, outline);
B(FC_COLOR, color);
E(FC_SPACING, spacing, pyspacing);
exit:
if (PyErr_Occurred()) Py_CLEAR(ans);
Py_CLEAR(p);
Py_CLEAR(list);
Py_INCREF(ans);
return ans;
#undef PS
#undef S
@@ -229,22 +232,28 @@ font_set(FcFontSet *fs) {
#define AP(func, which, in, desc) if (!func(pat, which, in)) { PyErr_Format(PyExc_ValueError, "Failed to add %s to fontconfig pattern", desc, NULL); goto end; }
static PyObject*
fc_list(PyObject UNUSED *self, PyObject *args) {
fc_list(PyObject UNUSED *self, PyObject *args, PyObject *kw) {
ensure_initialized();
int allow_bitmapped_fonts = 0, spacing = -1;
int allow_bitmapped_fonts = 0, spacing = -1, only_variable = 0;
PyObject *ans = NULL;
FcObjectSet *os = NULL;
FcPattern *pat = NULL;
FcFontSet *fs = NULL;
if (!PyArg_ParseTuple(args, "|ip", &spacing, &allow_bitmapped_fonts)) return NULL;
static char *kwds[] = {"spacing", "allow_bitmapped_fonts", "only_variable", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kw, "|ipp", kwds, &spacing, &allow_bitmapped_fonts, &only_variable)) return NULL;
pat = FcPatternCreate();
if (pat == NULL) return PyErr_NoMemory();
if (!allow_bitmapped_fonts) {
AP(FcPatternAddBool, FC_OUTLINE, true, "outline");
AP(FcPatternAddBool, FC_SCALABLE, true, "scalable");
AP(FcPatternAddBool, FC_OUTLINE, FcTrue, "outline");
AP(FcPatternAddBool, FC_SCALABLE, FcTrue, "scalable");
}
if (spacing > -1) AP(FcPatternAddInteger, FC_SPACING, spacing, "spacing");
os = FcObjectSetBuild(FC_FILE, FC_POSTSCRIPT_NAME, FC_FAMILY, FC_STYLE, FC_FULLNAME, FC_WEIGHT, FC_WIDTH, FC_SLANT, FC_HINT_STYLE, FC_INDEX, FC_HINTING, FC_SCALABLE, FC_OUTLINE, FC_COLOR, FC_SPACING, NULL);
if (only_variable) AP(FcPatternAddBool, FC_VARIABLE, FcTrue, "variable");
os = FcObjectSetBuild(FC_FILE, FC_POSTSCRIPT_NAME, FC_FAMILY, FC_STYLE, FC_FULLNAME, FC_WEIGHT, FC_WIDTH, FC_SLANT, FC_HINT_STYLE, FC_INDEX, FC_HINTING, FC_SCALABLE, FC_OUTLINE, FC_COLOR, FC_SPACING, FC_VARIABLE,
#ifdef FC_NAMED_INSTANCE
FC_NAMED_INSTANCE,
#endif
NULL);
if (!os) { PyErr_SetString(PyExc_ValueError, "Failed to create fontconfig object set"); goto end; }
fs = FcFontList(NULL, pat, os);
if (!fs) { PyErr_SetString(PyExc_ValueError, "Failed to create fontconfig font set"); goto end; }
@@ -393,27 +402,46 @@ end:
}
PyObject*
specialize_font_descriptor(PyObject *base_descriptor, FONTS_DATA_HANDLE fg) {
specialize_font_descriptor(PyObject *base_descriptor, double font_sz_in_pts, double dpi_x, double dpi_y) {
ensure_initialized();
PyObject *p = PyDict_GetItemString(base_descriptor, "path"), *ans = NULL;
PyObject *p = PyDict_GetItemString(base_descriptor, "path");
PyObject *idx = PyDict_GetItemString(base_descriptor, "index");
if (p == NULL) { PyErr_SetString(PyExc_ValueError, "Base descriptor has no path"); return NULL; }
if (idx == NULL) { PyErr_SetString(PyExc_ValueError, "Base descriptor has no index"); return NULL; }
unsigned long face_idx = PyLong_AsUnsignedLong(idx);
if (PyErr_Occurred()) return NULL;
FcPattern *pat = FcPatternCreate();
if (pat == NULL) return PyErr_NoMemory();
long face_idx = MAX(0, PyLong_AsLong(idx));
RAII_PyObject(ans, NULL);
AP(FcPatternAddString, FC_FILE, (const FcChar8*)PyUnicode_AsUTF8(p), "path");
AP(FcPatternAddInteger, FC_INDEX, face_idx, "index");
AP(FcPatternAddDouble, FC_SIZE, fg->font_sz_in_pts, "size");
AP(FcPatternAddDouble, FC_DPI, (fg->logical_dpi_x + fg->logical_dpi_y) / 2.0, "dpi");
AP(FcPatternAddDouble, FC_SIZE, font_sz_in_pts, "size");
AP(FcPatternAddDouble, FC_DPI, (dpi_x + dpi_y) / 2.0, "dpi");
ans = _fc_match(pat);
FcPatternDestroy(pat); pat = NULL;
if (face_idx > 0) {
// For some reason FcFontMatch sets the index to zero, so manually restore it.
PyDict_SetItemString(ans, "index", idx);
if (PyDict_SetItemString(ans, "index", idx) != 0) return NULL;
}
end:
if (pat != NULL) FcPatternDestroy(pat);
PyObject *named_style = PyDict_GetItemString(base_descriptor, "named_style");
if (named_style) {
if (PyDict_SetItemString(ans, "named_style", named_style) != 0) return NULL;
}
PyObject *axes = PyDict_GetItemString(base_descriptor, "axes");
if (axes) {
if (PyDict_SetItemString(ans, "axes", axes) != 0) return NULL;
}
PyObject *features = PyDict_GetItemString(base_descriptor, "features");
if (features) {
if (PyDict_SetItemString(ans, "features", features) != 0) return NULL;
}
Py_INCREF(ans);
return ans;
end:
if (pat) FcPatternDestroy(pat);
return NULL;
}
bool
@@ -464,7 +492,7 @@ end:
#undef AP
static PyMethodDef module_methods[] = {
METHODB(fc_list, METH_VARARGS),
{"fc_list", (PyCFunction)(void (*) (void))(fc_list), METH_VARARGS | METH_KEYWORDS, NULL},
METHODB(fc_match, METH_VARARGS),
METHODB(fc_match_postscript_name, METH_VARARGS),
{NULL, NULL, 0, NULL} /* Sentinel */

View File

@@ -7,6 +7,7 @@
*/
#include "fonts.h"
#include "pyport.h"
#include "state.h"
#include "emoji.h"
#include "unicode-data.h"
@@ -43,7 +44,6 @@ static hb_feature_t hb_features[3] = {{0}};
static char_type shape_buffer[4096] = {0};
static size_t max_texture_size = 1024, max_array_len = 1024;
typedef enum { LIGA_FEATURE, DLIG_FEATURE, CALT_FEATURE } HBFeature;
static PyObject* font_feature_settings = NULL;
typedef struct {
char_type left, right;
@@ -283,47 +283,89 @@ sprite_tracker_set_layout(GPUSpriteTracker *sprite_tracker, unsigned int cell_wi
static PyObject*
desc_to_face(PyObject *desc, FONTS_DATA_HANDLE fg) {
PyObject *d = specialize_font_descriptor(desc, fg);
PyObject *d = specialize_font_descriptor(desc, fg->font_sz_in_pts, fg->logical_dpi_x, fg->logical_dpi_y);
if (d == NULL) return NULL;
PyObject *ans = face_from_descriptor(d, fg);
Py_DECREF(d);
return ans;
}
static void
add_feature(FontFeatures *output, const hb_feature_t *feature) {
for (size_t i = 0; i < output->count; i++) {
if (output->features[i].tag == feature->tag) {
output->features[i] = *feature;
return;
}
}
output->features[output->count++] = *feature;
}
static const char*
tag_to_string(uint32_t tag, uint8_t bytes[5]) {
bytes[0] = (tag >> 24) & 0xff;
bytes[1] = (tag >> 16) & 0xff;
bytes[2] = (tag >> 8) & 0xff;
bytes[3] = (tag) & 0xff;
bytes[4] = 0;
return (const char*)bytes;
}
PyObject*
font_features_as_dict(const FontFeatures *font_features) {
RAII_PyObject(ans, PyDict_New());
if (!ans) return NULL;
char buf[256];
char tag[5] = {0};
for (size_t i = 0; i < font_features->count; i++) {
tag_to_string(font_features->features[i].tag, (unsigned char*)tag);
hb_feature_to_string(&font_features->features[i], buf, arraysz(buf));
PyObject *t = PyUnicode_FromString(buf);
if (!t) return NULL;
if (PyDict_SetItemString(ans, tag, t) != 0) return NULL;
}
Py_INCREF(ans); return ans;
}
bool
create_features_for_face(const char *psname, PyObject *features, FontFeatures *output) {
size_t count_from_descriptor = features ? PyTuple_GET_SIZE(features): 0;
__typeof__(OPT(font_features).entries) from_opts = NULL;
if (psname) {
for (size_t i = 0; i < OPT(font_features).num && !from_opts; i++) {
__typeof__(OPT(font_features).entries) e = OPT(font_features).entries + i;
if (strcmp(e->psname, psname) == 0) from_opts = e;
}
}
size_t count_from_opts = from_opts ? from_opts->num : 0;
output->features = calloc(MAX(2u, count_from_opts + count_from_descriptor), sizeof(output->features[0]));
if (!output->features) { PyErr_NoMemory(); return false; }
for (size_t i = 0; i < count_from_opts; i++) {
add_feature(output, &from_opts->features[i]);
}
for (size_t i = 0; i < count_from_descriptor; i++) {
ParsedFontFeature *f = (ParsedFontFeature*)PyTuple_GET_ITEM(features, i);
add_feature(output, &f->feature);
}
if (!output->count) {
if (strstr(psname, "NimbusMonoPS-") == psname) {
add_feature(output, &hb_features[LIGA_FEATURE]);
add_feature(output, &hb_features[DLIG_FEATURE]);
}
}
return true;
}
static bool
init_font(Font *f, PyObject *face, bool bold, bool italic, bool emoji_presentation) {
f->face = face; Py_INCREF(f->face);
f->bold = bold; f->italic = italic; f->emoji_presentation = emoji_presentation;
f->num_ffs_hb_features = 0;
const char *psname = postscript_name_for_face(face);
if (font_feature_settings != NULL){
PyObject* o = PyDict_GetItemString(font_feature_settings, psname);
if (o != NULL && PyTuple_Check(o)) {
Py_ssize_t len = PyTuple_GET_SIZE(o);
if (len > 0) {
f->num_ffs_hb_features = len + 1;
f->ffs_hb_features = calloc(f->num_ffs_hb_features, sizeof(hb_feature_t));
if (!f->ffs_hb_features) return false;
for (Py_ssize_t i = 0; i < len; i++) {
PyObject* parsed = PyObject_GetAttrString(PyTuple_GET_ITEM(o, i), "parsed");
if (parsed) {
memcpy(f->ffs_hb_features + i, PyBytes_AS_STRING(parsed), sizeof(hb_feature_t));
Py_DECREF(parsed);
}
}
memcpy(f->ffs_hb_features + len, &hb_features[CALT_FEATURE], sizeof(hb_feature_t));
}
}
}
if (!f->num_ffs_hb_features) {
f->ffs_hb_features = calloc(4, sizeof(hb_feature_t));
if (!f->ffs_hb_features) return false;
if (strstr(psname, "NimbusMonoPS-") == psname) {
memcpy(f->ffs_hb_features + f->num_ffs_hb_features++, &hb_features[LIGA_FEATURE], sizeof(hb_feature_t));
memcpy(f->ffs_hb_features + f->num_ffs_hb_features++, &hb_features[DLIG_FEATURE], sizeof(hb_feature_t));
}
memcpy(f->ffs_hb_features + f->num_ffs_hb_features++, &hb_features[CALT_FEATURE], sizeof(hb_feature_t));
}
const FontFeatures *features = features_for_face(face);
f->ffs_hb_features = calloc(1 + features->count, sizeof(hb_feature_t));
if (!f->ffs_hb_features) { PyErr_NoMemory(); return false; }
f->num_ffs_hb_features = features->count;
memcpy(f->ffs_hb_features, features->features, sizeof(hb_feature_t) * features->count);
memcpy(f->ffs_hb_features + f->num_ffs_hb_features++, &hb_features[CALT_FEATURE], sizeof(hb_feature_t));
return true;
}
@@ -631,14 +673,15 @@ END_ALLOW_CASE_RANGE
static PyObject* box_drawing_function = NULL, *prerender_function = NULL, *descriptor_for_idx = NULL;
void
render_alpha_mask(const uint8_t *alpha_mask, pixel* dest, Region *src_rect, Region *dest_rect, size_t src_stride, size_t dest_stride) {
render_alpha_mask(const uint8_t *alpha_mask, pixel* dest, Region *src_rect, Region *dest_rect, size_t src_stride, size_t dest_stride, pixel color_rgb) {
pixel col = color_rgb << 8;
for (size_t sr = src_rect->top, dr = dest_rect->top; sr < src_rect->bottom && dr < dest_rect->bottom; sr++, dr++) {
pixel *d = dest + dest_stride * dr;
const uint8_t *s = alpha_mask + src_stride * sr;
for(size_t sc = src_rect->left, dc = dest_rect->left; sc < src_rect->right && dc < dest_rect->right; sc++, dc++) {
uint8_t src_alpha = d[dc] & 0xff;
uint8_t alpha = s[sc];
d[dc] = 0xffffff00 | MAX(alpha, src_alpha);
d[dc] = col | MAX(alpha, src_alpha);
}
}
}
@@ -662,7 +705,7 @@ render_box_cell(FontGroup *fg, CPUCell *cpu_cell, GPUCell *gpu_cell) {
uint8_t *alpha_mask = PyLong_AsVoidPtr(PyTuple_GET_ITEM(ret, 0));
ensure_canvas_can_fit(fg, 1);
Region r = { .right = fg->cell_width, .bottom = fg->cell_height };
render_alpha_mask(alpha_mask, fg->canvas.buf, &r, &r, fg->cell_width, fg->cell_width);
render_alpha_mask(alpha_mask, fg->canvas.buf, &r, &r, fg->cell_width, fg->cell_width, 0xffffff);
current_send_sprite_to_gpu((FONTS_DATA_HANDLE)fg, sp->x, sp->y, sp->z, fg->canvas.buf);
Py_DECREF(ret);
}
@@ -1432,12 +1475,12 @@ set_symbol_maps(SymbolMap **maps, size_t *num, const PyObject *sm) {
static PyObject*
set_font_data(PyObject UNUSED *m, PyObject *args) {
PyObject *sm, *ns;
Py_CLEAR(box_drawing_function); Py_CLEAR(prerender_function); Py_CLEAR(descriptor_for_idx); Py_CLEAR(font_feature_settings);
if (!PyArg_ParseTuple(args, "OOOIIIIO!dOO!",
Py_CLEAR(box_drawing_function); Py_CLEAR(prerender_function); Py_CLEAR(descriptor_for_idx);
if (!PyArg_ParseTuple(args, "OOOIIIIO!dO!",
&box_drawing_function, &prerender_function, &descriptor_for_idx,
&descriptor_indices.bold, &descriptor_indices.italic, &descriptor_indices.bi, &descriptor_indices.num_symbol_fonts,
&PyTuple_Type, &sm, &OPT(font_size), &font_feature_settings, &PyTuple_Type, &ns)) return NULL;
Py_INCREF(box_drawing_function); Py_INCREF(prerender_function); Py_INCREF(descriptor_for_idx); Py_INCREF(font_feature_settings);
&PyTuple_Type, &sm, &OPT(font_size), &PyTuple_Type, &ns)) return NULL;
Py_INCREF(box_drawing_function); Py_INCREF(prerender_function); Py_INCREF(descriptor_for_idx);
free_font_groups();
clear_symbol_maps();
set_symbol_maps(&symbol_maps, &num_symbol_maps, sm);
@@ -1465,7 +1508,7 @@ send_prerendered_sprites(FontGroup *fg) {
uint8_t *alpha_mask = PyLong_AsVoidPtr(PyTuple_GET_ITEM(cell_addresses, i));
ensure_canvas_can_fit(fg, 1); // clear canvas
Region r = { .right = fg->cell_width, .bottom = fg->cell_height };
render_alpha_mask(alpha_mask, fg->canvas.buf, &r, &r, fg->cell_width, fg->cell_width);
render_alpha_mask(alpha_mask, fg->canvas.buf, &r, &r, fg->cell_width, fg->cell_width, 0xffffff);
current_send_sprite_to_gpu((FONTS_DATA_HANDLE)fg, x, y, z, fg->canvas.buf);
}
Py_CLEAR(args);
@@ -1538,7 +1581,6 @@ finalize(void) {
Py_CLEAR(box_drawing_function);
Py_CLEAR(prerender_function);
Py_CLEAR(descriptor_for_idx);
Py_CLEAR(font_feature_settings);
free_font_groups();
free(ligature_types);
if (harfbuzz_buffer) { hb_buffer_destroy(harfbuzz_buffer); harfbuzz_buffer = NULL; }
@@ -1633,27 +1675,43 @@ concat_cells(PyObject UNUSED *self, PyObject *args) {
}
static PyObject*
current_fonts(PYNOARG) {
current_fonts(PyObject *self UNUSED, PyObject *args) {
unsigned long long os_window_id = 0;
if (!PyArg_ParseTuple(args, "|K", &os_window_id)) return NULL;
if (!num_font_groups) { PyErr_SetString(PyExc_RuntimeError, "must create font group first"); return NULL; }
PyObject *ans = PyDict_New();
if (!ans) return NULL;
FontGroup *fg = font_groups;
#define SET(key, val) {if (PyDict_SetItemString(ans, #key, fg->fonts[val].face) != 0) { goto error; }}
if (os_window_id) {
OSWindow *os_window = os_window_for_id(os_window_id);
if (!os_window) { PyErr_SetString(PyExc_KeyError, "no oswindow with the specified id exists"); return NULL; }
fg = (FontGroup*)os_window->fonts_data;
}
RAII_PyObject(ans, PyDict_New());
if (!ans) return NULL;
#define SET(key, val) {if (PyDict_SetItemString(ans, #key, fg->fonts[val].face) != 0) { return NULL; }}
SET(medium, fg->medium_font_idx);
if (fg->bold_font_idx > 0) SET(bold, fg->bold_font_idx);
if (fg->italic_font_idx > 0) SET(italic, fg->italic_font_idx);
if (fg->bi_font_idx > 0) SET(bi, fg->bi_font_idx);
PyObject *ff = PyTuple_New(fg->fallback_fonts_count);
if (!ff) goto error;
unsigned num_symbol_fonts = fg->first_fallback_font_idx - fg->first_symbol_font_idx;
RAII_PyObject(ss, PyTuple_New(num_symbol_fonts));
if (!ss) return NULL;
for (size_t i = 0; i < num_symbol_fonts; i++) {
Py_INCREF(fg->fonts[fg->first_symbol_font_idx + i].face);
PyTuple_SET_ITEM(ss, i, fg->fonts[fg->first_symbol_font_idx + i].face);
}
if (PyDict_SetItemString(ans, "symbol", ss) != 0) return NULL;
RAII_PyObject(ff, PyTuple_New(fg->fallback_fonts_count));
if (!ff) return NULL;
for (size_t i = 0; i < fg->fallback_fonts_count; i++) {
Py_INCREF(fg->fonts[fg->first_fallback_font_idx + i].face);
PyTuple_SET_ITEM(ff, i, fg->fonts[fg->first_fallback_font_idx + i].face);
}
PyDict_SetItemString(ans, "fallback", ff);
Py_CLEAR(ff);
if (PyDict_SetItemString(ans, "fallback", ff) != 0) return NULL;
#define p(x) { RAII_PyObject(t, PyFloat_FromDouble(fg->x)); if (!t) return NULL; if (PyDict_SetItemString(ans, #x, t) != 0) return NULL; }
p(font_sz_in_pts); p(logical_dpi_x); p(logical_dpi_y);
#undef p
Py_INCREF(ans);
return ans;
error:
Py_CLEAR(ans); return NULL;
#undef SET
}
@@ -1693,35 +1751,104 @@ free_font_data(PyObject *self UNUSED, PyObject *args UNUSED) {
Py_RETURN_NONE;
}
static PyObject *
parsed_font_feature_new(PyTypeObject *type, PyObject *args, PyObject *kwds UNUSED) {
const char *s;
if (!PyArg_ParseTuple(args, "s", &s)) return NULL;
ParsedFontFeature *self = (ParsedFontFeature *)type->tp_alloc(type, 0);
if (self != NULL) {
if (!hb_feature_from_string(s, -1, &self->feature)) {
PyErr_Format(PyExc_ValueError, "%s is not a valid font feature", s);
Py_CLEAR(self);
}
}
return (PyObject*) self;
}
static PyObject*
parse_font_feature(PyObject *self UNUSED, PyObject *feature) {
if (!PyUnicode_Check(feature)) {
PyErr_SetString(PyExc_TypeError, "feature must be a unicode object");
return NULL;
parsed_font_feature_str(PyObject *self_) {
char buf[128];
hb_feature_to_string(&((ParsedFontFeature*)self_)->feature, buf, arraysz(buf));
return PyUnicode_FromString(buf);
}
static PyObject*
parsed_font_feature_repr(PyObject *self_) {
RAII_PyObject(s, parsed_font_feature_str(self_));
return s ? PyObject_Repr(s) : NULL;
}
PyTypeObject ParsedFontFeature_Type;
static PyObject*
parsed_font_feature_cmp(PyObject *self, PyObject *other, int op) {
if (op != Py_EQ && op != Py_NE) return Py_NotImplemented;
if (!PyObject_TypeCheck(other, &ParsedFontFeature_Type)) {
if (op == Py_EQ) Py_RETURN_FALSE;
Py_RETURN_TRUE;
}
PyObject *ans = PyBytes_FromStringAndSize(NULL, sizeof(hb_feature_t));
if (!ans) return NULL;
if (!hb_feature_from_string(PyUnicode_AsUTF8(feature), -1, (hb_feature_t*)PyBytes_AS_STRING(ans))) {
Py_CLEAR(ans);
PyErr_Format(PyExc_ValueError, "%U is not a valid font feature", feature);
return NULL;
ParsedFontFeature *a = (ParsedFontFeature*)self, *b = (ParsedFontFeature*)other;
PyObject *ret = Py_True;
if (memcmp(&a->feature, &b->feature, sizeof(hb_feature_t)) == 0) {
if (op == Py_NE) ret = Py_False;
} else {
if (op == Py_EQ) ret = Py_False;
}
return ans;
Py_INCREF(ret); return ret;
}
static Py_hash_t
parsed_font_feature_hash(PyObject *s) {
ParsedFontFeature *self = (ParsedFontFeature*)s;
if (self->hash_computed) return self->hashval;
self->hash_computed = true;
HASH_FUNCTION(&self->feature, sizeof(hb_feature_t), self->hashval);
return self->hashval;
}
static PyObject*
parsed_font_feature_call(PyObject *s, PyObject *args, PyObject *kwargs UNUSED) {
ParsedFontFeature *self = (ParsedFontFeature*)s;
void *dest = PyLong_AsVoidPtr(args);
memcpy(dest, &self->feature, sizeof(hb_feature_t));
Py_RETURN_NONE;
}
PyTypeObject ParsedFontFeature_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "kitty.fast_data_types.ParsedFontFeature",
.tp_basicsize = sizeof(ParsedFontFeature),
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = "FontFeature",
.tp_new = parsed_font_feature_new,
.tp_str = parsed_font_feature_str,
.tp_repr = parsed_font_feature_repr,
.tp_richcompare = parsed_font_feature_cmp,
.tp_hash = parsed_font_feature_hash,
.tp_call = parsed_font_feature_call,
};
static PyObject*
pyspecialize_font_descriptor(PyObject *self UNUSED, PyObject *args) {
PyObject *desc; double font_sz, dpi_x, dpi_y;
if (!PyArg_ParseTuple(args, "Offf", &desc, &font_sz, &dpi_x, &dpi_y)) return NULL;
return specialize_font_descriptor(desc, font_sz, dpi_x, dpi_y);
}
static PyMethodDef module_methods[] = {
METHODB(set_font_data, METH_VARARGS),
METHODB(free_font_data, METH_NOARGS),
METHODB(parse_font_feature, METH_O),
METHODB(create_test_font_group, METH_VARARGS),
METHODB(sprite_map_set_layout, METH_VARARGS),
METHODB(test_sprite_position_for, METH_VARARGS),
METHODB(concat_cells, METH_VARARGS),
METHODB(set_send_sprite_to_gpu, METH_O),
METHODB(test_shape, METH_VARARGS),
METHODB(current_fonts, METH_NOARGS),
METHODB(current_fonts, METH_VARARGS),
METHODB(test_render_line, METH_VARARGS),
METHODB(get_fallback_font, METH_VARARGS),
{"specialize_font_descriptor", (PyCFunction)pyspecialize_font_descriptor, METH_VARARGS, ""},
{NULL, NULL, 0, NULL} /* Sentinel */
};
@@ -1740,5 +1867,9 @@ init_fonts(PyObject *module) {
create_feature("-calt", CALT_FEATURE);
#undef create_feature
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
if (PyType_Ready(&ParsedFontFeature_Type) < 0) return 0;
if (PyModule_AddObject(module, "ParsedFontFeature", (PyObject *)&ParsedFontFeature_Type) != 0) return 0;
Py_INCREF(&ParsedFontFeature_Type);
return true;
}

View File

@@ -18,6 +18,20 @@ typedef struct {
size_t width, height;
} StringCanvas;
typedef struct FontFeatures {
size_t count;
hb_feature_t *features;
} FontFeatures;
typedef struct ParsedFontFeature {
PyObject_HEAD
hb_feature_t feature;
Py_hash_t hashval;
bool hash_computed;
} ParsedFontFeature;
// API that font backends need to implement
unsigned int glyph_id_for_codepoint(PyObject *, char_type);
int get_glyph_width(PyObject *, glyph_index);
@@ -27,7 +41,7 @@ bool set_size_for_face(PyObject*, unsigned int, bool, FONTS_DATA_HANDLE);
void cell_metrics(PyObject*, unsigned int*, unsigned int*, unsigned int*, unsigned int*, unsigned int*, unsigned int*, unsigned int*);
bool render_glyphs_in_cells(PyObject *f, bool bold, bool italic, hb_glyph_info_t *info, hb_glyph_position_t *positions, unsigned int num_glyphs, pixel *canvas, unsigned int cell_width, unsigned int cell_height, unsigned int num_cells, unsigned int baseline, bool *was_colored, FONTS_DATA_HANDLE, bool center_glyph);
PyObject* create_fallback_face(PyObject *base_face, CPUCell* cell, bool bold, bool italic, bool emoji_presentation, FONTS_DATA_HANDLE fg);
PyObject* specialize_font_descriptor(PyObject *base_descriptor, FONTS_DATA_HANDLE);
PyObject* specialize_font_descriptor(PyObject *base_descriptor, double, double, double);
PyObject* face_from_path(const char *path, int index, FONTS_DATA_HANDLE);
PyObject* face_from_descriptor(PyObject*, FONTS_DATA_HANDLE);
PyObject* iter_fallback_faces(FONTS_DATA_HANDLE fgh, ssize_t *idx);
@@ -35,13 +49,30 @@ bool face_equals_descriptor(PyObject *face_, PyObject *descriptor);
const char* postscript_name_for_face(const PyObject*);
void sprite_tracker_current_layout(FONTS_DATA_HANDLE data, unsigned int *x, unsigned int *y, unsigned int *z);
void render_alpha_mask(const uint8_t *alpha_mask, pixel* dest, Region *src_rect, Region *dest_rect, size_t src_stride, size_t dest_stride);
void render_alpha_mask(const uint8_t *alpha_mask, pixel* dest, Region *src_rect, Region *dest_rect, size_t src_stride, size_t dest_stride, pixel color_rgb);
void render_line(FONTS_DATA_HANDLE, Line *line, index_type lnum, Cursor *cursor, DisableLigature);
void sprite_tracker_set_limits(size_t max_texture_size, size_t max_array_len);
typedef void (*free_extra_data_func)(void*);
StringCanvas render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline);
StringCanvas render_simple_text(FONTS_DATA_HANDLE fg_, const char *text);
bool
add_font_name_record(PyObject *table, uint16_t platform_id, uint16_t encoding_id, uint16_t language_id, uint16_t name_id, const char *string, uint16_t string_len);
PyObject*
get_best_name_from_name_table(PyObject *table, PyObject *name_id);
PyObject*
read_name_font_table(const uint8_t *table, size_t table_len);
bool
read_fvar_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table, PyObject *output);
bool
read_STAT_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table, PyObject *output);
bool
read_features_from_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table, PyObject *output);
FontFeatures* features_for_face(PyObject *);
bool create_features_for_face(const char* psname, PyObject *features, FontFeatures* output);
PyObject*
font_features_as_dict(const FontFeatures *font_features);
static inline void
right_shift_canvas(pixel *canvas, size_t width, size_t height, size_t amt) {
pixel *src;

View File

@@ -1,27 +1,92 @@
try:
from typing import NamedTuple, TypedDict
except ImportError:
TypedDict = dict
from enum import Enum, IntEnum, auto
from typing import TYPE_CHECKING, Dict, List, Literal, NamedTuple, Optional, Sequence, Tuple, TypedDict, TypeVar, Union
from kitty.fast_data_types import ParsedFontFeature
from kitty.types import run_once
from kitty.typing import CoreTextFont, FontConfigPattern
from kitty.utils import shlex_split
if TYPE_CHECKING:
import re
class ListedFont(TypedDict):
family: str
style: str
full_name: str
postscript_name: str
is_monospace: bool
is_variable: bool
descriptor: Union[FontConfigPattern, CoreTextFont]
class FontFeature:
class VariableAxis(TypedDict):
minimum: float
maximum: float
default: float
hidden: bool
tag: str
strid: str # Can be empty string when not present
__slots__ = 'name', 'parsed'
def __init__(self, name: str, parsed: bytes):
self.name = name
self.parsed = parsed
class NamedStyle(TypedDict):
axis_values: Dict[str, float]
name: str
psname: str # can be empty string when not present
def __repr__(self) -> str:
return repr(self.name)
class DesignValue1(TypedDict):
format: Literal[1]
flags: int
name: str
value: float
class DesignValue2(TypedDict):
format: Literal[2]
flags: int
name: str
value: float
minimum: float
maximum: float
class DesignValue3(TypedDict):
format: Literal[3]
flags: int
name: str
value: float
linked_value: float
DesignValue = Union[DesignValue1, DesignValue2, DesignValue3]
class DesignAxis(TypedDict):
name: str
ordering: int
tag: str
values: List[DesignValue]
class AxisValue(TypedDict):
design_index: int
value: float
class MultiAxisStyle(TypedDict):
flags: int
name: str
values: Tuple[AxisValue, ...]
class VariableData(TypedDict):
axes: Tuple[VariableAxis, ...]
named_styles: Tuple[NamedStyle, ...]
variations_postscript_name_prefix: str
elided_fallback_name: str
design_axes: Tuple[DesignAxis, ...]
multi_axis_styles: Tuple[MultiAxisStyle, ...]
class ModificationType(Enum):
@@ -58,3 +123,118 @@ class FontModification(NamedTuple):
def __repr__(self) -> str:
fn = f' {self.font_name}' if self.font_name else ''
return f'{self.mod_type.name}{fn} {self.mod_value}'
class FontSpec(NamedTuple):
family: Optional[str] = None
style: Optional[str] = None
postscript_name: Optional[str] = None
full_name: Optional[str] = None
system: Optional[str] = None
axes: Tuple[Tuple[str, float], ...] = ()
variable_name: Optional[str] = None
features: Tuple[ParsedFontFeature, ...] = ()
created_from_string: str = ''
@classmethod
def from_setting(cls, spec: str) -> 'FontSpec':
if spec == 'auto':
return FontSpec(system='auto', created_from_string=spec)
items = tuple(shlex_split(spec))
if '=' not in items[0]:
return FontSpec(system=spec, created_from_string=spec)
axes = {}
defined = {}
features: Tuple[ParsedFontFeature, ...] = ()
for item in items:
k, sep, v = item.partition('=')
if sep != '=':
raise ValueError(f'The font specification: {spec} is not valid as {item} does not contain an =')
if k in ('family', 'style', 'full_name', 'postscript_name', 'variable_name'):
defined[k] = v
elif k == 'features':
features += tuple(ParsedFontFeature(x) for x in v.split())
else:
try:
axes[k] = float(v)
except Exception:
raise ValueError(f'The font specification: {spec} is not valid as {v} is not a number')
return FontSpec(axes=tuple(axes.items()), created_from_string=spec, features=features, **defined)
@property
def is_system(self) -> bool:
return bool(self.system)
@property
def is_auto(self) -> bool:
return self.system == 'auto'
@property
def as_setting(self) -> str:
if self.created_from_string:
return self.created_from_string
if self.system:
return self.system
ans = []
from shlex import quote
def a(key: str, val: str) -> None:
ans.append(f'{key}={quote(val)}')
if self.family is not None:
a('family', self.family)
if self.postscript_name is not None:
a('postscript_name', self.postscript_name)
if self.full_name is not None:
a('full_name', self.full_name)
if self.variable_name is not None:
a('variable_name', self.variable_name)
if self.style is not None:
a('style', self.style)
if self.features:
a('features', ' '.join(str(f) for f in self.features))
if self.axes:
for (key, val) in self.axes:
a(key, f'{val:g}')
return ' '.join(ans)
def __str__(self) -> str:
return self.as_setting
# Cannot change __repr__ as it will break config generation
Descriptor = Union[FontConfigPattern, CoreTextFont]
DescriptorVar = TypeVar('DescriptorVar', FontConfigPattern, CoreTextFont, Descriptor)
class Score(NamedTuple):
variable_score: int
style_score: float
monospace_score: int
width_score: int
class Scorer:
def __init__(self, bold: bool = False, italic: bool = False, monospaced: bool = True, prefer_variable: bool = False) -> None:
self.bold = bold
self.italic = italic
self.monospaced = monospaced
self.prefer_variable = prefer_variable
def sorted_candidates(self, candidates: Sequence[DescriptorVar], dump: bool = False) -> List[DescriptorVar]:
raise NotImplementedError()
def __repr__(self) -> str:
return f'{self.__class__.__name__}(bold={self.bold}, italic={self.italic}, monospaced={self.monospaced}, prefer_variable={self.prefer_variable})'
__str__ = __repr__
@run_once
def fnname_pat() -> 're.Pattern[str]':
import re
return re.compile(r'\s+')
def family_name_to_key(family: str) -> str:
return fnname_pat().sub(' ', family).strip().lower()

506
kitty/fonts/common.py Normal file
View File

@@ -0,0 +1,506 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union
from kitty.constants import is_macos
from kitty.fast_data_types import ParsedFontFeature
from kitty.fonts import Descriptor, DescriptorVar, DesignAxis, FontSpec, NamedStyle, Scorer, VariableAxis, VariableData, family_name_to_key
from kitty.options.types import Options
if TYPE_CHECKING:
from kitty.fast_data_types import CTFace
from kitty.fast_data_types import Face as FT_Face
FontCollectionMapType = Literal['family_map', 'ps_map', 'full_map', 'variable_map']
FontMap = Dict[FontCollectionMapType, Dict[str, List[Descriptor]]]
Face = Union[FT_Face, CTFace]
def all_fonts_map(monospaced: bool) -> FontMap: ...
def create_scorer(bold: bool = False, italic: bool = False, monospaced: bool = True, prefer_variable: bool = False) -> Scorer: ...
def find_best_match(
family: str, bold: bool = False, italic: bool = False, monospaced: bool = True, ignore_face: Optional[Descriptor] = None,
prefer_variable: bool = False,
) -> Descriptor: ...
def find_last_resort_text_font(bold: bool = False, italic: bool = False, monospaced: bool = True) -> Descriptor: ...
def face_from_descriptor(descriptor: Descriptor, font_sz_in_pts: Optional[float] = None, dpi_x: Optional[float] = None, dpi_y: Optional[float] = None
) -> Face: ...
def is_monospace(descriptor: Descriptor) -> bool: ...
def is_variable(descriptor: Descriptor) -> bool: ...
def set_named_style(name: str, font: Descriptor, vd: VariableData) -> bool: ...
def set_axis_values(tag_map: Dict[str, float], font: Descriptor, vd: VariableData) -> bool: ...
def get_axis_values(font: Descriptor, vd: VariableData) -> Dict[str, float]: ...
else:
FontCollectionMapType = FontMap = None
from kitty.fast_data_types import specialize_font_descriptor
if is_macos:
from kitty.fast_data_types import CTFace as Face
from kitty.fonts.core_text import (
all_fonts_map,
create_scorer,
find_best_match,
find_last_resort_text_font,
get_axis_values,
is_monospace,
is_variable,
set_axis_values,
set_named_style,
)
else:
from kitty.fast_data_types import Face
from kitty.fonts.fontconfig import (
all_fonts_map,
create_scorer,
find_best_match,
find_last_resort_text_font,
get_axis_values,
is_monospace,
is_variable,
set_axis_values,
set_named_style,
)
def face_from_descriptor(descriptor, font_sz_in_pts = None, dpi_x = None, dpi_y = None):
if font_sz_in_pts is not None:
descriptor = specialize_font_descriptor(descriptor, font_sz_in_pts, dpi_x, dpi_y)
return Face(descriptor=descriptor)
cache_for_variable_data_by_path: Dict[str, VariableData] = {}
attr_map = {(False, False): 'font_family', (True, False): 'bold_font', (False, True): 'italic_font', (True, True): 'bold_italic_font'}
class Event:
is_set: bool = False
class FamilyAxisValues:
regular_weight: Optional[float] = None
regular_slant: Optional[float] = None
regular_ital: Optional[float] = None
regular_width: Optional[float] = None
bold_weight: Optional[float] = None
italic_slant: Optional[float] = None
italic_ital: Optional[float] = None
def get_wght(self, bold: bool, italic: bool) -> Optional[float]:
return self.bold_weight if bold else self.regular_weight
def get_ital(self, bold: bool, italic: bool) -> Optional[float]:
return self.italic_ital if italic else self.regular_ital
def get_slnt(self, bold: bool, italic: bool) -> Optional[float]:
return self.italic_slant if italic else self.regular_slant
def get_wdth(self, bold: bool, italic: bool) -> Optional[float]:
return self.regular_width
def get(self, tag: str, bold: bool, italic: bool) -> Optional[float]:
f = getattr(self, f'get_{tag}', None)
return None if f is None else f(bold, italic)
def set_regular_values(self, axis_values: Dict[str, float]) -> None:
self.regular_weight = axis_values.get('wght')
self.regular_width = axis_values.get('wdth')
self.regular_ital = axis_values.get('ital')
self.regular_slant = axis_values.get('slnt')
def set_bold_values(self, axis_values: Dict[str, float]) -> None:
self.bold_weight = axis_values.get('wght')
def set_italic_values(self, axis_values: Dict[str, float]) -> None:
self.italic_ital = axis_values.get('ital')
self.italic_slant = axis_values.get('slnt')
def get_variable_data_for_descriptor(d: Descriptor) -> VariableData:
if not d['path']:
return face_from_descriptor(d).get_variable_data()
ans = cache_for_variable_data_by_path.get(d['path'])
if ans is None:
ans = cache_for_variable_data_by_path[d['path']] = face_from_descriptor(d).get_variable_data()
return ans
def get_variable_data_for_face(d: Face) -> VariableData:
path = d.path
if not path:
return d.get_variable_data()
ans = cache_for_variable_data_by_path.get(path)
if ans is None:
ans = cache_for_variable_data_by_path[path] = d.get_variable_data()
return ans
def find_best_match_in_candidates(
candidates: List[DescriptorVar], scorer: Scorer, is_medium_face: bool, ignore_face: Optional[DescriptorVar] = None
) -> Optional[DescriptorVar]:
if candidates:
for x in scorer.sorted_candidates(candidates):
if ignore_face is None or x != ignore_face:
return x
return None
def pprint(*a: Any, **kw: Any) -> None:
from pprint import pprint
pprint(*a, **kw)
def find_medium_variant(font: DescriptorVar) -> DescriptorVar:
font = font.copy()
vd = get_variable_data_for_descriptor(font)
for i, ns in enumerate(vd['named_styles']):
if ns['name'] == 'Regular':
set_named_style(ns['psname'] or ns['name'], font, vd)
return font
axis_values = {}
for i, ax in enumerate(vd['axes']):
tag = ax['tag']
for dax in vd['design_axes']:
if dax['tag'] == tag:
for x in dax['values']:
if x['format'] in (1, 2):
if x['name'] == 'Regular':
axis_values[tag] = x['value']
break
if axis_values:
set_axis_values(axis_values, font, vd)
return font
def get_bold_design_weight(dax: DesignAxis, ax: VariableAxis, regular_weight: float) -> float:
ans = regular_weight
candidates = []
for x in dax['values']:
if x['format'] in (1, 2):
if x['value'] > regular_weight:
candidates.append(x['value'])
if candidates:
ans = min(candidates)
return ans
def get_design_value_for(dax: DesignAxis, ax: VariableAxis, bold: bool, italic: bool, family_axis_values: FamilyAxisValues) -> float:
family_val = family_axis_values.get(ax['tag'], bold, italic)
if family_val is not None and ax['minimum'] <= family_val <= ax['maximum']:
return family_val
default = ax['default']
if dax['tag'] == 'wght':
keys = ('semibold', 'bold', 'heavy', 'black') if bold else ('regular', 'medium')
elif dax['tag'] in ('ital', 'slnt'):
keys = ('italic', 'oblique', 'slanted', 'slant') if italic else ('regular', 'normal', 'medium', 'upright')
else:
return default
priorities = {}
for x in dax['values']:
if x['format'] in (1, 2):
q = x['name'].lower()
try:
idx = keys.index(q)
except ValueError:
continue
priorities[x['value']] = idx
ans = default
if priorities:
ans = sorted(priorities, key=priorities.__getitem__)[0]
if bold and ax['tag'] == 'wght' and family_axis_values.regular_weight is not None and ans <= family_axis_values.regular_weight:
ans = get_bold_design_weight(dax, ax, family_axis_values.regular_weight)
return ans
def find_bold_italic_variant(medium: Descriptor, bold: bool, italic: bool, family_axis_values: FamilyAxisValues) -> Descriptor:
# we first pick the best font file for bold/italic if there are more than
# one. For example SourceCodeVF has Italic and Upright faces with variable
# weights in each, so we rely on the OS font matcher to give us the best
# font file.
monospaced = is_monospace(medium)
unsorted = all_fonts_map(monospaced)['variable_map'][family_name_to_key(medium['family'])]
fonts = create_scorer(bold, italic, monospaced).sorted_candidates(unsorted)
vd = get_variable_data_for_descriptor(fonts[0])
ans = fonts[0].copy()
# now we need to specialise all axes in ans
axis_values = {}
dax_map = {dax['tag']: dax for dax in vd['design_axes']}
for ax in vd['axes']:
tag = ax['tag']
dax = dax_map.get(tag)
if dax is not None:
axis_values[tag] = get_design_value_for(dax, ax, bold, italic, family_axis_values)
if axis_values:
set_axis_values(axis_values, ans, vd)
return ans
def find_best_variable_face(spec: FontSpec, bold: bool, italic: bool, monospaced: bool, candidates: List[Descriptor]) -> Descriptor:
if spec.variable_name is not None:
q = spec.variable_name.lower()
for font in candidates:
vd = get_variable_data_for_descriptor(font)
if vd['variations_postscript_name_prefix'].lower() == q:
return font
if spec.style:
q = spec.style.lower()
for font in candidates:
vd = get_variable_data_for_descriptor(font)
for x in vd['named_styles']:
if x['psname'].lower() == q:
return font
for x in vd['named_styles']:
if x['name'].lower() == q:
return font
return create_scorer(bold, italic, monospaced).sorted_candidates(candidates)[0]
def get_fine_grained_font(
spec: FontSpec, bold: bool = False, italic: bool = False, family_axis_values: FamilyAxisValues = FamilyAxisValues(),
resolved_medium_font: Optional[Descriptor] = None, monospaced: bool = True, match_is_more_specific_than_family: Event = Event()
) -> Descriptor:
font_map = all_fonts_map(monospaced)
is_medium_face = resolved_medium_font is None
scorer = create_scorer(bold, italic, monospaced)
if spec.postscript_name:
q = find_best_match_in_candidates(font_map['ps_map'].get(family_name_to_key(spec.postscript_name), []), scorer, is_medium_face)
if q:
match_is_more_specific_than_family.is_set = True
return q
if spec.full_name:
q = find_best_match_in_candidates(font_map['full_map'].get(family_name_to_key(spec.full_name), []), scorer, is_medium_face)
if q:
match_is_more_specific_than_family.is_set = True
return q
if spec.family:
key = family_name_to_key(spec.family)
# First look for a variable font
candidates = font_map['variable_map'].get(key, [])
if candidates:
q = candidates[0] if len(candidates) == 1 else find_best_variable_face(spec, bold, italic, monospaced, candidates)
q, applied = apply_variation_to_pattern(q, spec)
if applied:
match_is_more_specific_than_family.is_set = True
return q
return find_medium_variant(q) if resolved_medium_font is None else find_bold_italic_variant(resolved_medium_font, bold, italic, family_axis_values)
# Now look for any font
candidates = font_map['family_map'].get(key, [])
if candidates:
if spec.style:
qs = spec.style.lower()
candidates = [x for x in candidates if x['style'].lower() == qs]
q = find_best_match_in_candidates(candidates, scorer, is_medium_face)
if q:
return q
return find_last_resort_text_font(bold, italic, monospaced)
def apply_variation_to_pattern(pat: Descriptor, spec: FontSpec) -> Tuple[Descriptor, bool]:
vd = face_from_descriptor(pat).get_variable_data()
pat = pat.copy()
if spec.style:
if set_named_style(spec.style, pat, vd):
return pat, True
tag_map, name_map = {}, {}
for i, ax in enumerate(vd['axes']):
tag_map[ax['tag']] = i
if ax['strid']:
name_map[ax['strid'].lower()] = ax['tag']
axis_values = {}
for axspec in spec.axes:
qname = axspec[0]
if qname in tag_map:
axis_values[qname] = axspec[1]
continue
tag = name_map.get(qname.lower())
if tag:
axis_values[tag] = axspec[1]
return pat, set_axis_values(axis_values, pat, vd)
def get_font_from_spec(
spec: FontSpec, bold: bool = False, italic: bool = False, family_axis_values: FamilyAxisValues = FamilyAxisValues(),
resolved_medium_font: Optional[Descriptor] = None, match_is_more_specific_than_family: Event = Event()
) -> Descriptor:
if not spec.is_system:
ans = get_fine_grained_font(spec, bold, italic, resolved_medium_font=resolved_medium_font, family_axis_values=family_axis_values,
match_is_more_specific_than_family=match_is_more_specific_than_family)
if spec.features:
ans = ans.copy()
ans['features'] = spec.features
return ans
family = spec.system or ''
if family == 'auto':
if bold or italic:
assert resolved_medium_font is not None
family = resolved_medium_font['family']
if is_variable(resolved_medium_font) or is_actually_variable_despite_fontconfigs_lies(resolved_medium_font):
v = find_bold_italic_variant(resolved_medium_font, bold, italic, family_axis_values=family_axis_values)
if v is not None:
return v
else:
family = 'monospace'
return find_best_match(family, bold, italic, ignore_face=resolved_medium_font)
class FontFiles(TypedDict):
medium: Descriptor
bold: Descriptor
italic: Descriptor
bi: Descriptor
actually_variable_cache: Dict[str, bool] = {}
def is_actually_variable_despite_fontconfigs_lies(d: Descriptor) -> bool:
if d['descriptor_type'] != 'fontconfig':
return False
path = d['path']
ans = actually_variable_cache.get(path)
if ans is not None:
return ans
m = all_fonts_map(is_monospace(d))['variable_map']
for x in m.get(family_name_to_key(d['family']), ()):
if x['path'] == path:
actually_variable_cache[path] = True
return True
actually_variable_cache[path] = False
return False
def get_font_files(opts: Options) -> FontFiles:
ans: Dict[str, Descriptor] = {}
match_is_more_specific_than_family = Event()
medium_font = get_font_from_spec(opts.font_family, match_is_more_specific_than_family=match_is_more_specific_than_family)
medium_font_is_variable = is_variable(medium_font) or is_actually_variable_despite_fontconfigs_lies(medium_font)
if not match_is_more_specific_than_family.is_set and medium_font_is_variable:
medium_font = find_medium_variant(medium_font)
family_axis_values = FamilyAxisValues()
if medium_font_is_variable:
family_axis_values.set_regular_values(get_axis_values(medium_font, get_variable_data_for_descriptor(medium_font)))
kd = {(False, False): 'medium', (True, False): 'bold', (False, True): 'italic', (True, True): 'bi'}
for (bold, italic), attr in attr_map.items():
if bold or italic:
spec: FontSpec = getattr(opts, attr)
font = get_font_from_spec(spec, bold, italic, resolved_medium_font=medium_font, family_axis_values=family_axis_values)
# Set family axis values based on the values in font
if not (bold and italic) and (is_variable(medium_font) or is_actually_variable_despite_fontconfigs_lies(medium_font)):
av = get_axis_values(font, get_variable_data_for_descriptor(font))
(family_axis_values.set_italic_values if italic else family_axis_values.set_bold_values)(av)
if spec.is_auto and not font.get('features') and medium_font.get('features'):
# Set font features based on medium face features
font = font.copy()
font['features'] = medium_font['features']
else:
font = medium_font
key = kd[(bold, italic)]
ans[key] = font
return {'medium': ans['medium'], 'bold': ans['bold'], 'italic': ans['italic'], 'bi': ans['bi']}
def axis_values_are_equal(defaults: Dict[str, float], a: Dict[str, float], b: Dict[str, float]) -> bool:
ad, bd = defaults.copy(), defaults.copy()
ad.update(a)
bd.update(b)
return ad == bd
def _get_named_style(axis_map: Dict[str, float], vd: VariableData) -> Optional[NamedStyle]:
defaults = {ax['tag']: ax['default'] for ax in vd['axes']}
for ns in vd['named_styles']:
if axis_values_are_equal(defaults, ns['axis_values'], axis_map):
return ns
return None
def get_named_style(face_or_descriptor: Union[Face, Descriptor]) -> Optional[NamedStyle]:
if isinstance(face_or_descriptor, dict):
d: Descriptor = face_or_descriptor
vd = get_variable_data_for_descriptor(d)
if d['descriptor_type'] == 'fontconfig':
ns = d.get('named_style', -1)
if ns > -1 and ns < len(vd['named_styles']):
return vd['named_styles'][ns]
axis_map = {}
axes = vd['axes']
for i, val in enumerate(d.get('axes', ())):
if i < len(axes):
axis_map[axes[i]['tag']] = val
else:
axis_map = d.get('axis_map', {}).copy()
else:
face: Face = face_or_descriptor
vd = get_variable_data_for_face(face)
q = face.get_variation()
if q is None:
return None
axis_map = q
return _get_named_style(axis_map, vd)
def get_axis_map(face_or_descriptor: Union[Face, Descriptor]) -> Dict[str, float]:
base_axis_map = {}
axis_map: Dict[str, float] = {}
if isinstance(face_or_descriptor, dict):
d: Descriptor = face_or_descriptor
vd = get_variable_data_for_descriptor(d)
if d['descriptor_type'] == 'fontconfig':
ns = d.get('named_style', -1)
if ns > -1 and ns < len(vd['named_styles']):
base_axis_map = vd['named_styles'][ns]['axis_values'].copy()
axis_map = {}
axes = vd['axes']
for i, val in enumerate(d.get('axes', ())):
if i < len(axes):
axis_map[axes[i]['tag']] = val
else:
axis_map = d.get('axis_map', {}).copy()
else:
face: Face = face_or_descriptor
q = face.get_variation()
if q is not None:
axis_map = q
base_axis_map.update(axis_map)
return base_axis_map
def spec_for_face(family: str, face: Face) -> FontSpec:
v = face.get_variation()
features = tuple(map(ParsedFontFeature, face.applied_features().values()))
if v is None:
return FontSpec(family=family, postscript_name=face.postscript_name(), features=features)
vd = face.get_variable_data()
varname = vd['variations_postscript_name_prefix']
ns = get_named_style(face)
if ns is None:
axes = []
for key, val in get_axis_map(face).items():
axes.append((key, val))
return FontSpec(family=family, variable_name=varname, axes=tuple(axes), features=features)
return FontSpec(family=family, variable_name=varname, style=ns['psname'] or ns['name'], features=features)
def develop(family: str = '') -> None:
import sys
family = family or sys.argv[-1]
from kitty.options.utils import parse_font_spec
opts = Options()
opts.font_family = parse_font_spec(family)
ff = get_font_files(opts)
def s(name: str, d: Descriptor) -> None:
f = face_from_descriptor(d)
print(name, str(f))
features = f.get_features()
print(' Features :', features)
s('Medium :', ff['medium'])
print()
s('Bold :', ff['bold'])
print()
s('Italic :', ff['italic'])
print()
s('Bold-Italic:', ff['bi'])
if __name__ == '__main__':
develop()

View File

@@ -1,16 +1,17 @@
#!/usr/bin/env python
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import re
from typing import Dict, Generator, Iterable, List, Optional, Tuple
import itertools
import operator
from collections import defaultdict
from functools import lru_cache
from typing import Dict, Generator, Iterable, List, NamedTuple, Optional, Sequence, Tuple
from kitty.fast_data_types import coretext_all_fonts
from kitty.fonts import FontFeature
from kitty.options.types import Options
from kitty.fast_data_types import CTFace, coretext_all_fonts
from kitty.typing import CoreTextFont
from kitty.utils import log_error
from . import ListedFont
from . import Descriptor, DescriptorVar, ListedFont, Score, Scorer, VariableData, family_name_to_key
attr_map = {(False, False): 'font_family',
(True, False): 'bold_font',
@@ -22,95 +23,238 @@ FontMap = Dict[str, Dict[str, List[CoreTextFont]]]
def create_font_map(all_fonts: Iterable[CoreTextFont]) -> FontMap:
ans: FontMap = {'family_map': {}, 'ps_map': {}, 'full_map': {}}
ans: FontMap = {'family_map': {}, 'ps_map': {}, 'full_map': {}, 'variable_map': {}}
vmap: Dict[str, List[CoreTextFont]] = defaultdict(list)
for x in all_fonts:
f = (x['family'] or '').lower()
s = (x['style'] or '').lower()
ps = (x['postscript_name'] or '').lower()
f = family_name_to_key(x['family'])
s = family_name_to_key(x['style'])
ps = family_name_to_key(x['postscript_name'])
ans['family_map'].setdefault(f, []).append(x)
ans['ps_map'].setdefault(ps, []).append(x)
ans['full_map'].setdefault(f'{f} {s}', []).append(x)
if x['variation'] is not None:
vmap[f].append(x)
# CoreText makes a separate descriptor for each named style in each
# variable font file. Keep only the default style descriptor, which has an
# empty variation dictionary. If no default exists, pick the one with the
# smallest variation dictionary size.
keyfunc = operator.itemgetter('path')
for k, v in vmap.items():
v.sort(key=keyfunc)
uniq_per_path = []
for _, g in itertools.groupby(v, keyfunc):
uniq_per_path.append(sorted(g, key=lambda x: len(x['variation'] or ()))[0])
ans['variable_map'][k] = uniq_per_path
return ans
def all_fonts_map() -> FontMap:
ans: Optional[FontMap] = getattr(all_fonts_map, 'ans', None)
if ans is None:
ans = create_font_map(coretext_all_fonts())
setattr(all_fonts_map, 'ans', ans)
return ans
@lru_cache(maxsize=2)
def all_fonts_map(monospaced: bool = True) -> FontMap:
return create_font_map(coretext_all_fonts(monospaced))
def is_monospace(descriptor: CoreTextFont) -> bool:
return descriptor['monospace']
def is_variable(descriptor: CoreTextFont) -> bool:
return descriptor['variation'] is not None
def list_fonts() -> Generator[ListedFont, None, None]:
for fd in coretext_all_fonts():
for fd in coretext_all_fonts(False):
f = fd['family']
if f:
fn = f'{f} {fd.get("style", "")}'.strip()
is_mono = bool(fd['monospace'])
yield {'family': f, 'full_name': fn, 'postscript_name': fd['postscript_name'] or '', 'is_monospace': is_mono}
fn = fd['display_name']
if not fn:
fn = f'{f} {fd["style"]}'.strip()
yield {'family': f, 'full_name': fn, 'postscript_name': fd['postscript_name'] or '', 'is_monospace': fd['monospace'],
'is_variable': is_variable(fd), 'descriptor': fd, 'style': fd['style']}
def find_font_features(postscript_name: str) -> Tuple[FontFeature, ...]:
"""Not Implemented"""
return ()
class WeightRange(NamedTuple):
minimum: float = 99999
maximum: float = -99999
medium: float = -99999
bold: float = -99999
@property
def is_valid(self) -> bool:
return self.minimum != wr.minimum and self.maximum != wr.maximum and self.medium != wr.medium and self.bold != wr.bold
wr = WeightRange()
def find_best_match(family: str, bold: bool = False, italic: bool = False, ignore_face: Optional[CoreTextFont] = None) -> CoreTextFont:
q = re.sub(r'\s+', ' ', family.lower())
font_map = all_fonts_map()
@lru_cache()
def weight_range_for_family(family: str) -> WeightRange:
faces = all_fonts_map(True)['family_map'].get(family_name_to_key(family), ())
mini, maxi, medium, bold = wr.minimum, wr.maximum, wr.medium, wr.bold
for face in faces:
w = face['weight']
mini, maxi = min(w, mini), max(w, maxi)
s = face['style'].lower()
if not s:
continue
s = s.split()[0]
if s == 'semibold':
bold = w
elif s == 'bold' and bold == wr.bold:
bold = w
elif s == 'regular':
medium = w
elif s == 'medium' and medium == wr.medium:
medium = w
return WeightRange(mini, maxi, medium, bold)
def score(candidate: CoreTextFont) -> Tuple[int, int, int, float]:
style_match = 1 if candidate['bold'] == bold and candidate[
'italic'
] == italic else 0
monospace_match = 1 if candidate['monospace'] else 0
class CTScorer(Scorer):
weight_range: Optional[WeightRange] = None
def score(self, candidate: Descriptor) -> Score:
assert candidate['descriptor_type'] == 'core_text'
variable_score = 0 if self.prefer_variable and candidate['variation'] is not None else 1
bold_score = candidate['weight'] # -1 to 1 with 0 being normal
if self.weight_range is None:
if bold_score < 0: # thinner than normal, reject
bold_score = 2.0
else:
if self.bold:
# prefer semibold=0.3 to full bold = 0.4
bold_score = abs(bold_score - 0.3)
else:
anchor = self.weight_range.bold if self.bold else self.weight_range.medium
bold_score = abs(bold_score - anchor)
italic_score = candidate['slant'] # -1 to 1 with 0 being upright < 0 being backward slant, abs(slant) == 1 implies 30 deg rotation
if self.italic:
if italic_score < 0:
italic_score = 2.0
else:
italic_score = abs(1 - italic_score)
monospace_match = 0 if candidate['monospace'] else 1
is_regular_width = not candidate['expanded'] and not candidate['condensed']
# prefer semi-bold to bold to heavy, less bold means less chance of
# overflow
weight_distance_from_medium = abs(candidate['weight'])
return style_match, monospace_match, 1 if is_regular_width else 0, 1 - weight_distance_from_medium
return Score(variable_score, bold_score + italic_score, monospace_match, 0 if is_regular_width else 1)
def sorted_candidates(self, candidates: Sequence[DescriptorVar], dump: bool = False) -> List[DescriptorVar]:
self.weight_range = None
families = {x['family'] for x in candidates}
if len(families) == 1:
wr = weight_range_for_family(next(iter(families)))
if wr.is_valid and wr.medium < 0: # Operator Mono is an example of this craziness
self.weight_range = wr
candidates = sorted(candidates, key=self.score)
if dump:
print(self)
if self.weight_range:
print(self.weight_range)
for x in candidates:
assert x['descriptor_type'] == 'core_text'
print(CTFace(descriptor=x).postscript_name(),
f'bold={x["bold"]}', f'italic={x["italic"]}', f'weight={x["weight"]:.2f}', f'slant={x["slant"]:.2f}')
print(' ', self.score(x))
print()
return candidates
def create_scorer(bold: bool = False, italic: bool = False, monospaced: bool = True, prefer_variable: bool = False) -> Scorer:
return CTScorer(bold, italic, monospaced, prefer_variable)
def find_last_resort_text_font(bold: bool = False, italic: bool = False, monospaced: bool = True) -> CoreTextFont:
font_map = all_fonts_map(monospaced)
candidates = font_map['family_map']['menlo']
return create_scorer(bold, italic, monospaced).sorted_candidates(candidates)[0]
def find_best_match(
family: str, bold: bool = False, italic: bool = False, monospaced: bool = True, ignore_face: Optional[CoreTextFont] = None,
prefer_variable: bool = False
) -> CoreTextFont:
q = family_name_to_key(family)
font_map = all_fonts_map(monospaced)
scorer = create_scorer(bold, italic, monospaced, prefer_variable=prefer_variable)
# First look for an exact match
for selector in ('ps_map', 'full_map'):
candidates = font_map[selector].get(q)
if candidates:
possible = sorted(candidates, key=score)[-1]
candidates = scorer.sorted_candidates(candidates)
possible = candidates[0]
if possible != ignore_face:
return possible
# See if we have a variable font
if not bold and not italic and font_map['variable_map'].get(q):
candidates = font_map['variable_map'][q]
candidates = scorer.sorted_candidates(candidates)
possible = candidates[0]
if possible != ignore_face:
from .common import find_medium_variant
return find_medium_variant(possible)
# Let CoreText choose the font if the family exists, otherwise
# fallback to Menlo
if q not in font_map['family_map']:
log_error(f'The font {family} was not found, falling back to Menlo')
if family != "monospace":
log_error(f'The font {family} was not found, falling back to Menlo')
q = 'menlo'
candidates = font_map['family_map'][q]
return sorted(candidates, key=score)[-1]
def resolve_family(f: str, main_family: str, bold: bool = False, italic: bool = False) -> str:
if (bold or italic) and f == 'auto':
f = main_family
if f.lower() == 'monospace':
f = 'Menlo'
return f
def get_font_files(opts: Options) -> Dict[str, CoreTextFont]:
ans: Dict[str, CoreTextFont] = {}
for (bold, italic) in sorted(attr_map):
attr = attr_map[(bold, italic)]
key = {(False, False): 'medium',
(True, False): 'bold',
(False, True): 'italic',
(True, True): 'bi'}[(bold, italic)]
ignore_face = None if key == 'medium' else ans['medium']
face = find_best_match(resolve_family(getattr(opts, attr), opts.font_family, bold, italic), bold, italic, ignore_face=ignore_face)
ans[key] = face
if key == 'medium':
setattr(get_font_files, 'medium_family', face['family'])
return ans
candidates = scorer.sorted_candidates(font_map['family_map'][q])
return candidates[0]
def font_for_family(family: str) -> Tuple[CoreTextFont, bool, bool]:
ans = find_best_match(resolve_family(family, getattr(get_font_files, 'medium_family')))
ans = find_best_match(family, monospaced=False)
return ans, ans['bold'], ans['italic']
def descriptor(f: ListedFont) -> CoreTextFont:
d = f['descriptor']
assert d['descriptor_type'] == 'core_text'
return d
def prune_family_group(g: List[ListedFont]) -> List[ListedFont]:
# CoreText returns a separate font for every style in the variable font, so
# merge them.
variable_paths = {descriptor(f)['path']: False for f in g if f['is_variable']}
if not variable_paths:
return g
def is_ok(d: CoreTextFont) -> bool:
if d['path'] not in variable_paths:
return True
if not variable_paths[d['path']]:
variable_paths[d['path']] = True
return True
return False
return [x for x in g if is_ok(descriptor(x))]
def set_axis_values(tag_map: Dict[str, float], font: CoreTextFont, vd: VariableData) -> bool:
known_axes = {ax['tag'] for ax in vd['axes']}
previous = font.get('axis_map', {})
new = previous.copy()
for tag in known_axes:
val = tag_map.get(tag)
if val is not None:
new[tag] = val
font['axis_map'] = new
return new != previous
def set_named_style(name: str, font: CoreTextFont, vd: VariableData) -> bool:
q = name.lower()
for i, ns in enumerate(vd['named_styles']):
if ns['psname'].lower() == q:
return set_axis_values(ns['axis_values'], font, vd)
for i, ns in enumerate(vd['named_styles']):
if ns['name'].lower() == q:
return set_axis_values(ns['axis_values'], font, vd)
if vd['elided_fallback_name']:
for i, ns in enumerate(vd['named_styles']):
eq = ' '.join(ns['name'].replace(vd['elided_fallback_name'], '').strip().split()).lower()
if q == eq:
return set_axis_values(ns['axis_values'], font, vd)
return False
def get_axis_values(font: CoreTextFont, vd: VariableData) -> Dict[str, float]:
return font.get('axis_map', {})

149
kitty/fonts/features.py Normal file
View File

@@ -0,0 +1,149 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
from enum import IntEnum
from typing import Dict, NamedTuple
class Type(IntEnum):
boolean: int = 1
index: int = 2
hidden: int = 3
class FeatureDefinition(NamedTuple):
name: str
type: Type
# From: https://learn.microsoft.com/en-ca/typography/opentype/spec/featurelist
known_features: Dict[str, FeatureDefinition] = { # {{{
'aalt': FeatureDefinition('Access All Alternates', Type.index),
'abvf': FeatureDefinition('Above-base Forms', Type.hidden),
'abvm': FeatureDefinition('Above-base Mark Positioning', Type.hidden),
'abvs': FeatureDefinition('Above-base Substitutions', Type.hidden),
'afrc': FeatureDefinition('Alternative Fractions', Type.boolean),
'akhn': FeatureDefinition('Akhand', Type.hidden),
'blwf': FeatureDefinition('Below-base Forms', Type.hidden),
'blwm': FeatureDefinition('Below-base Mark Positioning', Type.hidden),
'blws': FeatureDefinition('Below-base Substitutions', Type.hidden),
'calt': FeatureDefinition('Contextual Alternates', Type.boolean),
'case': FeatureDefinition('Case-Sensitive Forms', Type.hidden),
'ccmp': FeatureDefinition('Glyph Composition / Decomposition', Type.hidden),
'cfar': FeatureDefinition('Conjunct Form After Ro', Type.hidden),
'chws': FeatureDefinition('Contextual Half-width Spacing', Type.boolean),
'cjct': FeatureDefinition('Conjunct Forms', Type.hidden),
'clig': FeatureDefinition('Contextual Ligatures', Type.boolean),
'cpct': FeatureDefinition('Centered CJK Punctuation', Type.boolean),
'cpsp': FeatureDefinition('Capital Spacing', Type.boolean),
'cswh': FeatureDefinition('Contextual Swash', Type.boolean),
'curs': FeatureDefinition('Cursive Positioning', Type.hidden),
'c2pc': FeatureDefinition('Petite Capitals From Capitals', Type.boolean),
'c2sc': FeatureDefinition('Small Capitals From Capitals', Type.boolean),
'dist': FeatureDefinition('Distances', Type.hidden),
'dlig': FeatureDefinition('Discretionary Ligatures', Type.boolean),
'dnom': FeatureDefinition('Denominators', Type.hidden),
'dtls': FeatureDefinition('Dotless Forms', Type.hidden),
'expt': FeatureDefinition('Expert Forms', Type.boolean),
'falt': FeatureDefinition('Final Glyph on Line Alternates', Type.boolean),
'fin2': FeatureDefinition('Terminal Forms #2', Type.hidden),
'fin3': FeatureDefinition('Terminal Forms #3', Type.hidden),
'fina': FeatureDefinition('Terminal Forms', Type.hidden),
'flac': FeatureDefinition('Flattened accent forms', Type.hidden),
'frac': FeatureDefinition('Fractions', Type.boolean),
'fwid': FeatureDefinition('Full Widths', Type.boolean),
'half': FeatureDefinition('Half Forms', Type.hidden),
'haln': FeatureDefinition('Halant Forms', Type.hidden),
'halt': FeatureDefinition('Alternate Half Widths', Type.boolean),
'hist': FeatureDefinition('Historical Forms', Type.boolean),
'hkna': FeatureDefinition('Horizontal Kana Alternates', Type.boolean),
'hlig': FeatureDefinition('Historical Ligatures', Type.boolean),
'hngl': FeatureDefinition('Hangul', Type.boolean),
'hojo': FeatureDefinition('Hojo Kanji Forms (JIS X 0212-1990 Kanji Forms)', Type.boolean),
'hwid': FeatureDefinition('Half Widths', Type.boolean),
'init': FeatureDefinition('Initial Forms', Type.hidden),
'isol': FeatureDefinition('Isolated Forms', Type.hidden),
'ital': FeatureDefinition('Italics', Type.boolean),
'jalt': FeatureDefinition('Justification Alternates', Type.boolean),
'jp78': FeatureDefinition('JIS78 Forms', Type.boolean),
'jp83': FeatureDefinition('JIS83 Forms', Type.boolean),
'jp90': FeatureDefinition('JIS90 Forms', Type.boolean),
'jp04': FeatureDefinition('JIS2004 Forms', Type.boolean),
'kern': FeatureDefinition('Kerning', Type.boolean),
'lfbd': FeatureDefinition('Left Bounds', Type.boolean),
'liga': FeatureDefinition('Standard Ligatures', Type.boolean),
'ljmo': FeatureDefinition('Leading Jamo Forms', Type.hidden),
'lnum': FeatureDefinition('Lining Figures', Type.boolean),
'locl': FeatureDefinition('Localized Forms', Type.hidden),
'ltra': FeatureDefinition('Left-to-right alternates', Type.hidden),
'ltrm': FeatureDefinition('Left-to-right mirrored forms', Type.hidden),
'mark': FeatureDefinition('Mark Positioning', Type.hidden),
'med2': FeatureDefinition('Medial Forms #2', Type.hidden),
'medi': FeatureDefinition('Medial Forms', Type.hidden),
'mgrk': FeatureDefinition('Mathematical Greek', Type.boolean),
'mkmk': FeatureDefinition('Mark to Mark Positioning', Type.hidden),
'mset': FeatureDefinition('Mark Positioning via Substitution', Type.hidden),
'nalt': FeatureDefinition('Alternate Annotation Forms', Type.index),
'nlck': FeatureDefinition('NLC Kanji Forms', Type.boolean),
'nukt': FeatureDefinition('Nukta Forms', Type.hidden),
'numr': FeatureDefinition('Numerators', Type.hidden),
'onum': FeatureDefinition('Oldstyle Figures', Type.boolean),
'opbd': FeatureDefinition('Optical Bounds', Type.boolean),
'ordn': FeatureDefinition('Ordinals', Type.boolean),
'ornm': FeatureDefinition('Ornaments', Type.index),
'palt': FeatureDefinition('Proportional Alternate Widths', Type.boolean),
'pcap': FeatureDefinition('Petite Capitals', Type.boolean),
'pkna': FeatureDefinition('Proportional Kana', Type.boolean),
'pnum': FeatureDefinition('Proportional Figures', Type.boolean),
'pref': FeatureDefinition('Pre-Base Forms', Type.hidden),
'pres': FeatureDefinition('Pre-base Substitutions', Type.hidden),
'pstf': FeatureDefinition('Post-base Forms', Type.hidden),
'psts': FeatureDefinition('Post-base Substitutions', Type.hidden),
'pwid': FeatureDefinition('Proportional Widths', Type.boolean),
'qwid': FeatureDefinition('Quarter Widths', Type.boolean),
'rand': FeatureDefinition('Randomize', Type.boolean),
'rclt': FeatureDefinition('Required Contextual Alternates', Type.hidden),
'rkrf': FeatureDefinition('Rakar Forms', Type.hidden),
'rlig': FeatureDefinition('Required Ligatures', Type.hidden),
'rphf': FeatureDefinition('Reph Forms', Type.hidden),
'rtbd': FeatureDefinition('Right Bounds', Type.boolean),
'rtla': FeatureDefinition('Right-to-left alternates', Type.hidden),
'rtlm': FeatureDefinition('Right-to-left mirrored forms', Type.hidden),
'ruby': FeatureDefinition('Ruby Notation Forms', Type.boolean),
'rvrn': FeatureDefinition('Required Variation Alternates', Type.hidden),
'salt': FeatureDefinition('Stylistic Alternates', Type.index),
'sinf': FeatureDefinition('Scientific Inferiors', Type.boolean),
'size': FeatureDefinition('Optical size', Type.hidden),
'smcp': FeatureDefinition('Small Capitals', Type.boolean),
'smpl': FeatureDefinition('Simplified Forms', Type.boolean),
'ssty': FeatureDefinition('Math script style alternates', Type.hidden),
'stch': FeatureDefinition('Stretching Glyph Decomposition', Type.hidden),
'subs': FeatureDefinition('Subscript', Type.boolean),
'sups': FeatureDefinition('Superscript', Type.boolean),
'swsh': FeatureDefinition('Swash', Type.index),
'titl': FeatureDefinition('Titling', Type.boolean),
'tjmo': FeatureDefinition('Trailing Jamo Forms', Type.hidden),
'tnam': FeatureDefinition('Traditional Name Forms', Type.boolean),
'tnum': FeatureDefinition('Tabular Figures', Type.boolean),
'trad': FeatureDefinition('Traditional Forms', Type.index),
'twid': FeatureDefinition('Third Widths', Type.boolean),
'unic': FeatureDefinition('Unicase', Type.boolean),
'valt': FeatureDefinition('Alternate Vertical Metrics', Type.boolean),
'vatu': FeatureDefinition('Vattu Variants', Type.hidden),
'vchw': FeatureDefinition('Vertical Contextual Half-width Spacing', Type.hidden),
'vert': FeatureDefinition('Vertical Writing', Type.boolean),
'vhal': FeatureDefinition('Alternate Vertical Half Metrics', Type.boolean),
'vjmo': FeatureDefinition('Vowel Jamo Forms', Type.hidden),
'vkna': FeatureDefinition('Vertical Kana Alternates', Type.boolean),
'vkrn': FeatureDefinition('Vertical Kerning', Type.boolean),
'vpal': FeatureDefinition('Proportional Alternate Vertical Metrics', Type.boolean),
'vrt2': FeatureDefinition('Vertical Alternates and Rotation', Type.boolean),
'vrtr': FeatureDefinition('Vertical Alternates for Rotation', Type.boolean),
'zero': FeatureDefinition('Slashed Zero', Type.boolean),
}
for i in range(1, 100):
known_features[f'cv{i:02d}'] = FeatureDefinition(f'Character Variant {i}', Type.index)
for i in range(1, 20):
known_features[f'ss{i:02d}'] = FeatureDefinition(f'Stylistic Set {i}', Type.boolean)
# }}}

View File

@@ -1,65 +1,69 @@
#!/usr/bin/env python
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import re
import sys
from functools import lru_cache
from typing import Dict, Generator, List, Optional, Tuple, cast
from typing import Dict, Generator, List, Literal, NamedTuple, Optional, Sequence, Tuple, cast
from kitty.fast_data_types import (
FC_DUAL,
FC_MONO,
FC_SLANT_ITALIC,
FC_SLANT_ROMAN,
FC_WEIGHT_BOLD,
FC_WEIGHT_REGULAR,
FC_WIDTH_NORMAL,
Face,
fc_list,
fc_match_postscript_name,
parse_font_feature,
)
from kitty.fast_data_types import (
FC_WEIGHT_SEMIBOLD as FC_WEIGHT_BOLD,
)
from kitty.fast_data_types import fc_match as fc_match_impl
from kitty.options.types import Options
from kitty.typing import FontConfigPattern
from kitty.utils import log_error
from . import FontFeature, ListedFont
from . import Descriptor, DescriptorVar, ListedFont, Score, Scorer, VariableData, family_name_to_key
attr_map = {(False, False): 'font_family',
(True, False): 'bold_font',
(False, True): 'italic_font',
(True, True): 'bold_italic_font'}
FontMap = Dict[str, Dict[str, List[FontConfigPattern]]]
FontCollectionMapType = Literal['family_map', 'ps_map', 'full_map', 'variable_map']
FontMap = Dict[FontCollectionMapType, Dict[str, List[FontConfigPattern]]]
def create_font_map(all_fonts: Tuple[FontConfigPattern, ...]) -> FontMap:
ans: FontMap = {'family_map': {}, 'ps_map': {}, 'full_map': {}}
ans: FontMap = {'family_map': {}, 'ps_map': {}, 'full_map': {}, 'variable_map': {}}
for x in all_fonts:
if not x.get('path'):
continue
f = (x.get('family') or '').lower()
full = (x.get('full_name') or '').lower()
ps = (x.get('postscript_name') or '').lower()
f = family_name_to_key(x['family'])
full = family_name_to_key(x['full_name'])
ps = family_name_to_key(x['postscript_name'])
ans['family_map'].setdefault(f, []).append(x)
ans['ps_map'].setdefault(ps, []).append(x)
ans['full_map'].setdefault(full, []).append(x)
if x['variable']:
ans['variable_map'].setdefault(f, []).append(x)
return ans
@lru_cache()
@lru_cache(maxsize=2)
def all_fonts_map(monospaced: bool = True) -> FontMap:
if monospaced:
ans = fc_list(FC_DUAL) + fc_list(FC_MONO)
ans = fc_list(spacing=FC_DUAL) + fc_list(spacing=FC_MONO)
else:
# allow non-monospaced and bitmapped fonts as these are used for
# symbol_map
ans = fc_list(-1, True)
ans = fc_list(allow_bitmapped_fonts=True)
return create_font_map(ans)
def list_fonts() -> Generator[ListedFont, None, None]:
for fd in fc_list():
def is_monospace(descriptor: FontConfigPattern) -> bool:
return descriptor['spacing'] in ('MONO', 'DUAL')
def is_variable(descriptor: FontConfigPattern) -> bool:
return descriptor['variable']
def list_fonts(only_variable: bool = False) -> Generator[ListedFont, None, None]:
for fd in fc_list(only_variable=only_variable):
f = fd.get('family')
if f and isinstance(f, str):
fn_ = fd.get('full_name')
@@ -67,12 +71,11 @@ def list_fonts() -> Generator[ListedFont, None, None]:
fn = str(fn_)
else:
fn = f'{f} {fd.get("style", "")}'.strip()
is_mono = fd.get('spacing') in ('MONO', 'DUAL')
yield {'family': f, 'full_name': fn, 'postscript_name': str(fd.get('postscript_name', '')), 'is_monospace': is_mono}
def family_name_to_key(family: str) -> str:
return re.sub(r'\s+', ' ', family.lower())
yield {
'family': f, 'full_name': fn, 'postscript_name': str(fd.get('postscript_name', '')),
'is_monospace': is_monospace(fd), 'descriptor': fd, 'is_variable': is_variable(fd),
'style': fd['style'],
}
@lru_cache()
@@ -80,47 +83,110 @@ def fc_match(family: str, bold: bool, italic: bool, spacing: int = FC_MONO) -> F
return fc_match_impl(family, bold, italic, spacing)
def find_font_features(postscript_name: str) -> Tuple[FontFeature, ...]:
pat = fc_match_postscript_name(postscript_name)
class WeightRange(NamedTuple):
minimum: int = sys.maxsize
maximum: int = -1
medium: int = -1
bold: int = -1
if pat.get('postscript_name') != postscript_name or 'fontfeatures' not in pat:
return ()
@property
def is_valid(self) -> bool:
return self.minimum != wr.minimum and self.maximum != wr.maximum and self.medium != wr.medium and self.bold != wr.bold
features = []
for feat in pat['fontfeatures']:
try:
parsed = parse_font_feature(feat)
except ValueError:
log_error(f'Ignoring invalid font feature: {feat}')
wr = WeightRange()
@lru_cache()
def weight_range_for_family(family: str) -> WeightRange:
faces = all_fonts_map(True)['family_map'].get(family_name_to_key(family), ())
mini, maxi, medium, bold = wr.minimum, wr.maximum, wr.medium, wr.bold
for face in faces:
w = face['weight']
mini, maxi = min(w, mini), max(w, maxi)
s = face['style'].lower()
if not s:
continue
s = s.split()[0]
if s == 'semibold':
bold = w
elif s == 'bold' and bold == wr.bold:
bold = w
elif s == 'regular':
medium = w
elif s == 'medium' and medium == wr.medium:
medium = w
return WeightRange(mini, maxi, medium, bold)
class FCScorer(Scorer):
weight_range: Optional[WeightRange] = None
def score(self, candidate: Descriptor) -> Score:
assert candidate['descriptor_type'] == 'fontconfig'
variable_score = 0 if self.prefer_variable and candidate['variable'] else 1
if self.weight_range is None:
bold_score = abs((FC_WEIGHT_BOLD if self.bold else FC_WEIGHT_REGULAR) - candidate['weight'])
else:
features.append(FontFeature(feat, parsed))
bold_score = abs((self.weight_range.bold if self.bold else self.weight_range.medium) - candidate['weight'])
italic_score = abs((FC_SLANT_ITALIC if self.italic else FC_SLANT_ROMAN) - candidate['slant'])
monospace_match = 0
if self.monospaced:
monospace_match = 0 if candidate.get('spacing') == 'MONO' else 1
width_score = abs(candidate['width'] - FC_WIDTH_NORMAL)
return Score(variable_score, bold_score / 1000 + italic_score / 110, monospace_match, width_score)
return tuple(features)
def sorted_candidates(self, candidates: Sequence[DescriptorVar], dump: bool = False) -> List[DescriptorVar]:
self.weight_range = None
families = {x['family'] for x in candidates}
if len(families) == 1:
wr = weight_range_for_family(next(iter(families)))
if wr.is_valid and wr.medium < 100: # Operator Mono and Cascadia Code are examples
self.weight_range = wr
candidates = sorted(candidates, key=self.score)
if dump:
print(self)
if self.weight_range:
print(self.weight_range)
for x in candidates:
assert x['descriptor_type'] == 'fontconfig'
print(Face(descriptor=x).postscript_name(), f'weight={x["weight"]}', f'slant={x["slant"]}')
print(' ', self.score(x))
print()
return candidates
def find_best_match(family: str, bold: bool = False, italic: bool = False, monospaced: bool = True) -> FontConfigPattern:
def create_scorer(bold: bool = False, italic: bool = False, monospaced: bool = True, prefer_variable: bool = False) -> Scorer:
return FCScorer(bold, italic, monospaced, prefer_variable)
def find_last_resort_text_font(bold: bool = False, italic: bool = False, monospaced: bool = True) -> FontConfigPattern:
# Use fc-match with a generic family
family = 'monospace' if monospaced else 'sans-serif'
return fc_match(family, bold, italic)
def find_best_match(
family: str, bold: bool = False, italic: bool = False, monospaced: bool = True,
ignore_face: Optional[FontConfigPattern] = None, prefer_variable: bool = False,
) -> FontConfigPattern:
from .common import find_best_match_in_candidates
q = family_name_to_key(family)
font_map = all_fonts_map(monospaced)
def score(candidate: FontConfigPattern) -> Tuple[int, int, int]:
bold_score = abs((FC_WEIGHT_BOLD if bold else FC_WEIGHT_REGULAR) - candidate.get('weight', 0))
italic_score = abs((FC_SLANT_ITALIC if italic else FC_SLANT_ROMAN) - candidate.get('slant', 0))
monospace_match = 0 if candidate.get('spacing') == 'MONO' else 1
width_score = abs(candidate.get('width', FC_WIDTH_NORMAL) - FC_WIDTH_NORMAL)
return bold_score + italic_score, monospace_match, width_score
scorer = create_scorer(bold, italic, monospaced, prefer_variable=prefer_variable)
is_medium_face = not bold and not italic
# First look for an exact match
for selector in ('ps_map', 'full_map', 'family_map'):
candidates = font_map[selector].get(q)
if not candidates:
continue
if len(candidates) == 1 and (bold or italic) and candidates[0].get('family') == candidates[0].get('full_name'):
# IBM Plex Mono does this, where the full name of the regular font
# face is the same as its family name
continue
candidates.sort(key=score)
return candidates[0]
groups: Tuple[FontCollectionMapType, ...] = ('ps_map', 'full_map', 'family_map')
for which in groups:
m = font_map[which]
cq = m.get(q, [])
if cq:
if which == 'full_map' and cq[0]['family'] == cq[0]['full_name']:
continue # IBM Plex Mono has fullname of regular face == family_name under fontconfig
exact_match = find_best_match_in_candidates(cq, scorer, is_medium_face, ignore_face=ignore_face)
if exact_match:
return exact_match
# Use fc-match to see if we can find a monospaced font that matches family
# When aliases are defined, spacing can cause the incorrect font to be
@@ -131,6 +197,7 @@ def find_best_match(family: str, bold: bool = False, italic: bool = False, monos
tries = (dual_possibility, mono_possibility) if any_possibility == dual_possibility else (mono_possibility, dual_possibility)
for possibility in tries:
for key, map_key in (('postscript_name', 'ps_map'), ('full_name', 'full_map'), ('family', 'family_map')):
map_key = cast(FontCollectionMapType, map_key)
val: Optional[str] = cast(Optional[str], possibility.get(key))
if val:
candidates = font_map[map_key].get(family_name_to_key(val))
@@ -142,32 +209,89 @@ def find_best_match(family: str, bold: bool = False, italic: bool = False, monos
family_name_candidates = font_map['family_map'].get(family_name_to_key(candidates[0]['family']))
if family_name_candidates and len(family_name_candidates) > 1:
candidates = family_name_candidates
return sorted(candidates, key=score)[0]
# Use fc-match with a generic family
family = 'monospace' if monospaced else 'sans-serif'
return fc_match(family, bold, italic)
def resolve_family(f: str, main_family: str, bold: bool, italic: bool) -> str:
if (bold or italic) and f == 'auto':
f = main_family
return f
def get_font_files(opts: Options) -> Dict[str, FontConfigPattern]:
ans: Dict[str, FontConfigPattern] = {}
for (bold, italic), attr in attr_map.items():
rf = resolve_family(getattr(opts, attr), opts.font_family, bold, italic)
font = find_best_match(rf, bold, italic)
key = {(False, False): 'medium',
(True, False): 'bold',
(False, True): 'italic',
(True, True): 'bi'}[(bold, italic)]
ans[key] = font
return ans
return scorer.sorted_candidates(candidates)[0]
return find_last_resort_text_font(bold, italic, monospaced)
def font_for_family(family: str) -> Tuple[FontConfigPattern, bool, bool]:
ans = find_best_match(family, monospaced=False)
return ans, ans.get('weight', 0) >= FC_WEIGHT_BOLD, ans.get('slant', FC_SLANT_ROMAN) != FC_SLANT_ROMAN
def descriptor(f: ListedFont) -> FontConfigPattern:
d = f['descriptor']
assert d['descriptor_type'] == 'fontconfig'
return d
def prune_family_group(g: List[ListedFont]) -> List[ListedFont]:
# fontconfig creates dummy entries for named styles in variable fonts, prune them
variable_paths = {descriptor(f)['path'] for f in g if f['is_variable']}
if not variable_paths:
return g
def is_ok(d: FontConfigPattern) -> bool:
return d['variable'] or d['path'] not in variable_paths
return [x for x in g if is_ok(descriptor(x))]
def set_named_style(name: str, font: FontConfigPattern, vd: VariableData) -> bool:
q = name.lower()
for i, ns in enumerate(vd['named_styles']):
if ns['psname'].lower() == q:
font['named_style'] = i
return True
for i, ns in enumerate(vd['named_styles']):
if ns['name'].lower() == q:
font['named_style'] = i
return True
if vd['elided_fallback_name']:
for i, ns in enumerate(vd['named_styles']):
eq = ' '.join(ns['name'].replace(vd['elided_fallback_name'], '').strip().split()).lower()
if q == eq:
font['named_style'] = i
return True
return False
def lift_axes_to_named_style_if_possible(font: FontConfigPattern, vd: VariableData) -> bool:
axes = font.get('axes', tuple(ax['default'] for ax in vd['axes']))
q = {vd['axes'][i]['tag']: val for i, val in enumerate(axes)}
for i, ns in enumerate(vd['named_styles']):
if ns['axis_values'] == q:
font.pop('axes', None)
font['named_style'] = i
return True
return False
def set_axis_values(tag_map: Dict[str, float], font: FontConfigPattern, vd: VariableData) -> bool:
axes = list(font.get('axes', ())) or [ax['default'] for ax in vd['axes']]
changed = False
for i, ax in enumerate(vd['axes']):
val = tag_map.get(ax['tag'])
if val is not None:
changed = True
axes[i] = val
if changed:
font['axes'] = tuple(axes)
lift_axes_to_named_style_if_possible(font, vd)
return changed
def get_axis_values(font: FontConfigPattern, vd: VariableData) -> Dict[str, float]:
ans: Dict[str, float] = {}
ns = font.get('named_style')
if ns is not None:
if ns > -1 and ns < len(vd['named_styles']):
ans = vd['named_styles'][ns]['axis_values']
axis_values = font.get('axes', ())
for i, ax in enumerate(vd['axes']):
tag = ax['tag']
if i < len(axis_values):
ans[tag] = axis_values[i]
else:
if tag not in ans:
ans[tag] = ax['default']
return ans

View File

@@ -1,17 +1,17 @@
#!/usr/bin/env python
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import sys
from typing import Dict, List, Sequence
from typing import Dict, List, Optional, Sequence
from kitty.constants import is_macos
from . import ListedFont
from .common import get_variable_data_for_descriptor
if is_macos:
from .core_text import list_fonts
from .core_text import list_fonts, prune_family_group
else:
from .fontconfig import list_fonts
from .fontconfig import list_fonts, prune_family_group
def create_family_groups(monospaced: bool = True) -> Dict[str, List[ListedFont]]:
@@ -19,23 +19,24 @@ def create_family_groups(monospaced: bool = True) -> Dict[str, List[ListedFont]]
for f in list_fonts():
if not monospaced or f['is_monospace']:
g.setdefault(f['family'], []).append(f)
return g
return {k: prune_family_group(v) for k, v in g.items()}
def as_json(indent: Optional[int] = None) -> str:
import json
groups = create_family_groups()
for v in groups.values():
for f in v:
f['variable_data'] = get_variable_data_for_descriptor(f['descriptor']) # type: ignore
return json.dumps(groups, indent=indent)
def main(argv: Sequence[str]) -> None:
psnames = '--psnames' in argv
isatty = sys.stdout.isatty()
groups = create_family_groups()
for k in sorted(groups, key=lambda x: x.lower()):
if isatty:
print(f'\033[1;32m{k}\033[m')
else:
print(k)
for f in sorted(groups[k], key=lambda x: x['full_name'].lower()):
p = f['full_name']
if isatty:
p = f'\033[3m{p}\033[m'
if psnames:
p += ' ({})'.format(f['postscript_name'])
print(' ', p)
print()
import os
from kitty.constants import kitten_exe, kitty_exe
argv = list(argv)
if '--psnames' in argv:
argv.remove('--psnames')
os.environ['KITTY_PATH_TO_KITTY_EXE'] = kitty_exe()
os.execlp(kitten_exe(), 'kitten', 'choose-fonts')

View File

@@ -5,13 +5,14 @@ import ctypes
import sys
from functools import partial
from math import ceil, cos, floor, pi
from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, List, Optional, Tuple, Union, cast
from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, List, Literal, Optional, Tuple, Union, cast
from kitty.constants import is_macos
from kitty.fast_data_types import (
NUM_UNDERLINE_STYLES,
Screen,
create_test_font_group,
current_fonts,
get_fallback_font,
get_options,
set_font_data,
@@ -23,29 +24,22 @@ from kitty.fast_data_types import (
)
from kitty.fonts.box_drawing import BufType, distribute_dots, render_box_char, render_missing_glyph
from kitty.options.types import Options, defaults
from kitty.options.utils import parse_font_spec
from kitty.types import _T
from kitty.typing import CoreTextFont, FontConfigPattern
from kitty.utils import log_error
from .common import get_font_files
if is_macos:
from .core_text import find_font_features
from .core_text import font_for_family as font_for_family_macos
from .core_text import get_font_files as get_font_files_coretext
else:
from .fontconfig import find_font_features
from .fontconfig import font_for_family as font_for_family_fontconfig
from .fontconfig import get_font_files as get_font_files_fontconfig
FontObject = Union[CoreTextFont, FontConfigPattern]
current_faces: List[Tuple[FontObject, bool, bool]] = []
def get_font_files(opts: Options) -> Dict[str, Any]:
if is_macos:
return get_font_files_coretext(opts)
return get_font_files_fontconfig(opts)
def font_for_family(family: str) -> Tuple[FontObject, bool, bool]:
if is_macos:
return font_for_family_macos(family)
@@ -164,33 +158,25 @@ def descriptor_for_idx(idx: int) -> Tuple[FontObject, bool, bool]:
return current_faces[idx]
def dump_faces(ftypes: List[str], indices: Dict[str, int]) -> None:
def face_str(f: Tuple[FontObject, bool, bool]) -> str:
fo = f[0]
if 'index' in fo:
return '{}:{}'.format(fo['path'], cast('FontConfigPattern', fo)['index'])
fo = cast('CoreTextFont', fo)
return fo['path']
log_error('Preloaded font faces:')
log_error('normal face:', face_str(current_faces[0]))
for ftype in ftypes:
if indices[ftype]:
log_error(ftype, 'face:', face_str(current_faces[indices[ftype]]))
si_faces = current_faces[max(indices.values())+1:]
if si_faces:
log_error('Symbol map faces:')
for face in si_faces:
log_error(face_str(face))
def dump_font_debug() -> None:
cf = current_fonts()
log_error('Text fonts:')
for key, text in {'medium': 'Normal', 'bold': 'Bold', 'italic': 'Italic', 'bi': 'Bold-Italic'}.items():
log_error(f' {text}:', cf[key].identify_for_debug()) # type: ignore
ss = cf['symbol']
if ss:
log_error('Symbol map fonts:')
for s in ss:
log_error(' ' + s.identify_for_debug())
def set_font_family(opts: Optional[Options] = None, override_font_size: Optional[float] = None, debug_font_matching: bool = False) -> None:
def set_font_family(opts: Optional[Options] = None, override_font_size: Optional[float] = None) -> None:
global current_faces
opts = opts or defaults
sz = override_font_size or opts.font_size
font_map = get_font_files(opts)
current_faces = [(font_map['medium'], False, False)]
ftypes = 'bold italic bi'.split()
ftypes: List[Literal['bold', 'italic', 'bi']] = ['bold', 'italic', 'bi']
indices = {k: 0 for k in ftypes}
for k in ftypes:
if k in font_map:
@@ -200,16 +186,10 @@ def set_font_family(opts: Optional[Options] = None, override_font_size: Optional
sm = create_symbol_map(opts)
ns = create_narrow_symbols(opts)
num_symbol_fonts = len(current_faces) - before
font_features = {}
for face, _, _ in current_faces:
font_features[face['postscript_name']] = find_font_features(face['postscript_name'])
font_features.update(opts.font_features)
if debug_font_matching:
dump_faces(ftypes, indices)
set_font_data(
render_box_drawing, prerender_function, descriptor_for_idx,
indices['bold'], indices['italic'], indices['bi'], num_symbol_fonts,
sm, sz, font_features, ns
sm, sz, ns
)
@@ -431,7 +411,7 @@ class setup_for_testing:
self.family, self.size, self.dpi = family, size, dpi
def __enter__(self) -> Tuple[Dict[Tuple[int, int, int], bytes], int, int]:
opts = defaults._replace(font_family=self.family, font_size=self.size)
opts = defaults._replace(font_family=parse_font_spec(self.family), font_size=self.size)
set_options(opts)
sprites = {}
@@ -481,29 +461,30 @@ def shape_string(
return test_shape(line, path)
def show(outfile: str, width: int, height: int, fmt: int) -> None:
import os
def show(rgba_data: bytes, width: int, height: int, fmt: int = 32) -> None:
from base64 import standard_b64encode
from kittens.tui.images import GraphicsCommand
data = memoryview(standard_b64encode(rgba_data))
cmd = GraphicsCommand()
cmd.a = 'T'
cmd.f = fmt
cmd.s = width
cmd.v = height
cmd.t = 't'
while data:
chunk, data = data[:4096], data[4096:]
cmd.m = 1 if data else 0
sys.stdout.buffer.write(cmd.serialize(chunk))
cmd.clear()
sys.stdout.flush()
sys.stdout.buffer.write(cmd.serialize(standard_b64encode(os.path.abspath(outfile).encode())))
sys.stdout.buffer.flush()
def display_bitmap(rgb_data: bytes, width: int, height: int) -> None:
from tempfile import NamedTemporaryFile
setattr(display_bitmap, 'detected', True)
with NamedTemporaryFile(suffix='.rgba', delete=False) as f:
f.write(rgb_data)
assert len(rgb_data) == 4 * width * height
show(f.name, width, height, 32)
show(rgb_data, width, height)
def test_render_string(
@@ -517,8 +498,8 @@ def test_render_string(
cell_width, cell_height, cells = render_string(text, family, size, dpi)
rgb_data = concat_cells(cell_width, cell_height, True, tuple(cells))
cf = current_fonts()
fonts = [cf['medium'].display_name()]
fonts.extend(f.display_name() for f in cf['fallback'])
fonts = [cf['medium'].postscript_name()]
fonts.extend(f.postscript_name() for f in cf['fallback'])
msg = 'Rendered string {} below, with fonts: {}\n'.format(text, ', '.join(fonts))
try:
print(msg)

View File

@@ -19,6 +19,8 @@
#include FT_BITMAP_H
#include FT_TRUETYPE_TABLES_H
#include FT_MULTIPLE_MASTERS_H
#include FT_SFNT_NAMES_H
typedef union FaceIndex {
struct {
@@ -35,8 +37,7 @@ typedef struct {
unsigned int units_per_EM;
int ascender, descender, height, max_advance_width, max_advance_height, underline_position, underline_thickness, strikethrough_position, strikethrough_thickness;
int hinting, hintstyle;
FaceIndex instance;
bool is_scalable, has_color;
bool is_scalable, has_color, is_variable, has_svg;
float size_in_pts;
FT_F26Dot6 char_width, char_height;
FT_UInt xdpi, ydpi;
@@ -46,6 +47,8 @@ typedef struct {
void *extra_data;
free_extra_data_func free_extra_data;
float apple_leading;
PyObject *name_lookup_table;
FontFeatures font_features;
} Face;
PyTypeObject Face_Type;
@@ -194,6 +197,18 @@ set_size_for_face(PyObject *s, unsigned int desired_height, bool force, FONTS_DA
return set_font_size(self, w, w, xdpi, ydpi, desired_height, fg->cell_height);
}
static PyObject*
set_size(Face *self, PyObject *args) {
double font_sz_in_pts, dpi_x, dpi_y;
if (!PyArg_ParseTuple(args, "ddd", &font_sz_in_pts, &dpi_x, &dpi_y)) return NULL;
FT_F26Dot6 w = (FT_F26Dot6)(ceil(font_sz_in_pts * 64.0));
FT_UInt xdpi = (FT_UInt)dpi_x, ydpi = (FT_UInt)dpi_y;
if (self->char_width == w && self->char_height == w && self->xdpi == xdpi && self->ydpi == ydpi) { Py_RETURN_NONE; }
self->size_in_pts = (float)font_sz_in_pts;
if (!set_font_size(self, w, w, xdpi, ydpi, 0, 0)) return NULL;
Py_RETURN_NONE;
}
static bool
init_ft_face(Face *self, PyObject *path, int hinting, int hintstyle, FONTS_DATA_HANDLE fg) {
#define CPY(n) self->n = self->face->n;
@@ -201,8 +216,14 @@ init_ft_face(Face *self, PyObject *path, int hinting, int hintstyle, FONTS_DATA_
#undef CPY
self->is_scalable = FT_IS_SCALABLE(self->face);
self->has_color = FT_HAS_COLOR(self->face);
self->is_variable = FT_HAS_MULTIPLE_MASTERS(self->face);
#ifdef FT_HAS_SVG
self->has_svg = FT_HAS_SVG(self->face);
#else
self->has_svg = false;
#endif
self->hinting = hinting; self->hintstyle = hintstyle;
if (!set_size_for_face((PyObject*)self, 0, false, fg)) return false;
if (fg && !set_size_for_face((PyObject*)self, 0, false, fg)) return false;
self->harfbuzz_font = hb_ft_font_create(self->face, NULL);
if (self->harfbuzz_font == NULL) { PyErr_NoMemory(); return false; }
hb_ft_font_set_load_flags(self->harfbuzz_font, get_load_flags(self->hinting, self->hintstyle, FT_LOAD_DEFAULT));
@@ -213,9 +234,7 @@ init_ft_face(Face *self, PyObject *path, int hinting, int hintstyle, FONTS_DATA_
self->strikethrough_thickness = os2->yStrikeoutSize;
}
self->path = path;
Py_INCREF(self->path);
self->instance.val = self->face->face_index;
self->path = path; Py_INCREF(self->path);
self->space_glyph_id = glyph_id_for_codepoint((PyObject*)self, ' ');
return true;
}
@@ -235,7 +254,7 @@ face_equals_descriptor(PyObject *face_, PyObject *descriptor) {
if (!t) return false;
if (PyObject_RichCompareBool(face->path, t, Py_EQ) != 1) return false;
t = PyDict_GetItemString(descriptor, "index");
if (t && PyLong_AsLong(t) != face->instance.val) return false;
if (t && PyLong_AsLong(t) != face->face->face_index) return false;
return true;
}
@@ -256,13 +275,53 @@ face_from_descriptor(PyObject *descriptor, FONTS_DATA_HANDLE fg) {
D(hinting, PyObject_IsTrue, true);
D(hint_style, PyLong_AsLong, true);
#undef D
Face *self = (Face *)Face_Type.tp_alloc(&Face_Type, 0);
if (self != NULL) {
int error = FT_New_Face(library, path, index, &(self->face));
if(error) { Py_CLEAR(self); return set_load_error(path, error); }
if (!init_ft_face(self, PyDict_GetItemString(descriptor, "path"), hinting, hint_style, fg)) { Py_CLEAR(self); return NULL; }
RAII_PyObject(retval, Face_Type.tp_alloc(&Face_Type, 0));
if (retval != NULL) {
Face *self = (Face *)retval;
int error;
if ((error = FT_New_Face(library, path, index, &(self->face)))) { self->face = NULL; set_load_error(path, error); }
if (!init_ft_face(self, PyDict_GetItemString(descriptor, "path"), hinting, hint_style, fg)) return NULL;
PyObject *ns = PyDict_GetItemString(descriptor, "named_style");
if (ns) {
unsigned long index = PyLong_AsUnsignedLong(ns);
if (PyErr_Occurred()) return NULL;
if ((error = FT_Set_Named_Instance(self->face, index + 1))) return set_load_error(path, error);
}
PyObject *axes = PyDict_GetItemString(descriptor, "axes");
Py_ssize_t sz;
if (axes && (sz = PyTuple_GET_SIZE(axes))) {
RAII_ALLOC(FT_Fixed, coords, malloc(sizeof(FT_Fixed) * sz));
for (Py_ssize_t i = 0; i < sz; i++) {
PyObject *t = PyTuple_GET_ITEM(axes, i);
double val = PyFloat_AsDouble(t);
if (PyErr_Occurred()) return NULL;
coords[i] = (FT_Fixed)(val * 65536.0);
}
if ((error = FT_Set_Var_Design_Coordinates(self->face, sz, coords))) return set_load_error(path, error);
}
if (!create_features_for_face(postscript_name_for_face((PyObject*)self), PyDict_GetItemString(descriptor, "features"), &self->font_features)) return NULL;
}
return (PyObject*)self;
Py_XINCREF(retval);
return retval;
}
FontFeatures*
features_for_face(PyObject *s) { return &((Face*)s)->font_features; }
static PyObject*
new(PyTypeObject *type UNUSED, PyObject *args, PyObject *kw) {
const char *path = NULL;
long index = 0;
PyObject *descriptor = NULL;
static char *kwds[] = {"descriptor", "path", "index", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kw, "|Osi", kwds, &descriptor, &path, &index)) return NULL;
if (descriptor) {
return face_from_descriptor(descriptor, NULL);
}
if (path) return face_from_path(path, index, NULL);
PyErr_SetString(PyExc_TypeError, "Must specify either path or descriptor");
return NULL;
}
FT_Face
@@ -290,20 +349,25 @@ dealloc(Face* self) {
if (self->harfbuzz_font) hb_font_destroy(self->harfbuzz_font);
if (self->face) FT_Done_Face(self->face);
if (self->extra_data && self->free_extra_data) self->free_extra_data(self->extra_data);
free(self->font_features.features);
Py_CLEAR(self->path);
Py_CLEAR(self->name_lookup_table);
Py_TYPE(self)->tp_free((PyObject*)self);
}
static PyObject *
repr(Face *self) {
const char *ps_name = FT_Get_Postscript_Name(self->face);
#define B(x) ((x) ? Py_True : Py_False)
FaceIndex instance;
instance.val = self->face->face_index;
return PyUnicode_FromFormat(
"Face(family=%s, style=%s, ps_name=%s, path=%S, ttc_index=%d, variation_index=0x%x is_scalable=%S, has_color=%S, ascender=%i, descender=%i, height=%i, underline_position=%i, underline_thickness=%i, strikethrough_position=%i, strikethrough_thickness=%i)",
"Face(family=%s style=%s ps_name=%s path=%S ttc_index=%d variant=%S named_instance=%S scalable=%S color=%S)",
self->face->family_name ? self->face->family_name : "", self->face->style_name ? self->face->style_name : "",
ps_name ? ps_name: "",
self->path, self->instance.ttc_index, self->instance.variation_index, self->is_scalable ? Py_True : Py_False, self->has_color ? Py_True : Py_False,
self->ascender, self->descender, self->height, self->underline_position, self->underline_thickness, self->strikethrough_position, self->strikethrough_thickness
ps_name ? ps_name: "", self->path, instance.ttc_index,
B(FT_IS_VARIATION(self->face)), B(FT_IS_NAMED_INSTANCE(self->face)), B(self->is_scalable), B(self->has_color)
);
#undef B
}
const char*
@@ -578,7 +642,7 @@ copy_color_bitmap(uint8_t *src, pixel* dest, Region *src_rect, Region *dest_rect
static const bool debug_placement = false;
static void
place_bitmap_in_canvas(pixel *cell, ProcessedBitmap *bm, size_t cell_width, size_t cell_height, float x_offset, float y_offset, size_t baseline, unsigned int glyph_num) {
place_bitmap_in_canvas(pixel *cell, ProcessedBitmap *bm, size_t cell_width, size_t cell_height, float x_offset, float y_offset, size_t baseline, unsigned int glyph_num, pixel fg_rgb, size_t x_in_canvas, size_t y_in_canvas) {
// We want the glyph to be positioned inside the cell based on the bearingX
// and bearingY values, making sure that it does not overflow the cell.
@@ -594,6 +658,7 @@ place_bitmap_in_canvas(pixel *cell, ProcessedBitmap *bm, size_t cell_width, size
uint32_t extra = dest.left + bm->width - cell_width;
dest.left = extra > dest.left ? 0 : dest.left - extra;
}
dest.left += x_in_canvas;
// Calculate row bounds
int32_t yoff = (ssize_t)(y_offset + bm->bitmap_top);
@@ -602,12 +667,13 @@ place_bitmap_in_canvas(pixel *cell, ProcessedBitmap *bm, size_t cell_width, size
} else {
dest.top = baseline - yoff;
}
dest.top += y_in_canvas;
/* printf("x_offset: %d y_offset: %d src_start_row: %u src_start_column: %u dest_start_row: %u dest_start_column: %u bm_width: %lu bitmap_rows: %lu\n", xoff, yoff, src.top, src.left, dest.top, dest.left, bm->width, bm->rows); */
// printf("x_offset: %d y_offset: %d src_start_row: %u src_start_column: %u dest_start_row: %u dest_start_column: %u bm_width: %lu bitmap_rows: %lu\n", xoff, yoff, src.top, src.left, dest.top, dest.left, bm->width, bm->rows);
if (bm->pixel_mode == FT_PIXEL_MODE_BGRA) {
copy_color_bitmap(bm->buf, cell, &src, &dest, bm->stride, cell_width);
} else render_alpha_mask(bm->buf, cell, &src, &dest, bm->stride, cell_width);
} else render_alpha_mask(bm->buf, cell, &src, &dest, bm->stride, cell_width, fg_rgb);
}
static const ProcessedBitmap EMPTY_PBM = {.factor = 1};
@@ -643,7 +709,7 @@ render_glyphs_in_cells(PyObject *f, bool bold, bool italic, hb_glyph_info_t *inf
y = (float)positions[i].y_offset / 64.0f;
if (debug_placement) printf("%d: x=%f canvas: %u", i, x_offset, canvas_width);
if ((*was_colored || self->face->glyph->metrics.width > 0) && bm.width > 0) {
place_bitmap_in_canvas(canvas, &bm, canvas_width, cell_height, x_offset, y, baseline, i);
place_bitmap_in_canvas(canvas, &bm, canvas_width, cell_height, x_offset, y, baseline, i, 0xffffff, 0, 0);
}
if (debug_placement) printf(" adv: %f\n", (float)positions[i].x_advance / 64.0f);
// the roundf() below is needed for infinite length ligatures, for a test case
@@ -667,7 +733,7 @@ render_glyphs_in_cells(PyObject *f, bool bold, bool italic, hb_glyph_info_t *inf
}
static PyObject*
display_name(PyObject *s, PyObject *a UNUSED) {
postscript_name(PyObject *s, PyObject *a UNUSED) {
Face *self = (Face*)s;
const char *psname = FT_Get_Postscript_Name(self->face);
if (psname) return Py_BuildValue("s", psname);
@@ -675,11 +741,187 @@ display_name(PyObject *s, PyObject *a UNUSED) {
return self->path;
}
static PyObject*
identify_for_debug(PyObject *s, PyObject *a UNUSED) {
Face *self = (Face*)s;
FaceIndex instance;
instance.val = self->face->face_index;
return PyUnicode_FromFormat("%s: %V:%d", FT_Get_Postscript_Name(self->face), self->path, "[path]", instance.val);
}
static PyObject*
extra_data(PyObject *self, PyObject *a UNUSED) {
return PyLong_FromVoidPtr(((Face*)self)->extra_data);
}
// NAME table {{{
static bool
ensure_name_table(Face *self) {
if (self->name_lookup_table) return true;
RAII_PyObject(ans, PyDict_New());
if (!ans) return false;
FT_SfntName temp;
for (FT_UInt i = 0; i < FT_Get_Sfnt_Name_Count(self->face); i++) {
FT_Error err = FT_Get_Sfnt_Name(self->face, i, &temp);
if (err != 0) continue;
if (!add_font_name_record(ans, temp.platform_id, temp.encoding_id, temp.language_id, temp.name_id, (const char*)temp.string, temp.string_len)) return NULL;
}
self->name_lookup_table = ans; Py_INCREF(ans);
return true;
}
static PyObject*
get_best_name(Face *self, PyObject *nameid) {
if (!ensure_name_table(self)) return NULL;
return get_best_name_from_name_table(self->name_lookup_table, nameid);
}
static PyObject*
_get_best_name(Face *self, unsigned long nameid) {
RAII_PyObject(key, PyLong_FromUnsignedLong(nameid));
return key ? get_best_name(self, key) : NULL;
}
// }}}
static inline void cleanup_ftmm(FT_MM_Var **p) { if (*p) FT_Done_MM_Var(library, *p); *p = NULL; }
#define RAII_FTMMVar(name) __attribute__((cleanup(cleanup_ftmm))) FT_MM_Var *name = NULL
static const char*
tag_to_string(uint32_t tag, uint8_t bytes[5]) {
bytes[0] = (tag >> 24) & 0xff;
bytes[1] = (tag >> 16) & 0xff;
bytes[2] = (tag >> 8) & 0xff;
bytes[3] = (tag) & 0xff;
bytes[4] = 0;
return (const char*)bytes;
}
static PyObject*
convert_named_style_to_python(Face *face, const FT_Var_Named_Style *src, FT_Var_Axis *axes, unsigned num_of_axes) {
RAII_PyObject(axis_values, PyDict_New());
if (!axis_values) return NULL;
uint8_t tag_buf[5] = {0};
for (FT_UInt i = 0; i < num_of_axes; i++) {
double val = src->coords[i] / 65536.0;
RAII_PyObject(pval, PyFloat_FromDouble(val));
if (!pval) return NULL;
if (PyDict_SetItemString(axis_values, tag_to_string(axes[i].tag, tag_buf), pval) != 0) return NULL;
}
RAII_PyObject(name, _get_best_name(face, src->strid));
if (!name) PyErr_Clear();
RAII_PyObject(psname, src->psid == 0xffff ? NULL : _get_best_name(face, src->psid));
if (!psname) PyErr_Clear();
return Py_BuildValue("{sO sO sO}", "axis_values", axis_values, "name", name ? name : PyUnicode_FromString(""), "psname", psname ? psname : PyUnicode_FromString(""));
}
static PyObject*
convert_axis_to_python(Face *face, const FT_Var_Axis *src, FT_UInt flags) {
PyObject *strid = _get_best_name(face, src->strid);
if (!strid) { PyErr_Clear(); strid = PyUnicode_FromString(""); }
uint8_t tag_buf[5] = {0};
return Py_BuildValue("{sd sd sd sO ss ss sN}",
"minimum", src->minimum / 65536.0, "maximum", src->maximum / 65536.0, "default", src->def / 65536.0,
"hidden", flags & FT_VAR_AXIS_FLAG_HIDDEN ? Py_True : Py_False, "name", src->name, "tag", tag_to_string(src->tag, tag_buf),
"strid", strid
);
}
static PyObject*
get_variation(Face *self, PyObject *a UNUSED) {
RAII_FTMMVar(mm);
FT_Error err;
if ((err = FT_Get_MM_Var(self->face, &mm))) { Py_RETURN_NONE; }
RAII_ALLOC(FT_Fixed, coords, malloc(mm->num_axis * sizeof(FT_Fixed)));
if (!coords) return PyErr_NoMemory();
if ((err = FT_Get_Var_Design_Coordinates(self->face, mm->num_axis, coords))) {
set_freetype_error("Failed to load the variation data from font with error:", err); return NULL;
}
RAII_PyObject(ans, PyDict_New()); if (!ans) return NULL;
uint8_t tag[5];
for (FT_UInt i = 0; i < mm->num_axis; i++) {
double val = coords[i] / 65536.0;
tag_to_string(mm->axis[i].tag, tag);
RAII_PyObject(pval, PyFloat_FromDouble(val));
if (!pval) return NULL;
if (PyDict_SetItemString(ans, (const char*)tag, pval) != 0) return NULL;
}
Py_INCREF(ans); return ans;
}
static PyObject*
applied_features(Face *self, PyObject *a UNUSED) {
return font_features_as_dict(&self->font_features);
}
static PyObject*
get_features(Face *self, PyObject *a UNUSED) {
FT_Error err;
FT_ULong length = 0;
if (!ensure_name_table(self)) return NULL;
RAII_PyObject(output, PyDict_New()); if (!output) return NULL;
if ((err = FT_Load_Sfnt_Table(self->face, FT_MAKE_TAG('G', 'S', 'U', 'B'), 0, NULL, &length)) == 0) {
RAII_ALLOC(uint8_t, table, malloc(length));
if (!table) return PyErr_NoMemory();
if ((err = FT_Load_Sfnt_Table(self->face, FT_MAKE_TAG('G', 'S', 'U', 'B'), 0, table, &length))) {
set_freetype_error("Failed to load the GSUB table from font with error:", err); return NULL;
}
if (!read_features_from_font_table(table, length, self->name_lookup_table, output)) return NULL;
}
length = 0;
if ((err = FT_Load_Sfnt_Table(self->face, FT_MAKE_TAG('G', 'P', 'O', 'S'), 0, NULL, &length)) == 0) {
RAII_ALLOC(uint8_t, table, malloc(length));
if (!table) return PyErr_NoMemory();
if ((err = FT_Load_Sfnt_Table(self->face, FT_MAKE_TAG('G', 'P', 'O', 'S'), 0, table, &length))) {
set_freetype_error("Failed to load the GSUB table from font with error:", err); return NULL;
}
if (!read_features_from_font_table(table, length, self->name_lookup_table, output)) return NULL;
}
Py_INCREF(output); return output;
}
static PyObject*
get_variable_data(Face *self, PyObject *a UNUSED) {
if (!ensure_name_table(self)) return NULL;
RAII_PyObject(output, PyDict_New()); if (!output) return NULL;
RAII_PyObject(axes, PyTuple_New(0));
RAII_PyObject(named_styles, PyTuple_New(0));
if (!axes || !named_styles) return NULL;
FT_Error err;
FT_ULong length = 0;
if ((err = FT_Load_Sfnt_Table(self->face, FT_MAKE_TAG('S', 'T', 'A', 'T'), 0, NULL, &length)) == 0) {
RAII_ALLOC(uint8_t, table, malloc(length));
if (!table) return PyErr_NoMemory();
if ((err = FT_Load_Sfnt_Table(self->face, FT_MAKE_TAG('S', 'T', 'A', 'T'), 0, table, &length))) {
set_freetype_error("Failed to load the STAT table from font with error:", err); return NULL;
}
if (!read_STAT_font_table(table, length, self->name_lookup_table, output)) return NULL;
} else if (!read_STAT_font_table(NULL, 0, self->name_lookup_table, output)) return NULL;
if (self->is_variable) {
RAII_FTMMVar(mm);
if ((err = FT_Get_MM_Var(self->face, &mm))) { set_freetype_error("Failed to get variable axis data from font with error:", err); return NULL; }
if (_PyTuple_Resize(&axes, mm->num_axis) == -1) return NULL;
if (_PyTuple_Resize(&named_styles, mm->num_namedstyles) == -1) return NULL;
for (FT_UInt i = 0; i < mm->num_namedstyles; i++) {
PyObject *s = convert_named_style_to_python(self, mm->namedstyle + i, mm->axis, mm->num_axis);
if (!s) return NULL;
PyTuple_SET_ITEM(named_styles, i, s);
}
for (FT_UInt i = 0; i < mm->num_axis; i++) {
FT_UInt flags;
FT_Get_Var_Axis_Flags(mm, i, &flags);
PyObject *s = convert_axis_to_python(self, mm->axis + i, flags);
if (!s) return NULL;
PyTuple_SET_ITEM(axes, i, s);
}
}
if (PyDict_SetItemString(output, "variations_postscript_name_prefix", _get_best_name(self, 25)) != 0) return NULL;
if (PyDict_SetItemString(output, "axes", axes) != 0) return NULL;
if (PyDict_SetItemString(output, "named_styles", named_styles) != 0) return NULL;
Py_INCREF(output); return output;
}
StringCanvas
render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) {
@@ -702,7 +944,7 @@ render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) {
FT_Bitmap *bitmap = &self->face->glyph->bitmap;
pbm = EMPTY_PBM;
populate_processed_bitmap(self->face->glyph, bitmap, &pbm, false);
place_bitmap_in_canvas(canvas, &pbm, canvas_width, canvas_height, pen_x, 0, baseline, n);
place_bitmap_in_canvas(canvas, &pbm, canvas_width, canvas_height, 0, 0, baseline, n, 0xffffff, pen_x, 0);
pen_x += self->face->glyph->advance.x >> 6;
}
ans.width = pen_x; ans.height = canvas_height;
@@ -718,6 +960,68 @@ render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) {
return ans;
}
static void destroy_hb_buffer(hb_buffer_t **x) { if (*x) hb_buffer_destroy(*x); }
static PyObject*
render_sample_text(Face *self, PyObject *args) {
unsigned long canvas_width, canvas_height;
unsigned long fg = 0xffffff;
PyObject *ptext;
if (!PyArg_ParseTuple(args, "Ukk|k", &ptext, &canvas_width, &canvas_height, &fg)) return NULL;
unsigned int cell_width, cell_height, baseline, underline_position, underline_thickness, strikethrough_position, strikethrough_thickness;
cell_metrics((PyObject*)self, &cell_width, &cell_height, &baseline, &underline_position, &underline_thickness, &strikethrough_position, &strikethrough_thickness);
int num_chars_per_line = canvas_width / cell_width, num_of_lines = (int)ceil((float)PyUnicode_GET_LENGTH(ptext) / (float)num_chars_per_line);
canvas_height = MIN(canvas_height, num_of_lines * cell_height);
RAII_PyObject(pbuf, PyBytes_FromStringAndSize(NULL, sizeof(pixel) * canvas_width * canvas_height));
if (!pbuf) return NULL;
memset(PyBytes_AS_STRING(pbuf), 0, PyBytes_GET_SIZE(pbuf));
__attribute__((cleanup(destroy_hb_buffer))) hb_buffer_t *hb_buffer = hb_buffer_create();
if (!hb_buffer_pre_allocate(hb_buffer, 4*PyUnicode_GET_LENGTH(ptext))) { PyErr_NoMemory(); return NULL; }
for (ssize_t n = 0; n < PyUnicode_GET_LENGTH(ptext); n++) {
Py_UCS4 codep = PyUnicode_READ_CHAR(ptext, n);
hb_buffer_add_utf32(hb_buffer, &codep, 1, 0, 1);
}
hb_buffer_guess_segment_properties(hb_buffer);
if (!HB_DIRECTION_IS_HORIZONTAL(hb_buffer_get_direction(hb_buffer))) goto end;
hb_shape(harfbuzz_font_for_face((PyObject*)self), hb_buffer, self->font_features.features, self->font_features.count);
unsigned int len = hb_buffer_get_length(hb_buffer);
hb_glyph_info_t *info = hb_buffer_get_glyph_infos(hb_buffer, NULL);
hb_glyph_position_t *positions = hb_buffer_get_glyph_positions(hb_buffer, NULL);
if (cell_width > canvas_width) goto end;
pixel *canvas = (pixel*)PyBytes_AS_STRING(pbuf);
int load_flags = get_load_flags(self->hinting, self->hintstyle, FT_LOAD_RENDER);
int error;
float pen_x = 0, pen_y = 0;
for (unsigned int i = 0; i < len; i++) {
float advance = (float)positions[i].x_advance / 64.0f;
if (pen_x + advance > canvas_width) {
pen_y += cell_height;
pen_x = 0;
if (pen_y >= canvas_height) break;
}
size_t x = (size_t)round(pen_x + (float)positions[i].x_offset / 64.0f);
size_t y = (size_t)round(pen_y + (float)positions[i].y_offset / 64.0f);
pen_x += advance;
if ((error = FT_Load_Glyph(self->face, info[i].codepoint, load_flags))) continue;
if ((error = FT_Render_Glyph(self->face->glyph, FT_RENDER_MODE_NORMAL))) continue;
FT_Bitmap *bitmap = &self->face->glyph->bitmap;
ProcessedBitmap pbm = EMPTY_PBM;
populate_processed_bitmap(self->face->glyph, bitmap, &pbm, false);
place_bitmap_in_canvas(canvas, &pbm, canvas_width, canvas_height, 0, 0, baseline, 99999, fg, x, y);
}
for (uint8_t *p = (uint8_t*)PyBytes_AS_STRING(pbuf); p < (uint8_t*)PyBytes_AS_STRING(pbuf) + sizeof(pixel) * canvas_width * canvas_height; p += 4) {
uint8_t a = p[0], b = p[1], g = p[2], r = p[3];
p[0] = r; p[1] = g; p[2] = b; p[3] = a;
}
end:
return Py_BuildValue("OII", pbuf, cell_width, cell_height);
}
// Boilerplate {{{
static PyMemberDef members[] = {
@@ -733,19 +1037,31 @@ static PyMemberDef members[] = {
MEM(strikethrough_position, T_INT),
MEM(strikethrough_thickness, T_INT),
MEM(is_scalable, T_BOOL),
MEM(is_variable, T_BOOL),
MEM(has_svg, T_BOOL),
MEM(has_color, T_BOOL),
MEM(path, T_OBJECT_EX),
{NULL} /* Sentinel */
};
static PyMethodDef methods[] = {
METHODB(display_name, METH_NOARGS),
METHODB(postscript_name, METH_NOARGS),
METHODB(identify_for_debug, METH_NOARGS),
METHODB(extra_data, METH_NOARGS),
METHODB(get_variable_data, METH_NOARGS),
METHODB(applied_features, METH_NOARGS),
METHODB(get_features, METH_NOARGS),
METHODB(get_variation, METH_NOARGS),
METHODB(get_best_name, METH_O),
METHODB(set_size, METH_VARARGS),
METHODB(render_sample_text, METH_VARARGS),
{NULL} /* Sentinel */
};
PyTypeObject Face_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "fast_data_types.Face",
.tp_new = new,
.tp_basicsize = sizeof(Face),
.tp_dealloc = (destructor)dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,

View File

@@ -44,7 +44,7 @@ from .fast_data_types import (
set_options,
)
from .fonts.box_drawing import set_scale
from .fonts.render import set_font_family
from .fonts.render import dump_font_debug, set_font_family
from .options.types import Options
from .options.utils import DELETE_ENV_VAR
from .os_window_size import edge_spacing, initial_window_size_func
@@ -272,6 +272,8 @@ def _run_app(opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = ())
wincls, wstate, load_all_shaders, disallow_override_title=bool(args.title), layer_shell_config=run_app.layer_shell_config)
boss = Boss(opts, args, cached_values, global_shortcuts)
boss.start(window_id, startup_sessions)
if args.debug_font_fallback:
dump_font_debug()
if bad_lines or boss.misc_config_errors:
boss.show_bad_config_lines(bad_lines, boss.misc_config_errors)
boss.misc_config_errors = []
@@ -293,7 +295,7 @@ class AppRunner:
set_scale(opts.box_drawing_scale)
set_options(opts, is_wayland(), args.debug_rendering, args.debug_font_fallback)
try:
set_font_family(opts, debug_font_matching=args.debug_font_fallback)
set_font_family(opts)
_run_app(opts, args, bad_lines)
finally:
set_options(None)

View File

@@ -32,15 +32,15 @@ kitty has very powerful font management. You can configure individual font faces
and even specify special fonts for particular characters.
''')
opt('font_family', 'monospace',
opt('font_family', 'monospace', option_type='parse_font_spec',
long_text='''
You can specify different fonts for the bold/italic/bold-italic variants.
To get a full list of supported fonts use the ``kitty +list-fonts`` command.
By default they are derived automatically, by the OSes font system. When
:opt:`bold_font` or :opt:`bold_italic_font` is set to :code:`auto` on macOS, the
priority of bold fonts is semi-bold, bold, heavy. Setting them manually is
useful for font families that have many weight variants like Book, Medium,
Thick, etc.
By default, they are derived automatically, via the Operating System's font
management. When :opt:`bold_font` or :opt:`bold_italic_font` is set to
:code:`auto` on macOS, the priority of bold fonts is semi-bold, bold, heavy.
Setting them manually is useful for font families that have many weight variants
like Book, Medium, Thick, etc.
For example::
font_family Operator Mono Book
@@ -50,11 +50,11 @@ For example::
'''
)
opt('bold_font', 'auto')
opt('bold_font', 'auto', option_type='parse_font_spec')
opt('italic_font', 'auto')
opt('italic_font', 'auto', option_type='parse_font_spec')
opt('bold_italic_font', 'auto')
opt('bold_italic_font', 'auto', option_type='parse_font_spec')
opt('font_size', '11.0',
option_type='to_font_size', ctype='double',
@@ -132,10 +132,8 @@ Note that this refers to programming ligatures, typically implemented using the
'''
)
opt('+font_features', 'none',
option_type='font_features',
add_to_default=False,
long_text='''
opt('+font_features', 'none', option_type='font_features', ctype='!font_features',
add_to_default=False, long_text='''
Choose exactly which OpenType features to enable or disable. This is useful as
some fonts might have features worthwhile in a terminal. For example, Fira Code
includes a discretionary feature, :code:`zero`, which in that font changes the
@@ -4279,7 +4277,7 @@ This will send "Special text" when you press the :kbd:`Ctrl+Alt+A` key
combination. The text to be sent decodes :link:`ANSI C escapes <https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html>`
so you can use escapes like :code:`\\\\e` to send control codes or :code:`\\\\u21fb` to send
Unicode characters (or you can just input the Unicode characters directly as
UTF-8 text). You can use ``kitten show_key`` to get the key escape
UTF-8 text). You can use ``kitten show-key`` to get the key escape
codes you want to emulate.
The first argument to :code:`send_text` is the keyboard modes in which to

16
kitty/options/parse.py generated
View File

@@ -13,10 +13,10 @@ from kitty.options.utils import (
deprecated_hide_window_decorations_aliases, deprecated_macos_show_window_title_in_menubar_alias,
deprecated_send_text, disable_ligatures, edge_width, env, font_features, hide_window_decorations,
macos_option_as_alt, macos_titlebar_color, menu_map, modify_font, narrow_symbols,
notify_on_cmd_finish, optional_edge_width, parse_map, parse_mouse_map, paste_actions,
remote_control_password, resize_debounce_time, scrollback_lines, scrollback_pager_history_size,
shell_integration, store_multiple, symbol_map, tab_activity_symbol, tab_bar_edge,
tab_bar_margin_height, tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator,
notify_on_cmd_finish, optional_edge_width, parse_font_spec, parse_map, parse_mouse_map,
paste_actions, remote_control_password, resize_debounce_time, scrollback_lines,
scrollback_pager_history_size, shell_integration, store_multiple, symbol_map, tab_activity_symbol,
tab_bar_edge, tab_bar_margin_height, tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator,
tab_title_template, titlebar_color, to_cursor_shape, to_cursor_unfocused_shape, to_font_size,
to_layout_names, to_modifiers, url_prefixes, url_style, visual_window_select_characters,
window_border_width, window_logo_scale, window_size
@@ -102,10 +102,10 @@ class Parser:
ans['bell_path'] = config_or_absolute_path(val)
def bold_font(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['bold_font'] = str(val)
ans['bold_font'] = parse_font_spec(val)
def bold_italic_font(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['bold_italic_font'] = str(val)
ans['bold_italic_font'] = parse_font_spec(val)
def box_drawing_scale(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['box_drawing_scale'] = box_drawing_scale(val)
@@ -979,7 +979,7 @@ class Parser:
ans['focus_follows_mouse'] = to_bool(val)
def font_family(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['font_family'] = str(val)
ans['font_family'] = parse_font_spec(val)
def font_features(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
for k, v in font_features(val):
@@ -1025,7 +1025,7 @@ class Parser:
ans['input_delay'] = positive_int(val)
def italic_font(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['italic_font'] = str(val)
ans['italic_font'] = parse_font_spec(val)
def kitten_alias(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
for k, v in action_alias(val):

View File

@@ -44,6 +44,19 @@ convert_from_opts_disable_ligatures(PyObject *py_opts, Options *opts) {
Py_DECREF(ret);
}
static void
convert_from_python_font_features(PyObject *val, Options *opts) {
font_features(val, opts);
}
static void
convert_from_opts_font_features(PyObject *py_opts, Options *opts) {
PyObject *ret = PyObject_GetAttrString(py_opts, "font_features");
if (ret == NULL) return;
convert_from_python_font_features(ret, opts);
Py_DECREF(ret);
}
static void
convert_from_python_modify_font(PyObject *val, Options *opts) {
modify_font(val, opts);
@@ -1170,6 +1183,8 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) {
if (PyErr_Occurred()) return false;
convert_from_opts_disable_ligatures(py_opts, opts);
if (PyErr_Occurred()) return false;
convert_from_opts_font_features(py_opts, opts);
if (PyErr_Occurred()) return false;
convert_from_opts_modify_font(py_opts, opts);
if (PyErr_Occurred()) return false;
convert_from_opts_text_composition_strategy(py_opts, opts);

View File

@@ -8,6 +8,7 @@
#include "../state.h"
#include "../colors.h"
#include "../fonts.h"
static inline float
PyFloat_AsFloat(PyObject *o) {
@@ -44,7 +45,7 @@ parse_ms_long_to_monotonic_t(PyObject *val) {
return ms_to_monotonic_t(PyLong_AsUnsignedLong(val));
}
static WindowTitleIn
static inline WindowTitleIn
window_title_in(PyObject *title_in) {
const char *in = PyUnicode_AsUTF8(title_in);
switch(in[0]) {
@@ -57,7 +58,7 @@ window_title_in(PyObject *title_in) {
return ALL;
}
static UnderlineHyperlinks
static inline UnderlineHyperlinks
underline_hyperlinks(PyObject *x) {
const char *in = PyUnicode_AsUTF8(x);
switch(in[0]) {
@@ -67,7 +68,7 @@ underline_hyperlinks(PyObject *x) {
}
}
static BackgroundImageLayout
static inline BackgroundImageLayout
bglayout(PyObject *layout_name) {
const char *name = PyUnicode_AsUTF8(layout_name);
switch(name[0]) {
@@ -82,7 +83,7 @@ bglayout(PyObject *layout_name) {
return TILING;
}
static ImageAnchorPosition
static inline ImageAnchorPosition
bganchor(PyObject *anchor_name) {
const char *name = PyUnicode_AsUTF8(anchor_name);
ImageAnchorPosition anchor = {0.5f, 0.5f, 0.5f, 0.5f};
@@ -108,16 +109,16 @@ bganchor(PyObject *anchor_name) {
if (opts->name) memcpy(opts->name, s, sz); \
}
static void
static inline void
background_image(PyObject *src, Options *opts) { STR_SETTER(background_image); }
static void
static inline void
bell_path(PyObject *src, Options *opts) { STR_SETTER(bell_path); }
static void
static inline void
bell_theme(PyObject *src, Options *opts) { STR_SETTER(bell_theme); }
static void
static inline void
window_logo_path(PyObject *src, Options *opts) { STR_SETTER(default_window_logo); }
#undef STR_SETTER
@@ -132,7 +133,7 @@ parse_font_mod_size(PyObject *val, float *sz, AdjustmentUnit *unit) {
}
}
static void
static inline void
modify_font(PyObject *mf, Options *opts) {
#define S(which) { PyObject *v = PyDict_GetItemString(mf, #which); if (v) parse_font_mod_size(v, &opts->which.val, &opts->which.unit); }
S(underline_position); S(underline_thickness); S(strikethrough_thickness); S(strikethrough_position);
@@ -140,7 +141,45 @@ modify_font(PyObject *mf, Options *opts) {
#undef S
}
static MouseShape
static inline void
free_font_features(Options *opts) {
if (opts->font_features.entries) {
for (size_t i = 0; i < opts->font_features.num; i++) {
free((void*)opts->font_features.entries[i].psname);
free((void*)opts->font_features.entries[i].features);
}
free(opts->font_features.entries);
}
memset(&opts->font_features, 0, sizeof(opts->font_features));
}
static inline void
font_features(PyObject *mf, Options *opts) {
free_font_features(opts);
opts->font_features.num = PyDict_GET_SIZE(mf);
if (!opts->font_features.num) return;
opts->font_features.entries = calloc(opts->font_features.num, sizeof(opts->font_features.entries[0]));
if (!opts->font_features.entries) { PyErr_NoMemory(); return; }
PyObject *key, *value;
Py_ssize_t pos = 0, i = 0;
while (PyDict_Next(mf, &pos, &key, &value)) {
__typeof__(opts->font_features.entries) e = opts->font_features.entries + i++;
Py_ssize_t psname_sz; const char *psname = PyUnicode_AsUTF8AndSize(key, &psname_sz);
e->psname = strndup(psname, psname_sz);
if (!e->psname) { PyErr_NoMemory(); return; }
e->num = PyTuple_GET_SIZE(value);
if (e->num) {
e->features = calloc(e->num, sizeof(e->features[0]));
if (!e->features) { PyErr_NoMemory(); return; }
for (size_t n = 0; n < e->num; n++) {
ParsedFontFeature *f = (ParsedFontFeature*)PyTuple_GET_ITEM(value, n);
e->features[n] = f->feature;
}
}
}
}
static inline MouseShape
pointer_shape(PyObject *shape_name) {
const char *name = PyUnicode_AsUTF8(shape_name);
if (!name) return TEXT_POINTER;
@@ -181,7 +220,7 @@ pointer_shape(PyObject *shape_name) {
return TEXT_POINTER;
}
static int
static inline int
macos_colorspace(PyObject *csname) {
if (PyUnicode_CompareWithASCIIString(csname, "srgb") == 0) return 1;
if (PyUnicode_CompareWithASCIIString(csname, "displayp3") == 0) return 2;
@@ -198,7 +237,7 @@ free_url_prefixes(Options *opts) {
}
}
static void
static inline void
url_prefixes(PyObject *up, Options *opts) {
if (!PyTuple_Check(up)) { PyErr_SetString(PyExc_TypeError, "url_prefixes must be a tuple"); return; }
free_url_prefixes(opts);
@@ -233,7 +272,7 @@ free_menu_map(Options *opts) {
opts->global_menu.count = 0;
}
static void
static inline void
menu_map(PyObject *entry_dict, Options *opts) {
if (!PyDict_Check(entry_dict)) { PyErr_SetString(PyExc_TypeError, "menu_map entries must be a dict"); return; }
free_menu_map(opts);
@@ -261,7 +300,7 @@ menu_map(PyObject *entry_dict, Options *opts) {
}
}
static void
static inline void
text_composition_strategy(PyObject *val, Options *opts) {
if (!PyUnicode_Check(val)) { PyErr_SetString(PyExc_TypeError, "text_rendering_strategy must be a string"); return; }
opts->text_old_gamma = false;
@@ -305,30 +344,30 @@ list_of_chars(PyObject *chars) {
return ans;
}
static void
static inline void
url_excluded_characters(PyObject *chars, Options *opts) {
free(opts->url_excluded_characters);
opts->url_excluded_characters = list_of_chars(chars);
}
static void
static inline void
select_by_word_characters(PyObject *chars, Options *opts) {
free(opts->select_by_word_characters);
opts->select_by_word_characters = list_of_chars(chars);
}
static void
static inline void
select_by_word_characters_forward(PyObject *chars, Options *opts) {
free(opts->select_by_word_characters_forward);
opts->select_by_word_characters_forward = list_of_chars(chars);
}
static void
static inline void
tab_bar_style(PyObject *val, Options *opts) {
opts->tab_bar_hidden = PyUnicode_CompareWithASCIIString(val, "hidden") == 0 ? true: false;
}
static void
static inline void
tab_bar_margin_height(PyObject *val, Options *opts) {
if (!PyTuple_Check(val) || PyTuple_GET_SIZE(val) != 2) {
PyErr_SetString(PyExc_TypeError, "tab_bar_margin_height is not a 2-item tuple");
@@ -344,16 +383,17 @@ window_logo_scale(PyObject *src, Options *opts) {
opts->window_logo_scale.height = PyFloat_AsFloat(PyTuple_GET_ITEM(src, 1));
}
static void
static inline void
resize_debounce_time(PyObject *src, Options *opts) {
opts->resize_debounce_time.on_end = s_double_to_monotonic_t(PyFloat_AsDouble(PyTuple_GET_ITEM(src, 0)));
opts->resize_debounce_time.on_pause = s_double_to_monotonic_t(PyFloat_AsDouble(PyTuple_GET_ITEM(src, 1)));
}
static void
static inline void
free_allocs_in_options(Options *opts) {
free_menu_map(opts);
free_url_prefixes(opts);
free_font_features(opts);
#define F(x) free(opts->x); opts->x = NULL;
F(select_by_word_characters); F(url_excluded_characters); F(select_by_word_characters_forward);
F(background_image); F(bell_path); F(bell_theme); F(default_window_logo);

11
kitty/options/types.py generated
View File

@@ -7,6 +7,7 @@ from kitty.constants import is_macos
import kitty.constants
from kitty.fast_data_types import Color, SingleKey
import kitty.fast_data_types
from kitty.fonts import FontSpec
import kitty.fonts
from kitty.options.utils import (
AliasMap, KeyDefinition, KeyboardModeMap, MouseMap, MouseMapping, NotifyOnCmdFinish,
@@ -487,8 +488,8 @@ class Options:
bell_border_color: Color = Color(255, 90, 0)
bell_on_tab: str = '🔔 '
bell_path: typing.Optional[str] = None
bold_font: str = 'auto'
bold_italic_font: str = 'auto'
bold_font: FontSpec = FontSpec(family=None, style=None, postscript_name=None, full_name=None, system='auto', axes=(), variable_name=None, features=(), created_from_string='auto')
bold_italic_font: FontSpec = FontSpec(family=None, style=None, postscript_name=None, full_name=None, system='auto', axes=(), variable_name=None, features=(), created_from_string='auto')
box_drawing_scale: typing.Tuple[float, float, float, float] = (0.001, 1.0, 1.5, 2.0)
clear_all_mouse_actions: bool = False
clear_all_shortcuts: bool = False
@@ -519,7 +520,7 @@ class Options:
enabled_layouts: typing.List[str] = ['fat', 'grid', 'horizontal', 'splits', 'stack', 'tall', 'vertical']
file_transfer_confirmation_bypass: str = ''
focus_follows_mouse: bool = False
font_family: str = 'monospace'
font_family: FontSpec = FontSpec(family=None, style=None, postscript_name=None, full_name=None, system='monospace', axes=(), variable_name=None, features=(), created_from_string='monospace')
font_size: float = 11.0
force_ltr: bool = False
foreground: Color = Color(221, 221, 221)
@@ -533,7 +534,7 @@ class Options:
initial_window_height: typing.Tuple[int, str] = (400, 'px')
initial_window_width: typing.Tuple[int, str] = (640, 'px')
input_delay: int = 3
italic_font: str = 'auto'
italic_font: FontSpec = FontSpec(family=None, style=None, postscript_name=None, full_name=None, system='auto', axes=(), variable_name=None, features=(), created_from_string='auto')
kitty_mod: int = 5
linux_bell_theme: str = '__custom'
linux_display_server: choices_for_linux_display_server = 'auto'
@@ -630,7 +631,7 @@ class Options:
action_alias: typing.Dict[str, str] = {}
env: typing.Dict[str, str] = {}
exe_search_path: typing.Dict[str, str] = {}
font_features: typing.Dict[str, typing.Tuple[kitty.fonts.FontFeature, ...]] = {}
font_features: typing.Dict[str, typing.Tuple[kitty.fast_data_types.ParsedFontFeature, ...]] = {}
kitten_alias: typing.Dict[str, str] = {}
menu_map: typing.Dict[typing.Tuple[str, ...], str] = {}
modify_font: typing.Dict[str, kitty.fonts.FontModification] = {}

View File

@@ -45,7 +45,7 @@ from kitty.conf.utils import (
)
from kitty.constants import is_macos
from kitty.fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE, NO_CURSOR_SHAPE, Color, Shlex, SingleKey
from kitty.fonts import FontFeature, FontModification, ModificationType, ModificationUnit, ModificationValue
from kitty.fonts import FontModification, FontSpec, ModificationType, ModificationUnit, ModificationValue
from kitty.key_names import character_key_name_aliases, functional_key_name_aliases, get_key_name_lookup
from kitty.rgb import color_as_int
from kitty.types import FloatEdges, MouseEvent
@@ -879,7 +879,7 @@ def clear_all_shortcuts(val: str, dict_with_parse_results: Optional[Dict[str, An
return ans
def font_features(val: str) -> Iterable[Tuple[str, Tuple[FontFeature, ...]]]:
def font_features(val: str) -> Iterable[Tuple[str, Tuple[defines.ParsedFontFeature, ...]]]:
if val == 'none':
return
parts = val.split()
@@ -890,11 +890,9 @@ def font_features(val: str) -> Iterable[Tuple[str, Tuple[FontFeature, ...]]]:
features = []
for feat in parts[1:]:
try:
parsed = defines.parse_font_feature(feat)
features.append(defines.ParsedFontFeature(feat))
except ValueError:
log_error(f'Ignoring invalid font feature: {feat}')
else:
features.append(FontFeature(feat, parsed))
yield parts[0], tuple(features)
@@ -1390,6 +1388,10 @@ def parse_mouse_map(val: str) -> Iterable[MouseMapping]:
yield MouseMapping(button, mods, count, mode == 'grabbed', definition=action)
def parse_font_spec(spec: str) -> FontSpec:
return FontSpec.from_setting(spec)
def deprecated_hide_window_decorations_aliases(key: str, val: str, ans: Dict[str, Any]) -> None:
if not hasattr(deprecated_hide_window_decorations_aliases, key):
setattr(deprecated_hide_window_decorations_aliases, key, True)

View File

@@ -9,6 +9,10 @@
#include "screen.h"
#include "monotonic.h"
#include "window_logo.h"
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpedantic"
#include <hb.h>
#pragma GCC diagnostic pop
#define OPT(name) global_state.opts.name
#define debug_rendering(...) if (global_state.debug_rendering) { timed_debug_print(__VA_ARGS__); }
@@ -106,6 +110,14 @@ typedef struct {
unsigned long wayland_titlebar_color;
struct { struct MenuItem *entries; size_t count; } global_menu;
bool wayland_enable_ime;
struct {
size_t num;
struct {
const char *psname;
size_t num;
hb_feature_t *features;
} *entries;
} font_features;
} Options;
typedef struct WindowLogoRenderData {

View File

@@ -517,7 +517,7 @@ def key_as_bytes(name: str) -> bytes:
return ans.encode('ascii')
def get_capabilities(query_string: str, opts: 'Options') -> Generator[str, None, None]:
def get_capabilities(query_string: str, opts: 'Options', window_id: int = 0, os_window_id: int = 0) -> Generator[str, None, None]:
from .fast_data_types import ERROR_PREFIX
def result(encoded_query_name: str, x: Optional[str] = None) -> str:
@@ -533,7 +533,7 @@ def get_capabilities(query_string: str, opts: 'Options') -> Generator[str, None,
elif name.startswith('kitty-query-'):
from kittens.query_terminal.main import get_result
name = name[len('kitty-query-'):]
rval = get_result(name)
rval = get_result(name, window_id, os_window_id)
if rval is None:
from .utils import log_error
log_error('Unknown kitty terminfo query:', name)

View File

@@ -21,3 +21,4 @@ PowerlineStyle = str
MatchType = str
Protocol = object
OptionsProtocol = object
NotRequired = Tuple

View File

@@ -4,6 +4,7 @@ from socket import socket as Socket
from subprocess import CompletedProcess as CompletedProcess
from subprocess import Popen as PopenType
from typing import Literal
from typing import NotRequired as NotRequired
from typing import Protocol as Protocol
from typing import TypedDict as TypedDict
@@ -61,7 +62,7 @@ __all__ = (
'EdgeLiteral', 'MatchType', 'GRT_a', 'GRT_f', 'GRT_t', 'GRT_o', 'GRT_m', 'GRT_d',
'GraphicsCommandType', 'HandlerType', 'AbstractEventLoop', 'AddressFamily', 'Socket', 'CompletedProcess',
'PopenType', 'Protocol', 'TypedDict', 'MarkType', 'ImageManagerType', 'Debug', 'LoopType', 'MouseEvent',
'TermManagerType', 'BossType', 'ChildType', 'BadLineType', 'MouseButton',
'TermManagerType', 'BossType', 'ChildType', 'BadLineType', 'MouseButton', 'NotRequired',
'KeyActionType', 'KeyMap', 'KittyCommonOpts', 'AliasMap', 'CoreTextFont', 'WindowSystemMouseEvent',
'FontConfigPattern', 'ScreenType', 'StartupCtx', 'KeyEventType', 'LayoutType', 'PowerlineStyle',
'RemoteCommandType', 'SessionType', 'SessionTab', 'SpecialWindowInstance', 'TabType', 'ScreenSize', 'WindowType'

View File

@@ -1273,7 +1273,7 @@ class Window:
self.refresh()
def request_capabilities(self, q: str) -> None:
for result in get_capabilities(q, get_options()):
for result in get_capabilities(q, get_options(), self.id, self.os_window_id):
self.screen.send_escape_code_to_child(ESC_DCS, result)
def handle_remote_cmd(self, cmd: memoryview) -> None:

View File

@@ -8,13 +8,158 @@ import unittest
from functools import partial
from kitty.constants import is_macos, read_kitty_resource
from kitty.fast_data_types import DECAWM, get_fallback_font, sprite_map_set_layout, sprite_map_set_limits, test_render_line, test_sprite_position_for, wcwidth
from kitty.fast_data_types import (
DECAWM,
ParsedFontFeature,
get_fallback_font,
sprite_map_set_layout,
sprite_map_set_limits,
test_render_line,
test_sprite_position_for,
wcwidth,
)
from kitty.fonts import family_name_to_key
from kitty.fonts.box_drawing import box_chars
from kitty.fonts.common import FontSpec, all_fonts_map, face_from_descriptor, get_font_files, get_named_style, spec_for_face
from kitty.fonts.render import coalesce_symbol_maps, render_string, setup_for_testing, shape_string
from kitty.options.types import Options
from . import BaseTest
def parse_font_spec(spec):
return FontSpec.from_setting(spec)
class Selection(BaseTest):
def test_font_selection(self):
self.set_options({'font_features': {'LiberationMono': (ParsedFontFeature('-dlig'),)}})
opts = Options()
fonts_map = all_fonts_map(True)
names = set(fonts_map['family_map']) | set(fonts_map['variable_map'])
del fonts_map
def s(family: str, *expected: str, alternate=None) -> None:
opts.font_family = parse_font_spec(family)
ff = get_font_files(opts)
actual = tuple(face_from_descriptor(ff[x]).postscript_name() for x in ('medium', 'bold', 'italic', 'bi')) # type: ignore
del ff
for x in actual:
if '/' in x: # Old FreeType failed to generate postscript name for a variable font probably
return
with self.subTest(spec=family):
try:
self.ae(expected, actual)
except AssertionError:
if alternate:
self.ae(tuple(map(alternate, expected)), actual)
else:
raise
def both(family: str, *expected: str, alternate=None) -> None:
for family in (family, f'family="{family}"'):
s(family, *expected, alternate=alternate)
def has(family, allow_missing_in_ci=False):
ans = family_name_to_key(family) in names
if self.is_ci and not allow_missing_in_ci and not ans:
raise AssertionError(f'The family: {family} is not available')
return ans
def t(family, psprefix, bold='Bold', italic='Italic', bi='', reg='Regular', allow_missing_in_ci=False, alternate=None):
if has(family, allow_missing_in_ci=allow_missing_in_ci):
bi = bi or bold + italic
if reg:
reg = '-' + reg
both(family, f'{psprefix}{reg}', f'{psprefix}-{bold}', f'{psprefix}-{italic}', f'{psprefix}-{bi}', alternate=alternate)
t('Source Code Pro', 'SourceCodePro', 'Semibold', 'It')
t('sourcecodeVf', 'SourceCodeVF', 'Semibold')
t('fira code', 'FiraCodeRoman', 'SemiBold', 'Regular', 'SemiBold')
t('hack', 'Hack')
t('DejaVu Sans Mono', 'DejaVuSansMono', reg='', italic='Oblique')
t('ubuntu mono', 'UbuntuMono')
t('liberation mono', 'LiberationMono', reg='')
t('ibm plex mono', 'IBMPlexMono', 'SmBld', reg='')
t('iosevka fixed', 'Iosevka-Fixed', 'Semibold', reg='', bi='Semibold-Italic', allow_missing_in_ci=True)
t('iosevka term', 'Iosevka-Term', 'Semibold', reg='', bi='Semibold-Italic', allow_missing_in_ci=True)
t('fantasque sans mono', 'FantasqueSansMono')
t('jetbrains mono', 'JetBrainsMono', 'SemiBold')
t('consolas', 'Consolas', reg='', allow_missing_in_ci=True)
if has('cascadia code'):
if is_macos:
both('cascadia code', 'CascadiaCode-Regular', 'CascadiaCode-Regular_SemiBold', 'CascadiaCode-Italic', 'CascadiaCode-Italic_SemiBold-Italic')
else:
both('cascadia code', 'CascadiaCodeRoman-Regular', 'CascadiaCodeRoman-SemiBold', 'CascadiaCode-Italic', 'CascadiaCode-SemiBoldItalic')
if has('cascadia mono'):
if is_macos:
both('cascadia mono', 'CascadiaMono-Regular', 'CascadiaMono-Regular_SemiBold', 'CascadiaMono-Italic', 'CascadiaMono-Italic_SemiBold-Italic')
else:
both('cascadia mono', 'CascadiaMonoRoman-Regular', 'CascadiaMonoRoman-SemiBold', 'CascadiaMono-Italic', 'CascadiaMono-SemiBoldItalic')
if has('operator mono', allow_missing_in_ci=True):
both('operator mono', 'OperatorMono-Medium', 'OperatorMono-Bold', 'OperatorMono-MediumItalic', 'OperatorMono-BoldItalic')
# Test variable font selection
if has('SourceCodeVF'):
opts = Options()
opts.font_family = parse_font_spec('family="SourceCodeVF" variable_name="SourceCodeUpright" style="Bold"')
ff = get_font_files(opts)
face = face_from_descriptor(ff['medium'])
self.ae(get_named_style(face)['name'], 'Bold')
face = face_from_descriptor(ff['italic'])
self.ae(get_named_style(face)['name'], 'Bold Italic')
face = face_from_descriptor(ff['bold'])
self.ae(get_named_style(face)['name'], 'Black')
face = face_from_descriptor(ff['bi'])
self.ae(get_named_style(face)['name'], 'Black Italic')
opts.font_family = parse_font_spec('family=SourceCodeVF variable_name=SourceCodeUpright wght=470')
opts.italic_font = parse_font_spec('family=SourceCodeVF variable_name=SourceCodeItalic style=Black')
ff = get_font_files(opts)
self.assertFalse(get_named_style(ff['medium']))
self.ae(get_named_style(ff['italic'])['name'], 'Black Italic')
if has('cascadia code'):
opts = Options()
opts.font_family = parse_font_spec('family="cascadia code"')
opts.italic_font = parse_font_spec('family="cascadia code" variable_name= style="Light Italic"')
ff = get_font_files(opts)
def t(x, **kw):
if 'spec' in kw:
fs = FontSpec.from_setting('family="Cascadia Code" ' + kw['spec'])._replace(created_from_string='')
else:
kw['family'] = 'Cascadia Code'
fs = FontSpec(**kw)
face = face_from_descriptor(ff[x])
self.ae(fs.as_setting, spec_for_face('Cascadia Code', face).as_setting)
t('medium', variable_name='CascadiaCodeRoman', style='Regular')
t('italic', variable_name='', style='Light Italic')
opts = Options()
opts.font_family = parse_font_spec('family="cascadia code" variable_name=CascadiaCodeRoman wght=455')
opts.italic_font = parse_font_spec('family="cascadia code" variable_name= wght=405')
opts.bold_font = parse_font_spec('family="cascadia code" variable_name=CascadiaCodeRoman wght=603')
ff = get_font_files(opts)
t('medium', spec='variable_name=CascadiaCodeRoman wght=455')
t('italic', spec='variable_name= wght=405')
t('bold', spec='variable_name=CascadiaCodeRoman wght=603')
t('bi', spec='variable_name= wght=603')
# Test font features
if has('liberation mono'):
opts = Options()
opts.font_family = parse_font_spec('family="liberation mono"')
ff = get_font_files(opts)
self.ae(face_from_descriptor(ff['medium']).applied_features(), {'dlig': '-dlig'})
self.ae(face_from_descriptor(ff['bold']).applied_features(), {})
opts.font_family = parse_font_spec('family="liberation mono" features="dlig test=3"')
ff = get_font_files(opts)
self.ae(face_from_descriptor(ff['medium']).applied_features(), {'dlig': 'dlig', 'test': 'test=3'})
self.ae(face_from_descriptor(ff['bold']).applied_features(), {'dlig': 'dlig', 'test': 'test=3'})
class Rendering(BaseTest):
def setUp(self):

View File

@@ -308,9 +308,14 @@ def env_for_python_tests(report_env: bool = False) -> Iterator[None]:
print('Python:', python_for_type_check())
from kitty.fast_data_types import has_avx2, has_sse4_2
print(f'Intrinsics: {has_avx2=} {has_sse4_2=}')
# we need fonts installed in the user home directory as well, so initialize
# fontconfig before nuking $HOME and friends
from kitty.fonts.common import all_fonts_map
all_fonts_map(True)
with TemporaryDirectory() as tdir, env_vars(
HOME=tdir,
KT_ORIGINAL_HOME=os.path.expanduser('~'),
USERPROFILE=tdir,
PATH=path,
TERM='xterm-kitty',

View File

@@ -84,7 +84,7 @@ def run_build(args: Any) -> None:
time.sleep(25)
call(cmd, echo=True)
for x in ('64', '32', 'arm64'):
for x in ('64', 'arm64'):
prefix = f'python ../bypy linux --arch {x} '
run_with_retry(prefix + f'program --non-interactive --extra-program-data "{vcs_rev}"')
call(prefix + 'shutdown', echo=True)

View File

@@ -1996,7 +1996,7 @@ def build_dep() -> None:
p.add_argument(
'--platform',
default=Options.platform,
choices='all macos linux linux-32 linux-arm64 linux-64'.split(),
choices='all macos linux linux-arm64 linux-64'.split(),
help='Platforms to build the dep for'
)
p.add_argument(
@@ -2008,7 +2008,6 @@ def build_dep() -> None:
args = p.parse_args(sys.argv[2:], namespace=Options())
linux_platforms = [
['linux', '--arch=64'],
['linux', '--arch=32'],
['linux', '--arch=arm64'],
]
if args.platform == 'all':

View File

@@ -24,7 +24,7 @@ exec_kitty() {
is_wrapped_kitten() {
wrapped_kittens="clipboard icat hyperlinked_grep ask hints unicode_input ssh themes diff show_key transfer"
wrapped_kittens="clipboard icat hyperlinked_grep ask hints unicode_input ssh themes diff show_key transfer query_terminal"
[ -n "$1" ] && {
case " $wrapped_kittens " in
*" $1 "*) printf "%s" "$1" ;;

View File

@@ -8,8 +8,6 @@ import (
"kitty/tools/cli/markup"
"kitty/tools/utils"
"golang.org/x/exp/maps"
)
var _ = fmt.Print
@@ -23,7 +21,7 @@ func fish_completion_script(commands []string) (string, error) {
"kitten": true,
}
if len(commands) == 0 {
commands = append(commands, maps.Keys(all_commands)...)
commands = append(commands, utils.Keys(all_commands)...)
}
script := strings.Builder{}
script.WriteString(`function __ksi_completions

View File

@@ -7,10 +7,10 @@ import (
"io"
"os"
"os/exec"
"slices"
"strings"
"time"
"golang.org/x/exp/slices"
"golang.org/x/sys/unix"
"kitty"

View File

@@ -4,10 +4,9 @@ package cli
import (
"fmt"
"slices"
"strconv"
"strings"
"golang.org/x/exp/slices"
)
var _ = fmt.Print

View File

@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"math/rand/v2"
"slices"
"strings"
"time"
@@ -16,7 +17,6 @@ import (
"kitty/tools/tui/loop"
"kitty/tools/utils"
"golang.org/x/exp/slices"
"golang.org/x/sys/unix"
)

View File

@@ -6,11 +6,13 @@ import (
"fmt"
"kitty/kittens/ask"
"kitty/kittens/choose_fonts"
"kitty/kittens/clipboard"
"kitty/kittens/diff"
"kitty/kittens/hints"
"kitty/kittens/hyperlinked_grep"
"kitty/kittens/icat"
"kitty/kittens/query_terminal"
"kitty/kittens/show_key"
"kitty/kittens/ssh"
"kitty/kittens/themes"
@@ -76,6 +78,10 @@ func KittyToolEntryPoints(root *cli.Command) {
run_shell.EntryPoint(root)
// show_error
show_error.EntryPoint(root)
// choose-fonts
choose_fonts.EntryPoint(root)
// query-terminal
query_terminal.EntryPoint(root)
// __pytest__
pytest.EntryPoint(root)
// __hold_till_enter__

View File

@@ -12,10 +12,14 @@ import (
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"kitty/tools/utils"
"github.com/shirou/gopsutil/v3/process"
"golang.org/x/sys/unix"
)
var _ = fmt.Print
@@ -274,3 +278,94 @@ func (self *ConfigParser) ParseOverrides(overrides ...string) error {
self.seen_includes = make(map[string]bool)
return self.parse(&s, "<overrides>", utils.ConfigDir(), 0)
}
func is_kitty_gui_cmdline(cmd ...string) bool {
if len(cmd) == 0 {
return false
}
if filepath.Base(cmd[0]) != "kitty" {
return false
}
if len(cmd) == 1 {
return true
}
s := cmd[1][:1]
switch s {
case `@`:
return false
case `+`:
if cmd[1] == `+` {
return len(cmd) > 2 && cmd[2] == `open`
}
return cmd[1] == `+open`
}
return true
}
type Patcher struct {
Write_backup bool
Mode fs.FileMode
}
func (self Patcher) Patch(path, sentinel, content string, settings_to_comment_out ...string) (updated bool, err error) {
if self.Mode == 0 {
self.Mode = 0o644
}
backup_path := path
if q, err := filepath.EvalSymlinks(path); err == nil {
path = q
}
raw, err := os.ReadFile(path)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return false, err
}
if raw == nil {
raw = []byte{}
}
pat := utils.MustCompile(fmt.Sprintf(`(?m)^\s*(%s)\b`, strings.Join(settings_to_comment_out, "|")))
text := pat.ReplaceAllString(utils.UnsafeBytesToString(raw), `# $1`)
pat = utils.MustCompile(fmt.Sprintf(`(?ms)^# BEGIN_%s.+?# END_%s`, sentinel, sentinel))
replaced := false
addition := fmt.Sprintf("# BEGIN_%s\n%s\n# END_%s", sentinel, content, sentinel)
ntext := pat.ReplaceAllStringFunc(text, func(string) string {
replaced = true
return addition
})
if !replaced {
if text != "" {
text += "\n\n"
}
ntext = text + addition
}
nraw := utils.UnsafeStringToBytes(ntext)
if !bytes.Equal(raw, nraw) {
if len(raw) > 0 && self.Write_backup {
_ = os.WriteFile(backup_path+".bak", raw, self.Mode)
}
return true, utils.AtomicUpdateFile(path, nraw, self.Mode)
}
return false, nil
}
func ReloadConfigInKitty(in_parent_only bool) error {
if in_parent_only {
if pid, err := strconv.Atoi(os.Getenv("KITTY_PID")); err == nil {
if p, err := process.NewProcess(int32(pid)); err == nil {
if c, err := p.CmdlineSlice(); err == nil && is_kitty_gui_cmdline(c...) {
return p.SendSignal(unix.SIGUSR1)
}
}
}
return nil
}
if all, err := process.Processes(); err == nil {
for _, p := range all {
if c, err := p.CmdlineSlice(); err == nil && is_kitty_gui_cmdline(c...) {
_ = p.SendSignal(unix.SIGUSR1)
}
}
}
return nil
}

View File

@@ -7,12 +7,10 @@ import (
"kitty/tools/tui/loop"
"kitty/tools/utils"
"regexp"
"slices"
"strconv"
"strings"
"sync"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
var _ = fmt.Print
@@ -309,5 +307,5 @@ func ResolveShortcuts(actions []*KeyAction) []*KeyAction {
action_map[key] = ac
}
}
return maps.Values(action_map)
return utils.Values(action_map)
}

View File

@@ -15,10 +15,10 @@ import (
"fmt"
"hash"
"io"
"slices"
"strconv"
"github.com/zeebo/xxh3"
"golang.org/x/exp/slices"
)
// If no BlockSize is specified in the rsync instance, this value is used.

View File

@@ -7,12 +7,12 @@ import (
"encoding/hex"
"fmt"
"io"
"slices"
"strconv"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"golang.org/x/exp/slices"
"kitty/tools/utils"
)

View File

@@ -10,11 +10,13 @@ import (
"fmt"
"io"
"io/fs"
"maps"
"net/http"
"os"
"path"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
"sync"
@@ -26,11 +28,6 @@ import (
"kitty/tools/tui/subseq"
"kitty/tools/utils"
"kitty/tools/utils/style"
"github.com/shirou/gopsutil/v3/process"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"golang.org/x/sys/unix"
)
var _ = fmt.Print
@@ -580,76 +577,6 @@ func (self *Theme) Code() (string, error) {
return self.load_code()
}
func patch_conf(text, theme_name string) string {
addition := fmt.Sprintf("# BEGIN_KITTY_THEME\n# %s\ninclude current-theme.conf\n# END_KITTY_THEME", theme_name)
pat := utils.MustCompile(`(?ms)^# BEGIN_KITTY_THEME.+?# END_KITTY_THEME`)
replaced := false
ntext := pat.ReplaceAllStringFunc(text, func(string) string {
replaced = true
return addition
})
if !replaced {
if text != "" {
text += "\n\n"
}
ntext = text + addition
}
pat = utils.MustCompile(fmt.Sprintf(`(?m)^\s*(%s)\b`, strings.Join(maps.Keys(AllColorSettingNames), "|")))
return pat.ReplaceAllString(ntext, `# $1`)
}
func is_kitty_gui_cmdline(cmd ...string) bool {
if len(cmd) == 0 {
return false
}
if filepath.Base(cmd[0]) != "kitty" {
return false
}
if len(cmd) == 1 {
return true
}
s := cmd[1][:1]
switch s {
case `@`:
return false
case `+`:
if cmd[1] == `+` {
return len(cmd) > 2 && cmd[2] == `open`
}
return cmd[1] == `+open`
}
return true
}
type ReloadDestination string
const (
RELOAD_IN_PARENT ReloadDestination = "parent"
RELOAD_IN_ALL ReloadDestination = "all"
)
func reload_config(reload_in ReloadDestination) bool {
switch reload_in {
case RELOAD_IN_PARENT:
if pid, err := strconv.Atoi(os.Getenv("KITTY_PID")); err == nil {
if p, err := process.NewProcess(int32(pid)); err == nil {
if c, err := p.CmdlineSlice(); err == nil && is_kitty_gui_cmdline(c...) {
return p.SendSignal(unix.SIGUSR1) == nil
}
}
}
case RELOAD_IN_ALL:
if all, err := process.Processes(); err == nil {
for _, p := range all {
if c, err := p.CmdlineSlice(); err == nil && is_kitty_gui_cmdline(c...) {
_ = p.SendSignal(unix.SIGUSR1)
}
}
return true
}
}
return false
}
func (self *Theme) SaveInDir(dirpath string) (err error) {
path := filepath.Join(dirpath, self.Name()+".conf")
code, err := self.Code()
@@ -674,22 +601,18 @@ func (self *Theme) SaveInConf(config_dir, reload_in, config_file_name string) (e
if !filepath.IsAbs(config_file_name) {
confpath = filepath.Join(config_dir, config_file_name)
}
if q, err := filepath.EvalSymlinks(confpath); err == nil {
confpath = q
patcher := config.Patcher{Write_backup: true}
if _, err = patcher.Patch(
confpath, "KITTY_THEME", fmt.Sprintf("# %s\ninclude current-theme.conf", self.metadata.Name),
utils.Keys(AllColorSettingNames)...); err != nil {
return
}
raw, err := os.ReadFile(confpath)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
switch reload_in {
case "parent":
config.ReloadConfigInKitty(true)
case "all":
config.ReloadConfigInKitty(false)
}
nraw := patch_conf(utils.UnsafeBytesToString(raw), self.metadata.Name)
if len(raw) > 0 {
_ = os.WriteFile(confpath+".bak", raw, 0o600)
}
err = utils.AtomicUpdateFile(confpath, utils.UnsafeStringToBytes(nraw), 0o600)
if err != nil {
return err
}
reload_config(ReloadDestination(reload_in))
return
}
@@ -809,12 +732,12 @@ func (self *Themes) At(x int) *Theme {
func (self *Themes) Names() []string { return self.index_map }
func (self *Themes) create_index_map() {
self.index_map = maps.Keys(self.name_map)
self.index_map = utils.Keys(self.name_map)
self.index_map = utils.StableSortWithKey(self.index_map, strings.ToLower)
}
func (self *Themes) Filtered(is_ok func(*Theme) bool) *Themes {
themes := utils.Filter(maps.Values(self.name_map), is_ok)
themes := utils.Filter(utils.Values(self.name_map), is_ok)
ans := Themes{name_map: make(map[string]*Theme, len(themes))}
for _, theme := range themes {
ans.name_map[theme.metadata.Name] = theme

View File

@@ -15,8 +15,6 @@ import (
"kitty/tools/utils"
"kitty/tools/utils/images"
"kitty/tools/utils/shm"
"golang.org/x/exp/maps"
)
var _ = fmt.Print
@@ -137,7 +135,7 @@ func (self *ImageCollection) ResizeForPageSize(width, height int) {
defer self.mutex.Unlock()
ctx := images.Context{}
keys := maps.Keys(self.images)
keys := utils.Keys(self.images)
ctx.Parallel(0, len(keys), func(nums <-chan int) {
for i := range nums {
img := self.images[keys[i]]
@@ -295,7 +293,7 @@ func (self *ImageCollection) LoadAll() {
self.mutex.Lock()
defer self.mutex.Unlock()
ctx := images.Context{}
all := maps.Values(self.images)
all := utils.Values(self.images)
ctx.Parallel(0, len(self.images), func(nums <-chan int) {
for i := range nums {
img := all[i]

View File

@@ -4,6 +4,7 @@ package loop
import (
"encoding/base64"
"encoding/hex"
"fmt"
"os"
"runtime"
@@ -89,6 +90,9 @@ type Loop struct {
// Called when a response to an rc command is received
OnRCResponse func(data []byte) error
// Called when a response to a query command is received
OnQueryResponse func(key, val string, valid bool) error
// Called when any input from tty is received
OnReceivedData func(data []byte) error
@@ -217,6 +221,24 @@ func (self *Loop) Println(args ...any) {
self.QueueWriteString("\r")
}
func (self *Loop) style_region(style string, start_x, start_y, end_x, end_y int) string {
sgr := self.SprintStyled(style, "|")[2:]
sgr = sgr[:strings.IndexByte(sgr, 'm')]
return fmt.Sprintf("\x1b[%d;%d;%d;%d;%s$r", start_y+1, start_x+1, end_y+1, end_x+1, sgr)
}
// Apply the specified style to the specified region of the screen (0-based
// indexing). The region is all cells from the start cell to the end cell. See
// StyleRectangle to apply style to a rectangular area.
func (self *Loop) StyleRegion(style string, start_x, start_y, end_x, end_y int) IdType {
return self.QueueWriteString(self.style_region(style, start_x, start_y, end_x, end_y))
}
// Apply the specified style to the specified rectangle of the screen (0-based indexing).
func (self *Loop) StyleRectangle(style string, start_x, start_y, end_x, end_y int) IdType {
return self.QueueWriteString("\x1b[2*x" + self.style_region(style, start_x, start_y, end_x, end_y) + "\x1b[*x")
}
func (self *Loop) SprintStyled(style string, args ...any) string {
f := self.style_cache[style]
if f == nil {
@@ -426,6 +448,10 @@ func (self *Loop) ClearScreen() {
self.QueueWriteString("\x1b[H\x1b[2J")
}
func (self *Loop) ClearScreenButNotGraphics() {
self.QueueWriteString("\x1b[H\x1b[J")
}
func (self *Loop) SendOverlayReady() {
self.QueueWriteString("\x1bP@kitty-overlay-ready|\x1b\\")
}
@@ -463,6 +489,17 @@ func (self *Loop) CopyTextToClipboard(text string) {
self.copy_text_to(text, "c")
}
func (self *Loop) QueryTerminal(fields ...string) IdType {
if len(fields) == 0 {
return 0
}
q := make([]string, len(fields))
for i, x := range fields {
q[i] = hex.EncodeToString(utils.UnsafeStringToBytes("kitty-query-" + x))
}
return self.QueueWriteString(fmt.Sprintf("\x1bP+q%s\a", strings.Join(q, ";")))
}
func (self *Loop) PushPointerShape(s PointerShape) {
self.pointer_shapes = append(self.pointer_shapes, s)
self.QueueWriteString("\x1b]22;" + s.String() + "\x1b\\")
@@ -475,6 +512,8 @@ func (self *Loop) PopPointerShape() {
}
}
// Remove all pointer shapes from the shape stack resetting to default pointer
// shape. This is called automatically on loop termination.
func (self *Loop) ClearPointerShapes() (ans []PointerShape) {
ans = self.pointer_shapes
for i := len(self.pointer_shapes) - 1; i >= 0; i-- {

View File

@@ -4,6 +4,7 @@ package loop
import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"io"
@@ -172,6 +173,24 @@ func (self *Loop) handle_dcs(raw []byte) error {
if self.OnRCResponse != nil && bytes.HasPrefix(raw, utils.UnsafeStringToBytes("@kitty-cmd")) {
return self.OnRCResponse(raw[len("@kitty-cmd"):])
}
if self.OnQueryResponse != nil && (bytes.HasPrefix(raw, utils.UnsafeStringToBytes("1+r")) || bytes.HasPrefix(raw, utils.UnsafeStringToBytes("0+r"))) {
valid := raw[0] == '1'
s := utils.NewSeparatorScanner(utils.UnsafeBytesToString(raw[3:]), ";")
for s.Scan() {
key, val, _ := strings.Cut(s.Text(), "=")
if k, err := hex.DecodeString(key); err == nil {
if bytes.HasPrefix(k, utils.UnsafeStringToBytes("kitty-query-")) {
k = k[len("kitty-query-"):]
if v, err := hex.DecodeString(val); err == nil {
if err = self.OnQueryResponse(string(k), string(v), valid); err != nil {
return err
}
}
}
}
}
return nil
}
if self.OnEscapeCode != nil {
return self.OnEscapeCode(DCS, raw)
}

View File

@@ -4,10 +4,9 @@ package loop
import (
"fmt"
"slices"
"time"
"golang.org/x/exp/slices"
"kitty/tools/tty"
"kitty/tools/utils"
)

View File

@@ -4,9 +4,13 @@ package tui
import (
"fmt"
"path/filepath"
"time"
"kitty"
"kitty/tools/config"
"kitty/tools/tui/loop"
"kitty/tools/utils"
)
var _ = fmt.Print
@@ -201,3 +205,150 @@ func (ms *MouseSelection) DragScroll(ev *loop.MouseEvent, lp *loop.Loop, callbac
}
ms.drag_scroll.mouse_event = *ev
}
type CellRegion struct {
TopLeft, BottomRight struct{ X, Y int }
Id string
OnClick []func(id string) error
}
func (c CellRegion) Contains(x, y int) bool { // 0-based
if c.TopLeft.Y > y || c.BottomRight.Y < y {
return false
}
return (y > c.TopLeft.Y || (y == c.TopLeft.Y && x >= c.TopLeft.X)) && (y < c.BottomRight.Y || (y == c.BottomRight.Y && x <= c.BottomRight.X))
}
type MouseState struct {
Cell, Pixel struct{ X, Y int }
Pressed struct{ Left, Right, Middle, Fourth, Fifth, Sixth, Seventh bool }
regions []*CellRegion
region_id_map map[string][]*CellRegion
hovered_ids *utils.Set[string]
default_url_style struct {
value string
loaded bool
}
}
func (m *MouseState) AddCellRegion(id string, start_x, start_y, end_x, end_y int, on_click ...func(id string) error) *CellRegion {
cr := CellRegion{TopLeft: struct{ X, Y int }{start_x, start_y}, BottomRight: struct{ X, Y int }{end_x, end_y}, Id: id, OnClick: on_click}
m.regions = append(m.regions, &cr)
if m.region_id_map == nil {
m.region_id_map = make(map[string][]*CellRegion)
}
m.region_id_map[id] = append(m.region_id_map[id], &cr)
return &cr
}
func (m *MouseState) ClearCellRegions() {
m.regions = nil
m.region_id_map = nil
m.hovered_ids = nil
}
func (m *MouseState) UpdateHoveredIds() (changed bool) {
h := utils.NewSet[string]()
for _, r := range m.regions {
if r.Contains(m.Cell.X, m.Cell.Y) {
h.Add(r.Id)
}
}
changed = !h.Equal(m.hovered_ids)
m.hovered_ids = h
return
}
func (m *MouseState) ApplyHoverStyles(lp *loop.Loop, style ...string) {
if m.hovered_ids == nil {
return
}
hs := ""
if len(style) == 0 {
if !m.default_url_style.loaded {
m.default_url_style.loaded = true
conf := filepath.Join(utils.ConfigDir(), "kitty.conf")
color, style := kitty.DefaultUrlColor, kitty.DefaultUrlStyle
cp := config.ConfigParser{LineHandler: func(key, val string) error {
switch key {
case "url_color":
color = val
case "url_style":
style = val
}
return nil
},
}
_ = cp.ParseFiles(conf) // ignore errors and use defaults
if style != "none" && style != "" {
m.default_url_style.value = fmt.Sprintf("u=%s uc=%s", style, color)
}
}
hs = m.default_url_style.value
} else {
hs = style[0]
}
is_hovered := false
for id := range m.hovered_ids.Iterable() {
for _, r := range m.region_id_map[id] {
lp.StyleRegion(hs, r.TopLeft.X, r.TopLeft.Y, r.BottomRight.X, r.BottomRight.Y)
is_hovered = true
}
}
if is_hovered {
if s, has := lp.CurrentPointerShape(); !has || s != loop.POINTER_POINTER {
lp.PushPointerShape(loop.POINTER_POINTER)
}
} else {
lp.ClearPointerShapes()
}
}
func (m *MouseState) ClickHoveredRegions() error {
seen := utils.NewSet[string]()
for id := range m.hovered_ids.Iterable() {
for _, r := range m.region_id_map[id] {
if seen.Has(r.Id) {
continue
}
seen.Add(r.Id)
for _, f := range r.OnClick {
if err := f(r.Id); err != nil {
return err
}
}
}
}
return nil
}
func (m *MouseState) UpdateState(ev *loop.MouseEvent) (hovered_ids_changed bool) {
m.Cell = ev.Cell
m.Pixel = ev.Pixel
if ev.Event_type == loop.MOUSE_PRESS || ev.Event_type == loop.MOUSE_RELEASE {
pressed := ev.Event_type == loop.MOUSE_PRESS
if ev.Buttons&loop.LEFT_MOUSE_BUTTON != 0 {
m.Pressed.Left = pressed
}
if ev.Buttons&loop.RIGHT_MOUSE_BUTTON != 0 {
m.Pressed.Right = pressed
}
if ev.Buttons&loop.MIDDLE_MOUSE_BUTTON != 0 {
m.Pressed.Middle = pressed
}
if ev.Buttons&loop.FOURTH_MOUSE_BUTTON != 0 {
m.Pressed.Fourth = pressed
}
if ev.Buttons&loop.FIFTH_MOUSE_BUTTON != 0 {
m.Pressed.Fifth = pressed
}
if ev.Buttons&loop.SIXTH_MOUSE_BUTTON != 0 {
m.Pressed.Sixth = pressed
}
if ev.Buttons&loop.SEVENTH_MOUSE_BUTTON != 0 {
m.Pressed.Seventh = pressed
}
}
return m.UpdateHoveredIds()
}

125
tools/tui/render_lines.go Normal file
View File

@@ -0,0 +1,125 @@
package tui
import (
"fmt"
"regexp"
"strings"
"sync"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/utils/style"
"kitty/tools/wcswidth"
)
var _ = fmt.Print
var _ = utils.Repr
const KittyInternalHyperlinkProtocol = "kitty-ih"
func InternalHyperlink(text, id string) string {
return fmt.Sprintf("\x1b]8;;%s:%s\x1b\\%s\x1b]8;;\x1b\\", KittyInternalHyperlinkProtocol, id, text)
}
type RenderLines struct {
}
var hyperlink_pat = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile("\x1b]8;([^;]*);(.*?)(?:\x1b\\\\|\a)")
})
// Render lines in the specified rectangle. If width > 0 then lines are wrapped
// to fit in the width. A string containing rendered lines with escape codes to
// move cursor is returned. Any internal hyperlinks are added to the
// MouseState.
func (r RenderLines) InRectangle(
lines []string, start_x, start_y, width, height int, mouse_state *MouseState, on_click ...func(id string) error,
) (all_rendered bool, y_after_last_line int, ans string) {
end_y := start_y + height - 1
if end_y < start_y {
return len(lines) == 0, start_y + 1, ""
}
x, y := start_x, start_y
buf := strings.Builder{}
buf.Grow(len(lines) * max(1, width) * 3)
move_cursor := func(x, y int) { buf.WriteString(fmt.Sprintf(loop.MoveCursorToTemplate, y+1, x+1)) }
var hyperlink_state struct {
action string
start_x, start_y int
}
start_hyperlink := func(action string) {
hyperlink_state.action = action
hyperlink_state.start_x, hyperlink_state.start_y = x, y
}
add_chunk := func(text string) {
if text != "" {
buf.WriteString(text)
x += wcswidth.Stringwidth(text)
}
}
commit_hyperlink := func() bool {
if hyperlink_state.action == "" {
return false
}
if y == hyperlink_state.start_y && x <= hyperlink_state.start_x {
return false
}
mouse_state.AddCellRegion(hyperlink_state.action, hyperlink_state.start_x, hyperlink_state.start_y, max(0, x-1), y, on_click...)
hyperlink_state.action = ``
return true
}
add_hyperlink := func(id, url string) {
is_closer := id == "" && url == ""
if is_closer {
if !commit_hyperlink() {
buf.WriteString("\x1b]8;;\x1b\\")
}
} else {
commit_hyperlink()
if strings.HasPrefix(url, KittyInternalHyperlinkProtocol+":") {
start_hyperlink(url[len(KittyInternalHyperlinkProtocol)+1:])
} else {
buf.WriteString(fmt.Sprintf("\x1b]8;%s;%s\x1b\\", id, url))
}
}
}
add_line := func(line string) {
x = start_x
indices := hyperlink_pat().FindAllStringSubmatchIndex(line, -1)
start := 0
for _, index := range indices {
full_hyperlink_start, full_hyperlink_end := index[0], index[1]
add_chunk(line[start:full_hyperlink_start])
start = full_hyperlink_end
add_hyperlink(line[index[2]:index[3]], line[index[4]:index[5]])
}
add_chunk(line[start:])
}
all_rendered = true
wo := style.WrapOptions{Trim_whitespace: true}
for _, line := range lines {
wrapped_lines := []string{line}
if width > 0 {
wrapped_lines = style.WrapTextAsLines(line, width, wo)
}
for _, line := range wrapped_lines {
move_cursor(start_x, y)
add_line(line)
y += 1
if y > end_y {
all_rendered = false
goto end
}
}
}
end:
commit_hyperlink()
return all_rendered, y, buf.String()
}

View File

@@ -4,6 +4,7 @@ package sgr
import (
"fmt"
"slices"
"strconv"
"strings"
"unicode/utf8"
@@ -11,8 +12,6 @@ import (
"kitty/tools/utils"
"kitty/tools/utils/style"
"kitty/tools/wcswidth"
"golang.org/x/exp/slices"
)
var _ = fmt.Print

View File

@@ -6,14 +6,13 @@ import (
"archive/tar"
"bytes"
"fmt"
"maps"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"kitty"
"kitty/tools/tty"
"kitty/tools/utils"

View File

@@ -4,12 +4,11 @@ package subseq
import (
"fmt"
"slices"
"strings"
"kitty/tools/utils"
"kitty/tools/utils/images"
"golang.org/x/exp/slices"
)
var _ = fmt.Print

View File

@@ -5,10 +5,10 @@ package unicode_names
import (
"fmt"
"kitty/tools/utils"
"slices"
"testing"
"github.com/google/go-cmp/cmp"
"golang.org/x/exp/slices"
)
var _ = fmt.Print

View File

@@ -14,6 +14,7 @@ import (
"os"
"os/exec"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
@@ -23,7 +24,6 @@ import (
"github.com/edwvee/exiffix"
"github.com/kovidgoyal/imaging"
"golang.org/x/exp/slices"
)
var _ = fmt.Print

View File

@@ -9,10 +9,10 @@ import (
"path/filepath"
"reflect"
"runtime"
"slices"
"strconv"
"golang.org/x/exp/constraints"
"golang.org/x/exp/slices"
)
var _ = fmt.Print

View File

@@ -19,6 +19,7 @@ import (
"sync"
"unicode/utf8"
"github.com/shirou/gopsutil/v3/process"
"golang.org/x/sys/unix"
)
@@ -66,8 +67,16 @@ func Abspath(path string) string {
}
var KittyExe = sync.OnceValue(func() string {
exe, err := os.Executable()
if err == nil {
if kitty_pid := os.Getenv("KITTY_PID"); kitty_pid != "" {
if kp, err := strconv.Atoi(kitty_pid); err == nil {
if p, err := process.NewProcess(int32(kp)); err == nil {
if exe, err := p.Exe(); err == nil && filepath.IsAbs(exe) && filepath.Base(exe) == "kitty" {
return exe
}
}
}
}
if exe, err := os.Executable(); err == nil {
ans := filepath.Join(filepath.Dir(exe), "kitty")
if s, err := os.Stat(ans); err == nil && !s.IsDir() {
return ans

View File

@@ -4,12 +4,26 @@ package utils
import (
"fmt"
"golang.org/x/exp/maps"
)
var _ = fmt.Print
func Keys[M ~map[K]V, K comparable, V any](m M) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
func Values[M ~map[K]V, K comparable, V any](m M) []V {
r := make([]V, 0, len(m))
for _, k := range m {
r = append(r, k)
}
return r
}
type Set[T comparable] struct {
items map[T]struct{}
}
@@ -25,7 +39,7 @@ func (self *Set[T]) AddItems(val ...T) {
}
func (self *Set[T]) String() string {
return fmt.Sprintf("%#v", maps.Keys(self.items))
return fmt.Sprintf("%#v", Keys(self.items))
}
func (self *Set[T]) Remove(val T) {
@@ -41,6 +55,10 @@ func (self *Set[T]) Has(val T) bool {
return ok
}
func (self *Set[T]) Clear() {
clear(self.items)
}
func (self *Set[T]) Len() int {
return len(self.items)
}
@@ -56,7 +74,7 @@ func (self *Set[T]) Iterable() map[T]struct{} {
}
func (self *Set[T]) AsSlice() []T {
return maps.Keys(self.items)
return Keys(self.items)
}
func (self *Set[T]) Intersect(other *Set[T]) (ans *Set[T]) {
@@ -103,6 +121,22 @@ func (self *Set[T]) IsSubsetOf(other *Set[T]) bool {
return true
}
func (self *Set[T]) Equal(other *Set[T]) bool {
l := self.Len()
if other == nil {
return l == 0
}
if l != other.Len() {
return false
}
for x := range self.items {
if !other.Has(x) {
return false
}
}
return true
}
func NewSet[T comparable](capacity ...int) (ans *Set[T]) {
if len(capacity) == 0 {
ans = &Set[T]{items: make(map[T]struct{}, 8)}

View File

@@ -17,6 +17,7 @@ package shlex
import (
"fmt"
"kitty/tools/utils"
"strings"
"unicode/utf8"
)
@@ -182,6 +183,16 @@ func Split(s string) (ans []string, err error) {
return
}
func Quote(s string) string {
if s == "" {
return s
}
if utils.MustCompile(`[^\w@%+=:,./-]`).MatchString(s) {
return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'"
}
return s
}
// SplitForCompletion partitions a string into a slice of strings. It differs from Split in being
// more relaxed about errors and also adding an empty string at the end if s ends with a Space.
func SplitForCompletion(s string) (argv []string, position_of_last_arg int) {

View File

@@ -167,3 +167,5 @@ func RuneOffsetsToByteOffsets(text string) func(int) int {
return self.byte_offset
}
}
func Repr(x any) string { return fmt.Sprintf("%#v", x) }