mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-10-20 12:19:36 +00:00
The ceiling in change_font_size is ten times the configured font_size, so
the largest size you can reach depends on the size you configured. With
font_size 6 zooming stops at 60pt, while font_size 20 reaches 200pt. That
is backwards: a small configured font is if anything a reason to want a
larger temporary zoom, not a smaller one.
Use the larger of that multiple and an absolute 256pt. No existing setup
loses range, since the multiple still wins whenever it is the bigger of
the two. The bounds and the clamp move into options/utils.py so the
numbers are in one place and can be tested directly.
increase_font_size also had its own cap of five times font_size. That was
the old limit, missed when 25d8d9fe6 raised change_font_size to ten, and
because the wrapper clamps before delegating, the stale bound is the one
that wins. Both legacy wrappers can simply drop their bounds: they go
through change_font_size, which already clamps.
580 lines
24 KiB
Python
580 lines
24 KiB
Python
#!/usr/bin/env python
|
|
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
|
|
from kitty.constants import kitty_exe
|
|
from kitty.fast_data_types import Color, test_cursor_blink_easing_function
|
|
from kitty.options.utils import DELETE_ENV_VAR, EasingFunction, to_color
|
|
from kitty.utils import log_error, shlex_split
|
|
|
|
from .base import BaseTest
|
|
|
|
|
|
class TestConfParsing(BaseTest):
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.error_messages = []
|
|
log_error.redirect = self.error_messages.append
|
|
self.tdir = tempfile.mkdtemp()
|
|
|
|
def tearDown(self):
|
|
del log_error.redirect
|
|
shutil.rmtree(self.tdir)
|
|
super().tearDown()
|
|
|
|
def test_conf_parsing(self):
|
|
conf_parsing(self)
|
|
|
|
def test_launcher(self):
|
|
launcher(self)
|
|
|
|
def test_cli_parsing(self):
|
|
cli_parsing(self)
|
|
|
|
def test_font_size_clamping(self):
|
|
font_size_clamping(self)
|
|
|
|
def test_session_discovery(self):
|
|
from unittest.mock import patch
|
|
|
|
from kitty.config import load_config
|
|
from kitty.constants import config_dir
|
|
from kitty.fast_data_types import set_options
|
|
from kitty.session import get_all_known_sessions, seen_session_paths
|
|
|
|
def discover(*lines):
|
|
bad_lines = []
|
|
opts = load_config(overrides=('clear_all_shortcuts yes', *lines), accumulate_bad_lines=bad_lines)
|
|
self.assertFalse(bad_lines)
|
|
set_options(opts)
|
|
return get_all_known_sessions()
|
|
|
|
with patch.dict(seen_session_paths, {}, clear=True):
|
|
for args, name in (
|
|
('work.kitty-session', 'work'),
|
|
('--sort-by alphabetical work.kitty-session', 'work'),
|
|
('--sort-by=alphabetical work.kitty-session', 'work'),
|
|
('--active-only=no --sort-by recent work.kitty-session', 'work'),
|
|
('work.kitty-session --sort-by alphabetical', 'work'),
|
|
('-- "-work project.kitty-session"', '-work project'),
|
|
('-- -0', '-0'),
|
|
('', ''),
|
|
('--sort-by alphabetical', ''),
|
|
('--sort-by=alphabetical', ''),
|
|
('--active-only', ''),
|
|
('-1', ''),
|
|
('-- -1', ''),
|
|
('--sort-by alphabetical -- -2', ''),
|
|
('--sort-by', ''),
|
|
('--sort-by invalid work.kitty-session', ''),
|
|
('--unknown work.kitty-session', ''),
|
|
('--active-only=invalid work.kitty-session', ''),
|
|
):
|
|
with self.subTest(args=args):
|
|
filename = f'{name}.kitty-session' if name != '-0' else name
|
|
expected = {name: os.path.join(config_dir, filename)} if name else {}
|
|
self.ae(discover(f'map f7 goto_session {args}'), expected)
|
|
|
|
seen_path = os.path.join(self.tdir, 'work.kitty-session')
|
|
seen_session_paths['work'] = seen_path
|
|
self.ae(
|
|
discover(
|
|
'action_alias project goto_session --sort-by alphabetical',
|
|
'map f7 project work.kitty-session',
|
|
'map f8 combine : goto_session --sort-by : project other.kitty-session',
|
|
),
|
|
{'work': seen_path, 'other': os.path.join(config_dir, 'other.kitty-session')},
|
|
)
|
|
|
|
|
|
def cli_parsing(self):
|
|
from kitty.cli import CLIOptions, Options, parse_cmdline, parse_option_spec
|
|
|
|
seq, disabled = parse_option_spec("""\
|
|
--simple-string -s
|
|
a simple string
|
|
|
|
|
|
--list -l
|
|
type=list
|
|
a list
|
|
|
|
|
|
--choice -c
|
|
choices=a,b,c
|
|
some choices
|
|
|
|
|
|
--bool-set -1
|
|
type=bool-set
|
|
set a bool
|
|
|
|
|
|
--bool-reset -0
|
|
type=bool-reset
|
|
unset a bool
|
|
|
|
|
|
--int -i
|
|
type=int
|
|
a integer
|
|
|
|
|
|
--float -f
|
|
type=float
|
|
a float
|
|
|
|
|
|
--version -v
|
|
type=bool-set
|
|
version
|
|
""")
|
|
oc = Options(seq, usage='xxx', message='yyy', appname='test')
|
|
oc.do_print = False
|
|
|
|
def t(args, leftover=(), fails=False, version_called=False, help_called=False, **expected):
|
|
oc.help_called = oc.version_called = False
|
|
args = list(shlex_split(args))
|
|
ans = CLIOptions()
|
|
if fails:
|
|
if isinstance(fails, str):
|
|
with self.assertRaisesRegex(SystemExit, fails):
|
|
parse_cmdline(oc, disabled, ans, args=args)
|
|
else:
|
|
with self.assertRaises(SystemExit):
|
|
parse_cmdline(oc, disabled, ans, args=args)
|
|
else:
|
|
actual_leftover = parse_cmdline(oc, disabled, ans, args=args)
|
|
self.assertEqual(tuple(leftover), tuple(actual_leftover), f'{args}\n{ans}')
|
|
for dest, defval in oc.values_map.items():
|
|
val = expected.get(dest, defval)
|
|
self.assertEqual(val, getattr(ans, dest, BaseTest), f'Failed to parse {dest} correctly for: {args}\n{ans}')
|
|
self.assertEqual(version_called, oc.version_called, f'Failed to call version for: {args}\n{ans}')
|
|
self.assertEqual(help_called, oc.help_called, f'Failed to call help for: {args}\n{ans}')
|
|
|
|
t('-1', bool_set=True)
|
|
t('-01', bool_reset=False, bool_set=True)
|
|
t('-01=y', bool_reset=False, bool_set=True)
|
|
t('-01=n', bool_reset=False, bool_set=False)
|
|
t('--simple-string xx moo -1', leftover=['moo', '-1'], simple_string='xx')
|
|
t('--si -0 -- -1', leftover=['-1'], simple_string='-0')
|
|
t('--simple-string=-0 -- -1', leftover=['-1'], simple_string='-0')
|
|
t('--simple-string=--help -- -1', leftover=['-1'], simple_string='--help')
|
|
t('--simple-string --help -- -1', leftover=['-1'], simple_string='--help')
|
|
t('-1l=a --list=b -c b --list c', bool_set=True, choice='b', list=list('abc'))
|
|
t('-1s= -l "" --list= x', leftover=['x'], bool_set=True, simple_string='', list=['', ''])
|
|
t('--choice moo', fails=r'moo')
|
|
t('--bool', fails='mbiguous')
|
|
t('-1c moo', fails=r'moo')
|
|
t('-10c=moo', fails=r'moo')
|
|
t('-1 -h', fails=True, help_called=True)
|
|
t('-1 --help', fails=True, help_called=True)
|
|
t('-1 -0v', fails=True, version_called=True)
|
|
t('-1 -v0', fails=True, version_called=True)
|
|
t('-1 --version', fails=True, version_called=True)
|
|
t('-f=3.142 --int 17', float=3.142, int=17)
|
|
|
|
|
|
def font_size_clamping(self: 'BaseTest') -> None:
|
|
from kitty.options.utils import MAXIMUM_FONT_SIZE, MINIMUM_FONT_SIZE, clamp_font_size
|
|
|
|
# the floor applies whatever the configured size
|
|
self.ae(clamp_font_size(0, 11), MINIMUM_FONT_SIZE)
|
|
self.ae(clamp_font_size(-100, 11), MINIMUM_FONT_SIZE)
|
|
|
|
# sizes inside the range come back untouched
|
|
self.ae(clamp_font_size(11, 11), 11)
|
|
self.ae(clamp_font_size(64, 11), 64)
|
|
|
|
# the ceiling is the larger of the multiple and the absolute maximum, so a
|
|
# small configured size no longer implies a small maximum zoom
|
|
self.ae(clamp_font_size(1e6, 4), MAXIMUM_FONT_SIZE)
|
|
self.ae(clamp_font_size(1e6, 11), MAXIMUM_FONT_SIZE)
|
|
self.ae(clamp_font_size(200, 4), 200)
|
|
|
|
# a large configured size keeps its proportional ceiling
|
|
self.ae(clamp_font_size(1e6, 40), 400)
|
|
self.ae(clamp_font_size(1e6, 100), 1000)
|
|
|
|
# and the ceiling is never below the absolute maximum
|
|
for configured in (1, 4, 11, 25.6, 40, 100):
|
|
self.assertGreaterEqual(clamp_font_size(1e6, configured), MAXIMUM_FONT_SIZE)
|
|
|
|
|
|
def launcher(self):
|
|
import tempfile
|
|
|
|
from kitty.constants import is_macos
|
|
|
|
kexe = kitty_exe()
|
|
cfgdir = None
|
|
|
|
def get_report(cmdline: str, launch_services=False, config_dir: str = ''):
|
|
nonlocal cfgdir
|
|
args = list(shlex_split(cmdline))
|
|
env = dict(os.environ)
|
|
if launch_services:
|
|
env['KITTY_LAUNCHED_BY_LAUNCH_SERVICES'] = '1'
|
|
if config_dir:
|
|
env['KITTY_CONFIG_DIRECTORY'] = config_dir
|
|
cp = subprocess.run([kexe, '+testing-launcher-code'] + args, env=env, stdout=subprocess.PIPE)
|
|
self.assertEqual(cp.returncode, 0)
|
|
ans = {}
|
|
for line in cp.stdout.decode().split('\n'):
|
|
if not line:
|
|
continue
|
|
try:
|
|
key, val = line.split(':')
|
|
except ValueError:
|
|
raise AssertionError(f'Unexpected output from launcher: {line!r}\n{cp.stdout.decode()}')
|
|
if '\x1e' in val:
|
|
val = [x for x in val.split('\x1e') if x]
|
|
else:
|
|
val = val.strip()
|
|
ans[key] = val
|
|
if key == 'config_dir':
|
|
cfgdir = val
|
|
return ans, cp.stdout.decode().replace('\x1e', ' ')
|
|
|
|
def test(cmdline, assertions):
|
|
r, output = get_report(cmdline, launch_services=assertions.get('launched_by_launch_services', '0') != '0')
|
|
for key, expected in assertions.items():
|
|
self.assertEqual(expected, r.get(key), f'Failed for {key} with command line: {cmdline}\nOutput:\n{output}')
|
|
return output
|
|
|
|
def t(cmdline, **assertions):
|
|
assertions['is_quick_access_terminal'] = '0'
|
|
if cfgdir:
|
|
assertions['config_dir'] = cfgdir
|
|
assertions.setdefault('launched_by_launch_services', '0')
|
|
test(cmdline, assertions)
|
|
|
|
def si(cmdline, **assertions):
|
|
assertions['single_instance'] = '1'
|
|
test(cmdline, assertions)
|
|
|
|
def dt(cmdline, **assertions):
|
|
assertions['detach'] = 'true'
|
|
test(cmdline, assertions)
|
|
|
|
def k(cmdline):
|
|
assertions = {}
|
|
assertions['argv'] = ['kitten'] + cmdline.split()
|
|
for prefix in ('+kitten', '+ kitten'):
|
|
output = test(prefix + ' ' + cmdline, assertions)
|
|
self.assertIn('kitten_exe:', output)
|
|
|
|
def pn(cmdline, **assertions):
|
|
ig = assertions.get('instance_group')
|
|
assertions['instance_group'] = f'panel-{ig}' if ig else 'panel'
|
|
assertions['single_instance'] = '1'
|
|
assertions['session'] = ''
|
|
test(cmdline, assertions)
|
|
|
|
t('', original_argv=[kexe], argv=[])
|
|
t(
|
|
'--title=xxx --start-as maximized -c=a -c b cat',
|
|
title='xxx',
|
|
start_as='maximized',
|
|
config=['a', 'b'],
|
|
original_argv=[kexe, '--title=xxx', '--start-as', 'maximized', '-c=a', '-c', 'b', 'cat'],
|
|
argv=['cat'],
|
|
)
|
|
k('icat abc xyz')
|
|
t('+kitten unwrapped xyz', argv=['+kitten', 'unwrapped', 'xyz'])
|
|
t('+ kitten unwrapped xyz', original_argv=[kexe, '+', 'kitten', 'unwrapped', 'xyz'])
|
|
si('--single-instance --instance-group=g -T 3', argv=[kexe, '--single-instance', '--instance-group=g', '-T', '3'])
|
|
t('+open --help', argv=['+open', '--help'])
|
|
t('+open -1 --help', argv=['+open', '-1', '--help'])
|
|
si('+open -1 moose', argv=[kexe, '+open', '-1', 'moose'], open_urls=['moose'])
|
|
si('+open -1 --instance-group=g x y', instance_group='g', open_urls=['x', 'y'])
|
|
dt('--detach --session=moose --detached-log=xyz', detached_log='xyz', session='moose')
|
|
pn('+kitten panel -1 --edge=left', edge='left')
|
|
|
|
if is_macos:
|
|
with tempfile.TemporaryDirectory() as tdir:
|
|
with open(tdir + '/macos-launch-services-cmdline', 'w') as f:
|
|
f.write('kitty --title from-file\n')
|
|
# File args are used when launched by launch services
|
|
r, output = get_report('', launch_services=True, config_dir=tdir)
|
|
self.assertEqual(r.get('title'), 'from-file', f'Expected title=from-file in:\n{output}')
|
|
# User args passed via open --args are appended after file args (and override them)
|
|
r, output = get_report('--title from-args', launch_services=True, config_dir=tdir)
|
|
self.assertEqual(r.get('title'), 'from-args', f'Expected title=from-args to override from-file in:\n{output}')
|
|
|
|
|
|
def conf_parsing(self):
|
|
from kitty.config import defaults, load_config
|
|
from kitty.constants import is_macos
|
|
from kitty.fast_data_types import LEFT_EDGE, RIGHT_EDGE
|
|
from kitty.fonts import FontModification, ModificationType, ModificationUnit, ModificationValue
|
|
from kitty.options.utils import to_modifiers
|
|
|
|
bad_lines = []
|
|
|
|
def p(*lines, bad_line_num=0, num_err=None):
|
|
del bad_lines[:]
|
|
del self.error_messages[:]
|
|
ans = load_config(overrides=lines, accumulate_bad_lines=bad_lines)
|
|
if bad_line_num:
|
|
self.ae(len(bad_lines), bad_line_num)
|
|
else:
|
|
self.assertFalse(bad_lines)
|
|
if num_err is not None:
|
|
self.ae(len(self.error_messages), num_err, '\n'.join(self.error_messages))
|
|
return ans
|
|
|
|
def keys_for_func(opts, name):
|
|
for key, defns in opts.keyboard_modes[''].keymap.items():
|
|
for action in opts.alias_map.resolve_aliases(defns[0].definition):
|
|
if action.func == name:
|
|
yield key
|
|
|
|
opts = p('font_size 11.37', 'clear_all_shortcuts y', 'color23 red')
|
|
self.ae(opts.font_size, 11.37)
|
|
radius_opts = p('window_border_radius 7px')
|
|
self.ae(radius_opts.window_border_radius, (7.0, 'px'))
|
|
radius_opts = p('window_border_radius -2')
|
|
self.ae(radius_opts.window_border_radius, (0.0, 'pt'))
|
|
self.ae(opts.mouse_hide_wait[0], 0 if is_macos else 3)
|
|
self.ae(opts.mouse_hide_wait[1], 0)
|
|
self.ae(opts.mouse_hide_wait[2], 40)
|
|
self.ae(opts.mouse_hide_wait[3], True)
|
|
self.ae(opts.color23, Color(255, 0, 0))
|
|
self.assertFalse(opts.keyboard_modes[''].keymap)
|
|
opts = p("url_excluded_characters '''")
|
|
self.ae(opts.url_excluded_characters, "'''")
|
|
opts = p("url_excluded_characters abc'")
|
|
self.ae(opts.url_excluded_characters, "abc'")
|
|
opts = p('tab_bar_edge left')
|
|
self.ae(opts.tab_bar_edge, LEFT_EDGE)
|
|
opts = p('tab_bar_edge right')
|
|
self.ae(opts.tab_bar_edge, RIGHT_EDGE)
|
|
opts = p('tab_bar_align start')
|
|
self.ae(opts.tab_bar_align, 'start')
|
|
opts = p('tab_bar_align end')
|
|
self.ae(opts.tab_bar_align, 'end')
|
|
opts = p('tab_bar_align left')
|
|
self.ae(opts.tab_bar_align, 'left')
|
|
opts = p('tab_bar_align right')
|
|
self.ae(opts.tab_bar_align, 'right')
|
|
opts = p('clear_all_shortcuts y', 'map f1 next_window')
|
|
self.ae(len(opts.keyboard_modes[''].keymap), 1)
|
|
opts = p('clear_all_mouse_actions y', 'mouse_map left click ungrabbed mouse_click_url_or_select')
|
|
self.ae(len(opts.mousemap), 1)
|
|
opts = p('strip_trailing_spaces always')
|
|
self.ae(opts.strip_trailing_spaces, 'always')
|
|
self.assertFalse(bad_lines)
|
|
opts = p('pointer_shape_when_grabbed XXX', bad_line_num=1)
|
|
self.ae(opts.pointer_shape_when_grabbed, defaults.pointer_shape_when_grabbed)
|
|
opts = p('modify_font underline_position -2', 'modify_font underline_thickness 150%', 'modify_font size Test -1px')
|
|
self.ae(
|
|
opts.modify_font,
|
|
{
|
|
'underline_position': FontModification(ModificationType.underline_position, ModificationValue(-2.0, ModificationUnit.pt)),
|
|
'underline_thickness': FontModification(ModificationType.underline_thickness, ModificationValue(150, ModificationUnit.percent)),
|
|
'size:Test': FontModification(ModificationType.size, ModificationValue(-1.0, ModificationUnit.pixel), 'Test'),
|
|
},
|
|
)
|
|
|
|
# test the aliasing options
|
|
opts = p('env A=1', 'env B=x$A', 'env C=', 'env D', 'clear_all_shortcuts y', 'kitten_alias a b --moo', 'map f1 kitten a arg')
|
|
self.ae(opts.env, {'A': '1', 'B': 'x1', 'C': '', 'D': DELETE_ENV_VAR})
|
|
|
|
def ac(which=0):
|
|
ka = tuple(opts.keyboard_modes[''].keymap.values())[0][0]
|
|
acs = opts.alias_map.resolve_aliases(ka.definition)
|
|
return acs[which]
|
|
|
|
ka = ac()
|
|
self.ae(ka.func, 'kitten')
|
|
self.ae(ka.args, ('b', '--moo', 'arg'))
|
|
|
|
opts = p('clear_all_shortcuts y', 'kitten_alias hints hints --hi', 'map f1 kitten hints XXX')
|
|
ka = ac()
|
|
self.ae(ka.func, 'kitten')
|
|
self.ae(ka.args, ('hints', '--hi', 'XXX'))
|
|
|
|
opts = p('clear_all_shortcuts y', 'action_alias la launch --moo', 'map f1 la XXX')
|
|
ka = ac()
|
|
self.ae(ka.func, 'launch')
|
|
self.ae(ka.args, ('--moo', 'XXX'))
|
|
|
|
opts = p('clear_all_shortcuts y', 'action_alias one launch --moo', 'action_alias two one recursive', 'map f1 two XXX')
|
|
ka = ac()
|
|
self.ae(ka.func, 'launch')
|
|
self.ae(ka.args, ('--moo', 'recursive', 'XXX'))
|
|
|
|
opts = p('clear_all_shortcuts y', 'action_alias launch two 1', 'action_alias two launch 2', 'map f1 launch 3')
|
|
ka = ac()
|
|
self.ae(ka.func, 'launch')
|
|
self.ae(ka.args, ('2', '1', '3'))
|
|
|
|
opts = p('clear_all_shortcuts y', 'action_alias launch launch --moo', 'map f1 launch XXX')
|
|
ka = ac()
|
|
self.ae(ka.func, 'launch')
|
|
self.ae(ka.args, ('--moo', 'XXX'))
|
|
|
|
opts = p('clear_all_shortcuts y', 'action_alias cfs change_font_size current', 'map f1 cfs +2')
|
|
ka = ac()
|
|
self.ae(ka.func, 'change_font_size')
|
|
self.ae(ka.args, (False, '+', 2.0))
|
|
|
|
opts = p('clear_all_shortcuts y', 'action_alias la launch --moo', 'map f1 combine : new_window : la ')
|
|
self.ae((ac().func, ac(1).func), ('new_window', 'launch'))
|
|
|
|
opts = p('clear_all_shortcuts y', 'action_alias cc combine : new_window : launch --moo', 'map f1 cc XXX')
|
|
self.ae((ac().func, ac(1).func), ('new_window', 'launch'))
|
|
self.ae(ac(1).args, ('--moo', 'XXX'))
|
|
|
|
opts = p('clear_all_shortcuts y', 'action_alias ss kitten "space 1"', 'map f1 ss "space 2"')
|
|
self.ae(ac().args, ('space 1', 'space 2'))
|
|
|
|
opts = p('kitty_mod alt')
|
|
self.ae(opts.kitty_mod, to_modifiers('alt'))
|
|
self.ae(next(keys_for_func(opts, 'next_layout')).mods, opts.kitty_mod)
|
|
|
|
opts = p('inactive_text_alpha 0.25')
|
|
self.ae(opts.inactive_text_alpha, 0.25)
|
|
opts = p('inactive_text_alpha -0.25')
|
|
self.ae(opts.inactive_text_alpha, -0.25)
|
|
opts = p('inactive_text_alpha 2')
|
|
self.ae(opts.inactive_text_alpha, 1.0)
|
|
opts = p('inactive_text_alpha -2')
|
|
self.ae(opts.inactive_text_alpha, -1.0)
|
|
|
|
# deprecation handling
|
|
opts = p('clear_all_shortcuts y', 'send_text all f1 hello')
|
|
self.ae(len(opts.keyboard_modes[''].keymap), 1)
|
|
opts = p('macos_hide_titlebar y' if is_macos else 'x11_hide_window_decorations y')
|
|
self.assertTrue(opts.hide_window_decorations)
|
|
self.ae(len(self.error_messages), 1)
|
|
|
|
# line breaks
|
|
opts = p(' font', ' \t \t \\_size', ' \\ 12', '\\.35', 'col', '\\o', '\t \t\\r', '\\25', ' \\ blue')
|
|
self.ae(opts.font_size, 12.35)
|
|
self.ae(opts.color25, Color(0, 0, 255))
|
|
|
|
# cursor_blink_interval
|
|
def cb(src, interval=-1, first=EasingFunction(), second=EasingFunction()):
|
|
opts = p('cursor_blink_interval ' + src)
|
|
self.ae((float(interval), first, second), (float(opts.cursor_blink_interval[0]), opts.cursor_blink_interval[1], opts.cursor_blink_interval[2]))
|
|
|
|
cb('3', 3)
|
|
cb('-2.3', -2.3)
|
|
cb('linear', first=EasingFunction('cubic-bezier', cubic_bezier_points=(0, 0.0, 1.0, 1.0)))
|
|
cb('linear 19', 19, EasingFunction('cubic-bezier', cubic_bezier_points=(0, 0.0, 1.0, 1.0)))
|
|
cb(
|
|
'ease-in-out cubic-bezier(0.1, 0.2, 0.3, 0.4) 11',
|
|
11,
|
|
EasingFunction('cubic-bezier', cubic_bezier_points=(0.42, 0, 0.58, 1)),
|
|
EasingFunction('cubic-bezier', cubic_bezier_points=(0.1, 0.2, 0.3, 0.4)),
|
|
)
|
|
cb('step-start', first=EasingFunction('steps', num_steps=1, jump_type='start'))
|
|
cb('steps(7, jump-none)', first=EasingFunction('steps', num_steps=7, jump_type='none'))
|
|
cb('linear(0, 0.25, 1)', first=EasingFunction('linear', linear_x=(0.0, 0.5, 1.0), linear_y=(0, 0.25, 1.0)))
|
|
cb('linear(0, 0.25 75%, 1)', first=EasingFunction('linear', linear_x=(0.0, 0.75, 1.0), linear_y=(0, 0.25, 1.0)))
|
|
cb('linear(0, 0.25 25% 75%, 1)', first=EasingFunction('linear', linear_x=(0.0, 0.25, 0.75, 1.0), linear_y=(0, 0.25, 0.25, 1.0)))
|
|
|
|
# test that easing functions give expected values
|
|
def ef(spec, tests, only_single=True, duration=0.5, accuracy=0):
|
|
cfv = p('cursor_blink_interval ' + spec).cursor_blink_interval
|
|
self.set_options({'cursor_blink_interval': cfv})
|
|
for t, expected in tests.items():
|
|
actual = test_cursor_blink_easing_function(t, only_single, duration)
|
|
if abs(actual - expected) > accuracy:
|
|
self.ae(expected, actual, f'Failed for {spec=} with {t=}: {expected} != {actual}')
|
|
|
|
ef('linear', {0: 1, 0.25: 0.5, 0.5: 0, 0.75: 0.5, 1: 1}, only_single=False)
|
|
|
|
ef('linear(0, 0.25 25% 75%, 1)', {0: 0, 0.25: 0.25, 0.3: 0.25, 0.75: 0.25, 1: 1})
|
|
linear_vals = {0: 0, 1: 1, 0.1234: 0.1234, 0.6453: 0.6453}
|
|
for spec in ('linear', 'linear(0, 1)', 'cubic-bezier(0, 0, 1, 1)', 'cubic-bezier(0.2, 0.2, 0.7, 0.7)'):
|
|
ef(spec, linear_vals)
|
|
# test an almost linear function to test cubic bezier implementation
|
|
ef('cubic-bezier(0.2, 0.2, 0.7, 0.71)', linear_vals, accuracy=0.01)
|
|
ef('cubic-bezier(0.23, 0.2, 0.7, 0.71)', linear_vals, accuracy=0.01)
|
|
|
|
ef('steps(5)', {0: 0, 0.1: 0, 0.3: 0.2, 0.9: 0.8})
|
|
ef('steps(5, start)', {0: 0.2, 0.1: 0.2, 0.3: 0.4, 0.9: 1})
|
|
ef('steps(4, jump-both)', {0: 0.2, 0.1: 0.2, 0.3: 0.4, 0.9: 1})
|
|
ef('steps(6, jump-none)', {0: 0, 0.1: 0.0, 0.3: 0.2, 0.9: 1})
|
|
|
|
# test various include modes
|
|
base = os.path.join(self.tdir, 'glob')
|
|
os.mkdir(base)
|
|
with open(os.path.join(base, 'fg'), 'w') as f:
|
|
print('foreground red', file=f)
|
|
opts = p(f'include {f.name}', num_err=0)
|
|
self.ae(opts.foreground, to_color('red'))
|
|
with open(os.path.join(self.tdir, 'bg'), 'w') as f:
|
|
print('background white', file=f)
|
|
print('globinclude glob/*', file=f)
|
|
print('envinclude ENVINCLUDE', file=f)
|
|
print('geninclude g.py', file=f)
|
|
print('geninclude g', file=f)
|
|
with open(os.path.join(self.tdir, 'g.py'), 'w') as g:
|
|
print('print("background_opacity .77")', file=g)
|
|
print('print("background_blur 77")', file=g)
|
|
with open(os.path.join(self.tdir, 'g'), 'w') as g:
|
|
print('#!/bin/sh', file=g)
|
|
print('echo background_image_linear y', file=g)
|
|
print('echo background_image_layout clamped', file=g)
|
|
os.chmod(g.fileno(), 0o700)
|
|
os.environ['ENVINCLUDE'] = 'cursor yellow'
|
|
opts = p(f'include {f.name}', num_err=0)
|
|
os.environ.pop('ENVINCLUDE')
|
|
self.ae(opts.foreground, to_color('red'))
|
|
self.ae(opts.background, to_color('white'))
|
|
self.ae(opts.cursor, to_color('yellow'))
|
|
self.ae(opts.background_opacity, 0.77)
|
|
self.ae(opts.background_blur, 77)
|
|
self.ae(opts.background_image_linear, True)
|
|
self.ae(opts.background_image_layout, 'clamped')
|
|
with open(os.path.join(self.tdir, 'a'), 'w') as a:
|
|
print('background red', file=a)
|
|
print('include b', file=a)
|
|
with open(os.path.join(self.tdir, 'b'), 'w') as a:
|
|
print('foreground red', file=a)
|
|
print('include a', file=a)
|
|
opts = p(f'include {a.name}', num_err=0, bad_line_num=1)
|
|
self.ae(opts.foreground, to_color('red'))
|
|
self.ae(opts.background, to_color('red'))
|
|
|
|
# remap_modifier. NOTE: parsing only. Whether the permutation is applied
|
|
# simultaneously rather than sequentially is a property of
|
|
# apply_modifier_remap() in C and is NOT observable from this dict - a
|
|
# sequential implementation would produce an identical one. That property is
|
|
# covered end to end by tests/test-remap-e2e.sh, which asserts that the
|
|
# encoded bytes actually exchange.
|
|
self.ae(p().remap_modifiers, {})
|
|
ctrl, hyper, sup = to_modifiers('ctrl'), to_modifiers('hyper'), to_modifiers('super')
|
|
self.ae(p('remap_modifiers ctrl:hyper').remap_modifiers, {ctrl: hyper})
|
|
# both directions of a swap are recorded
|
|
self.ae(p('remap_modifiers ctrl:hyper hyper:ctrl').remap_modifiers, {ctrl: hyper, hyper: ctrl})
|
|
# the last declaration for a given source wins, wherever it appears
|
|
self.ae(p('remap_modifiers ctrl:hyper', 'kitty_mod ctrl', 'remap_modifiers ctrl:super').remap_modifiers, {ctrl: sup})
|
|
# the destination may name more than one modifier
|
|
self.ae(p('remap_modifiers ctrl:ctrl+shift').remap_modifiers, {ctrl: to_modifiers('ctrl+shift')})
|
|
# every rejection must be REPORTED, never silently ignored: wrong arity, unknown
|
|
# or non-remappable modifier names, a source naming more than one modifier, and
|
|
# a mapping that does nothing
|
|
for bad in (
|
|
'remap_modifiers ctrl',
|
|
'remap_modifiers ctrl:hyper super',
|
|
'remap_modifiers ctrl:nosuchmod',
|
|
'remap_modifiers nosuchmod:ctrl',
|
|
'remap_modifiers ctrl+shift:hyper',
|
|
'remap_modifiers ctrl:none',
|
|
'remap_modifiers none:ctrl',
|
|
'remap_modifiers ctrl:kitty_mod',
|
|
'remap_modifiers kitty_mod:ctrl',
|
|
'remap_modifiers caps_lock:ctrl',
|
|
'remap_modifiers ctrl:num_lock',
|
|
'remap_modifiers ctrl:ctrl',
|
|
):
|
|
self.ae(p(bad, num_err=1).remap_modifiers, {}, f'not rejected: {bad}')
|