mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-14 20:47:58 +02:00
Compare commits
145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
656d2179c9 | ||
|
|
da582b5622 | ||
|
|
ba4292e912 | ||
|
|
785726d21d | ||
|
|
e3a155266e | ||
|
|
e3eb179be2 | ||
|
|
e88ae3397f | ||
|
|
8e0ef0c430 | ||
|
|
02df66733e | ||
|
|
4a038ea581 | ||
|
|
e41d57dffd | ||
|
|
b63be88bac | ||
|
|
8bfff51d23 | ||
|
|
72268539ef | ||
|
|
5b83a33888 | ||
|
|
daaec1b47f | ||
|
|
bc56fce38d | ||
|
|
788b3dc4b2 | ||
|
|
f4e22ebe3c | ||
|
|
349c32f60e | ||
|
|
3f919db0c7 | ||
|
|
c4dad85f99 | ||
|
|
a164c73389 | ||
|
|
3e430e1a70 | ||
|
|
12314cc33f | ||
|
|
c3bba2e926 | ||
|
|
a4f67b7424 | ||
|
|
b217c9acde | ||
|
|
23e777ea9e | ||
|
|
ecb106e92c | ||
|
|
23deaae5e7 | ||
|
|
bc6230d90c | ||
|
|
03f35812eb | ||
|
|
7c0007c1bd | ||
|
|
d2d2f6c503 | ||
|
|
69fb2e4231 | ||
|
|
94d056ed4f | ||
|
|
726f62b948 | ||
|
|
7e15839141 | ||
|
|
13a6ff25a2 | ||
|
|
e050557db7 | ||
|
|
3e2b3a89ce | ||
|
|
81c30cc5fa | ||
|
|
73a6668b17 | ||
|
|
f8e2dc1eca | ||
|
|
32b8077c89 | ||
|
|
b90fede2c1 | ||
|
|
87d1a97486 | ||
|
|
98450a0605 | ||
|
|
29377db94c | ||
|
|
8d8c2d7170 | ||
|
|
5be4f7b566 | ||
|
|
ce74706c95 | ||
|
|
8f8da2d2c6 | ||
|
|
540e11fa01 | ||
|
|
a230eb87a1 | ||
|
|
4ce7da044f | ||
|
|
ceccacafa8 | ||
|
|
df80da7cb5 | ||
|
|
08bffde4ad | ||
|
|
983bf134a1 | ||
|
|
e25c61f781 | ||
|
|
d6026c377b | ||
|
|
a766a95de6 | ||
|
|
36d906d4f5 | ||
|
|
881b72fea7 | ||
|
|
d6a7d4a8a1 | ||
|
|
db092eb88d | ||
|
|
33df5355a6 | ||
|
|
99e279cbe9 | ||
|
|
82ffb376b2 | ||
|
|
fc72f4961e | ||
|
|
880078cab8 | ||
|
|
88058c9075 | ||
|
|
ecdea9d4d3 | ||
|
|
5ace209024 | ||
|
|
b68aac9f28 | ||
|
|
71bbf4ecb9 | ||
|
|
1590462038 | ||
|
|
1735404a05 | ||
|
|
3c18d20d1b | ||
|
|
e51a1362ca | ||
|
|
5cea5cce81 | ||
|
|
bb722ed6dc | ||
|
|
78e81ec589 | ||
|
|
80721e1e63 | ||
|
|
7186b862f2 | ||
|
|
e4d82074ff | ||
|
|
804ad62a33 | ||
|
|
4431cff7fa | ||
|
|
78b77b0d01 | ||
|
|
2c2fbd4445 | ||
|
|
333e94622e | ||
|
|
41869d88c2 | ||
|
|
65b790df40 | ||
|
|
5d74d210ee | ||
|
|
431fc98659 | ||
|
|
21d3759f62 | ||
|
|
24dea14795 | ||
|
|
94628dca21 | ||
|
|
d6016f4246 | ||
|
|
688736c4cf | ||
|
|
1ce31a339e | ||
|
|
d86da0616e | ||
|
|
926dfd7ba1 | ||
|
|
e1b367e1b3 | ||
|
|
4998fe66b9 | ||
|
|
7f965eba5f | ||
|
|
0b743464fb | ||
|
|
ffed63a048 | ||
|
|
96c17b0a67 | ||
|
|
8a0b562f4f | ||
|
|
198aec84c2 | ||
|
|
217ded7b3f | ||
|
|
7522675553 | ||
|
|
8a8158f287 | ||
|
|
1646c297b3 | ||
|
|
de9d9fd157 | ||
|
|
c2e0ecef13 | ||
|
|
6e55949094 | ||
|
|
21f824e825 | ||
|
|
086185e409 | ||
|
|
e3e7bded06 | ||
|
|
7e232b33be | ||
|
|
53be33f14d | ||
|
|
54b50858ec | ||
|
|
46d75ea48f | ||
|
|
bf73720805 | ||
|
|
eb580ed91b | ||
|
|
7f8ce387be | ||
|
|
8ddd20770b | ||
|
|
53ea650c27 | ||
|
|
3b84498af9 | ||
|
|
fa52066156 | ||
|
|
e2e33cc11c | ||
|
|
71a8801a68 | ||
|
|
e54e17acf8 | ||
|
|
b57cb95522 | ||
|
|
12fd1472f5 | ||
|
|
5a86960acd | ||
|
|
dbbc0ea9d4 | ||
|
|
39f1ff6ead | ||
|
|
2d088889f7 | ||
|
|
48a0a7fe82 | ||
|
|
fbeead1661 |
39
.github/workflows/ci.py
vendored
39
.github/workflows/ci.py
vendored
@@ -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:
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)"
|
||||
|
||||
51
docs/kittens/choose-fonts.rst
Normal file
51
docs/kittens/choose-fonts.rst
Normal 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
BIN
docs/screenshots/family-selection.png
Normal file
BIN
docs/screenshots/family-selection.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
BIN
docs/screenshots/font-fine-tune.png
Normal file
BIN
docs/screenshots/font-fine-tune.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 193 KiB |
@@ -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
|
||||
|
||||
0
kittens/choose_fonts/__init__.py
Normal file
0
kittens/choose_fonts/__init__.py
Normal file
165
kittens/choose_fonts/backend.go
Normal file
165
kittens/choose_fonts/backend.go
Normal 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
|
||||
}
|
||||
256
kittens/choose_fonts/backend.py
Normal file
256
kittens/choose_fonts/backend.py
Normal 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'])
|
||||
489
kittens/choose_fonts/face.go
Normal file
489
kittens/choose_fonts/face.go
Normal 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()
|
||||
}
|
||||
161
kittens/choose_fonts/faces.go
Normal file
161
kittens/choose_fonts/faces.go
Normal 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()
|
||||
}
|
||||
161
kittens/choose_fonts/family_list.go
Normal file
161
kittens/choose_fonts/family_list.go
Normal 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 ""
|
||||
}
|
||||
118
kittens/choose_fonts/final.go
Normal file
118
kittens/choose_fonts/final.go
Normal 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()
|
||||
}
|
||||
106
kittens/choose_fonts/graphics.go
Normal file
106
kittens/choose_fonts/graphics.go
Normal 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())
|
||||
}
|
||||
149
kittens/choose_fonts/index_feature.go
Normal file
149
kittens/choose_fonts/index_feature.go
Normal 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()
|
||||
}
|
||||
319
kittens/choose_fonts/list.go
Normal file
319
kittens/choose_fonts/list.go
Normal 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
|
||||
}
|
||||
99
kittens/choose_fonts/main.go
Normal file
99
kittens/choose_fonts/main.go
Normal 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"
|
||||
}
|
||||
0
kittens/choose_fonts/main.py
Normal file
0
kittens/choose_fonts/main.py
Normal file
113
kittens/choose_fonts/styles.go
Normal file
113
kittens/choose_fonts/styles.go
Normal 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
|
||||
}
|
||||
333
kittens/choose_fonts/types.go
Normal file
333
kittens/choose_fonts/types.go
Normal 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
225
kittens/choose_fonts/ui.go
Normal 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
|
||||
}
|
||||
|
||||
// }}}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
82
kittens/query_terminal/main.go
Normal file
82
kittens/query_terminal/main.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 + "…"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
369
kitty/font-names.c
Normal 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
|
||||
@@ -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 */
|
||||
|
||||
259
kitty/fonts.c
259
kitty/fonts.c
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
506
kitty/fonts/common.py
Normal 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()
|
||||
@@ -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
149
kitty/fonts/features.py
Normal 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)
|
||||
# }}}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
364
kitty/freetype.c
364
kitty/freetype.c
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
16
kitty/options/parse.py
generated
@@ -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):
|
||||
|
||||
15
kitty/options/to-c-generated.h
generated
15
kitty/options/to-c-generated.h
generated
@@ -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);
|
||||
|
||||
@@ -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
11
kitty/options/types.py
generated
@@ -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] = {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -21,3 +21,4 @@ PowerlineStyle = str
|
||||
MatchType = str
|
||||
Protocol = object
|
||||
OptionsProtocol = object
|
||||
NotRequired = Tuple
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
3
setup.py
3
setup.py
@@ -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':
|
||||
|
||||
@@ -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" ;;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"kitty"
|
||||
|
||||
@@ -4,10 +4,9 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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-- {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -4,10 +4,9 @@ package loop
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"kitty/tools/tty"
|
||||
"kitty/tools/utils"
|
||||
)
|
||||
|
||||
@@ -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
125
tools/tui/render_lines.go
Normal 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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/exp/constraints"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) }
|
||||
|
||||
Reference in New Issue
Block a user