Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90164dfee7 | ||
|
|
73f641cb66 | ||
|
|
3d51835b9b | ||
|
|
f358d76409 | ||
|
|
ce486e9244 | ||
|
|
691b7215a0 | ||
|
|
5be45d0ff2 | ||
|
|
8a4b326127 | ||
|
|
051374cd55 | ||
|
|
71cd92da29 | ||
|
|
c2d3a0c8b4 | ||
|
|
fe8b151666 | ||
|
|
9e2d67f7a1 | ||
|
|
23ea3745ca | ||
|
|
d8d5a8fada | ||
|
|
21ce0e90bf | ||
|
|
ec420b8012 | ||
|
|
bd67899943 | ||
|
|
4b7600f3a5 | ||
|
|
5eabd69659 | ||
|
|
e83be21756 | ||
|
|
13c58536be | ||
|
|
512dccdbfa | ||
|
|
bb3a8453e0 | ||
|
|
e337fcaadc | ||
|
|
890a149a5d | ||
|
|
5b3ac259ce | ||
|
|
ce50043048 | ||
|
|
ebfbf6082f | ||
|
|
b44bec2207 | ||
|
|
576ce21fc8 | ||
|
|
2ba015d0be | ||
|
|
98ae2ac96a | ||
|
|
1c25ed7666 | ||
|
|
fd4c7a4ed2 | ||
|
|
32d275c138 | ||
|
|
f8924286ce | ||
|
|
6e77345263 | ||
|
|
1438c64b9e | ||
|
|
2ff1256aef | ||
|
|
5d169854c4 | ||
|
|
e61d328cb0 | ||
|
|
0c196888fb | ||
|
|
85efed8308 | ||
|
|
857c88a951 | ||
|
|
74b0a7f6f8 | ||
|
|
5da5d00aec | ||
|
|
eb59fae9b0 | ||
|
|
b6b38d6f44 | ||
|
|
4f4d9e0ae3 | ||
|
|
43c1776594 | ||
|
|
397638998b | ||
|
|
6ddbda00df | ||
|
|
df0bf74bfe | ||
|
|
5ee0651f56 | ||
|
|
9ec1c28b35 | ||
|
|
bfdbf06731 | ||
|
|
ca58b30c47 | ||
|
|
0fb2f383ac | ||
|
|
5d0eb1d330 | ||
|
|
ee77144e2b | ||
|
|
9d3a2cc219 | ||
|
|
e964ac86d5 | ||
|
|
bb26b3d549 | ||
|
|
71c8d82c33 | ||
|
|
9fffe0d710 | ||
|
|
51f8b22dfa | ||
|
|
63a36a1054 | ||
|
|
057084a708 | ||
|
|
79b130ed23 | ||
|
|
75e8b16ea3 | ||
|
|
1b35708d89 | ||
|
|
ebff343a55 | ||
|
|
c7b91e5f19 | ||
|
|
b9d52dfaf7 | ||
|
|
2c8f66586f | ||
|
|
219bf564f7 | ||
|
|
291be6f5a6 | ||
|
|
1da2344aa3 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -22,7 +22,7 @@ If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment details**
|
||||
```
|
||||
Press Ctrl+Shift+F6 in kitty, to copy debug output about kitty and its
|
||||
Press Ctrl+Shift+F6 (cmd+option+, on macOS) in kitty, to copy debug output about kitty and its
|
||||
configuration to the clipboard and paste it here.
|
||||
|
||||
On older versions of kitty, run kitty --debug-config instead
|
||||
|
||||
10
__main__.py
10
__main__.py
@@ -80,8 +80,14 @@ def edit_config_file(args: List[str]) -> None:
|
||||
|
||||
|
||||
def namespaced(args: List[str]) -> None:
|
||||
func = namespaced_entry_points[args[1]]
|
||||
func(args[1:])
|
||||
try:
|
||||
func = namespaced_entry_points[args[1]]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
func(args[1:])
|
||||
return
|
||||
raise SystemExit(f'{args[1]} is not a known entry point. Choices are: ' + ', '.join(namespaced_entry_points))
|
||||
|
||||
|
||||
entry_points = {
|
||||
|
||||
@@ -116,8 +116,8 @@ Toggle maximized :sc:`toggle_maximized`
|
||||
Input unicode character :sc:`input_unicode_character` (also :kbd:`^+⌘+space` on macOS)
|
||||
Click URL using the keyboard :sc:`open_url`
|
||||
Reset the terminal :sc:`reset_terminal`
|
||||
Reload :file:`kitty.conf` :sc:`reload_config_file`
|
||||
Debug :file:`kitty.conf` :sc:`debug_config`
|
||||
Reload :file:`kitty.conf` :sc:`reload_config_file` (also :kbd:`^+⌘+f5` on macOS)
|
||||
Debug :file:`kitty.conf` :sc:`debug_config` (also :kbd:`⌘+option+f6` on macOS)
|
||||
Pass current selection to program :sc:`pass_selection_to_program`
|
||||
Edit |kitty| config file :sc:`edit_config_file`
|
||||
Open a |kitty| shell :sc:`kitty_shell`
|
||||
|
||||
@@ -4,6 +4,62 @@ Changelog
|
||||
|kitty| is a feature-rich, cross-platform, *fast*, GPU based terminal.
|
||||
To update |kitty|, :doc:`follow the instructions <binary>`.
|
||||
|
||||
0.21.2 [2021-06-28]
|
||||
----------------------
|
||||
|
||||
- A new :opt:`adjust_baseline` option to adjust the vertical alignment of text
|
||||
inside a line (:pull:`3734`)
|
||||
|
||||
- A new :opt:`url_excluded_characters` option to exclude additional characters
|
||||
when detecting URLs under the mouse (:pull:`3738`)
|
||||
|
||||
- Fix a regression in 0.21.0 that broke rendering of private use Unicode symbols followed
|
||||
by spaces, when they also exist not followed by spaces (:iss:`3729`)
|
||||
|
||||
- ssh kitten: Support systems where the login shell is a non-POSIX shell
|
||||
(:iss:`3405`)
|
||||
|
||||
- ssh kitten: Add completion (:iss:`3760`)
|
||||
|
||||
- ssh kitten: Fix "Connection closed" message being printed by ssh when running
|
||||
remote commands
|
||||
|
||||
- Add support for the XTVERSION escape code
|
||||
|
||||
- macOS: Fix a regression in 0.21.0 that broke middle-click to paste from clipboard (:iss:`3730`)
|
||||
|
||||
- macOS: Fix shortcuts in the global menu bar responding slowly when cursor blink
|
||||
is disabled/timed out (:iss:`3693`)
|
||||
|
||||
- When displaying scrollback ensure that the window does not quit if the amount
|
||||
of scrollback is less than a screen and the user has the ``--quit-if-one-screen``
|
||||
option enabled for less (:iss:`3740`)
|
||||
|
||||
- Linux: Fix Emoji/bitmapped fonts not use able in symbol_map
|
||||
|
||||
- query terminal kitten: Allow querying font face and size information
|
||||
(:iss:`3756`)
|
||||
|
||||
- hyperlinked grep kitten: Fix context options not generating contextual output (:iss:`3759`)
|
||||
|
||||
- Allow using superscripts in tab titles (:iss:`3763`)
|
||||
|
||||
- Unicode input kitten: Fix searching when a word has more than 1024 matches (:iss:`3773`)
|
||||
|
||||
|
||||
0.21.1 [2021-06-14]
|
||||
----------------------
|
||||
|
||||
- macOS: Fix a regression in the previous release that broke rendering of
|
||||
strikeout (:iss:`3717`)
|
||||
|
||||
- macOS: Fix a crash when rendering ligatures larger than 128 characters
|
||||
(:iss:`3724`)
|
||||
|
||||
- Fix a regression in the previous release that could cause a crash when
|
||||
changing layouts and mousing (:iss:`3713`)
|
||||
|
||||
|
||||
0.21.0 [2021-06-12]
|
||||
----------------------
|
||||
|
||||
@@ -14,7 +70,7 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
|
||||
:kbd:`ctrl+shift`
|
||||
|
||||
- Allow remapping all mouse button press/release events to perform arbitrary
|
||||
actions. :ref:`See details <conf-kitty-mouse.mousemap>`.
|
||||
actions. :ref:`See details <conf-kitty-mouse.mousemap>` (:iss:`1033`)
|
||||
|
||||
- Support infinite length ligatures (:iss:`3504`)
|
||||
|
||||
|
||||
@@ -54,8 +54,11 @@ type it each time::
|
||||
|
||||
alias ssh="kitty +kitten ssh"
|
||||
|
||||
Remember to also setup :ref:`completion`.
|
||||
|
||||
If for some reason that does not work (typically because the server is using a
|
||||
non POSIX compliant shell), you can try using it with python instead::
|
||||
non POSIX compliant shell as ``/bin/sh``), you can try using it with ``python``
|
||||
instead::
|
||||
|
||||
kitty +kitten ssh use-python myserver
|
||||
|
||||
|
||||
@@ -101,7 +101,8 @@ features of the graphics protocol:
|
||||
import sys
|
||||
from base64 import standard_b64encode
|
||||
|
||||
def serialize_gr_command(cmd, payload=None):
|
||||
def serialize_gr_command(**cmd):
|
||||
payload = cmd.pop('payload', None)
|
||||
cmd = ','.join('{}={}'.format(k, v) for k, v in cmd.items())
|
||||
ans = []
|
||||
w = ans.append
|
||||
@@ -112,18 +113,17 @@ features of the graphics protocol:
|
||||
w(b'\033\\')
|
||||
return b''.join(ans)
|
||||
|
||||
def write_chunked(cmd, data):
|
||||
data = standard_b64encode(data)
|
||||
def write_chunked(**cmd):
|
||||
data = standard_b64encode(cmd.pop('data'))
|
||||
while data:
|
||||
chunk, data = data[:4096], data[4096:]
|
||||
m = 1 if data else 0
|
||||
cmd['m'] = m
|
||||
sys.stdout.buffer.write(serialize_gr_command(cmd, chunk))
|
||||
sys.stdout.buffer.write(serialize_gr_command(payload=chunk, m=m, **cmd))
|
||||
sys.stdout.flush()
|
||||
cmd.clear()
|
||||
|
||||
with open(sys.argv[-1], 'rb') as f:
|
||||
write_chunked({'a': 'T', 'f': 100}, f.read())
|
||||
write_chunked(a='T', f=100, data=f.read())
|
||||
|
||||
|
||||
Save this script as :file:`png.py`, then you can use it to display any PNG
|
||||
|
||||
@@ -345,8 +345,6 @@ Frequently Asked Questions
|
||||
|
||||
The list of Frequently Asked Questions (*FAQ*) is :doc:`available here <faq>`.
|
||||
|
||||
.. _completion:
|
||||
|
||||
|
||||
Cool integrations for kitty with other CLI tools
|
||||
--------------------------------------------------
|
||||
@@ -356,6 +354,8 @@ kitty provides extremely powerful interfaces such as :doc:`remote-control` and
|
||||
that allow it to be integrated with other tools seamlessly. For a list of such
|
||||
user created integrations, see: :doc:`integrations`.
|
||||
|
||||
.. _completion:
|
||||
|
||||
Completion for kitty
|
||||
---------------------------------
|
||||
|
||||
|
||||
@@ -41,12 +41,14 @@ If you are an application or library developer just interested in using this
|
||||
protocol to make keyboard handling simpler and more robust in your application,
|
||||
without too many changes, do the following:
|
||||
|
||||
#. Emit the escape code ``CSI > 1 u`` at application startup or when entering
|
||||
alternate screen mode
|
||||
#. Emit the escape code ``CSI > 1 u`` at application startup if using the main
|
||||
screen or when entering alternate screen mode, if using the alternate
|
||||
screen.
|
||||
#. All key events will now be sent in only a few forms to your application,
|
||||
that are easy to parse unambiguously.
|
||||
#. Emit the escape sequence ``CSI < u`` at application exit or just before
|
||||
leaving alternate screen mode to restore the previously used keyboard mode.
|
||||
#. Emit the escape sequence ``CSI < u`` at application exit if using the main
|
||||
screen or just before leaving alternate screen mode if using the alternate screen,
|
||||
to restore the previously used keyboard mode.
|
||||
|
||||
Key events will all be delivered to your application either as plain UTF-8
|
||||
text, or using the following escape codes, for those keys that do not produce
|
||||
@@ -266,6 +268,15 @@ and alternate screens. If a pop request is received that empties the stack,
|
||||
all flags are reset. If a push request is received and the stack is full, the
|
||||
oldest entry from the stack must be evicted.
|
||||
|
||||
.. note:: The main and alternate screens in the terminal emulator must maintain
|
||||
their own, independent, keyboard mode stacks. This is so that a program that
|
||||
uses the alternate screen such as an editor, can change the keyboard mode
|
||||
in the alternate screen only, without affecting the mode in the main screen
|
||||
or even knowing what that mode is. Without this, and if no stack is
|
||||
implemented for keyboard modes (such as in some legacy terminal emulators)
|
||||
the editor would have to somehow know what the keyboard mode of the main
|
||||
screen is and restore to that mode on exit.
|
||||
|
||||
.. note:: In the interests of interoperation, the XTerm specific sequences
|
||||
`CSI > 4; 1 m` and `CSI > 4; 0 m` are treated as `CSI > 1 u` and `CSI < 1 u`.
|
||||
These codes cause XTerm to use the CSI u encoding for more keys and are therefore
|
||||
|
||||
@@ -185,6 +185,104 @@ The output of print statements will go to the ``STDOUT`` of the kitty process.
|
||||
So if you run kitty from another kitty instance, the output will be visible
|
||||
in the first kitty instance.
|
||||
|
||||
Adding options to kittens
|
||||
----------------------------
|
||||
|
||||
If you would like to use kitty's config framework to make your kittens
|
||||
configurable, you will need some boilerplate. In the directory
|
||||
of your kitten make the following files.
|
||||
|
||||
:file:`kitten_options_definition.py`
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from kitty.conf.types import Action, Definition
|
||||
|
||||
definition = Definition(
|
||||
'!kitten_options_utils',
|
||||
Action(
|
||||
'map', 'parse_map',
|
||||
{'key_definitions': 'kitty.conf.utils.KittensKeyMap'},
|
||||
['kitty.types.ParsedShortcut', 'kitty.conf.utils.KeyAction']
|
||||
),
|
||||
)
|
||||
|
||||
agr = definition.add_group
|
||||
egr = definition.end_group
|
||||
opt = definition.add_option
|
||||
map = definition.add_map
|
||||
|
||||
# main options {{{
|
||||
agr('main', 'Main')
|
||||
|
||||
opt('some_option', '33',
|
||||
option_type='some_option_parser',
|
||||
long_text='''
|
||||
Help text for this option
|
||||
'''
|
||||
)
|
||||
egr() # }}}
|
||||
|
||||
# shortcuts {{{
|
||||
agr('shortcuts', 'Keyboard shortcuts')
|
||||
|
||||
map('Quit', 'quit q quit')
|
||||
egr() # }}}
|
||||
|
||||
|
||||
:file:`kitten_options_utils.py`
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from kitty.conf.utils import KittensKeyDefinition, key_func, parse_kittens_key
|
||||
|
||||
func_with_args, args_funcs = key_func()
|
||||
FuncArgsType = Tuple[str, Sequence[Any]]
|
||||
|
||||
def some_option_parser(val: str) -> int:
|
||||
return int(val) + 3000
|
||||
|
||||
def parse_map(val: str) -> Iterable[KittensKeyDefinition]:
|
||||
x = parse_kittens_key(val, args_funcs)
|
||||
if x is not None:
|
||||
yield x
|
||||
|
||||
Then run::
|
||||
|
||||
kitty +runpy 'from kitty.conf.generate import main; main()' /path/to/kitten_options_definition.py
|
||||
|
||||
You can parse and read the options in your kitten using the following code:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from .kitten_options_types import Options, defaults
|
||||
from kitty.conf.utils import load_config as _load_config, parse_config_base
|
||||
from typing import Optional, Iterable, Dict, Any
|
||||
|
||||
def load_config(*paths: str, overrides: Optional[Iterable[str]] = None) -> Options:
|
||||
from .kitten_options_parse import (
|
||||
create_result_dict, merge_result_dicts, parse_conf_item
|
||||
)
|
||||
|
||||
def parse_config(lines: Iterable[str]) -> Dict[str, Any]:
|
||||
ans: Dict[str, Any] = create_result_dict()
|
||||
parse_config_base(
|
||||
lines,
|
||||
parse_conf_item,
|
||||
ans,
|
||||
)
|
||||
return ans
|
||||
|
||||
overrides = tuple(overrides) if overrides is not None else ()
|
||||
opts_dict, paths = _load_config(defaults, parse_config, merge_result_dicts, *paths, overrides=overrides)
|
||||
opts = Options(opts_dict)
|
||||
opts.config_paths = paths
|
||||
opts.config_overrides = overrides
|
||||
return opts
|
||||
|
||||
See the code for the builtin diff kitten for examples of creating more options
|
||||
and keyboard shortcuts.
|
||||
|
||||
.. _external_kittens:
|
||||
|
||||
Kittens created by kitty users
|
||||
|
||||
420
gen-config.py
420
gen-config.py
@@ -2,426 +2,8 @@
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import inspect
|
||||
import os
|
||||
import pprint
|
||||
import re
|
||||
import textwrap
|
||||
from typing import (
|
||||
Any, Callable, Dict, Iterator, List, Set, Tuple, Union, get_type_hints
|
||||
)
|
||||
|
||||
from kitty.conf.types import Definition, MultiOption, Option, unset
|
||||
|
||||
|
||||
def chunks(lst: List, n: int) -> Iterator[List]:
|
||||
for i in range(0, len(lst), n):
|
||||
yield lst[i:i + n]
|
||||
|
||||
|
||||
def atoi(text: str) -> str:
|
||||
return f'{int(text):08d}' if text.isdigit() else text
|
||||
|
||||
|
||||
def natural_keys(text: str) -> Tuple[str, ...]:
|
||||
return tuple(atoi(c) for c in re.split(r'(\d+)', text))
|
||||
|
||||
|
||||
def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
|
||||
class_lines: List[str] = []
|
||||
tc_lines: List[str] = []
|
||||
a = class_lines.append
|
||||
t = tc_lines.append
|
||||
a('class Options:')
|
||||
t('class Parser:')
|
||||
choices = {}
|
||||
imports: Set[Tuple[str, str]] = set()
|
||||
tc_imports: Set[Tuple[str, str]] = set()
|
||||
|
||||
def type_name(x: type) -> str:
|
||||
ans = x.__name__
|
||||
if x.__module__ and x.__module__ != 'builtins':
|
||||
imports.add((x.__module__, x.__name__))
|
||||
return ans
|
||||
|
||||
def option_type_as_str(x: Any) -> str:
|
||||
if hasattr(x, '__name__'):
|
||||
return type_name(x)
|
||||
ans = repr(x)
|
||||
ans = ans.replace('NoneType', 'None')
|
||||
return ans
|
||||
|
||||
def option_type_data(option: Union[Option, MultiOption]) -> Tuple[Callable, str]:
|
||||
func = option.parser_func
|
||||
if func.__module__ == 'builtins':
|
||||
return func, func.__name__
|
||||
th = get_type_hints(func)
|
||||
rettype = th['return']
|
||||
typ = option_type_as_str(rettype)
|
||||
if isinstance(option, MultiOption):
|
||||
typ = typ[typ.index('[') + 1:-1]
|
||||
typ = typ.replace('Tuple', 'Dict', 1)
|
||||
return func, typ
|
||||
|
||||
is_mutiple_vars = {}
|
||||
option_names = set()
|
||||
color_table = list(map(str, range(256)))
|
||||
|
||||
def parser_function_declaration(option_name: str) -> None:
|
||||
t('')
|
||||
t(f' def {option_name}(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:')
|
||||
|
||||
for option in sorted(defn.iter_all_options(), key=lambda a: natural_keys(a.name)):
|
||||
option_names.add(option.name)
|
||||
parser_function_declaration(option.name)
|
||||
if isinstance(option, MultiOption):
|
||||
mval: Dict[str, Dict[str, Any]] = {'macos': {}, 'linux': {}, '': {}}
|
||||
func, typ = option_type_data(option)
|
||||
for val in option:
|
||||
if val.add_to_default:
|
||||
gr = mval[val.only]
|
||||
for k, v in func(val.defval_as_str):
|
||||
gr[k] = v
|
||||
is_mutiple_vars[option.name] = typ, mval
|
||||
sig = inspect.signature(func)
|
||||
tc_imports.add((func.__module__, func.__name__))
|
||||
if len(sig.parameters) == 1:
|
||||
t(f' for k, v in {func.__name__}(val):')
|
||||
t(f' ans["{option.name}"][k] = v')
|
||||
else:
|
||||
t(f' for k, v in {func.__name__}(val, ans["{option.name}"]):')
|
||||
t(f' ans["{option.name}"][k] = v')
|
||||
continue
|
||||
|
||||
if option.choices:
|
||||
typ = 'typing.Literal[{}]'.format(', '.join(repr(x) for x in option.choices))
|
||||
ename = f'choices_for_{option.name}'
|
||||
choices[ename] = typ
|
||||
typ = ename
|
||||
func = str
|
||||
elif defn.has_color_table and option.is_color_table_color:
|
||||
func, typ = option_type_data(option)
|
||||
t(f' ans[{option.name!r}] = {func.__name__}(val)')
|
||||
tc_imports.add((func.__module__, func.__name__))
|
||||
cnum = int(option.name[5:])
|
||||
color_table[cnum] = '0x{:06x}'.format(func(option.defval_as_string).__int__())
|
||||
continue
|
||||
else:
|
||||
func, typ = option_type_data(option)
|
||||
try:
|
||||
params = inspect.signature(func).parameters
|
||||
except Exception:
|
||||
params = {}
|
||||
if 'dict_with_parse_results' in params:
|
||||
t(f' {func.__name__}(val, ans)')
|
||||
else:
|
||||
t(f' ans[{option.name!r}] = {func.__name__}(val)')
|
||||
if func.__module__ != 'builtins':
|
||||
tc_imports.add((func.__module__, func.__name__))
|
||||
|
||||
defval = repr(func(option.defval_as_string))
|
||||
if option.macos_defval is not unset:
|
||||
md = repr(func(option.macos_defval))
|
||||
defval = f'{md} if is_macos else {defval}'
|
||||
imports.add(('kitty.constants', 'is_macos'))
|
||||
a(f' {option.name}: {typ} = {defval}')
|
||||
if option.choices:
|
||||
t(' val = val.lower()')
|
||||
t(f' if val not in self.choices_for_{option.name}:')
|
||||
t(f' raise ValueError(f"The value {{val}} is not a valid choice for {option.name}")')
|
||||
t(f' ans["{option.name}"] = val')
|
||||
t('')
|
||||
t(f' choices_for_{option.name} = frozenset({option.choices!r})')
|
||||
|
||||
for option_name, (typ, mval) in is_mutiple_vars.items():
|
||||
a(f' {option_name}: {typ} = ' '{}')
|
||||
|
||||
for parser, aliases in defn.deprecations.items():
|
||||
for alias in aliases:
|
||||
parser_function_declaration(alias)
|
||||
tc_imports.add((parser.__module__, parser.__name__))
|
||||
t(f' {parser.__name__}({alias!r}, val, ans)')
|
||||
|
||||
action_parsers = {}
|
||||
|
||||
def resolve_import(ftype: str) -> str:
|
||||
if '.' in ftype:
|
||||
fmod, ftype = ftype.rpartition('.')[::2]
|
||||
else:
|
||||
fmod = f'{loc}.options.utils'
|
||||
imports.add((fmod, ftype))
|
||||
return ftype
|
||||
|
||||
for aname, action in defn.actions.items():
|
||||
option_names.add(aname)
|
||||
action_parsers[aname] = func = action.parser_func
|
||||
th = get_type_hints(func)
|
||||
rettype = th['return']
|
||||
typ = option_type_as_str(rettype)
|
||||
typ = typ[typ.index('[') + 1:-1]
|
||||
a(f' {aname}: typing.List[{typ}] = []')
|
||||
for imp in action.imports:
|
||||
resolve_import(imp)
|
||||
for fname, ftype in action.fields.items():
|
||||
ftype = resolve_import(ftype)
|
||||
a(f' {fname}: {ftype} = ' '{}')
|
||||
parser_function_declaration(aname)
|
||||
t(f' for k in {func.__name__}(val):')
|
||||
t(f' ans[{aname!r}].append(k)')
|
||||
tc_imports.add((func.__module__, func.__name__))
|
||||
|
||||
if defn.has_color_table:
|
||||
imports.add(('array', 'array'))
|
||||
a(' color_table: array = array("L", (')
|
||||
for grp in chunks(color_table, 8):
|
||||
a(' ' + ', '.join(grp) + ',')
|
||||
a(' ))')
|
||||
|
||||
a(' config_paths: typing.Tuple[str, ...] = ()')
|
||||
a(' config_overrides: typing.Tuple[str, ...] = ()')
|
||||
a('')
|
||||
a(' def __init__(self, options_dict: typing.Optional[typing.Dict[str, typing.Any]] = None) -> None:')
|
||||
a(' if options_dict is not None:')
|
||||
a(' for key in option_names:')
|
||||
a(' setattr(self, key, options_dict[key])')
|
||||
|
||||
a('')
|
||||
a(' @property')
|
||||
a(' def _fields(self) -> typing.Tuple[str, ...]:')
|
||||
a(' return option_names')
|
||||
|
||||
a('')
|
||||
a(' def __iter__(self) -> typing.Iterator[str]:')
|
||||
a(' return iter(self._fields)')
|
||||
|
||||
a('')
|
||||
a(' def __len__(self) -> int:')
|
||||
a(' return len(self._fields)')
|
||||
|
||||
a('')
|
||||
a(' def _copy_of_val(self, name: str) -> typing.Any:')
|
||||
a(' ans = getattr(self, name)')
|
||||
a(' if isinstance(ans, dict):\n ans = ans.copy()')
|
||||
a(' elif isinstance(ans, list):\n ans = ans[:]')
|
||||
a(' return ans')
|
||||
|
||||
a('')
|
||||
a(' def _asdict(self) -> typing.Dict[str, typing.Any]:')
|
||||
a(' return {k: self._copy_of_val(k) for k in self}')
|
||||
|
||||
a('')
|
||||
a(' def _replace(self, **kw: typing.Any) -> "Options":')
|
||||
a(' ans = Options()')
|
||||
a(' for name in self:')
|
||||
a(' setattr(ans, name, self._copy_of_val(name))')
|
||||
a(' for name, val in kw.items():')
|
||||
a(' setattr(ans, name, val)')
|
||||
a(' return ans')
|
||||
|
||||
a('')
|
||||
a(' def __getitem__(self, key: typing.Union[int, str]) -> typing.Any:')
|
||||
a(' k = option_names[key] if isinstance(key, int) else key')
|
||||
a(' try:')
|
||||
a(' return getattr(self, k)')
|
||||
a(' except AttributeError:')
|
||||
a(' pass')
|
||||
a(' raise KeyError(f"No option named: {k}")')
|
||||
|
||||
if defn.has_color_table:
|
||||
a('')
|
||||
a(' def __getattr__(self, key: str) -> typing.Any:')
|
||||
a(' if key.startswith("color"):')
|
||||
a(' q = key[5:]')
|
||||
a(' if q.isdigit():')
|
||||
a(' k = int(q)')
|
||||
a(' if 0 <= k <= 255:')
|
||||
a(' x = self.color_table[k]')
|
||||
a(' return Color((x >> 16) & 255, (x >> 8) & 255, x & 255)')
|
||||
a(' raise AttributeError(key)')
|
||||
a('')
|
||||
a(' def __setattr__(self, key: str, val: typing.Any) -> typing.Any:')
|
||||
a(' if key.startswith("color"):')
|
||||
a(' q = key[5:]')
|
||||
a(' if q.isdigit():')
|
||||
a(' k = int(q)')
|
||||
a(' if 0 <= k <= 255:')
|
||||
a(' self.color_table[k] = int(val)')
|
||||
a(' return')
|
||||
a(' object.__setattr__(self, key, val)')
|
||||
|
||||
a('')
|
||||
a('')
|
||||
a('defaults = Options()')
|
||||
for option_name, (typ, mval) in is_mutiple_vars.items():
|
||||
a(f'defaults.{option_name} = {mval[""]!r}')
|
||||
if mval['macos']:
|
||||
imports.add(('kitty.constants', 'is_macos'))
|
||||
a('if is_macos:')
|
||||
a(f' defaults.{option_name}.update({mval["macos"]!r}')
|
||||
if mval['macos']:
|
||||
imports.add(('kitty.constants', 'is_macos'))
|
||||
a('if not is_macos:')
|
||||
a(f' defaults.{option_name}.update({mval["linux"]!r}')
|
||||
|
||||
for aname, func in action_parsers.items():
|
||||
a(f'defaults.{aname} = [')
|
||||
only: Dict[str, List[Tuple[str, Callable]]] = {}
|
||||
for sc in defn.iter_all_maps(aname):
|
||||
if not sc.add_to_default:
|
||||
continue
|
||||
text = sc.parseable_text
|
||||
if sc.only:
|
||||
only.setdefault(sc.only, []).append((text, func))
|
||||
else:
|
||||
for val in func(text):
|
||||
a(f' # {sc.name}')
|
||||
a(f' {val!r},')
|
||||
a(']')
|
||||
if only:
|
||||
imports.add(('kitty.constants', 'is_macos'))
|
||||
for cond, items in only.items():
|
||||
cond = 'is_macos' if cond == 'macos' else 'not is_macos'
|
||||
a(f'if {cond}:')
|
||||
for (text, func) in items:
|
||||
for val in func(text):
|
||||
a(f' defaults.{aname}.append({val!r})')
|
||||
|
||||
t('')
|
||||
t('')
|
||||
t('def create_result_dict() -> typing.Dict[str, typing.Any]:')
|
||||
t(' return {')
|
||||
for oname in is_mutiple_vars:
|
||||
t(f' {oname!r}: {{}},')
|
||||
for aname in defn.actions:
|
||||
t(f' {aname!r}: [],')
|
||||
t(' }')
|
||||
|
||||
t('')
|
||||
t('')
|
||||
t(f'actions = frozenset({tuple(defn.actions)!r})')
|
||||
t('')
|
||||
t('')
|
||||
t('def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:')
|
||||
t(' ans = {}')
|
||||
t(' for k, v in defaults.items():')
|
||||
t(' if isinstance(v, dict):')
|
||||
t(' ans[k] = merge_dicts(v, vals.get(k, {}))')
|
||||
t(' elif k in actions:')
|
||||
t(' ans[k] = v + vals.get(k, [])')
|
||||
t(' else:')
|
||||
t(' ans[k] = vals.get(k, v)')
|
||||
t(' return ans')
|
||||
tc_imports.add(('kitty.conf.utils', 'merge_dicts'))
|
||||
|
||||
t('')
|
||||
t('')
|
||||
t('parser = Parser()')
|
||||
t('')
|
||||
t('')
|
||||
t('def parse_conf_item(key: str, val: str, ans: typing.Dict[str, typing.Any]) -> bool:')
|
||||
t(' func = getattr(parser, key, None)')
|
||||
t(' if func is not None:')
|
||||
t(' func(val, ans)')
|
||||
t(' return True')
|
||||
t(' return False')
|
||||
|
||||
preamble = ['# generated by gen-config.py DO NOT edit', '# vim:fileencoding=utf-8', '']
|
||||
a = preamble.append
|
||||
|
||||
def output_imports(imports: Set, add_module_imports: bool = True) -> None:
|
||||
a('import typing')
|
||||
seen_mods = {'typing'}
|
||||
mmap: Dict[str, List[str]] = {}
|
||||
for mod, name in imports:
|
||||
mmap.setdefault(mod, []).append(name)
|
||||
for mod in sorted(mmap):
|
||||
names = sorted(mmap[mod])
|
||||
lines = textwrap.wrap(', '.join(names), 100)
|
||||
if len(lines) == 1:
|
||||
s = lines[0]
|
||||
else:
|
||||
s = '\n '.join(lines)
|
||||
s = f'(\n {s}\n)'
|
||||
a(f'from {mod} import {s}')
|
||||
if add_module_imports and mod not in seen_mods and mod != s:
|
||||
a(f'import {mod}')
|
||||
seen_mods.add(mod)
|
||||
|
||||
output_imports(imports)
|
||||
a('')
|
||||
if choices:
|
||||
a('if typing.TYPE_CHECKING:')
|
||||
for name, cdefn in choices.items():
|
||||
a(f' {name} = {cdefn}')
|
||||
a('else:')
|
||||
for name in choices:
|
||||
a(f' {name} = str')
|
||||
|
||||
a('')
|
||||
a('option_names = ( # {{''{')
|
||||
a(' ' + pprint.pformat(tuple(sorted(option_names, key=natural_keys)))[1:] + ' # }}''}')
|
||||
class_def = '\n'.join(preamble + ['', ''] + class_lines)
|
||||
|
||||
preamble = ['# generated by gen-config.py DO NOT edit', '# vim:fileencoding=utf-8', '']
|
||||
a = preamble.append
|
||||
output_imports(tc_imports, False)
|
||||
|
||||
return class_def, '\n'.join(preamble + ['', ''] + tc_lines)
|
||||
|
||||
|
||||
def generate_c_conversion(loc: str, ctypes: List[Option]) -> str:
|
||||
lines: List[str] = []
|
||||
basic_converters = {
|
||||
'int': 'PyLong_AsLong', 'uint': 'PyLong_AsUnsignedLong', 'bool': 'PyObject_IsTrue',
|
||||
'float': 'PyFloat_AsFloat', 'double': 'PyFloat_AsDouble',
|
||||
'time': 'parse_s_double_to_monotonic_t', 'time-ms': 'parse_ms_long_to_monotonic_t'
|
||||
}
|
||||
|
||||
for opt in ctypes:
|
||||
lines.append('')
|
||||
lines.append(f'static void\nconvert_from_python_{opt.name}(PyObject *val, Options *opts) ''{')
|
||||
is_special = opt.ctype.startswith('!')
|
||||
if is_special:
|
||||
func = opt.ctype[1:]
|
||||
lines.append(f' {func}(val, opts);')
|
||||
else:
|
||||
func = basic_converters.get(opt.ctype, opt.ctype)
|
||||
lines.append(f' opts->{opt.name} = {func}(val);')
|
||||
lines.append('}')
|
||||
lines.append('')
|
||||
lines.append(f'static void\nconvert_from_opts_{opt.name}(PyObject *py_opts, Options *opts) ''{')
|
||||
lines.append(f' PyObject *ret = PyObject_GetAttrString(py_opts, "{opt.name}");')
|
||||
lines.append(' if (ret == NULL) return;')
|
||||
lines.append(f' convert_from_python_{opt.name}(ret, opts);')
|
||||
lines.append(' Py_DECREF(ret);')
|
||||
lines.append('}')
|
||||
|
||||
lines.append('')
|
||||
lines.append('static bool\nconvert_opts_from_python_opts(PyObject *py_opts, Options *opts) ''{')
|
||||
for opt in ctypes:
|
||||
lines.append(f' convert_from_opts_{opt.name}(py_opts, opts);')
|
||||
lines.append(' if (PyErr_Occurred()) return false;')
|
||||
lines.append(' return true;')
|
||||
lines.append('}')
|
||||
|
||||
preamble = ['// generated by gen-config.py DO NOT edit', '// vim:fileencoding=utf-8', '#pragma once', '#include "to-c.h"']
|
||||
return '\n'.join(preamble + ['', ''] + lines)
|
||||
|
||||
|
||||
def write_output(loc: str, defn: Definition) -> None:
|
||||
cls, tc = generate_class(defn, loc)
|
||||
with open(os.path.join(*loc.split('.'), 'options', 'types.py'), 'w') as f:
|
||||
f.write(cls + '\n')
|
||||
with open(os.path.join(*loc.split('.'), 'options', 'parse.py'), 'w') as f:
|
||||
f.write(tc + '\n')
|
||||
ctypes = []
|
||||
for opt in defn.root_group.iter_all_non_groups():
|
||||
if isinstance(opt, Option) and opt.ctype:
|
||||
ctypes.append(opt)
|
||||
if ctypes:
|
||||
c = generate_c_conversion(loc, ctypes)
|
||||
with open(os.path.join(*loc.split('.'), 'options', 'to-c-generated.h'), 'w') as f:
|
||||
f.write(c + '\n')
|
||||
from kitty.conf.generate import write_output
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
@@ -47,7 +47,7 @@ static char* getDisplayName(CGDirectDisplayID displayID)
|
||||
io_service_t service;
|
||||
CFDictionaryRef info;
|
||||
|
||||
if (IOServiceGetMatchingServices(kIOMasterPortDefault,
|
||||
if (IOServiceGetMatchingServices(kIOMainPortDefault,
|
||||
IOServiceMatching("IODisplayConnect"),
|
||||
&it) != 0)
|
||||
{
|
||||
@@ -245,7 +245,7 @@ static double getFallbackRefreshRate(CGDirectDisplayID displayID)
|
||||
io_iterator_t it;
|
||||
io_service_t service;
|
||||
|
||||
if (IOServiceGetMatchingServices(kIOMasterPortDefault,
|
||||
if (IOServiceGetMatchingServices(kIOMainPortDefault,
|
||||
IOServiceMatching("IOFramebuffer"),
|
||||
&it) != 0)
|
||||
{
|
||||
|
||||
3
glfw/cocoa_platform.h
vendored
3
glfw/cocoa_platform.h
vendored
@@ -39,6 +39,9 @@ typedef void* CVDisplayLinkRef;
|
||||
// NOTE: Many Cocoa enum values have been renamed and we need to build across
|
||||
// SDK versions where one is unavailable or the other deprecated
|
||||
// We use the newer names in code and these macros to handle compatibility
|
||||
#if (MAC_OS_X_VERSION_MAX_ALLOWED < 120000) // Before macOS 12 Monterey
|
||||
#define kIOMainPortDefault kIOMasterPortDefault
|
||||
#endif
|
||||
#if MAC_OS_X_VERSION_MAX_ALLOWED < 101200
|
||||
#define NSBitmapFormatAlphaNonpremultiplied NSAlphaNonpremultipliedBitmapFormat
|
||||
#define NSEventMaskAny NSAnyEventMask
|
||||
|
||||
@@ -29,7 +29,7 @@ def main() -> None:
|
||||
write: Callable[[bytes], None] = cast(Callable[[bytes], None], sys.stdout.buffer.write)
|
||||
sgr_pat = re.compile(br'\x1b\[.*?m')
|
||||
osc_pat = re.compile(b'\x1b\\].*?\x1b\\\\')
|
||||
num_pat = re.compile(b'^(\\d+):')
|
||||
num_pat = re.compile(br'^(\d+)[:-]')
|
||||
|
||||
in_result: bytes = b''
|
||||
hostname = socket.gethostname().encode('utf-8')
|
||||
@@ -46,6 +46,8 @@ def main() -> None:
|
||||
m = num_pat.match(clean_line)
|
||||
if m is not None:
|
||||
write_hyperlink(write, in_result, line, frag=m.group(1))
|
||||
else:
|
||||
write(line)
|
||||
else:
|
||||
if line.strip():
|
||||
path = quote_from_bytes(os.path.abspath(clean_line)).encode('utf-8')
|
||||
@@ -55,8 +57,9 @@ def main() -> None:
|
||||
write(line)
|
||||
except KeyboardInterrupt:
|
||||
p.send_signal(signal.SIGINT)
|
||||
p.stdout.close()
|
||||
except EOFError:
|
||||
pass
|
||||
finally:
|
||||
p.stdout.close()
|
||||
raise SystemExit(p.wait())
|
||||
|
||||
|
||||
@@ -4,22 +4,27 @@
|
||||
|
||||
import re
|
||||
import sys
|
||||
from binascii import unhexlify, hexlify
|
||||
from binascii import hexlify, unhexlify
|
||||
from contextlib import suppress
|
||||
from typing import Dict, Iterable, List, Type
|
||||
from typing import Dict, Iterable, List, Type, Optional
|
||||
|
||||
from kitty.cli import parse_args
|
||||
from kitty.cli_stub import QueryTerminalCLIOptions
|
||||
from kitty.constants import appname
|
||||
from kitty.utils import TTYIO
|
||||
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:
|
||||
name: str = ''
|
||||
ans: str = ''
|
||||
query_name: str = ''
|
||||
help_text: str = ''
|
||||
override_query_name: str = ''
|
||||
|
||||
@property
|
||||
def query_name(self) -> str:
|
||||
return self.override_query_name or f'kitty-query-{self.name}'
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.encoded_query_name = hexlify(self.query_name.encode('utf-8')).decode('ascii')
|
||||
@@ -45,6 +50,10 @@ class Query:
|
||||
def output_line(self) -> str:
|
||||
return self.ans
|
||||
|
||||
@staticmethod
|
||||
def get_result(opts: Options) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
all_queries: Dict[str, Type[Query]] = {}
|
||||
|
||||
@@ -57,23 +66,99 @@ def query(cls: Type[Query]) -> Type[Query]:
|
||||
@query
|
||||
class TerminalName(Query):
|
||||
name: str = 'name'
|
||||
query_name: str = 'TN'
|
||||
override_query_name: str = 'name'
|
||||
help_text: str = f'Terminal name ({names[0]})'
|
||||
|
||||
@staticmethod
|
||||
def get_result(opts: Options) -> str:
|
||||
return appname
|
||||
|
||||
|
||||
@query
|
||||
class TerminalVersion(Query):
|
||||
name: str = 'version'
|
||||
query_name: str = 'kitty-query-version'
|
||||
help_text: str = 'Terminal version, for e.g.: 0.19.2'
|
||||
|
||||
@staticmethod
|
||||
def get_result(opts: Options) -> str:
|
||||
return str_version
|
||||
|
||||
|
||||
@query
|
||||
class AllowHyperlinks(Query):
|
||||
name: str = 'allow_hyperlinks'
|
||||
query_name: str = 'kitty-query-allow_hyperlinks'
|
||||
help_text: str = 'yes, no or ask'
|
||||
|
||||
@staticmethod
|
||||
def get_result(opts: Options) -> str:
|
||||
return 'ask' if opts.allow_hyperlinks == 0b11 else ('yes' if opts.allow_hyperlinks else 'no')
|
||||
|
||||
|
||||
@query
|
||||
class FontFamily(Query):
|
||||
name: str = 'font_family'
|
||||
help_text: str = 'The current font\'s PostScript name'
|
||||
|
||||
@staticmethod
|
||||
def get_result(opts: Options) -> str:
|
||||
from kitty.fast_data_types import current_fonts
|
||||
cf = current_fonts()
|
||||
return str(cf['medium'].display_name())
|
||||
|
||||
|
||||
@query
|
||||
class BoldFont(Query):
|
||||
name: str = 'bold_font'
|
||||
help_text: str = 'The current bold font\'s PostScript name'
|
||||
|
||||
@staticmethod
|
||||
def get_result(opts: Options) -> str:
|
||||
from kitty.fast_data_types import current_fonts
|
||||
cf = current_fonts()
|
||||
return str(cf['bold'].display_name())
|
||||
|
||||
|
||||
@query
|
||||
class ItalicFont(Query):
|
||||
name: str = 'italic_font'
|
||||
help_text: str = 'The current italic font\'s PostScript name'
|
||||
|
||||
@staticmethod
|
||||
def get_result(opts: Options) -> str:
|
||||
from kitty.fast_data_types import current_fonts
|
||||
cf = current_fonts()
|
||||
return str(cf['italic'].display_name())
|
||||
|
||||
|
||||
@query
|
||||
class BiFont(Query):
|
||||
name: str = 'bold_italic_font'
|
||||
help_text: str = 'The current bold-italic font\'s PostScript name'
|
||||
|
||||
@staticmethod
|
||||
def get_result(opts: Options) -> str:
|
||||
from kitty.fast_data_types import current_fonts
|
||||
cf = current_fonts()
|
||||
return str(cf['bi'].display_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)'
|
||||
|
||||
@staticmethod
|
||||
def get_result(opts: Options) -> str:
|
||||
return f'{opts.font_size:g}'
|
||||
|
||||
|
||||
def get_result(name: str) -> 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, cli_opts: QueryTerminalCLIOptions) -> Dict[str, str]:
|
||||
actions = tuple(all_queries[x]() for x in queries)
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from functools import partial
|
||||
from typing import Any, Dict, FrozenSet, List, TYPE_CHECKING, cast
|
||||
from typing import TYPE_CHECKING, Any, Dict, FrozenSet, Generator, List, cast
|
||||
|
||||
from kitty.types import run_once
|
||||
|
||||
@@ -30,21 +31,29 @@ def path_to_custom_kitten(config_dir: str, kitten: str) -> str:
|
||||
return path
|
||||
|
||||
|
||||
@contextmanager
|
||||
def preserve_sys_path() -> Generator[None, None, None]:
|
||||
orig = sys.path[:]
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if sys.path != orig:
|
||||
del sys.path[:]
|
||||
sys.path.extend(orig)
|
||||
|
||||
|
||||
def import_kitten_main_module(config_dir: str, kitten: str) -> Dict[str, Any]:
|
||||
if kitten.endswith('.py'):
|
||||
path_modified = False
|
||||
path = path_to_custom_kitten(config_dir, kitten)
|
||||
if os.path.dirname(path):
|
||||
sys.path.insert(0, os.path.dirname(path))
|
||||
path_modified = True
|
||||
with open(path) as f:
|
||||
src = f.read()
|
||||
code = compile(src, path, 'exec')
|
||||
g = {'__name__': 'kitten'}
|
||||
exec(code, g)
|
||||
hr = g.get('handle_result', lambda *a, **kw: None)
|
||||
if path_modified:
|
||||
del sys.path[0]
|
||||
with preserve_sys_path():
|
||||
path = path_to_custom_kitten(config_dir, kitten)
|
||||
if os.path.dirname(path):
|
||||
sys.path.insert(0, os.path.dirname(path))
|
||||
with open(path) as f:
|
||||
src = f.read()
|
||||
code = compile(src, path, 'exec')
|
||||
g = {'__name__': 'kitten'}
|
||||
exec(code, g)
|
||||
hr = g.get('handle_result', lambda *a, **kw: None)
|
||||
return {'start': g['main'], 'end': hr}
|
||||
|
||||
kitten = resolved_kitten(kitten)
|
||||
@@ -155,6 +164,14 @@ def get_kitten_cli_docs(kitten: str) -> Any:
|
||||
return ans
|
||||
|
||||
|
||||
def get_kitten_completer(kitten: str) -> Any:
|
||||
run_kitten(kitten, run_name='__completer__')
|
||||
ans = getattr(sys, 'kitten_completer', None)
|
||||
if ans is not None:
|
||||
delattr(sys, 'kitten_completer')
|
||||
return ans
|
||||
|
||||
|
||||
def get_kitten_conf_docs(kitten: str) -> Definition:
|
||||
setattr(sys, 'options_definition', None)
|
||||
run_kitten(kitten, run_name='__conf__')
|
||||
|
||||
348
kittens/ssh/completion.py
Normal file
348
kittens/ssh/completion.py
Normal file
@@ -0,0 +1,348 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from typing import Callable, Dict, Iterable, Iterator, Sequence, Tuple
|
||||
|
||||
from kitty.complete import Completions, complete_files_and_dirs, debug
|
||||
from kitty.types import run_once
|
||||
|
||||
debug
|
||||
|
||||
|
||||
def lines_from_file(path: str) -> Iterator[str]:
|
||||
try:
|
||||
f = open(os.path.expanduser(path))
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
yield from f
|
||||
|
||||
|
||||
def lines_from_command(*cmd: str) -> Iterator[str]:
|
||||
try:
|
||||
output = subprocess.check_output(cmd).decode('utf-8')
|
||||
except Exception:
|
||||
return
|
||||
yield from output.splitlines()
|
||||
|
||||
|
||||
def parts_yielder(lines: Iterable[str], pfilter: Callable[[str], Iterator[str]]) -> Iterator[str]:
|
||||
for line in lines:
|
||||
yield from pfilter(line)
|
||||
|
||||
|
||||
def hosts_from_config_lines(line: str) -> Iterator[str]:
|
||||
parts = line.strip().split()
|
||||
if len(parts) > 1 and parts[0] == 'Host':
|
||||
yield parts[1]
|
||||
|
||||
|
||||
def hosts_from_known_hosts(line: str) -> Iterator[str]:
|
||||
parts = line.strip().split()
|
||||
if parts:
|
||||
yield re.sub(r':\d+$', '', parts[0])
|
||||
|
||||
|
||||
def hosts_from_hosts(line: str) -> Iterator[str]:
|
||||
line = line.strip()
|
||||
if not line.startswith('#'):
|
||||
parts = line.split()
|
||||
if parts:
|
||||
yield parts[0]
|
||||
if len(parts) > 1:
|
||||
yield parts[1]
|
||||
if len(parts) > 2:
|
||||
yield parts[2]
|
||||
|
||||
|
||||
def iter_known_hosts() -> Iterator[str]:
|
||||
yield from parts_yielder(lines_from_file('~/.ssh/config'), hosts_from_config_lines)
|
||||
yield from parts_yielder(lines_from_file('~/.ssh/known_hosts'), hosts_from_known_hosts)
|
||||
yield from parts_yielder(lines_from_file('/etc/ssh/ssh_known_hosts'), hosts_from_known_hosts)
|
||||
yield from parts_yielder(lines_from_file('/etc/hosts'), hosts_from_hosts)
|
||||
yield from parts_yielder(lines_from_command('getent', 'hosts'), hosts_from_hosts)
|
||||
|
||||
|
||||
@run_once
|
||||
def known_hosts() -> Tuple[str, ...]:
|
||||
return tuple(sorted(filter(lambda x: '*' not in x and '[' not in x, set(iter_known_hosts()))))
|
||||
|
||||
|
||||
@run_once
|
||||
def ssh_options() -> Dict[str, str]:
|
||||
stderr = subprocess.Popen(['ssh'], stderr=subprocess.PIPE).stderr
|
||||
assert stderr is not None
|
||||
raw = stderr.read().decode('utf-8')
|
||||
ans: Dict[str, str] = {}
|
||||
pos = 0
|
||||
while True:
|
||||
pos = raw.find('[', pos)
|
||||
if pos < 0:
|
||||
break
|
||||
num = 1
|
||||
epos = pos
|
||||
while num > 0:
|
||||
epos += 1
|
||||
if raw[epos] not in '[]':
|
||||
continue
|
||||
num += 1 if raw[epos] == '[' else -1
|
||||
q = raw[pos+1:epos]
|
||||
pos = epos
|
||||
if len(q) < 2 or q[0] != '-':
|
||||
continue
|
||||
if ' ' in q:
|
||||
opt, desc = q.split(' ', 1)
|
||||
ans[opt[1:]] = desc
|
||||
else:
|
||||
ans.update(dict.fromkeys(q[1:], ''))
|
||||
return ans
|
||||
|
||||
|
||||
# option help {{{
|
||||
@run_once
|
||||
def option_help_map() -> Dict[str, str]:
|
||||
ans: Dict[str, str] = {}
|
||||
lines = '''
|
||||
-4 -- force ssh to use IPv4 addresses only
|
||||
-6 -- force ssh to use IPv6 addresses only
|
||||
-a -- disable forwarding of authentication agent connection
|
||||
-A -- enable forwarding of the authentication agent connection
|
||||
-B -- bind to specified interface before attempting to connect
|
||||
-b -- specify interface to transmit on
|
||||
-C -- compress data
|
||||
-c -- select encryption cipher
|
||||
-D -- specify a dynamic port forwarding
|
||||
-E -- append log output to file instead of stderr
|
||||
-e -- set escape character
|
||||
-f -- go to background
|
||||
-F -- specify alternate config file
|
||||
-g -- allow remote hosts to connect to local forwarded ports
|
||||
-G -- output configuration and exit
|
||||
-i -- select identity file
|
||||
-I -- specify smartcard device
|
||||
-J -- connect via a jump host
|
||||
-k -- disable forwarding of GSSAPI credentials
|
||||
-K -- enable GSSAPI-based authentication and forwarding
|
||||
-L -- specify local port forwarding
|
||||
-l -- specify login name
|
||||
-M -- master mode for connection sharing
|
||||
-m -- specify mac algorithms
|
||||
-N -- don't execute a remote command
|
||||
-n -- redirect stdin from /dev/null
|
||||
-O -- control an active connection multiplexing master process
|
||||
-o -- specify extra options
|
||||
-p -- specify port on remote host
|
||||
-P -- use non privileged port
|
||||
-Q -- query parameters
|
||||
-q -- quiet operation
|
||||
-R -- specify remote port forwarding
|
||||
-s -- invoke subsystem
|
||||
-S -- specify location of control socket for connection sharing
|
||||
-T -- disable pseudo-tty allocation
|
||||
-t -- force pseudo-tty allocation
|
||||
-V -- show version number
|
||||
-v -- verbose mode (multiple increase verbosity, up to 3)
|
||||
-W -- forward standard input and output to host
|
||||
-w -- request tunnel device forwarding
|
||||
-x -- disable X11 forwarding
|
||||
-X -- enable (untrusted) X11 forwarding
|
||||
-Y -- enable trusted X11 forwarding
|
||||
-y -- send log info via syslog instead of stderr
|
||||
'''.splitlines()
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line:
|
||||
parts = line.split(maxsplit=2)
|
||||
ans[parts[0]] = parts[2]
|
||||
return ans
|
||||
# }}}
|
||||
|
||||
|
||||
# option names {{{
|
||||
@run_once
|
||||
def option_names() -> Tuple[str, ...]:
|
||||
return tuple(filter(None, (
|
||||
line.strip() for line in '''
|
||||
AddKeysToAgent
|
||||
AddressFamily
|
||||
BatchMode
|
||||
BindAddress
|
||||
CanonicalDomains
|
||||
CanonicalizeFallbackLocal
|
||||
CanonicalizeHostname
|
||||
CanonicalizeMaxDots
|
||||
CanonicalizePermittedCNAMEs
|
||||
CASignatureAlgorithms
|
||||
CertificateFile
|
||||
ChallengeResponseAuthentication
|
||||
CheckHostIP
|
||||
Ciphers
|
||||
ClearAllForwardings
|
||||
Compression
|
||||
ConnectionAttempts
|
||||
ConnectTimeout
|
||||
ControlMaster
|
||||
ControlPath
|
||||
ControlPersist
|
||||
DynamicForward
|
||||
EscapeChar
|
||||
ExitOnForwardFailure
|
||||
FingerprintHash
|
||||
ForwardAgent
|
||||
ForwardX11
|
||||
ForwardX11Timeout
|
||||
ForwardX11Trusted
|
||||
GatewayPorts
|
||||
GlobalKnownHostsFile
|
||||
GSSAPIAuthentication
|
||||
GSSAPIDelegateCredentials
|
||||
HashKnownHosts
|
||||
Host
|
||||
HostbasedAcceptedAlgorithms
|
||||
HostbasedAuthentication
|
||||
HostKeyAlgorithms
|
||||
HostKeyAlias
|
||||
Hostname
|
||||
IdentitiesOnly
|
||||
IdentityAgent
|
||||
IdentityFile
|
||||
IPQoS
|
||||
KbdInteractiveAuthentication
|
||||
KbdInteractiveDevices
|
||||
KexAlgorithms
|
||||
KnownHostsCommand
|
||||
LocalCommand
|
||||
LocalForward
|
||||
LogLevel
|
||||
MACs
|
||||
Match
|
||||
NoHostAuthenticationForLocalhost
|
||||
NumberOfPasswordPrompts
|
||||
PasswordAuthentication
|
||||
PermitLocalCommand
|
||||
PermitRemoteOpen
|
||||
PKCS11Provider
|
||||
Port
|
||||
PreferredAuthentications
|
||||
ProxyCommand
|
||||
ProxyJump
|
||||
ProxyUseFdpass
|
||||
PubkeyAcceptedAlgorithms
|
||||
PubkeyAuthentication
|
||||
RekeyLimit
|
||||
RemoteCommand
|
||||
RemoteForward
|
||||
RequestTTY
|
||||
SendEnv
|
||||
ServerAliveInterval
|
||||
ServerAliveCountMax
|
||||
SetEnv
|
||||
StreamLocalBindMask
|
||||
StreamLocalBindUnlink
|
||||
StrictHostKeyChecking
|
||||
TCPKeepAlive
|
||||
Tunnel
|
||||
TunnelDevice
|
||||
UpdateHostKeys
|
||||
User
|
||||
UserKnownHostsFile
|
||||
VerifyHostKeyDNS
|
||||
VisualHostKey
|
||||
XAuthLocation
|
||||
'''.splitlines())))
|
||||
# }}}
|
||||
|
||||
|
||||
def complete_choices(ans: Completions, prefix: str, title: str, choices: Iterable[str], comma_separated: bool = False) -> None:
|
||||
matches: Dict[str, str] = {}
|
||||
word_transforms = {}
|
||||
effective_prefix = prefix
|
||||
hidden_prefix = ''
|
||||
if comma_separated:
|
||||
effective_prefix = prefix.split(',')[-1]
|
||||
hidden_prefix = ','.join(prefix.split(',')[:-1])
|
||||
if hidden_prefix:
|
||||
hidden_prefix += ','
|
||||
for q in choices:
|
||||
if q.startswith(effective_prefix):
|
||||
if comma_separated:
|
||||
tq = q
|
||||
q = hidden_prefix + q + ','
|
||||
word_transforms[q] = tq
|
||||
matches[q] = ''
|
||||
ans.add_match_group(title, matches, trailing_space=not comma_separated, word_transforms=word_transforms)
|
||||
|
||||
|
||||
def complete_q_choices(ans: Completions, prefix: str, title: str, key: str, comma_separated: bool) -> None:
|
||||
choices = (line.strip() for line in lines_from_command('ssh', '-Q', key))
|
||||
complete_choices(ans, prefix, title, choices, comma_separated)
|
||||
|
||||
|
||||
def complete_arg(ans: Completions, option_flag: str, prefix: str = '') -> None:
|
||||
options = ssh_options()
|
||||
option_name = options.get(option_flag[1:])
|
||||
if option_name.endswith('file') or option_name.endswith('path'):
|
||||
return complete_files_and_dirs(ans, prefix, option_name)
|
||||
choices = {
|
||||
'mac_spec': ('MAC algorithm', 'mac', True),
|
||||
'cipher_spec': ('encryption cipher', 'cipher', True),
|
||||
'query_option': ('query option', 'help', False),
|
||||
}
|
||||
if option_name in choices:
|
||||
return complete_q_choices(ans, prefix, *choices[option_name])
|
||||
if option_name == 'destination':
|
||||
return complete_destination(ans, prefix)
|
||||
if option_name == 'ctl_cmd':
|
||||
return complete_choices(ans, prefix, 'control command', ('check', 'forward', 'cancel', 'exit'))
|
||||
if option_name == 'option':
|
||||
matches = (x+'=' for x in option_names() if x.startswith(prefix))
|
||||
word_transforms = {x+'=': x for x in option_names()}
|
||||
ans.add_match_group('configure file option', matches, trailing_space=False, word_transforms=word_transforms)
|
||||
|
||||
|
||||
def complete_destination(ans: Completions, prefix: str = '') -> None:
|
||||
result = (k for k in known_hosts() if k.startswith(prefix))
|
||||
ans.add_match_group('remote host name', result)
|
||||
|
||||
|
||||
def complete_option(ans: Completions, prefix: str = '-') -> None:
|
||||
hm = option_help_map()
|
||||
if len(prefix) <= 1:
|
||||
result = {k: v for k, v in hm.items() if k.startswith(prefix)}
|
||||
ans.add_match_group('option', result)
|
||||
else:
|
||||
ans.add_match_group('option', {prefix: ''})
|
||||
|
||||
|
||||
def complete(ans: Completions, words: Sequence[str], new_word: bool) -> None:
|
||||
options = ssh_options()
|
||||
expecting_arg = False
|
||||
types = ['' for i in range(len(words))]
|
||||
for i, word in enumerate(words):
|
||||
if expecting_arg:
|
||||
types[i] = 'arg'
|
||||
expecting_arg = False
|
||||
continue
|
||||
if word.startswith('-'):
|
||||
types[i] = 'option'
|
||||
if len(word) == 2 and options.get(word[1]):
|
||||
expecting_arg = True
|
||||
continue
|
||||
types[i] = 'destination'
|
||||
break
|
||||
if new_word:
|
||||
if words:
|
||||
if expecting_arg:
|
||||
return complete_arg(ans, words[-1])
|
||||
return complete_destination(ans)
|
||||
if words:
|
||||
if types[-1] == 'arg' and len(words) > 1:
|
||||
return complete_arg(ans, words[-2], words[-1])
|
||||
if types[-1] == 'destination':
|
||||
return complete_destination(ans, words[-1])
|
||||
if types[-1] == 'option':
|
||||
return complete_option(ans, words[-1])
|
||||
@@ -3,12 +3,12 @@
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
from typing import List, NoReturn, Optional, Set, Tuple
|
||||
from .completion import ssh_options, complete
|
||||
|
||||
from kitty.utils import SSHConnectionData
|
||||
|
||||
@@ -21,27 +21,83 @@ cat >$tmp << 'TERMEOF'
|
||||
TERMINFO
|
||||
TERMEOF
|
||||
|
||||
tic_out=$(tic -x -o ~/.terminfo $tmp 2>&1)
|
||||
tic_out=$(tic -x -o $HOME/.terminfo $tmp 2>&1)
|
||||
rc=$?
|
||||
rm $tmp
|
||||
if [ "$rc" != "0" ]; then echo "$tic_out"; exit 1; fi
|
||||
if [ -z "$USER" ]; then export USER=$(whoami); fi
|
||||
shell_name=$(basename $0)
|
||||
export TERMINFO="$HOME/.terminfo"
|
||||
login_shell=""
|
||||
python=""
|
||||
|
||||
login_shell_is_ok() {
|
||||
if [ -z "$login_shell" ] || [ ! -x "$login_shell" ]; then return 1; fi
|
||||
case "$login_shell" in
|
||||
*sh) return 0;
|
||||
esac
|
||||
return 1;
|
||||
}
|
||||
|
||||
detect_python() {
|
||||
python=$(command -v python3)
|
||||
if [ -z "$python" ]; then python=$(command -v python2); fi
|
||||
if [ -z "$python" ]; then python=python; fi
|
||||
}
|
||||
|
||||
using_getent() {
|
||||
cmd=$(command -v getent)
|
||||
if [ -z "$cmd" ]; then return; fi
|
||||
output=$($cmd passwd $USER 2>/dev/null)
|
||||
if [ $? = 0 ]; then login_shell=$(echo $output | cut -d: -f7); fi
|
||||
}
|
||||
|
||||
using_id() {
|
||||
cmd=$(command -v id)
|
||||
if [ -z "$cmd" ]; then return; fi
|
||||
output=$($cmd -P $USER 2>/dev/null)
|
||||
if [ $? = 0 ]; then login_shell=$(echo $output | cut -d: -f7); fi
|
||||
}
|
||||
|
||||
using_passwd() {
|
||||
cmd=$(command -v grep)
|
||||
if [ -z "$cmd" ]; then return; fi
|
||||
output=$($cmd "^$USER:" /etc/passwd 2>/dev/null)
|
||||
if [ $? = 0 ]; then login_shell=$(echo $output | cut -d: -f7); fi
|
||||
}
|
||||
|
||||
using_python() {
|
||||
detect_python
|
||||
if [ ! -x "$python" ]; then return; fi
|
||||
output=$($python -c "import pwd, os; print(pwd.getpwuid(os.geteuid()).pw_shell)")
|
||||
if [ $? = 0 ]; then login_shell=$output; fi
|
||||
}
|
||||
|
||||
execute_with_python() {
|
||||
detect_python
|
||||
exec $python -c "import os; os.execl('$login_shell', '-' '$shell_name')"
|
||||
}
|
||||
|
||||
die() { echo "$*" 1>&2 ; exit 1; }
|
||||
|
||||
using_getent
|
||||
if ! login_shell_is_ok; then using_id; fi
|
||||
if ! login_shell_is_ok; then using_python; fi
|
||||
if ! login_shell_is_ok; then using_passwd; fi
|
||||
if ! login_shell_is_ok; then die "Could not detect login shell"; fi
|
||||
|
||||
|
||||
# If a command was passed to SSH execute it here
|
||||
EXEC_CMD
|
||||
|
||||
# We need to pass the first argument to the executed program with a leading -
|
||||
# to make sure the shell executes as a login shell. Note that not all shells
|
||||
# support exec -a so we use the below to try to detect such shells
|
||||
|
||||
case "dash" in
|
||||
*$shell_name*)
|
||||
python=$(command -v python3)
|
||||
if [ -z "$python" ]; then python=$(command -v python2); fi
|
||||
if [ -z "$python" ]; then python=python; fi
|
||||
exec $python -c "import os; os.execlp('$0', '-' '$shell_name')"
|
||||
;;
|
||||
esac
|
||||
exec -a "-$shell_name" "$0"
|
||||
shell_name=$(basename $login_shell)
|
||||
if [ -z "$PIPESTATUS" ]; then
|
||||
# the dash shell does not support exec -a and also does not define PIPESTATUS
|
||||
execute_with_python
|
||||
fi
|
||||
exec -a "-$shell_name" $login_shell
|
||||
'''
|
||||
|
||||
|
||||
@@ -73,20 +129,15 @@ os.execlp(shell_path, shell_name)
|
||||
|
||||
|
||||
def get_ssh_cli() -> Tuple[Set[str], Set[str]]:
|
||||
other_ssh_args: List[str] = []
|
||||
boolean_ssh_args: List[str] = []
|
||||
stderr = subprocess.Popen(['ssh'], stderr=subprocess.PIPE).stderr
|
||||
assert stderr is not None
|
||||
raw = stderr.read().decode('utf-8')
|
||||
for m in re.finditer(r'\[(.+?)\]', raw):
|
||||
q = m.group(1)
|
||||
if len(q) < 2 or q[0] != '-':
|
||||
continue
|
||||
if ' ' in q:
|
||||
other_ssh_args.append(q[1])
|
||||
other_ssh_args: Set[str] = set()
|
||||
boolean_ssh_args: Set[str] = set()
|
||||
for k, v in ssh_options().items():
|
||||
k = '-' + k
|
||||
if v:
|
||||
other_ssh_args.add(k)
|
||||
else:
|
||||
boolean_ssh_args.extend(q[1:])
|
||||
return set('-' + x for x in boolean_ssh_args), set('-' + x for x in other_ssh_args)
|
||||
boolean_ssh_args.add(k)
|
||||
return boolean_ssh_args, other_ssh_args
|
||||
|
||||
|
||||
def get_connection_data(args: List[str]) -> Optional[SSHConnectionData]:
|
||||
@@ -184,9 +235,9 @@ def get_posix_cmd(terminfo: str, remote_args: List[str]) -> List[str]:
|
||||
# line 1129 of ssh.c and on the remote side sshd.c runs the
|
||||
# concatenated command as shell -c cmd
|
||||
args = [c.replace("'", """'"'"'""") for c in remote_args]
|
||||
command_to_execute = "exec $0 -c '{}'".format(' '.join(args))
|
||||
command_to_execute = "exec $login_shell -c '{}'".format(' '.join(args))
|
||||
sh_script = sh_script.replace('EXEC_CMD', command_to_execute)
|
||||
return [sh_script] + remote_args
|
||||
return [f'sh -c {shlex.quote(sh_script)}']
|
||||
|
||||
|
||||
def get_python_cmd(terminfo: str, command_to_execute: List[str]) -> List[str]:
|
||||
@@ -210,7 +261,9 @@ def main(args: List[str]) -> NoReturn:
|
||||
cmd += server_args
|
||||
else:
|
||||
hostname, remote_args = server_args[0], server_args[1:]
|
||||
cmd += ['-t', hostname]
|
||||
if not remote_args:
|
||||
cmd.append('-t')
|
||||
cmd.append(hostname)
|
||||
terminfo = subprocess.check_output(['infocmp', '-a']).decode('utf-8')
|
||||
f = get_posix_cmd if use_posix else get_python_cmd
|
||||
cmd += f(terminfo, remote_args)
|
||||
@@ -219,3 +272,5 @@ def main(args: List[str]) -> NoReturn:
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
elif __name__ == '__completer__':
|
||||
setattr(sys, 'kitten_completer', complete)
|
||||
|
||||
@@ -67,7 +67,7 @@ def name(cp: Union[int, str]) -> str:
|
||||
|
||||
|
||||
@lru_cache(maxsize=256)
|
||||
def codepoints_matching_search(parts: Sequence[str]) -> List[int]:
|
||||
def codepoints_matching_search(parts: Tuple[str, ...]) -> List[int]:
|
||||
ans = []
|
||||
if parts and parts[0] and len(parts[0]) > 1:
|
||||
codepoints = points_for_word(parts[0])
|
||||
|
||||
@@ -19,27 +19,31 @@ all_words(PYNOARG) {
|
||||
return ans;
|
||||
}
|
||||
|
||||
static inline void
|
||||
add_matches(const word_trie *wt, char_type *codepoints, size_t *pos, const size_t sz) {
|
||||
static void
|
||||
add_matches(const word_trie *wt, PyObject *ans) {
|
||||
size_t num = mark_groups[wt->match_offset];
|
||||
for (size_t i = wt->match_offset + 1; i < wt->match_offset + 1 + num && *pos < sz; i++, (*pos)++) {
|
||||
codepoints[*pos] = mark_to_cp[mark_groups[i]];
|
||||
for (size_t i = wt->match_offset + 1; i < wt->match_offset + 1 + num; i++) {
|
||||
PyObject *t = PyLong_FromUnsignedLong(mark_to_cp[mark_groups[i]]);
|
||||
if (!t) return;
|
||||
int ret = PySet_Add(ans, t);
|
||||
Py_DECREF(t);
|
||||
if (ret != 0) return;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
process_trie_node(const word_trie *wt, char_type *codepoints, size_t *pos, const size_t sz) {
|
||||
if (wt->match_offset) add_matches(wt, codepoints, pos, sz);
|
||||
process_trie_node(const word_trie *wt, PyObject *ans) {
|
||||
if (wt->match_offset) { add_matches(wt, ans); if (PyErr_Occurred()) return; }
|
||||
size_t num_children = children_array[wt->children_offset];
|
||||
if (!num_children) return;
|
||||
for (size_t c = wt->children_offset + 1; c < wt->children_offset + 1 + num_children; c++) {
|
||||
if (*pos > sz) return;
|
||||
uint32_t x = children_array[c];
|
||||
process_trie_node(&all_trie_nodes[x >> 8], codepoints, pos, sz);
|
||||
process_trie_node(&all_trie_nodes[x >> 8], ans);
|
||||
if (PyErr_Occurred()) return;
|
||||
}
|
||||
}
|
||||
|
||||
static inline PyObject*
|
||||
static PyObject*
|
||||
codepoints_for_word(const char *word, size_t len) {
|
||||
const word_trie *wt = all_trie_nodes;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
@@ -57,14 +61,10 @@ codepoints_for_word(const char *word, size_t len) {
|
||||
}
|
||||
if (!found) return PyFrozenSet_New(NULL);
|
||||
}
|
||||
static char_type codepoints[1024];
|
||||
size_t cpos = 0;
|
||||
process_trie_node(wt, codepoints, &cpos, arraysz(codepoints));
|
||||
PyObject *ans = PyFrozenSet_New(NULL); if (ans == NULL) return NULL;
|
||||
for (size_t i = 0; i < cpos; i++) {
|
||||
PyObject *t = PyLong_FromUnsignedLong(codepoints[i]); if (t == NULL) { Py_DECREF(ans); return NULL; }
|
||||
int ret = PySet_Add(ans, t); Py_DECREF(t); if (ret != 0) { Py_DECREF(ans); return NULL; }
|
||||
}
|
||||
PyObject *ans = PyFrozenSet_New(NULL);
|
||||
if (!ans) return NULL;
|
||||
process_trie_node(wt, ans);
|
||||
if (PyErr_Occurred()) return NULL;
|
||||
return ans;
|
||||
}
|
||||
|
||||
|
||||
@@ -906,6 +906,8 @@ class Boss:
|
||||
if exe:
|
||||
cmd[0] = exe
|
||||
|
||||
if os.path.basename(cmd[0]) == 'less':
|
||||
cmd.append('-+F') # reset --quit-if-one-screen
|
||||
tab = self.active_tab
|
||||
if tab is not None:
|
||||
bdata = data.encode('utf-8') if isinstance(data, str) else data
|
||||
@@ -1774,3 +1776,7 @@ class Boss:
|
||||
set_clipboard_string(re.sub(r'\x1b.+?m', '', output))
|
||||
output += '\n\x1b[35mThis debug output has been copied to the clipboard\x1b[m'
|
||||
self.display_scrollback(w, output, title=_('Current kitty options'))
|
||||
|
||||
def discard_event(self) -> None:
|
||||
pass
|
||||
mouse_discard_event = discard_event
|
||||
|
||||
@@ -944,7 +944,8 @@ process_pending_closes(ChildMonitor *self) {
|
||||
// If we create new OS windows during wait_events(), using global menu actions
|
||||
// via the mouse causes a crash because of the way autorelease pools work in
|
||||
// glfw/cocoa. So we use a flag instead.
|
||||
static CocoaPendingAction cocoa_pending_actions = NO_COCOA_PENDING_ACTION;
|
||||
static bool cocoa_pending_actions[NUM_COCOA_PENDING_ACTIONS] = {0};
|
||||
static bool has_cocoa_pending_actions = false;
|
||||
typedef struct {
|
||||
char* wd;
|
||||
char **open_files;
|
||||
@@ -964,11 +965,47 @@ set_cocoa_pending_action(CocoaPendingAction action, const char *wd) {
|
||||
cocoa_pending_actions_data.wd = strdup(wd);
|
||||
}
|
||||
}
|
||||
cocoa_pending_actions |= action;
|
||||
cocoa_pending_actions[action] = true;
|
||||
has_cocoa_pending_actions = true;
|
||||
// The main loop may be blocking on the event queue, if e.g. unfocused.
|
||||
// Unjam it so the pending action is processed right now.
|
||||
wakeup_main_loop();
|
||||
}
|
||||
|
||||
static void
|
||||
process_cocoa_pending_actions(void) {
|
||||
if (cocoa_pending_actions[PREFERENCES_WINDOW]) { call_boss(edit_config_file, NULL); }
|
||||
if (cocoa_pending_actions[NEW_OS_WINDOW]) { call_boss(new_os_window, NULL); }
|
||||
if (cocoa_pending_actions[CLOSE_OS_WINDOW]) { call_boss(close_os_window, NULL); }
|
||||
if (cocoa_pending_actions[CLOSE_TAB]) { call_boss(close_tab, NULL); }
|
||||
if (cocoa_pending_actions[NEW_TAB]) { call_boss(new_tab, NULL); }
|
||||
if (cocoa_pending_actions[NEXT_TAB]) { call_boss(next_tab, NULL); }
|
||||
if (cocoa_pending_actions[PREVIOUS_TAB]) { call_boss(previous_tab, NULL); }
|
||||
if (cocoa_pending_actions[DETACH_TAB]) { call_boss(detach_tab, NULL); }
|
||||
if (cocoa_pending_actions[NEW_WINDOW]) { call_boss(new_window, NULL); }
|
||||
if (cocoa_pending_actions[CLOSE_WINDOW]) { call_boss(close_window, NULL); }
|
||||
if (cocoa_pending_actions[RESET_TERMINAL]) { call_boss(clear_terminal, "sO", "reset", Py_True ); }
|
||||
if (cocoa_pending_actions[RELOAD_CONFIG]) { call_boss(load_config_file, NULL); }
|
||||
if (cocoa_pending_actions_data.wd) {
|
||||
if (cocoa_pending_actions[NEW_OS_WINDOW_WITH_WD]) { call_boss(new_os_window_with_wd, "s", cocoa_pending_actions_data.wd); }
|
||||
if (cocoa_pending_actions[NEW_TAB_WITH_WD]) { call_boss(new_tab_with_wd, "s", cocoa_pending_actions_data.wd); }
|
||||
free(cocoa_pending_actions_data.wd);
|
||||
cocoa_pending_actions_data.wd = NULL;
|
||||
}
|
||||
if (cocoa_pending_actions_data.open_files_count) {
|
||||
for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.open_files_count; cpa++) {
|
||||
if (cocoa_pending_actions_data.open_files[cpa]) {
|
||||
call_boss(open_file, "s", cocoa_pending_actions_data.open_files[cpa]);
|
||||
free(cocoa_pending_actions_data.open_files[cpa]);
|
||||
cocoa_pending_actions_data.open_files[cpa] = NULL;
|
||||
}
|
||||
}
|
||||
cocoa_pending_actions_data.open_files_count = 0;
|
||||
}
|
||||
memset(cocoa_pending_actions, 0, sizeof(cocoa_pending_actions));
|
||||
has_cocoa_pending_actions = false;
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
static void process_global_state(void *data);
|
||||
@@ -997,36 +1034,10 @@ process_global_state(void *data) {
|
||||
if (parse_input(self)) input_read = true;
|
||||
render(now, input_read);
|
||||
#ifdef __APPLE__
|
||||
if (cocoa_pending_actions) {
|
||||
if (cocoa_pending_actions & PREFERENCES_WINDOW) { call_boss(edit_config_file, NULL); }
|
||||
if (cocoa_pending_actions & NEW_OS_WINDOW) { call_boss(new_os_window, NULL); }
|
||||
if (cocoa_pending_actions & CLOSE_OS_WINDOW) { call_boss(close_os_window, NULL); }
|
||||
if (cocoa_pending_actions & CLOSE_TAB) { call_boss(close_tab, NULL); }
|
||||
if (cocoa_pending_actions & NEW_TAB) { call_boss(new_tab, NULL); }
|
||||
if (cocoa_pending_actions & NEXT_TAB) { call_boss(next_tab, NULL); }
|
||||
if (cocoa_pending_actions & PREVIOUS_TAB) { call_boss(previous_tab, NULL); }
|
||||
if (cocoa_pending_actions & DETACH_TAB) { call_boss(detach_tab, NULL); }
|
||||
if (cocoa_pending_actions & NEW_WINDOW) { call_boss(new_window, NULL); }
|
||||
if (cocoa_pending_actions & CLOSE_WINDOW) { call_boss(close_window, NULL); }
|
||||
if (cocoa_pending_actions & RESET_TERMINAL) { call_boss(clear_terminal, "sO", "reset", Py_True ); }
|
||||
if (cocoa_pending_actions_data.wd) {
|
||||
if (cocoa_pending_actions & NEW_OS_WINDOW_WITH_WD) { call_boss(new_os_window_with_wd, "s", cocoa_pending_actions_data.wd); }
|
||||
if (cocoa_pending_actions & NEW_TAB_WITH_WD) { call_boss(new_tab_with_wd, "s", cocoa_pending_actions_data.wd); }
|
||||
free(cocoa_pending_actions_data.wd);
|
||||
cocoa_pending_actions_data.wd = NULL;
|
||||
}
|
||||
if (cocoa_pending_actions_data.open_files_count) {
|
||||
for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.open_files_count; cpa++) {
|
||||
if (cocoa_pending_actions_data.open_files[cpa]) {
|
||||
call_boss(open_file, "s", cocoa_pending_actions_data.open_files[cpa]);
|
||||
free(cocoa_pending_actions_data.open_files[cpa]);
|
||||
cocoa_pending_actions_data.open_files[cpa] = NULL;
|
||||
}
|
||||
}
|
||||
cocoa_pending_actions_data.open_files_count = 0;
|
||||
}
|
||||
cocoa_pending_actions = 0;
|
||||
}
|
||||
if (has_cocoa_pending_actions) {
|
||||
process_cocoa_pending_actions();
|
||||
maximum_wait = 0; // ensure loop ticks again so that the actions side effects are performed immediately
|
||||
}
|
||||
#endif
|
||||
report_reaped_pids();
|
||||
bool should_quit = false;
|
||||
|
||||
@@ -91,6 +91,7 @@ PENDING(previous_tab, PREVIOUS_TAB)
|
||||
PENDING(new_window, NEW_WINDOW)
|
||||
PENDING(close_window, CLOSE_WINDOW)
|
||||
PENDING(reset_terminal, RESET_TERMINAL)
|
||||
PENDING(reload_config, RELOAD_CONFIG)
|
||||
|
||||
- (void)open_kitty_website_url:(id)sender {
|
||||
(void)sender;
|
||||
@@ -117,7 +118,7 @@ typedef struct {
|
||||
NSEventModifierFlags mods;
|
||||
} GlobalShortcut;
|
||||
typedef struct {
|
||||
GlobalShortcut new_os_window, close_os_window, close_tab, edit_config_file;
|
||||
GlobalShortcut new_os_window, close_os_window, close_tab, edit_config_file, reload_config;
|
||||
GlobalShortcut previous_tab, next_tab, new_tab, new_window, close_window, reset_terminal;
|
||||
} GlobalShortcuts;
|
||||
static GlobalShortcuts global_shortcuts;
|
||||
@@ -132,7 +133,7 @@ cocoa_set_global_shortcut(PyObject *self UNUSED, PyObject *args) {
|
||||
#define Q(x) if (strcmp(name, #x) == 0) gs = &global_shortcuts.x
|
||||
Q(new_os_window); else Q(close_os_window); else Q(close_tab); else Q(edit_config_file);
|
||||
else Q(new_tab); else Q(next_tab); else Q(previous_tab);
|
||||
else Q(new_window); else Q(close_window); else Q(reset_terminal);
|
||||
else Q(new_window); else Q(close_window); else Q(reset_terminal); else Q(reload_config);
|
||||
#undef Q
|
||||
if (gs == NULL) { PyErr_SetString(PyExc_KeyError, "Unknown shortcut name"); return NULL; }
|
||||
int cocoa_mods;
|
||||
@@ -380,6 +381,7 @@ cocoa_create_global_menu(void) {
|
||||
keyEquivalent:@""];
|
||||
[appMenu addItem:[NSMenuItem separatorItem]];
|
||||
MENU_ITEM(appMenu, @"Preferences…", edit_config_file);
|
||||
MENU_ITEM(appMenu, @"Reload preferences", reload_config);
|
||||
|
||||
[appMenu addItemWithTitle:[NSString stringWithFormat:@"Hide %@", app_name]
|
||||
action:@selector(hide:)
|
||||
|
||||
@@ -6,17 +6,23 @@ import os
|
||||
import shlex
|
||||
import sys
|
||||
from typing import (
|
||||
Any, Callable, Dict, Iterable, List, Optional, Sequence, Set, Tuple
|
||||
Any, Callable, Dict, Iterable, Iterator, List, Optional, Sequence, Tuple,
|
||||
Union
|
||||
)
|
||||
|
||||
from kittens.runner import all_kitten_names, get_kitten_cli_docs
|
||||
from kittens.runner import (
|
||||
all_kitten_names, get_kitten_cli_docs, get_kitten_completer
|
||||
)
|
||||
|
||||
from .cli import (
|
||||
OptionDict, OptionSpecSeq, options_for_completion, parse_option_spec
|
||||
OptionDict, OptionSpecSeq, options_for_completion, parse_option_spec,
|
||||
prettify
|
||||
)
|
||||
from .fast_data_types import truncate_point_for_length, wcswidth
|
||||
from .rc.base import all_command_names, command_for_name
|
||||
from .shell import options_for_cmd
|
||||
from .types import run_once
|
||||
from .utils import screen_size_function
|
||||
|
||||
'''
|
||||
To add completion for a new shell, you need to:
|
||||
@@ -39,7 +45,37 @@ them into something your shell will understand.
|
||||
|
||||
parsers: Dict[str, Callable] = {}
|
||||
serializers: Dict[str, Callable] = {}
|
||||
MathGroup = Dict[str, str]
|
||||
|
||||
|
||||
class MatchGroup:
|
||||
|
||||
def __init__(
|
||||
self, x: Union[Dict[str, str], Iterable[str]],
|
||||
trailing_space: bool = True,
|
||||
is_files: bool = False,
|
||||
word_transforms: Optional[Dict[str, str]] = None,
|
||||
):
|
||||
self.mdict = x if isinstance(x, dict) else dict.fromkeys(x, '')
|
||||
self.trailing_space = trailing_space
|
||||
self.is_files = is_files
|
||||
self.word_transforms = word_transforms or {}
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return iter(self.mdict)
|
||||
|
||||
def transformed_words(self) -> Iterator[str]:
|
||||
for w in self:
|
||||
yield self.word_transforms.get(w, w)
|
||||
|
||||
def transformed_items(self) -> Iterator[Tuple[str, str]]:
|
||||
for w, desc in self.items():
|
||||
yield self.word_transforms.get(w, w), desc
|
||||
|
||||
def items(self) -> Iterator[Tuple[str, str]]:
|
||||
return iter(self.mdict.items())
|
||||
|
||||
def values(self) -> Iterator[str]:
|
||||
return iter(self.mdict.values())
|
||||
|
||||
|
||||
def debug(*a: Any, **kw: Any) -> None:
|
||||
@@ -69,11 +105,18 @@ class Delegate:
|
||||
class Completions:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.match_groups: Dict[str, MathGroup] = {}
|
||||
self.no_space_groups: Set[str] = set()
|
||||
self.files_groups: Set[str] = set()
|
||||
self.match_groups: Dict[str, MatchGroup] = {}
|
||||
self.delegate: Delegate = Delegate()
|
||||
|
||||
def add_match_group(
|
||||
self, name: str, x: Union[Dict[str, str], Iterable[str]],
|
||||
trailing_space: bool = True,
|
||||
is_files: bool = False,
|
||||
word_transforms: Optional[Dict[str, str]] = None
|
||||
) -> MatchGroup:
|
||||
self.match_groups[name] = m = MatchGroup(x, trailing_space, is_files, word_transforms)
|
||||
return m
|
||||
|
||||
|
||||
@run_once
|
||||
def remote_control_command_names() -> Tuple[str, ...]:
|
||||
@@ -159,16 +202,55 @@ def fish_input_parser(data: str) -> ParseResult:
|
||||
@output_serializer
|
||||
def zsh_output_serializer(ans: Completions) -> str:
|
||||
lines = []
|
||||
|
||||
screen = screen_size_function(sys.stderr.fileno())()
|
||||
width = screen.cols
|
||||
|
||||
def fmt_desc(word: str, desc: str, max_word_len: int) -> Iterator[str]:
|
||||
if not desc:
|
||||
yield word
|
||||
return
|
||||
desc = prettify(desc.splitlines()[0])
|
||||
multiline = False
|
||||
if wcswidth(word) > max_word_len:
|
||||
max_desc_len = width - 2
|
||||
multiline = True
|
||||
else:
|
||||
word = word.ljust(max_word_len)
|
||||
max_desc_len = width - max_word_len - 3
|
||||
if wcswidth(desc) > max_desc_len:
|
||||
desc = desc[:truncate_point_for_length(desc, max_desc_len - 2)]
|
||||
desc += '…'
|
||||
|
||||
word = f'\x1b[32m{word}\x1b[39m'
|
||||
if multiline:
|
||||
ans = f'{word}\n {desc}'
|
||||
else:
|
||||
ans = f'{word} {desc}'
|
||||
yield ans
|
||||
|
||||
for description, matches in ans.match_groups.items():
|
||||
cmd = ['compadd', '-U', '-J', shlex.quote(description), '-X', shlex.quote(description)]
|
||||
if description in ans.no_space_groups:
|
||||
cmd = ['compadd', '-U', '-J', shlex.quote(description), '-X', shlex.quote('%B' + description + '%b')]
|
||||
if not matches.trailing_space:
|
||||
cmd += ['-S', '""']
|
||||
if description in ans.files_groups:
|
||||
if matches.is_files:
|
||||
cmd.append('-f')
|
||||
common_prefix = os.path.commonprefix(tuple(matches))
|
||||
if common_prefix:
|
||||
cmd.extend(('-p', shlex.quote(common_prefix)))
|
||||
matches = {k[len(common_prefix):]: v for k, v in matches.items()}
|
||||
matches = MatchGroup({k[len(common_prefix):]: v for k, v in matches.items()})
|
||||
has_descriptions = any(matches.values())
|
||||
if has_descriptions or matches.word_transforms:
|
||||
lines.append('compdescriptions=(')
|
||||
sz = max(map(wcswidth, matches.transformed_words()))
|
||||
limit = min(16, sz)
|
||||
for word, desc in matches.transformed_items():
|
||||
lines.extend(map(shlex.quote, fmt_desc(word, desc, limit)))
|
||||
lines.append(')')
|
||||
if has_descriptions:
|
||||
cmd.append('-l')
|
||||
cmd.append('-d')
|
||||
cmd.append('compdescriptions')
|
||||
cmd.append('--')
|
||||
for word in matches:
|
||||
cmd.append(shlex.quote(word))
|
||||
@@ -190,9 +272,8 @@ def zsh_output_serializer(ans: Completions) -> str:
|
||||
def bash_output_serializer(ans: Completions) -> str:
|
||||
lines = []
|
||||
for description, matches in ans.match_groups.items():
|
||||
needs_space = description not in ans.no_space_groups
|
||||
for word in matches:
|
||||
if needs_space:
|
||||
if matches.trailing_space:
|
||||
word += ' '
|
||||
lines.append('COMPREPLY+=({})'.format(shlex.quote(word)))
|
||||
# debug('\n'.join(lines))
|
||||
@@ -212,11 +293,11 @@ def fish_output_serializer(ans: Completions) -> str:
|
||||
|
||||
def completions_for_first_word(ans: Completions, prefix: str, entry_points: Iterable[str], namespaced_entry_points: Iterable[str]) -> None:
|
||||
cmds = ['@' + c for c in remote_control_command_names()]
|
||||
ans.match_groups['Entry points'] = {
|
||||
ans.add_match_group('Entry points', {
|
||||
k: '' for k in
|
||||
list(entry_points) + cmds + ['+' + k for k in namespaced_entry_points]
|
||||
if not prefix or k.startswith(prefix)
|
||||
}
|
||||
})
|
||||
if prefix:
|
||||
ans.delegate = Delegate([prefix], 0)
|
||||
|
||||
@@ -229,7 +310,7 @@ def kitty_cli_opts(ans: Completions, prefix: Optional[str] = None) -> None:
|
||||
aliases = frozenset(x for x in opt['aliases'] if x.startswith(prefix)) if prefix else opt['aliases']
|
||||
for alias in aliases:
|
||||
matches[alias] = opt['help'].strip()
|
||||
ans.match_groups['Options'] = matches
|
||||
ans.add_match_group('Options', matches)
|
||||
|
||||
|
||||
def complete_kitty_cli_arg(ans: Completions, opt: Optional[OptionDict], prefix: str, unknown_args: Delegate) -> None:
|
||||
@@ -242,8 +323,7 @@ def complete_kitty_cli_arg(ans: Completions, opt: Optional[OptionDict], prefix:
|
||||
if dest == 'override':
|
||||
from kitty.config import option_names_for_completion
|
||||
k = 'Config directives'
|
||||
ans.match_groups[k] = {k+'=': '' for k in option_names_for_completion() if k.startswith(prefix)}
|
||||
ans.no_space_groups.add(k)
|
||||
ans.add_match_group(k, {k+'=': '' for k in option_names_for_completion() if k.startswith(prefix)}, trailing_space=False)
|
||||
elif dest == 'config':
|
||||
|
||||
def is_conf_file(x: str) -> bool:
|
||||
@@ -260,13 +340,11 @@ def complete_kitty_cli_arg(ans: Completions, opt: Optional[OptionDict], prefix:
|
||||
complete_files_and_dirs(ans, prefix, files_group_name='Directories', predicate=os.path.isdir)
|
||||
elif dest == 'start_as':
|
||||
k = 'Start as'
|
||||
ans.match_groups[k] = {x: x for x in 'normal,fullscreen,maximized,minimized'.split(',') if x.startswith(prefix)}
|
||||
ans.no_space_groups.add(k)
|
||||
ans.add_match_group(k, {x: x for x in 'normal,fullscreen,maximized,minimized'.split(',') if x.startswith(prefix)}, trailing_space=False)
|
||||
elif dest == 'listen_on':
|
||||
if ':' not in prefix:
|
||||
k = 'Address type'
|
||||
ans.match_groups[k] = {x: x for x in ('unix:', 'tcp:') if x.startswith(prefix)}
|
||||
ans.no_space_groups.add(k)
|
||||
ans.add_match_group(k, {x: x for x in ('unix:', 'tcp:') if x.startswith(prefix)}, trailing_space=False)
|
||||
elif prefix.startswith('unix:') and not prefix.startswith('@'):
|
||||
complete_files_and_dirs(ans, prefix[len('unix:'):], files_group_name='UNIX sockets', add_prefix='unix:')
|
||||
|
||||
@@ -295,7 +373,7 @@ def complete_alias_map(
|
||||
opt = option_map.get(w)
|
||||
if w is last_word and not new_word:
|
||||
if w.startswith('-'):
|
||||
ans.match_groups['Options'] = {k: opt['help'] for k, opt in option_map.items() if k.startswith(last_word)}
|
||||
ans.add_match_group('Options', {k: opt['help'] for k, opt in option_map.items() if k.startswith(last_word)})
|
||||
else:
|
||||
if complete_args is not None:
|
||||
complete_args(ans, None, last_word, Delegate(words, i))
|
||||
@@ -312,7 +390,7 @@ def complete_alias_map(
|
||||
prefix = '' if new_word else last_word
|
||||
if complete_args is not None:
|
||||
complete_args(ans, None, prefix, Delegate())
|
||||
ans.match_groups['Options'] = {k: opt['help'] for k, opt in option_map.items() if k.startswith(prefix)}
|
||||
ans.add_match_group('Options', {k: opt['help'] for k, opt in option_map.items() if k.startswith(prefix)})
|
||||
|
||||
|
||||
def complete_cli(
|
||||
@@ -391,12 +469,9 @@ def complete_files_and_dirs(
|
||||
files = (add_prefix + x for x in files)
|
||||
|
||||
if dirs:
|
||||
ans.match_groups['Directories'] = dict.fromkeys(dirs)
|
||||
ans.files_groups.add('Directories')
|
||||
ans.no_space_groups.add('Directories')
|
||||
ans.add_match_group('Directories', dirs, trailing_space=False, is_files=True)
|
||||
if files:
|
||||
ans.match_groups[files_group_name] = dict.fromkeys(files)
|
||||
ans.files_groups.add(files_group_name)
|
||||
ans.add_match_group(files_group_name, files, is_files=True)
|
||||
|
||||
|
||||
def complete_icat_args(ans: Completions, opt: Optional[OptionDict], prefix: str, unknown_args: Delegate) -> None:
|
||||
@@ -433,7 +508,7 @@ def remote_args_completer(title: str, words: Iterable[str]) -> CompleteArgsFunc:
|
||||
|
||||
def complete_names_for_arg(ans: Completions, opt: Optional[OptionDict], prefix: str, unknown_args: Delegate) -> None:
|
||||
if opt is None:
|
||||
ans.match_groups[title] = {c: '' for c in items if c.startswith(prefix)}
|
||||
ans.add_match_group(title, {c: '' for c in items if c.startswith(prefix)})
|
||||
|
||||
return complete_names_for_arg
|
||||
|
||||
@@ -450,6 +525,13 @@ def complete_diff_args(ans: Completions, opt: Optional[OptionDict], prefix: str,
|
||||
|
||||
|
||||
def complete_kitten(ans: Completions, kitten: str, words: Sequence[str], new_word: bool) -> None:
|
||||
try:
|
||||
completer = get_kitten_completer(kitten)
|
||||
except SystemExit:
|
||||
completer = None
|
||||
if completer is not None:
|
||||
completer(ans, words, new_word)
|
||||
return
|
||||
try:
|
||||
cd = get_kitten_cli_docs(kitten)
|
||||
except SystemExit:
|
||||
@@ -482,24 +564,24 @@ def find_completions(words: Sequence[str], new_word: bool, entry_points: Iterabl
|
||||
if words[0] == '@':
|
||||
if len(words) == 1 or (len(words) == 2 and not new_word):
|
||||
prefix = words[1] if len(words) > 1 else ''
|
||||
ans.match_groups['Remote control commands'] = {c: '' for c in remote_control_command_names() if c.startswith(prefix)}
|
||||
ans.add_match_group('Remote control commands', {c: '' for c in remote_control_command_names() if c.startswith(prefix)})
|
||||
else:
|
||||
complete_remote_command(ans, words[1], words[2:], new_word)
|
||||
return ans
|
||||
if words[0].startswith('@'):
|
||||
if len(words) == 1 and not new_word:
|
||||
prefix = words[0]
|
||||
ans.match_groups['Remote control commands'] = {'@' + c: '' for c in remote_control_command_names() if c.startswith(prefix)}
|
||||
ans.add_match_group('Remote control commands', {'@' + c: '' for c in remote_control_command_names() if c.startswith(prefix)})
|
||||
else:
|
||||
complete_remote_command(ans, words[0][1:], words[1:], new_word)
|
||||
if words[0] == '+':
|
||||
if len(words) == 1 or (len(words) == 2 and not new_word):
|
||||
prefix = words[1] if len(words) > 1 else ''
|
||||
ans.match_groups['Entry points'] = {c: '' for c in namespaced_entry_points if c.startswith(prefix)}
|
||||
ans.add_match_group('Entry points', {c: '' for c in namespaced_entry_points if c.startswith(prefix)})
|
||||
else:
|
||||
if words[1] == 'kitten':
|
||||
if len(words) == 2 or (len(words) == 3 and not new_word):
|
||||
ans.match_groups['Kittens'] = dict.fromkeys(k for k in all_kitten_names() if k.startswith('' if len(words) == 2 else words[2]))
|
||||
ans.add_match_group('Kittens', (k for k in all_kitten_names() if k.startswith('' if len(words) == 2 else words[2])))
|
||||
else:
|
||||
complete_kitten(ans, words[2], words[3:], new_word)
|
||||
return ans
|
||||
@@ -507,13 +589,13 @@ def find_completions(words: Sequence[str], new_word: bool, entry_points: Iterabl
|
||||
if len(words) == 1:
|
||||
if new_word:
|
||||
if words[0] == '+kitten':
|
||||
ans.match_groups['Kittens'] = dict.fromkeys(all_kitten_names())
|
||||
ans.add_match_group('Kittens', all_kitten_names())
|
||||
else:
|
||||
prefix = words[0]
|
||||
ans.match_groups['Entry points'] = {c: '' for c in namespaced_entry_points if c.startswith(prefix)}
|
||||
ans.add_match_group('Entry points', (c for c in namespaced_entry_points if c.startswith(prefix)))
|
||||
else:
|
||||
if len(words) == 2 and not new_word:
|
||||
ans.match_groups['Kittens'] = dict.fromkeys(k for k in all_kitten_names() if k.startswith(words[1]))
|
||||
ans.add_match_group('Kittens', (k for k in all_kitten_names() if k.startswith(words[1])))
|
||||
else:
|
||||
if words[0] == '+kitten':
|
||||
complete_kitten(ans, words[1], words[2:], new_word)
|
||||
|
||||
453
kitty/conf/generate.py
Normal file
453
kitty/conf/generate.py
Normal file
@@ -0,0 +1,453 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
import inspect
|
||||
import os
|
||||
import pprint
|
||||
import re
|
||||
import textwrap
|
||||
from typing import (
|
||||
Any, Callable, Dict, Iterator, List, Set, Tuple, Union, get_type_hints
|
||||
)
|
||||
|
||||
from kitty.conf.types import Definition, MultiOption, Option, unset
|
||||
|
||||
|
||||
def chunks(lst: List, n: int) -> Iterator[List]:
|
||||
for i in range(0, len(lst), n):
|
||||
yield lst[i:i + n]
|
||||
|
||||
|
||||
def atoi(text: str) -> str:
|
||||
return f'{int(text):08d}' if text.isdigit() else text
|
||||
|
||||
|
||||
def natural_keys(text: str) -> Tuple[str, ...]:
|
||||
return tuple(atoi(c) for c in re.split(r'(\d+)', text))
|
||||
|
||||
|
||||
def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
|
||||
class_lines: List[str] = []
|
||||
tc_lines: List[str] = []
|
||||
a = class_lines.append
|
||||
t = tc_lines.append
|
||||
a('class Options:')
|
||||
t('class Parser:')
|
||||
choices = {}
|
||||
imports: Set[Tuple[str, str]] = set()
|
||||
tc_imports: Set[Tuple[str, str]] = set()
|
||||
|
||||
def type_name(x: type) -> str:
|
||||
ans = x.__name__
|
||||
if x.__module__ and x.__module__ != 'builtins':
|
||||
imports.add((x.__module__, x.__name__))
|
||||
return ans
|
||||
|
||||
def option_type_as_str(x: Any) -> str:
|
||||
if hasattr(x, '__name__'):
|
||||
return type_name(x)
|
||||
ans = repr(x)
|
||||
ans = ans.replace('NoneType', 'None')
|
||||
return ans
|
||||
|
||||
def option_type_data(option: Union[Option, MultiOption]) -> Tuple[Callable, str]:
|
||||
func = option.parser_func
|
||||
if func.__module__ == 'builtins':
|
||||
return func, func.__name__
|
||||
th = get_type_hints(func)
|
||||
rettype = th['return']
|
||||
typ = option_type_as_str(rettype)
|
||||
if isinstance(option, MultiOption):
|
||||
typ = typ[typ.index('[') + 1:-1]
|
||||
typ = typ.replace('Tuple', 'Dict', 1)
|
||||
return func, typ
|
||||
|
||||
is_mutiple_vars = {}
|
||||
option_names = set()
|
||||
color_table = list(map(str, range(256)))
|
||||
|
||||
def parser_function_declaration(option_name: str) -> None:
|
||||
t('')
|
||||
t(f' def {option_name}(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:')
|
||||
|
||||
for option in sorted(defn.iter_all_options(), key=lambda a: natural_keys(a.name)):
|
||||
option_names.add(option.name)
|
||||
parser_function_declaration(option.name)
|
||||
if isinstance(option, MultiOption):
|
||||
mval: Dict[str, Dict[str, Any]] = {'macos': {}, 'linux': {}, '': {}}
|
||||
func, typ = option_type_data(option)
|
||||
for val in option:
|
||||
if val.add_to_default:
|
||||
gr = mval[val.only]
|
||||
for k, v in func(val.defval_as_str):
|
||||
gr[k] = v
|
||||
is_mutiple_vars[option.name] = typ, mval
|
||||
sig = inspect.signature(func)
|
||||
tc_imports.add((func.__module__, func.__name__))
|
||||
if len(sig.parameters) == 1:
|
||||
t(f' for k, v in {func.__name__}(val):')
|
||||
t(f' ans["{option.name}"][k] = v')
|
||||
else:
|
||||
t(f' for k, v in {func.__name__}(val, ans["{option.name}"]):')
|
||||
t(f' ans["{option.name}"][k] = v')
|
||||
continue
|
||||
|
||||
if option.choices:
|
||||
typ = 'typing.Literal[{}]'.format(', '.join(repr(x) for x in option.choices))
|
||||
ename = f'choices_for_{option.name}'
|
||||
choices[ename] = typ
|
||||
typ = ename
|
||||
func = str
|
||||
elif defn.has_color_table and option.is_color_table_color:
|
||||
func, typ = option_type_data(option)
|
||||
t(f' ans[{option.name!r}] = {func.__name__}(val)')
|
||||
tc_imports.add((func.__module__, func.__name__))
|
||||
cnum = int(option.name[5:])
|
||||
color_table[cnum] = '0x{:06x}'.format(func(option.defval_as_string).__int__())
|
||||
continue
|
||||
else:
|
||||
func, typ = option_type_data(option)
|
||||
try:
|
||||
params = inspect.signature(func).parameters
|
||||
except Exception:
|
||||
params = {}
|
||||
if 'dict_with_parse_results' in params:
|
||||
t(f' {func.__name__}(val, ans)')
|
||||
else:
|
||||
t(f' ans[{option.name!r}] = {func.__name__}(val)')
|
||||
if func.__module__ != 'builtins':
|
||||
tc_imports.add((func.__module__, func.__name__))
|
||||
|
||||
defval = repr(func(option.defval_as_string))
|
||||
if option.macos_defval is not unset:
|
||||
md = repr(func(option.macos_defval))
|
||||
defval = f'{md} if is_macos else {defval}'
|
||||
imports.add(('kitty.constants', 'is_macos'))
|
||||
a(f' {option.name}: {typ} = {defval}')
|
||||
if option.choices:
|
||||
t(' val = val.lower()')
|
||||
t(f' if val not in self.choices_for_{option.name}:')
|
||||
t(f' raise ValueError(f"The value {{val}} is not a valid choice for {option.name}")')
|
||||
t(f' ans["{option.name}"] = val')
|
||||
t('')
|
||||
t(f' choices_for_{option.name} = frozenset({option.choices!r})')
|
||||
|
||||
for option_name, (typ, mval) in is_mutiple_vars.items():
|
||||
a(f' {option_name}: {typ} = ' '{}')
|
||||
|
||||
for parser, aliases in defn.deprecations.items():
|
||||
for alias in aliases:
|
||||
parser_function_declaration(alias)
|
||||
tc_imports.add((parser.__module__, parser.__name__))
|
||||
t(f' {parser.__name__}({alias!r}, val, ans)')
|
||||
|
||||
action_parsers = {}
|
||||
|
||||
def resolve_import(ftype: str) -> str:
|
||||
if '.' in ftype:
|
||||
fmod, ftype = ftype.rpartition('.')[::2]
|
||||
else:
|
||||
fmod = f'{loc}.options.utils'
|
||||
imports.add((fmod, ftype))
|
||||
return ftype
|
||||
|
||||
for aname, action in defn.actions.items():
|
||||
option_names.add(aname)
|
||||
action_parsers[aname] = func = action.parser_func
|
||||
th = get_type_hints(func)
|
||||
rettype = th['return']
|
||||
typ = option_type_as_str(rettype)
|
||||
typ = typ[typ.index('[') + 1:-1]
|
||||
a(f' {aname}: typing.List[{typ}] = []')
|
||||
for imp in action.imports:
|
||||
resolve_import(imp)
|
||||
for fname, ftype in action.fields.items():
|
||||
ftype = resolve_import(ftype)
|
||||
a(f' {fname}: {ftype} = ' '{}')
|
||||
parser_function_declaration(aname)
|
||||
t(f' for k in {func.__name__}(val):')
|
||||
t(f' ans[{aname!r}].append(k)')
|
||||
tc_imports.add((func.__module__, func.__name__))
|
||||
|
||||
if defn.has_color_table:
|
||||
imports.add(('array', 'array'))
|
||||
a(' color_table: array = array("L", (')
|
||||
for grp in chunks(color_table, 8):
|
||||
a(' ' + ', '.join(grp) + ',')
|
||||
a(' ))')
|
||||
|
||||
a(' config_paths: typing.Tuple[str, ...] = ()')
|
||||
a(' config_overrides: typing.Tuple[str, ...] = ()')
|
||||
a('')
|
||||
a(' def __init__(self, options_dict: typing.Optional[typing.Dict[str, typing.Any]] = None) -> None:')
|
||||
a(' if options_dict is not None:')
|
||||
a(' for key in option_names:')
|
||||
a(' setattr(self, key, options_dict[key])')
|
||||
|
||||
a('')
|
||||
a(' @property')
|
||||
a(' def _fields(self) -> typing.Tuple[str, ...]:')
|
||||
a(' return option_names')
|
||||
|
||||
a('')
|
||||
a(' def __iter__(self) -> typing.Iterator[str]:')
|
||||
a(' return iter(self._fields)')
|
||||
|
||||
a('')
|
||||
a(' def __len__(self) -> int:')
|
||||
a(' return len(self._fields)')
|
||||
|
||||
a('')
|
||||
a(' def _copy_of_val(self, name: str) -> typing.Any:')
|
||||
a(' ans = getattr(self, name)')
|
||||
a(' if isinstance(ans, dict):\n ans = ans.copy()')
|
||||
a(' elif isinstance(ans, list):\n ans = ans[:]')
|
||||
a(' return ans')
|
||||
|
||||
a('')
|
||||
a(' def _asdict(self) -> typing.Dict[str, typing.Any]:')
|
||||
a(' return {k: self._copy_of_val(k) for k in self}')
|
||||
|
||||
a('')
|
||||
a(' def _replace(self, **kw: typing.Any) -> "Options":')
|
||||
a(' ans = Options()')
|
||||
a(' for name in self:')
|
||||
a(' setattr(ans, name, self._copy_of_val(name))')
|
||||
a(' for name, val in kw.items():')
|
||||
a(' setattr(ans, name, val)')
|
||||
a(' return ans')
|
||||
|
||||
a('')
|
||||
a(' def __getitem__(self, key: typing.Union[int, str]) -> typing.Any:')
|
||||
a(' k = option_names[key] if isinstance(key, int) else key')
|
||||
a(' try:')
|
||||
a(' return getattr(self, k)')
|
||||
a(' except AttributeError:')
|
||||
a(' pass')
|
||||
a(' raise KeyError(f"No option named: {k}")')
|
||||
|
||||
if defn.has_color_table:
|
||||
a('')
|
||||
a(' def __getattr__(self, key: str) -> typing.Any:')
|
||||
a(' if key.startswith("color"):')
|
||||
a(' q = key[5:]')
|
||||
a(' if q.isdigit():')
|
||||
a(' k = int(q)')
|
||||
a(' if 0 <= k <= 255:')
|
||||
a(' x = self.color_table[k]')
|
||||
a(' return Color((x >> 16) & 255, (x >> 8) & 255, x & 255)')
|
||||
a(' raise AttributeError(key)')
|
||||
a('')
|
||||
a(' def __setattr__(self, key: str, val: typing.Any) -> typing.Any:')
|
||||
a(' if key.startswith("color"):')
|
||||
a(' q = key[5:]')
|
||||
a(' if q.isdigit():')
|
||||
a(' k = int(q)')
|
||||
a(' if 0 <= k <= 255:')
|
||||
a(' self.color_table[k] = int(val)')
|
||||
a(' return')
|
||||
a(' object.__setattr__(self, key, val)')
|
||||
|
||||
a('')
|
||||
a('')
|
||||
a('defaults = Options()')
|
||||
for option_name, (typ, mval) in is_mutiple_vars.items():
|
||||
a(f'defaults.{option_name} = {mval[""]!r}')
|
||||
if mval['macos']:
|
||||
imports.add(('kitty.constants', 'is_macos'))
|
||||
a('if is_macos:')
|
||||
a(f' defaults.{option_name}.update({mval["macos"]!r}')
|
||||
if mval['macos']:
|
||||
imports.add(('kitty.constants', 'is_macos'))
|
||||
a('if not is_macos:')
|
||||
a(f' defaults.{option_name}.update({mval["linux"]!r}')
|
||||
|
||||
for aname, func in action_parsers.items():
|
||||
a(f'defaults.{aname} = [')
|
||||
only: Dict[str, List[Tuple[str, Callable]]] = {}
|
||||
for sc in defn.iter_all_maps(aname):
|
||||
if not sc.add_to_default:
|
||||
continue
|
||||
text = sc.parseable_text
|
||||
if sc.only:
|
||||
only.setdefault(sc.only, []).append((text, func))
|
||||
else:
|
||||
for val in func(text):
|
||||
a(f' # {sc.name}')
|
||||
a(f' {val!r},')
|
||||
a(']')
|
||||
if only:
|
||||
imports.add(('kitty.constants', 'is_macos'))
|
||||
for cond, items in only.items():
|
||||
cond = 'is_macos' if cond == 'macos' else 'not is_macos'
|
||||
a(f'if {cond}:')
|
||||
for (text, func) in items:
|
||||
for val in func(text):
|
||||
a(f' defaults.{aname}.append({val!r})')
|
||||
|
||||
t('')
|
||||
t('')
|
||||
t('def create_result_dict() -> typing.Dict[str, typing.Any]:')
|
||||
t(' return {')
|
||||
for oname in is_mutiple_vars:
|
||||
t(f' {oname!r}: {{}},')
|
||||
for aname in defn.actions:
|
||||
t(f' {aname!r}: [],')
|
||||
t(' }')
|
||||
|
||||
t('')
|
||||
t('')
|
||||
t(f'actions = frozenset({tuple(defn.actions)!r})')
|
||||
t('')
|
||||
t('')
|
||||
t('def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:')
|
||||
t(' ans = {}')
|
||||
t(' for k, v in defaults.items():')
|
||||
t(' if isinstance(v, dict):')
|
||||
t(' ans[k] = merge_dicts(v, vals.get(k, {}))')
|
||||
t(' elif k in actions:')
|
||||
t(' ans[k] = v + vals.get(k, [])')
|
||||
t(' else:')
|
||||
t(' ans[k] = vals.get(k, v)')
|
||||
t(' return ans')
|
||||
tc_imports.add(('kitty.conf.utils', 'merge_dicts'))
|
||||
|
||||
t('')
|
||||
t('')
|
||||
t('parser = Parser()')
|
||||
t('')
|
||||
t('')
|
||||
t('def parse_conf_item(key: str, val: str, ans: typing.Dict[str, typing.Any]) -> bool:')
|
||||
t(' func = getattr(parser, key, None)')
|
||||
t(' if func is not None:')
|
||||
t(' func(val, ans)')
|
||||
t(' return True')
|
||||
t(' return False')
|
||||
|
||||
preamble = ['# generated by gen-config.py DO NOT edit', '# vim:fileencoding=utf-8', '']
|
||||
a = preamble.append
|
||||
|
||||
def output_imports(imports: Set, add_module_imports: bool = True) -> None:
|
||||
a('import typing')
|
||||
seen_mods = {'typing'}
|
||||
mmap: Dict[str, List[str]] = {}
|
||||
for mod, name in imports:
|
||||
mmap.setdefault(mod, []).append(name)
|
||||
for mod in sorted(mmap):
|
||||
names = sorted(mmap[mod])
|
||||
lines = textwrap.wrap(', '.join(names), 100)
|
||||
if len(lines) == 1:
|
||||
s = lines[0]
|
||||
else:
|
||||
s = '\n '.join(lines)
|
||||
s = f'(\n {s}\n)'
|
||||
a(f'from {mod} import {s}')
|
||||
if add_module_imports and mod not in seen_mods and mod != s:
|
||||
a(f'import {mod}')
|
||||
seen_mods.add(mod)
|
||||
|
||||
output_imports(imports)
|
||||
a('')
|
||||
if choices:
|
||||
a('if typing.TYPE_CHECKING:')
|
||||
for name, cdefn in choices.items():
|
||||
a(f' {name} = {cdefn}')
|
||||
a('else:')
|
||||
for name in choices:
|
||||
a(f' {name} = str')
|
||||
|
||||
a('')
|
||||
a('option_names = ( # {{''{')
|
||||
a(' ' + pprint.pformat(tuple(sorted(option_names, key=natural_keys)))[1:] + ' # }}''}')
|
||||
class_def = '\n'.join(preamble + ['', ''] + class_lines)
|
||||
|
||||
preamble = ['# generated by gen-config.py DO NOT edit', '# vim:fileencoding=utf-8', '']
|
||||
a = preamble.append
|
||||
output_imports(tc_imports, False)
|
||||
|
||||
return class_def, '\n'.join(preamble + ['', ''] + tc_lines)
|
||||
|
||||
|
||||
def generate_c_conversion(loc: str, ctypes: List[Option]) -> str:
|
||||
lines: List[str] = []
|
||||
basic_converters = {
|
||||
'int': 'PyLong_AsLong', 'uint': 'PyLong_AsUnsignedLong', 'bool': 'PyObject_IsTrue',
|
||||
'float': 'PyFloat_AsFloat', 'double': 'PyFloat_AsDouble',
|
||||
'time': 'parse_s_double_to_monotonic_t', 'time-ms': 'parse_ms_long_to_monotonic_t'
|
||||
}
|
||||
|
||||
for opt in ctypes:
|
||||
lines.append('')
|
||||
lines.append(f'static void\nconvert_from_python_{opt.name}(PyObject *val, Options *opts) ''{')
|
||||
is_special = opt.ctype.startswith('!')
|
||||
if is_special:
|
||||
func = opt.ctype[1:]
|
||||
lines.append(f' {func}(val, opts);')
|
||||
else:
|
||||
func = basic_converters.get(opt.ctype, opt.ctype)
|
||||
lines.append(f' opts->{opt.name} = {func}(val);')
|
||||
lines.append('}')
|
||||
lines.append('')
|
||||
lines.append(f'static void\nconvert_from_opts_{opt.name}(PyObject *py_opts, Options *opts) ''{')
|
||||
lines.append(f' PyObject *ret = PyObject_GetAttrString(py_opts, "{opt.name}");')
|
||||
lines.append(' if (ret == NULL) return;')
|
||||
lines.append(f' convert_from_python_{opt.name}(ret, opts);')
|
||||
lines.append(' Py_DECREF(ret);')
|
||||
lines.append('}')
|
||||
|
||||
lines.append('')
|
||||
lines.append('static bool\nconvert_opts_from_python_opts(PyObject *py_opts, Options *opts) ''{')
|
||||
for opt in ctypes:
|
||||
lines.append(f' convert_from_opts_{opt.name}(py_opts, opts);')
|
||||
lines.append(' if (PyErr_Occurred()) return false;')
|
||||
lines.append(' return true;')
|
||||
lines.append('}')
|
||||
|
||||
preamble = ['// generated by gen-config.py DO NOT edit', '// vim:fileencoding=utf-8', '#pragma once', '#include "to-c.h"']
|
||||
return '\n'.join(preamble + ['', ''] + lines)
|
||||
|
||||
|
||||
def write_output(loc: str, defn: Definition) -> None:
|
||||
cls, tc = generate_class(defn, loc)
|
||||
with open(os.path.join(*loc.split('.'), 'options', 'types.py'), 'w') as f:
|
||||
f.write(cls + '\n')
|
||||
with open(os.path.join(*loc.split('.'), 'options', 'parse.py'), 'w') as f:
|
||||
f.write(tc + '\n')
|
||||
ctypes = []
|
||||
for opt in defn.root_group.iter_all_non_groups():
|
||||
if isinstance(opt, Option) and opt.ctype:
|
||||
ctypes.append(opt)
|
||||
if ctypes:
|
||||
c = generate_c_conversion(loc, ctypes)
|
||||
with open(os.path.join(*loc.split('.'), 'options', 'to-c-generated.h'), 'w') as f:
|
||||
f.write(c + '\n')
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# To use run it as:
|
||||
# kitty +runpy 'from kitty.conf.generate import main; main()' /path/to/kitten/file.py
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
from kittens.runner import path_to_custom_kitten, resolved_kitten
|
||||
from kitty.constants import config_dir
|
||||
|
||||
kitten = sys.argv[-1]
|
||||
if not kitten.endswith('.py'):
|
||||
kitten += '.py'
|
||||
kitten = resolved_kitten(kitten)
|
||||
path = os.path.realpath(path_to_custom_kitten(config_dir, kitten))
|
||||
if not os.path.dirname(path):
|
||||
raise SystemExit(f'No custom kitten named {kitten} found')
|
||||
sys.path.insert(0, os.path.dirname(path))
|
||||
package_name = os.path.basename(os.path.dirname(path))
|
||||
m = importlib.import_module('kitten_options_definition')
|
||||
defn = getattr(m, 'definition')
|
||||
loc = package_name
|
||||
cls, tc = generate_class(defn, loc)
|
||||
with open(os.path.join(os.path.dirname(path), 'kitten_options_types.py'), 'w') as f:
|
||||
f.write(cls + '\n')
|
||||
with open(os.path.join(os.path.dirname(path), 'kitten_options_parse.py'), 'w') as f:
|
||||
f.write(tc + '\n')
|
||||
@@ -541,9 +541,11 @@ class Action:
|
||||
class Definition:
|
||||
|
||||
def __init__(self, package: str, *actions: Action, has_color_table: bool = False) -> None:
|
||||
self.module_for_parsers = import_module(f'{package}.options.utils')
|
||||
if package.startswith('!'):
|
||||
self.module_for_parsers = import_module(package[1:])
|
||||
else:
|
||||
self.module_for_parsers = import_module(f'{package}.options.utils')
|
||||
self.has_color_table = has_color_table
|
||||
self.package = package
|
||||
self.coalesced_iterator_data = CoalescedIteratorData()
|
||||
self.root_group = Group('', '', self.coalesced_iterator_data)
|
||||
self.current_group = self.root_group
|
||||
|
||||
@@ -23,7 +23,7 @@ class Version(NamedTuple):
|
||||
|
||||
appname: str = 'kitty'
|
||||
kitty_face = '🐱'
|
||||
version: Version = Version(0, 21, 0)
|
||||
version: Version = Version(0, 21, 2)
|
||||
str_version: str = '.'.join(map(str, version))
|
||||
_plat = sys.platform.lower()
|
||||
is_macos: bool = 'darwin' in _plat
|
||||
|
||||
@@ -323,6 +323,13 @@ harfbuzz_font_for_face(PyObject* s) {
|
||||
return self->hb_font;
|
||||
}
|
||||
|
||||
static unsigned int
|
||||
adjust_ypos(unsigned int pos, unsigned int cell_height, int adjustment) {
|
||||
if (adjustment >= 0) adjustment = MIN(adjustment, (int)pos - 1);
|
||||
else adjustment = MAX(adjustment, (int)pos - (int)cell_height + 1);
|
||||
return pos - adjustment;
|
||||
}
|
||||
|
||||
void
|
||||
cell_metrics(PyObject *s, unsigned int* cell_width, unsigned int* cell_height, unsigned int* baseline, unsigned int* underline_position, unsigned int* underline_thickness, unsigned int* strikethrough_position, unsigned int* strikethrough_thickness) {
|
||||
// See https://developer.apple.com/library/content/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/TypoFeatures/TextSystemFeatures.html
|
||||
@@ -342,7 +349,6 @@ cell_metrics(PyObject *s, unsigned int* cell_width, unsigned int* cell_height, u
|
||||
}
|
||||
*cell_width = MAX(1u, width);
|
||||
*underline_thickness = (unsigned int)ceil(MAX(0.1, self->underline_thickness));
|
||||
*strikethrough_position = (unsigned int)floor(*baseline * 0.65);
|
||||
*strikethrough_thickness = *underline_thickness;
|
||||
// float line_height = MAX(1, floor(self->ascent + self->descent + MAX(0, self->leading) + 0.5));
|
||||
// Let CoreText's layout engine calculate the line height. Slower, but hopefully more accurate.
|
||||
@@ -367,12 +373,16 @@ cell_metrics(PyObject *s, unsigned int* cell_width, unsigned int* cell_height, u
|
||||
CGRect bounds_without_leading = CTLineGetBoundsWithOptions(line, kCTLineBoundsExcludeTypographicLeading);
|
||||
CGFloat typographic_ascent, typographic_descent, typographic_leading;
|
||||
CTLineGetTypographicBounds(line, &typographic_ascent, &typographic_descent, &typographic_leading);
|
||||
CGFloat bounds_ascent = bounds_without_leading.size.height + bounds_without_leading.origin.y;
|
||||
*baseline = (unsigned int)floor(bounds_ascent + 0.5);
|
||||
*cell_height = MAX(4u, (unsigned int)ceilf(line_height));
|
||||
CGFloat bounds_ascent = bounds_without_leading.size.height + bounds_without_leading.origin.y;
|
||||
int baseline_offset = 0;
|
||||
if (OPT(adjust_baseline_px) != 0) baseline_offset = OPT(adjust_baseline_px);
|
||||
else if (OPT(adjust_baseline_frac) != 0) baseline_offset = (int)(*cell_height * OPT(adjust_baseline_frac));
|
||||
*baseline = (unsigned int)floor(bounds_ascent + 0.5);
|
||||
// Not sure if we should add this to bounds ascent and then round it or add
|
||||
// it to already rounded baseline and round again.
|
||||
*underline_position = (unsigned int)floor(bounds_ascent - self->underline_position + 0.5);
|
||||
*strikethrough_position = (unsigned int)floor(*baseline * 0.65);
|
||||
|
||||
debug("Cell height calculation:\n");
|
||||
debug("\tline height from line origins: %f\n", line_height);
|
||||
@@ -381,7 +391,12 @@ cell_metrics(PyObject *s, unsigned int* cell_width, unsigned int* cell_height, u
|
||||
debug("\tbounds metrics: ascent: %f\n", bounds_ascent);
|
||||
debug("\tline metrics: ascent: %f descent: %f leading: %f\n", typographic_ascent, typographic_descent, typographic_leading);
|
||||
debug("\tfont metrics: ascent: %f descent: %f leading: %f underline_position: %f\n", self->ascent, self->descent, self->leading, self->underline_position);
|
||||
debug("\tcell_height: %u baseline: %u underline_position: %u\n", *cell_height, *baseline, *underline_position);
|
||||
debug("\tcell_height: %u baseline: %u underline_position: %u strikethrough_position: %u\n", *cell_height, *baseline, *underline_position, *strikethrough_position);
|
||||
if (baseline_offset) {
|
||||
*baseline = adjust_ypos(*baseline, *cell_height, baseline_offset);
|
||||
*underline_position = adjust_ypos(*underline_position, *cell_height, baseline_offset);
|
||||
*strikethrough_position = adjust_ypos(*strikethrough_position, *cell_height, baseline_offset);
|
||||
}
|
||||
|
||||
CFRelease(test_frame); CFRelease(path); CFRelease(framesetter);
|
||||
|
||||
@@ -415,16 +430,20 @@ specialize_font_descriptor(PyObject *base_descriptor, FONTS_DATA_HANDLE fg UNUSE
|
||||
return base_descriptor;
|
||||
}
|
||||
|
||||
static uint8_t *render_buf = NULL;
|
||||
static size_t render_buf_sz = 0;
|
||||
static CGGlyph glyphs[128];
|
||||
static CGRect boxes[128];
|
||||
static CGPoint positions[128];
|
||||
static CGSize advances[128];
|
||||
struct RenderBuffers {
|
||||
uint8_t *render_buf;
|
||||
size_t render_buf_sz, sz;
|
||||
CGGlyph *glyphs;
|
||||
CGRect *boxes;
|
||||
CGPoint *positions;
|
||||
CGSize *advances;
|
||||
};
|
||||
static struct RenderBuffers buffers = {0};
|
||||
|
||||
static void
|
||||
finalize(void) {
|
||||
free(render_buf);
|
||||
free(buffers.render_buf); free(buffers.glyphs); free(buffers.boxes); free(buffers.positions); free(buffers.advances);
|
||||
memset(&buffers, 0, sizeof(struct RenderBuffers));
|
||||
if (all_fonts_collection_data) CFRelease(all_fonts_collection_data);
|
||||
}
|
||||
|
||||
@@ -442,7 +461,7 @@ render_color_glyph(CTFontRef font, uint8_t *buf, int glyph_id, unsigned int widt
|
||||
CGContextSetTextDrawingMode(ctx, kCGTextFill);
|
||||
CGGlyph glyph = glyph_id;
|
||||
CGContextSetTextMatrix(ctx, transform);
|
||||
CGContextSetTextPosition(ctx, -boxes[0].origin.x, MAX(2, height - baseline));
|
||||
CGContextSetTextPosition(ctx, -buffers.boxes[0].origin.x, MAX(2, height - baseline));
|
||||
CGPoint p = CGPointMake(0, 0);
|
||||
CTFontDrawGlyphs(font, &glyph, &p, 1, ctx);
|
||||
CGContextRelease(ctx);
|
||||
@@ -455,21 +474,30 @@ render_color_glyph(CTFontRef font, uint8_t *buf, int glyph_id, unsigned int widt
|
||||
}
|
||||
}
|
||||
|
||||
static inline void
|
||||
ensure_render_space(size_t width, size_t height) {
|
||||
if (render_buf_sz >= width * height) return;
|
||||
free(render_buf);
|
||||
render_buf_sz = width * height;
|
||||
render_buf = malloc(render_buf_sz);
|
||||
if (render_buf == NULL) fatal("Out of memory");
|
||||
static void
|
||||
ensure_render_space(size_t width, size_t height, size_t num_glyphs) {
|
||||
if (buffers.render_buf_sz < width * height) {
|
||||
free(buffers.render_buf); buffers.render_buf = NULL;
|
||||
buffers.render_buf_sz = width * height;
|
||||
buffers.render_buf = malloc(buffers.render_buf_sz);
|
||||
if (buffers.render_buf == NULL) fatal("Out of memory");
|
||||
}
|
||||
if (buffers.sz < num_glyphs) {
|
||||
buffers.sz = MAX(128, num_glyphs * 2);
|
||||
buffers.advances = calloc(sizeof(buffers.advances[0]), buffers.sz);
|
||||
buffers.boxes = calloc(sizeof(buffers.boxes[0]), buffers.sz);
|
||||
buffers.glyphs = calloc(sizeof(buffers.glyphs[0]), buffers.sz);
|
||||
buffers.positions = calloc(sizeof(buffers.positions[0]), buffers.sz);
|
||||
if (!buffers.advances || !buffers.boxes || !buffers.glyphs || !buffers.positions) fatal("Out of memory");
|
||||
}
|
||||
}
|
||||
|
||||
static inline void
|
||||
render_glyphs(CTFontRef font, unsigned int width, unsigned int height, unsigned int baseline, unsigned int num_glyphs) {
|
||||
memset(render_buf, 0, width * height);
|
||||
memset(buffers.render_buf, 0, width * height);
|
||||
CGColorSpaceRef gray_color_space = CGColorSpaceCreateDeviceGray();
|
||||
if (gray_color_space == NULL) fatal("Out of memory");
|
||||
CGContextRef render_ctx = CGBitmapContextCreate(render_buf, width, height, 8, width, gray_color_space, (kCGBitmapAlphaInfoMask & kCGImageAlphaNone));
|
||||
CGContextRef render_ctx = CGBitmapContextCreate(buffers.render_buf, width, height, 8, width, gray_color_space, (kCGBitmapAlphaInfoMask & kCGImageAlphaNone));
|
||||
if (render_ctx == NULL) fatal("Out of memory");
|
||||
CGContextSetShouldAntialias(render_ctx, true);
|
||||
CGContextSetShouldSmoothFonts(render_ctx, true);
|
||||
@@ -479,7 +507,7 @@ render_glyphs(CTFontRef font, unsigned int width, unsigned int height, unsigned
|
||||
CGContextSetTextDrawingMode(render_ctx, kCGTextFillStroke);
|
||||
CGContextSetTextMatrix(render_ctx, CGAffineTransformIdentity);
|
||||
CGContextSetTextPosition(render_ctx, 0, height - baseline);
|
||||
CTFontDrawGlyphs(font, glyphs, positions, num_glyphs, render_ctx);
|
||||
CTFontDrawGlyphs(font, buffers.glyphs, buffers.positions, num_glyphs, render_ctx);
|
||||
CGContextRelease(render_ctx);
|
||||
CGColorSpaceRelease(gray_color_space);
|
||||
}
|
||||
@@ -492,19 +520,20 @@ render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) {
|
||||
unichar chars[num_chars];
|
||||
CGSize local_advances[num_chars];
|
||||
for (size_t i = 0; i < num_chars; i++) chars[i] = text[i];
|
||||
CTFontGetGlyphsForCharacters(font, chars, glyphs, num_chars);
|
||||
CTFontGetAdvancesForGlyphs(font, kCTFontOrientationDefault, glyphs, local_advances, num_chars);
|
||||
CGRect bounding_box = CTFontGetBoundingRectsForGlyphs(font, kCTFontOrientationDefault, glyphs, boxes, num_chars);
|
||||
ensure_render_space(0, 0, num_chars);
|
||||
CTFontGetGlyphsForCharacters(font, chars, buffers.glyphs, num_chars);
|
||||
CTFontGetAdvancesForGlyphs(font, kCTFontOrientationDefault, buffers.glyphs, local_advances, num_chars);
|
||||
CGRect bounding_box = CTFontGetBoundingRectsForGlyphs(font, kCTFontOrientationDefault, buffers.glyphs, buffers.boxes, num_chars);
|
||||
CGFloat x = 0, y = 0;
|
||||
for (size_t i = 0; i < num_chars; i++) {
|
||||
positions[i] = CGPointMake(x, y);
|
||||
buffers.positions[i] = CGPointMake(x, y);
|
||||
x += local_advances[i].width; y += local_advances[i].height;
|
||||
}
|
||||
StringCanvas ans = { .width = (size_t)ceil(x), .height = (size_t)(2 * bounding_box.size.height) };
|
||||
ensure_render_space(ans.width, ans.height);
|
||||
ensure_render_space(ans.width, ans.height, num_chars);
|
||||
render_glyphs(font, ans.width, ans.height, baseline, num_chars);
|
||||
ans.canvas = malloc(ans.width * ans.height);
|
||||
if (ans.canvas) memcpy(ans.canvas, render_buf, ans.width * ans.height);
|
||||
if (ans.canvas) memcpy(ans.canvas, buffers.render_buf, ans.width * ans.height);
|
||||
return ans;
|
||||
}
|
||||
|
||||
@@ -512,12 +541,13 @@ render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) {
|
||||
static inline bool
|
||||
do_render(CTFontRef ct_font, bool bold, bool italic, hb_glyph_info_t *info, hb_glyph_position_t *hb_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, bool allow_resize, FONTS_DATA_HANDLE fg, bool center_glyph) {
|
||||
unsigned int canvas_width = cell_width * num_cells;
|
||||
CGRect br = CTFontGetBoundingRectsForGlyphs(ct_font, kCTFontOrientationHorizontal, glyphs, boxes, num_glyphs);
|
||||
ensure_render_space(canvas_width, cell_height, num_glyphs);
|
||||
CGRect br = CTFontGetBoundingRectsForGlyphs(ct_font, kCTFontOrientationHorizontal, buffers.glyphs, buffers.boxes, num_glyphs);
|
||||
const bool debug_rendering = false;
|
||||
if (allow_resize) {
|
||||
// Resize glyphs that would bleed into neighboring cells, by scaling the font size
|
||||
float right = 0;
|
||||
for (unsigned i=0; i < num_glyphs; i++) right = MAX(right, boxes[i].origin.x + boxes[i].size.width);
|
||||
for (unsigned i=0; i < num_glyphs; i++) right = MAX(right, buffers.boxes[i].origin.x + buffers.boxes[i].size.width);
|
||||
if (!bold && !italic && right > canvas_width + 1) {
|
||||
if (debug_rendering) printf("resizing glyphs, right: %f canvas_width: %u\n", right, canvas_width);
|
||||
CGFloat sz = CTFontGetSize(ct_font);
|
||||
@@ -529,19 +559,18 @@ do_render(CTFontRef ct_font, bool bold, bool italic, hb_glyph_info_t *info, hb_g
|
||||
}
|
||||
}
|
||||
CGFloat x = 0, y = 0;
|
||||
CTFontGetAdvancesForGlyphs(ct_font, kCTFontOrientationDefault, glyphs, advances, num_glyphs);
|
||||
CTFontGetAdvancesForGlyphs(ct_font, kCTFontOrientationDefault, buffers.glyphs, buffers.advances, num_glyphs);
|
||||
for (unsigned i=0; i < num_glyphs; i++) {
|
||||
positions[i].x = x; positions[i].y = y;
|
||||
if (debug_rendering) printf("x=%f origin=%f width=%f advance=%f\n", x, boxes[i].origin.x, boxes[i].size.width, advances[i].width);
|
||||
x += advances[i].width; y += advances[i].height;
|
||||
buffers.positions[i].x = x; buffers.positions[i].y = y;
|
||||
if (debug_rendering) printf("x=%f origin=%f width=%f advance=%f\n", x, buffers.boxes[i].origin.x, buffers.boxes[i].size.width, buffers.advances[i].width);
|
||||
x += buffers.advances[i].width; y += buffers.advances[i].height;
|
||||
}
|
||||
if (*was_colored) {
|
||||
render_color_glyph(ct_font, (uint8_t*)canvas, info[0].codepoint, cell_width * num_cells, cell_height, baseline);
|
||||
} else {
|
||||
ensure_render_space(canvas_width, cell_height);
|
||||
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(render_buf, canvas, &src, &dest, canvas_width, canvas_width);
|
||||
render_alpha_mask(buffers.render_buf, canvas, &src, &dest, canvas_width, canvas_width);
|
||||
}
|
||||
if (num_cells && (center_glyph || (num_cells == 2 && *was_colored))) {
|
||||
if (debug_rendering) printf("centering glyphs: center_glyph: %d\n", center_glyph);
|
||||
@@ -557,7 +586,8 @@ do_render(CTFontRef ct_font, bool bold, bool italic, hb_glyph_info_t *info, hb_g
|
||||
bool
|
||||
render_glyphs_in_cells(PyObject *s, bool bold, bool italic, hb_glyph_info_t *info, hb_glyph_position_t *hb_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 fg, bool center_glyph) {
|
||||
CTFace *self = (CTFace*)s;
|
||||
for (unsigned i=0; i < num_glyphs; i++) glyphs[i] = info[i].codepoint;
|
||||
ensure_render_space(128, 128, num_glyphs);
|
||||
for (unsigned i=0; i < num_glyphs; i++) buffers.glyphs[i] = info[i].codepoint;
|
||||
return do_render(self->ct_font, bold, italic, info, hb_positions, num_glyphs, canvas, cell_width, cell_height, num_cells, baseline, was_colored, true, fg, center_glyph);
|
||||
}
|
||||
|
||||
|
||||
@@ -238,9 +238,9 @@ do_increment(FontGroup *fg, int *error) {
|
||||
|
||||
|
||||
static SpritePosition*
|
||||
sprite_position_for(FontGroup *fg, Font *font, glyph_index *glyphs, unsigned glyph_count, uint8_t ligature_index, int *error) {
|
||||
sprite_position_for(FontGroup *fg, Font *font, glyph_index *glyphs, unsigned glyph_count, uint8_t ligature_index, unsigned cell_count, int *error) {
|
||||
bool created;
|
||||
SpritePosition *s = find_or_create_sprite_position(&font->sprite_position_hash_table, glyphs, glyph_count, ligature_index, &created);
|
||||
SpritePosition *s = find_or_create_sprite_position(&font->sprite_position_hash_table, glyphs, glyph_count, ligature_index, cell_count, &created);
|
||||
if (!s) { *error = 1; return NULL; }
|
||||
if (created) {
|
||||
s->x = fg->sprite_tracker.x; s->y = fg->sprite_tracker.y; s->z = fg->sprite_tracker.z;
|
||||
@@ -560,10 +560,10 @@ END_ALLOW_CASE_RANGE
|
||||
static PyObject* box_drawing_function = NULL, *prerender_function = NULL, *descriptor_for_idx = NULL;
|
||||
|
||||
void
|
||||
render_alpha_mask(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) {
|
||||
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;
|
||||
uint8_t *s = alpha_mask + src_stride * sr;
|
||||
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];
|
||||
@@ -576,7 +576,7 @@ static void
|
||||
render_box_cell(FontGroup *fg, CPUCell *cpu_cell, GPUCell *gpu_cell) {
|
||||
int error = 0;
|
||||
glyph_index glyph = box_glyph_id(cpu_cell->ch);
|
||||
SpritePosition *sp = sprite_position_for(fg, &fg->fonts[BOX_FONT], &glyph, 1, false, &error);
|
||||
SpritePosition *sp = sprite_position_for(fg, &fg->fonts[BOX_FONT], &glyph, 1, 0, 1, &error);
|
||||
if (sp == NULL) {
|
||||
sprite_map_set_error(error); PyErr_Print();
|
||||
set_sprite(gpu_cell, 0, 0, 0);
|
||||
@@ -649,7 +649,7 @@ render_group(FontGroup *fg, unsigned int num_cells, unsigned int num_glyphs, CPU
|
||||
if (is_repeat_glyph) {
|
||||
sp[i] = sp[i-1];
|
||||
} else {
|
||||
sp[i] = sprite_position_for(fg, font, glyphs, glyph_count, ligature_index++, &error);
|
||||
sp[i] = sprite_position_for(fg, font, glyphs, glyph_count, ligature_index++, num_cells, &error);
|
||||
}
|
||||
if (error != 0) { sprite_map_set_error(error); PyErr_Print(); return; }
|
||||
if (!sp[i]->rendered) all_rendered = false;
|
||||
@@ -1449,7 +1449,7 @@ test_sprite_position_for(PyObject UNUSED *self, PyObject *args) {
|
||||
}
|
||||
FontGroup *fg = font_groups;
|
||||
if (!num_font_groups) { PyErr_SetString(PyExc_RuntimeError, "must create font group first"); return NULL; }
|
||||
SpritePosition *pos = sprite_position_for(fg, &fg->fonts[fg->medium_font_idx], glyphs, PyTuple_GET_SIZE(args), 0, &error);
|
||||
SpritePosition *pos = sprite_position_for(fg, &fg->fonts[fg->medium_font_idx], glyphs, PyTuple_GET_SIZE(args), 0, 1, &error);
|
||||
if (pos == NULL) { sprite_map_set_error(error); return NULL; }
|
||||
return Py_BuildValue("HHH", pos->x, pos->y, pos->z);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ PyObject* face_from_descriptor(PyObject*, FONTS_DATA_HANDLE);
|
||||
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(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);
|
||||
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*);
|
||||
|
||||
@@ -45,7 +45,9 @@ def all_fonts_map(monospaced: bool = True) -> FontMap:
|
||||
if monospaced:
|
||||
ans = fc_list(FC_DUAL) + fc_list(FC_MONO)
|
||||
else:
|
||||
ans = fc_list()
|
||||
# allow non-monospaced and bitmapped fonts as these are used for
|
||||
# symbol_map
|
||||
ans = fc_list(-1, True)
|
||||
return create_font_map(ans)
|
||||
|
||||
|
||||
|
||||
@@ -302,11 +302,22 @@ calc_cell_width(Face *self) {
|
||||
return ans;
|
||||
}
|
||||
|
||||
|
||||
static unsigned int
|
||||
adjust_ypos(unsigned int pos, unsigned int cell_height, int adjustment) {
|
||||
if (adjustment >= 0) adjustment = MIN(adjustment, (int)pos - 1);
|
||||
else adjustment = MAX(adjustment, (int)pos - (int)cell_height + 1);
|
||||
return pos - adjustment;
|
||||
}
|
||||
|
||||
void
|
||||
cell_metrics(PyObject *s, unsigned int* cell_width, unsigned int* cell_height, unsigned int* baseline, unsigned int* underline_position, unsigned int* underline_thickness, unsigned int* strikethrough_position, unsigned int* strikethrough_thickness) {
|
||||
Face *self = (Face*)s;
|
||||
*cell_width = calc_cell_width(self);
|
||||
*cell_height = calc_cell_height(self, true);
|
||||
int baseline_offset = 0;
|
||||
if (OPT(adjust_baseline_px) != 0) baseline_offset = OPT(adjust_baseline_px);
|
||||
else if (OPT(adjust_baseline_frac) != 0) baseline_offset = (int)(*cell_height * OPT(adjust_baseline_frac));
|
||||
*baseline = font_units_to_pixels_y(self, self->ascender);
|
||||
*underline_position = MIN(*cell_height - 1, (unsigned int)font_units_to_pixels_y(self, MAX(0, self->ascender - self->underline_position)));
|
||||
*underline_thickness = MAX(1, font_units_to_pixels_y(self, self->underline_thickness));
|
||||
@@ -321,6 +332,11 @@ cell_metrics(PyObject *s, unsigned int* cell_width, unsigned int* cell_height, u
|
||||
} else {
|
||||
*strikethrough_thickness = *underline_thickness;
|
||||
}
|
||||
if (baseline_offset) {
|
||||
*baseline = adjust_ypos(*baseline, *cell_height, baseline_offset);
|
||||
*underline_position = adjust_ypos(*underline_position, *cell_height, baseline_offset);
|
||||
*strikethrough_position = adjust_ypos(*strikethrough_position, *cell_height, baseline_offset);
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int
|
||||
|
||||
@@ -26,12 +26,12 @@ free_glyph_cache_global_resources(void) {
|
||||
}
|
||||
|
||||
|
||||
static inline unsigned
|
||||
key_size_for_glyph_count(unsigned count) { return count + 2; }
|
||||
static unsigned
|
||||
key_size_for_glyph_count(unsigned count) { return count + 3; }
|
||||
|
||||
|
||||
SpritePosition*
|
||||
find_or_create_sprite_position(SpritePosition **head_, glyph_index *glyphs, glyph_index count, glyph_index ligature_index, bool *created) {
|
||||
find_or_create_sprite_position(SpritePosition **head_, glyph_index *glyphs, glyph_index count, glyph_index ligature_index, glyph_index cell_count, bool *created) {
|
||||
SpritePosItem **head = (SpritePosItem**)head_, *p;
|
||||
const unsigned key_sz = key_size_for_glyph_count(count);
|
||||
if (key_sz > scratch_sz) {
|
||||
@@ -40,8 +40,8 @@ find_or_create_sprite_position(SpritePosition **head_, glyph_index *glyphs, glyp
|
||||
scratch_sz = key_sz + 16;
|
||||
}
|
||||
const unsigned key_sz_bytes = key_sz * sizeof(glyph_index);
|
||||
scratch[0] = count; scratch[1] = ligature_index;
|
||||
memcpy(scratch + 2, glyphs, count * sizeof(glyph_index));
|
||||
scratch[0] = count; scratch[1] = ligature_index; scratch[2] = cell_count;
|
||||
memcpy(scratch + 3, glyphs, count * sizeof(glyph_index));
|
||||
HASH_FIND(hh, *head, scratch, key_sz_bytes, p);
|
||||
if (p) { *created = false; return (SpritePosition*)p; }
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ typedef struct SpritePosition {
|
||||
|
||||
void free_sprite_position_hash_table(SpritePosition **head);
|
||||
SpritePosition*
|
||||
find_or_create_sprite_position(SpritePosition **head, glyph_index *glyphs, glyph_index count, glyph_index ligature_index, bool *created);
|
||||
find_or_create_sprite_position(SpritePosition **head, glyph_index *glyphs, glyph_index count, glyph_index ligature_index, glyph_index cell_count, bool *created);
|
||||
|
||||
#define GlyphPropertiesHead \
|
||||
uint8_t data;
|
||||
|
||||
@@ -143,6 +143,9 @@ def _run_app(opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = ())
|
||||
val = get_macos_shortcut_for(opts, 'clear_terminal', args=('reset', True), lookup_name='reset_terminal')
|
||||
if val is not None:
|
||||
global_shortcuts['reset_terminal'] = val
|
||||
val = get_macos_shortcut_for(opts, 'load_config_file', args=(), lookup_name='reload_config')
|
||||
if val is not None:
|
||||
global_shortcuts['reload_config'] = val
|
||||
if is_macos and opts.macos_custom_beam_cursor:
|
||||
set_custom_ibeam_cursor()
|
||||
if not is_wayland() and not is_macos: # no window icons on wayland
|
||||
|
||||
@@ -94,6 +94,19 @@ opt('adjust_column_width', '0',
|
||||
option_type='adjust_line_height', ctype='!adjust_column_width',
|
||||
)
|
||||
|
||||
opt('adjust_baseline', '0',
|
||||
option_type='adjust_baseline', ctype='!adjust_baseline',
|
||||
add_to_default=False,
|
||||
long_text='''
|
||||
Adjust the vertical alignment of text (the height in the cell at which text is
|
||||
positioned). You can use either numbers, which are interpreted as pixels or a
|
||||
percentages (number followed by %), which are interpreted as the percentage of
|
||||
the line height. A positive value moves the baseline up, and a negative value
|
||||
moves them down. The underline and strikethrough positions are adjusted
|
||||
accordingly.
|
||||
'''
|
||||
)
|
||||
|
||||
opt('+symbol_map', 'U+E0A0-U+E0A3,U+E0C0-U+E0C7 PowerlineSymbols',
|
||||
option_type='symbol_map',
|
||||
add_to_default=False,
|
||||
@@ -140,6 +153,10 @@ changes the appearance of the zero (0), to make it more easily distinguishable
|
||||
from Ø. Fira Code Retina also includes other discretionary features known as
|
||||
Stylistic Sets which have the tags :code:`ss01` through :code:`ss20`.
|
||||
|
||||
For the exact syntax to use for individual features, see the
|
||||
:link:`Harfbuzz documentation
|
||||
<https://harfbuzz.github.io/harfbuzz-hb-common.html#hb-feature-from-string>`.
|
||||
|
||||
Note that this code is indexed by PostScript name, and not the font
|
||||
family. This allows you to define very precise feature settings; e.g. you can
|
||||
disable a feature in the italic font but not in the regular font.
|
||||
@@ -361,6 +378,14 @@ are still clickable.
|
||||
'''
|
||||
)
|
||||
|
||||
opt('url_excluded_characters', '',
|
||||
ctype='!url_excluded_characters',
|
||||
long_text='''
|
||||
Additional characters to be disallowed from URLs, when detecting URLs under the
|
||||
mouse cursor. By default, all characters legal in URLs are allowed.
|
||||
'''
|
||||
)
|
||||
|
||||
opt('copy_on_select', 'no',
|
||||
option_type='copy_on_select',
|
||||
long_text='''
|
||||
@@ -455,15 +480,19 @@ number ``b1 ... b8`` can be used to refer to upto eight buttons on a mouse.
|
||||
|
||||
``event-type`` is one ``press``, ``release``, ``doublepress``, ``triplepress``,
|
||||
``click`` and ``doubleclick``. ``modes`` indicates whether the action is
|
||||
performed when the mouse is grabbed by the terminal application or not. It can
|
||||
have one or more or the values, ``grabbed,ungrabbed``. Note that the click
|
||||
and double click events have a delay of :opt:`click_interval` to disambiguate
|
||||
from double and triple presses.
|
||||
performed when the mouse is grabbed by the program running in the terminal, or
|
||||
not. It can have one or more or the values, ``grabbed,ungrabbed``. ``grabbed``
|
||||
refers to when the program running in the terminal has requested mouse events.
|
||||
Note that the click and double click events have a delay of
|
||||
:opt:`click_interval` to disambiguate from double and triple presses.
|
||||
|
||||
You can run kitty with the :option:`kitty --debug-input` command line option
|
||||
to see mouse events. See the builtin actions below to get a sense of what is possible.
|
||||
|
||||
If you want to unmap an action map it to ``no-op``.
|
||||
If you want to unmap an action map it to ``no-op``. For example, to disable opening
|
||||
of URLs with a plain click::
|
||||
|
||||
mouse_map left click ungrabbed no-op
|
||||
|
||||
.. note::
|
||||
Once a selection is started, releasing the button that started it will
|
||||
@@ -485,14 +514,14 @@ mma('Click the link under the mouse cursor',
|
||||
)
|
||||
|
||||
mma('Discard press event for link click',
|
||||
'click_url_discard ctrl+shift+left press grabbed mouse_discard_event',
|
||||
'click_url_discard ctrl+shift+left press grabbed discard_event',
|
||||
long_text='Prevent this press event from being sent to the program that has'
|
||||
' grabbed the mouse, as the corresponding release event is used to open a URL.'
|
||||
)
|
||||
|
||||
|
||||
mma('Paste from the primary selection',
|
||||
'paste_selection middle release ungrabbed paste_selection',
|
||||
'paste_selection middle release ungrabbed paste_from_selection',
|
||||
)
|
||||
|
||||
mma('Start selecting text',
|
||||
@@ -909,7 +938,8 @@ opt('tab_title_template', '"{title}"',
|
||||
long_text='''
|
||||
A template to render the tab title. The default just renders the title. If you
|
||||
wish to include the tab-index as well, use something like: :code:`{index}:
|
||||
{title}`. Useful if you have shortcuts mapped for :code:`goto_tab N`. In
|
||||
{title}`. Useful if you have shortcuts mapped for :code:`goto_tab N`. If you
|
||||
prefer to see the index as a superscript, use {sup.index}. In
|
||||
addition you can use :code:`{layout_name}` for the current layout name and
|
||||
:code:`{num_windows}` for the number of windows in the tab. Note that formatting
|
||||
is done by Python's string formatting machinery, so you can use, for instance,
|
||||
@@ -3329,6 +3359,11 @@ the new conf settings *replace* the old ones.
|
||||
'''
|
||||
)
|
||||
|
||||
map('Reload kitty.conf',
|
||||
'reload_config_file cmd+control+, load_config_file',
|
||||
only='macos'
|
||||
)
|
||||
|
||||
map('Debug kitty configuration',
|
||||
'debug_config kitty_mod+f6 debug_config',
|
||||
long_text='''
|
||||
@@ -3337,6 +3372,12 @@ its host environment. Useful for debugging issues.
|
||||
'''
|
||||
)
|
||||
|
||||
map('Debug kitty configuration',
|
||||
'debug_config cmd+option+, debug_config',
|
||||
only='macos'
|
||||
)
|
||||
|
||||
|
||||
map('Send arbitrary text on key presses',
|
||||
'send_text ctrl+shift+alt+h send_text all Hello World',
|
||||
add_to_default=False,
|
||||
|
||||
26
kitty/options/parse.py
generated
26
kitty/options/parse.py
generated
@@ -7,16 +7,16 @@ from kitty.conf.utils import (
|
||||
unit_float
|
||||
)
|
||||
from kitty.options.utils import (
|
||||
active_tab_title_template, adjust_line_height, allow_hyperlinks, allow_remote_control,
|
||||
box_drawing_scale, clear_all_shortcuts, clipboard_control, config_or_absolute_path, copy_on_select,
|
||||
cursor_text_color, 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, kitten_alias, macos_option_as_alt,
|
||||
macos_titlebar_color, optional_edge_width, parse_map, parse_mouse_map, resize_draw_strategy,
|
||||
scrollback_lines, scrollback_pager_history_size, 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, to_cursor_shape, to_font_size, to_layout_names, to_modifiers, url_prefixes,
|
||||
url_style, window_border_width, window_size
|
||||
active_tab_title_template, adjust_baseline, adjust_line_height, allow_hyperlinks,
|
||||
allow_remote_control, box_drawing_scale, clear_all_shortcuts, clipboard_control,
|
||||
config_or_absolute_path, copy_on_select, cursor_text_color,
|
||||
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,
|
||||
kitten_alias, macos_option_as_alt, macos_titlebar_color, optional_edge_width, parse_map,
|
||||
parse_mouse_map, resize_draw_strategy, scrollback_lines, scrollback_pager_history_size, 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, to_cursor_shape, to_font_size, to_layout_names,
|
||||
to_modifiers, url_prefixes, url_style, window_border_width, window_size
|
||||
)
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ class Parser:
|
||||
def active_tab_title_template(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['active_tab_title_template'] = active_tab_title_template(val)
|
||||
|
||||
def adjust_baseline(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['adjust_baseline'] = adjust_baseline(val)
|
||||
|
||||
def adjust_column_width(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['adjust_column_width'] = adjust_line_height(val)
|
||||
|
||||
@@ -1203,6 +1206,9 @@ class Parser:
|
||||
def url_color(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['url_color'] = to_color(val)
|
||||
|
||||
def url_excluded_characters(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['url_excluded_characters'] = str(val)
|
||||
|
||||
def url_prefixes(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
||||
ans['url_prefixes'] = url_prefixes(val)
|
||||
|
||||
|
||||
30
kitty/options/to-c-generated.h
generated
30
kitty/options/to-c-generated.h
generated
@@ -57,6 +57,19 @@ convert_from_opts_adjust_column_width(PyObject *py_opts, Options *opts) {
|
||||
Py_DECREF(ret);
|
||||
}
|
||||
|
||||
static void
|
||||
convert_from_python_adjust_baseline(PyObject *val, Options *opts) {
|
||||
adjust_baseline(val, opts);
|
||||
}
|
||||
|
||||
static void
|
||||
convert_from_opts_adjust_baseline(PyObject *py_opts, Options *opts) {
|
||||
PyObject *ret = PyObject_GetAttrString(py_opts, "adjust_baseline");
|
||||
if (ret == NULL) return;
|
||||
convert_from_python_adjust_baseline(ret, opts);
|
||||
Py_DECREF(ret);
|
||||
}
|
||||
|
||||
static void
|
||||
convert_from_python_disable_ligatures(PyObject *val, Options *opts) {
|
||||
opts->disable_ligatures = PyLong_AsLong(val);
|
||||
@@ -252,6 +265,19 @@ convert_from_opts_detect_urls(PyObject *py_opts, Options *opts) {
|
||||
Py_DECREF(ret);
|
||||
}
|
||||
|
||||
static void
|
||||
convert_from_python_url_excluded_characters(PyObject *val, Options *opts) {
|
||||
url_excluded_characters(val, opts);
|
||||
}
|
||||
|
||||
static void
|
||||
convert_from_opts_url_excluded_characters(PyObject *py_opts, Options *opts) {
|
||||
PyObject *ret = PyObject_GetAttrString(py_opts, "url_excluded_characters");
|
||||
if (ret == NULL) return;
|
||||
convert_from_python_url_excluded_characters(ret, opts);
|
||||
Py_DECREF(ret);
|
||||
}
|
||||
|
||||
static void
|
||||
convert_from_python_select_by_word_characters(PyObject *val, Options *opts) {
|
||||
select_by_word_characters(val, opts);
|
||||
@@ -886,6 +912,8 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) {
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_adjust_column_width(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_adjust_baseline(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_disable_ligatures(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_cursor_shape(py_opts, opts);
|
||||
@@ -916,6 +944,8 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) {
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_detect_urls(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_url_excluded_characters(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_select_by_word_characters(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_click_interval(py_opts, opts);
|
||||
|
||||
@@ -124,13 +124,28 @@ url_prefixes(PyObject *up, Options *opts) {
|
||||
}
|
||||
}
|
||||
|
||||
static char_type*
|
||||
list_of_chars(PyObject *chars) {
|
||||
if (!PyUnicode_Check(chars)) { PyErr_SetString(PyExc_TypeError, "list_of_chars must be a string"); return NULL; }
|
||||
char_type *ans = calloc(PyUnicode_GET_LENGTH(chars) + 1, sizeof(char_type));
|
||||
if (ans) {
|
||||
for (ssize_t i = 0; i < PyUnicode_GET_LENGTH(chars); i++) {
|
||||
ans[i] = PyUnicode_READ(PyUnicode_KIND(chars), PyUnicode_DATA(chars), i);
|
||||
}
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
static void
|
||||
url_excluded_characters(PyObject *chars, Options *opts) {
|
||||
free(opts->url_excluded_characters);
|
||||
opts->url_excluded_characters = list_of_chars(chars);
|
||||
}
|
||||
|
||||
static void
|
||||
select_by_word_characters(PyObject *chars, Options *opts) {
|
||||
if (!PyUnicode_Check(chars)) { PyErr_SetString(PyExc_TypeError, "select_by_word_characters must be a string"); return; }
|
||||
for (size_t i = 0; i < MIN((size_t)PyUnicode_GET_LENGTH(chars), sizeof(opts->select_by_word_characters)/sizeof(opts->select_by_word_characters[0])); i++) {
|
||||
opts->select_by_word_characters[i] = PyUnicode_READ(PyUnicode_KIND(chars), PyUnicode_DATA(chars), i);
|
||||
}
|
||||
opts->select_by_word_characters_count = PyUnicode_GET_LENGTH(chars);
|
||||
free(opts->select_by_word_characters);
|
||||
opts->select_by_word_characters = list_of_chars(chars);
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -162,4 +177,6 @@ static void
|
||||
adjust_line_height(PyObject *al, Options *opts) { read_adjust(adjust_line_height); }
|
||||
static void
|
||||
adjust_column_width(PyObject *al, Options *opts) { read_adjust(adjust_column_width); }
|
||||
static void
|
||||
adjust_baseline(PyObject *al, Options *opts) { read_adjust(adjust_baseline); }
|
||||
#undef read_adjust
|
||||
|
||||
10
kitty/options/types.py
generated
10
kitty/options/types.py
generated
@@ -45,6 +45,7 @@ option_names = ( # {{{
|
||||
'active_tab_font_style',
|
||||
'active_tab_foreground',
|
||||
'active_tab_title_template',
|
||||
'adjust_baseline',
|
||||
'adjust_column_width',
|
||||
'adjust_line_height',
|
||||
'allow_hyperlinks',
|
||||
@@ -416,6 +417,7 @@ option_names = ( # {{{
|
||||
'touch_scroll_multiplier',
|
||||
'update_check_interval',
|
||||
'url_color',
|
||||
'url_excluded_characters',
|
||||
'url_prefixes',
|
||||
'url_style',
|
||||
'visual_bell_duration',
|
||||
@@ -435,6 +437,7 @@ class Options:
|
||||
active_tab_font_style: typing.Tuple[bool, bool] = (True, True)
|
||||
active_tab_foreground: Color = Color(red=0, green=0, blue=0)
|
||||
active_tab_title_template: typing.Optional[str] = None
|
||||
adjust_baseline: typing.Union[int, float] = 0
|
||||
adjust_column_width: typing.Union[int, float] = 0
|
||||
adjust_line_height: typing.Union[int, float] = 0
|
||||
allow_hyperlinks: int = 1
|
||||
@@ -544,6 +547,7 @@ class Options:
|
||||
touch_scroll_multiplier: float = 1.0
|
||||
update_check_interval: float = 24.0
|
||||
url_color: Color = Color(red=0, green=135, blue=189)
|
||||
url_excluded_characters: str = ''
|
||||
url_prefixes: typing.Tuple[str, ...] = ('http', 'https', 'file', 'ftp', 'gemini', 'irc', 'gopher', 'mailto', 'news', 'git')
|
||||
url_style: int = 3
|
||||
visual_bell_duration: float = 0
|
||||
@@ -848,6 +852,8 @@ if is_macos:
|
||||
defaults.map.append(KeyDefinition(False, KeyAction('kitten', ('unicode_input',)), 12, False, 32, ()))
|
||||
defaults.map.append(KeyDefinition(False, KeyAction('edit_config_file'), 8, False, 44, ()))
|
||||
defaults.map.append(KeyDefinition(False, KeyAction('clear_terminal', ('reset', True)), 10, False, 114, ()))
|
||||
defaults.map.append(KeyDefinition(False, KeyAction('load_config_file'), 12, False, 44, ()))
|
||||
defaults.map.append(KeyDefinition(False, KeyAction('debug_config'), 10, False, 44, ()))
|
||||
defaults.mouse_map = [
|
||||
# click_url_or_select
|
||||
MouseMapping(0, 0, -2, False, KeyAction('mouse_click_url_or_select')),
|
||||
@@ -860,9 +866,9 @@ defaults.mouse_map = [
|
||||
# click_url
|
||||
MouseMapping(0, 5, -1, False, KeyAction('mouse_click_url')),
|
||||
# click_url_discard
|
||||
MouseMapping(0, 5, 1, True, KeyAction('mouse_discard_event')),
|
||||
MouseMapping(0, 5, 1, True, KeyAction('discard_event')),
|
||||
# paste_selection
|
||||
MouseMapping(2, 0, -1, False, KeyAction('paste_selection')),
|
||||
MouseMapping(2, 0, -1, False, KeyAction('paste_from_selection')),
|
||||
# start_simple_selection
|
||||
MouseMapping(0, 0, 1, False, KeyAction('mouse_selection', (0,))),
|
||||
# start_rectangle_selection
|
||||
|
||||
@@ -424,6 +424,16 @@ def adjust_line_height(x: str) -> Union[int, float]:
|
||||
return int(x)
|
||||
|
||||
|
||||
def adjust_baseline(x: str) -> Union[int, float]:
|
||||
if x.endswith('%'):
|
||||
ans = float(x[:-1].strip()) / 100.0
|
||||
if abs(ans) > 1:
|
||||
log_error('Percentage adjustments of the baseline cannot exceed 100%')
|
||||
return 0
|
||||
return ans
|
||||
return int(x)
|
||||
|
||||
|
||||
def to_font_size(x: str) -> float:
|
||||
return max(MINIMUM_FONT_SIZE, float(x))
|
||||
|
||||
|
||||
@@ -935,7 +935,14 @@ dispatch_csi(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
|
||||
REPORT_ERROR("Unknown CSI x sequence with start and end modifiers: '%c' '%c'", start_modifier, end_modifier);
|
||||
break;
|
||||
case DECSCUSR:
|
||||
CALL_CSI_HANDLER1M(screen_set_cursor, 1);
|
||||
if (!start_modifier && end_modifier == ' ') {
|
||||
CALL_CSI_HANDLER1M(screen_set_cursor, 1);
|
||||
}
|
||||
if (start_modifier == '>' && !end_modifier) {
|
||||
CALL_CSI_HANDLER1(screen_xtversion, 0);
|
||||
}
|
||||
REPORT_ERROR("Unknown CSI q sequence with start and end modifiers: '%c' '%c'", start_modifier, end_modifier);
|
||||
break;
|
||||
case SU:
|
||||
NO_MODIFIERS(end_modifier, ' ', "Select presentation directions escape code not implemented");
|
||||
CALL_CSI_HANDLER1(screen_scroll, 1);
|
||||
|
||||
@@ -1527,6 +1527,13 @@ report_device_attributes(Screen *self, unsigned int mode, char start_modifier) {
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
screen_xtversion(Screen *self, unsigned int mode) {
|
||||
if (mode == 0) {
|
||||
write_escape_code_to_child(self, DCS, ">|kitty(" XT_VERSION ")");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
screen_report_size(Screen *self, unsigned int which) {
|
||||
char buf[32] = {0};
|
||||
@@ -2167,11 +2174,12 @@ screen_detect_url(Screen *screen, unsigned int x, unsigned int y) {
|
||||
bool has_url = false;
|
||||
index_type url_start, url_end = 0;
|
||||
Line *line = screen_visual_line(screen, y);
|
||||
if (!line || x >= screen->columns) return false;
|
||||
if (line->cpu_cells[x].hyperlink_id) {
|
||||
screen_mark_hyperlink(screen, x, y);
|
||||
return true;
|
||||
}
|
||||
char_type sentinel;
|
||||
char_type sentinel = 0;
|
||||
if (line) {
|
||||
url_start = line_url_start_at(line, x);
|
||||
if (url_start < line->xnum) {
|
||||
@@ -2555,8 +2563,10 @@ screen_selection_range_for_line(Screen *self, index_type y, index_type *start, i
|
||||
|
||||
static inline bool
|
||||
is_opt_word_char(char_type ch) {
|
||||
for (size_t i = 0; i < OPT(select_by_word_characters_count); i++) {
|
||||
if (OPT(select_by_word_characters[i]) == ch) return true;
|
||||
if (OPT(select_by_word_characters)) {
|
||||
for (const char_type *p = OPT(select_by_word_characters); *p; p++) {
|
||||
if (ch == *p) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -166,6 +166,7 @@ void screen_clear_tab_stop(Screen *self, unsigned int how);
|
||||
void screen_set_mode(Screen *self, unsigned int mode);
|
||||
void screen_reset_mode(Screen *self, unsigned int mode);
|
||||
void screen_decsace(Screen *self, unsigned int);
|
||||
void screen_xtversion(Screen *self, unsigned int);
|
||||
void screen_insert_characters(Screen *self, unsigned int count);
|
||||
void screen_cursor_up(Screen *self, unsigned int count/*=1*/, bool do_carriage_return/*=false*/, int move_direction/*=-1*/);
|
||||
void screen_set_cursor(Screen *self, unsigned int mode, uint8_t secondary);
|
||||
|
||||
@@ -1091,6 +1091,8 @@ finalize(void) {
|
||||
free_bgimage(&global_state.bgimage, false);
|
||||
global_state.bgimage = NULL;
|
||||
free_url_prefixes();
|
||||
free(OPT(select_by_word_characters)); OPT(select_by_word_characters) = NULL;
|
||||
free(OPT(url_excluded_characters)); OPT(url_excluded_characters) = NULL;
|
||||
}
|
||||
|
||||
bool
|
||||
|
||||
@@ -30,7 +30,7 @@ typedef struct {
|
||||
unsigned int url_style;
|
||||
unsigned int scrollback_pager_history_size;
|
||||
bool scrollback_fill_enlarged_window;
|
||||
char_type select_by_word_characters[256]; size_t select_by_word_characters_count;
|
||||
char_type *select_by_word_characters;
|
||||
color_type url_color, background, foreground, active_border_color, inactive_border_color, bell_border_color;
|
||||
color_type mark1_foreground, mark1_background, mark2_foreground, mark2_background, mark3_foreground, mark3_background;
|
||||
monotonic_t repaint_delay, input_delay;
|
||||
@@ -40,8 +40,8 @@ typedef struct {
|
||||
unsigned int macos_option_as_alt;
|
||||
float macos_thicken_font;
|
||||
WindowTitleIn macos_show_window_title_in;
|
||||
int adjust_line_height_px, adjust_column_width_px;
|
||||
float adjust_line_height_frac, adjust_column_width_frac;
|
||||
int adjust_line_height_px, adjust_column_width_px, adjust_baseline_px;
|
||||
float adjust_line_height_frac, adjust_column_width_frac, adjust_baseline_frac;
|
||||
float background_opacity, dim_opacity;
|
||||
|
||||
char* background_image;
|
||||
@@ -70,6 +70,7 @@ typedef struct {
|
||||
UrlPrefix *values;
|
||||
size_t num, max_prefix_len;
|
||||
} url_prefixes;
|
||||
char_type *url_excluded_characters;
|
||||
bool detect_urls;
|
||||
bool tab_bar_hidden;
|
||||
double font_size;
|
||||
@@ -263,21 +264,23 @@ void send_prerendered_sprites_for_window(OSWindow *w);
|
||||
#ifdef __APPLE__
|
||||
void get_cocoa_key_equivalent(uint32_t, int, char *key, size_t key_sz, int*);
|
||||
typedef enum {
|
||||
NO_COCOA_PENDING_ACTION = 0,
|
||||
PREFERENCES_WINDOW = 1,
|
||||
NEW_OS_WINDOW = 2,
|
||||
NEW_OS_WINDOW_WITH_WD = 4,
|
||||
NEW_TAB_WITH_WD = 8,
|
||||
CLOSE_OS_WINDOW = 16,
|
||||
CLOSE_TAB = 32,
|
||||
NEW_TAB = 64,
|
||||
NEXT_TAB = 128,
|
||||
PREVIOUS_TAB = 256,
|
||||
DETACH_TAB = 512,
|
||||
OPEN_FILE = 1024,
|
||||
NEW_WINDOW = 2048,
|
||||
CLOSE_WINDOW = 4096,
|
||||
RESET_TERMINAL = 8192,
|
||||
PREFERENCES_WINDOW,
|
||||
NEW_OS_WINDOW,
|
||||
NEW_OS_WINDOW_WITH_WD,
|
||||
NEW_TAB_WITH_WD,
|
||||
CLOSE_OS_WINDOW,
|
||||
CLOSE_TAB,
|
||||
NEW_TAB,
|
||||
NEXT_TAB,
|
||||
PREVIOUS_TAB,
|
||||
DETACH_TAB,
|
||||
OPEN_FILE,
|
||||
NEW_WINDOW,
|
||||
CLOSE_WINDOW,
|
||||
RESET_TERMINAL,
|
||||
RELOAD_CONFIG,
|
||||
|
||||
NUM_COCOA_PENDING_ACTIONS
|
||||
} CocoaPendingAction;
|
||||
void set_cocoa_pending_action(CocoaPendingAction action, const char*);
|
||||
#endif
|
||||
|
||||
@@ -12,7 +12,7 @@ from .fast_data_types import (
|
||||
)
|
||||
from .layout.base import Rect
|
||||
from .rgb import Color, alpha_blend, color_as_sgr, color_from_int, to_color
|
||||
from .types import WindowGeometry
|
||||
from .types import WindowGeometry, run_once
|
||||
from .typing import PowerlineStyle
|
||||
from .utils import color_as_int, log_error
|
||||
from .window import calculate_gl_geometry
|
||||
@@ -91,6 +91,30 @@ class Formatter:
|
||||
noitalic = '\x1b[23m'
|
||||
|
||||
|
||||
@run_once
|
||||
def super_sub_maps() -> Tuple[dict, dict]:
|
||||
import string
|
||||
sup_table = str.maketrans(
|
||||
string.ascii_lowercase + string.ascii_uppercase + string.digits + '+-=()',
|
||||
'ᵃᵇᶜᵈᵉᶠᵍʰⁱʲᵏˡᵐⁿᵒᵖqʳˢᵗᵘᵛʷˣʸᶻ' 'ᴬᴮᶜᴰᴱᶠᴳᴴᴵᴶᴷᴸᴹᴺᴼᴾQᴿˢᵀᵁⱽᵂˣʸᶻ' '⁰¹²³⁴⁵⁶⁷⁸⁹' '⁺⁻⁼⁽⁾')
|
||||
sub_table = str.maketrans(
|
||||
string.ascii_lowercase + string.ascii_uppercase + string.digits + '+-=()',
|
||||
'ₐbcdₑfgₕᵢⱼₖₗₘₙₒₚqᵣₛₜᵤᵥwₓyz' 'ₐbcdₑfgₕᵢⱼₖₗₘₙₒₚqᵣₛₜᵤᵥwₓyz' '₀₁₂₃₄₅₆₇₈₉' '₊₋₌₍₎')
|
||||
return sup_table, sub_table
|
||||
|
||||
|
||||
class SupSub:
|
||||
|
||||
def __init__(self, data: dict, is_subscript: bool = False):
|
||||
self.__data = data
|
||||
self.__is_subscript = is_subscript
|
||||
|
||||
def __getattr__(self, name: str) -> str:
|
||||
name = str(self.__data.get(name, name))
|
||||
table = super_sub_maps()[int(self.__is_subscript)]
|
||||
return name.translate(table)
|
||||
|
||||
|
||||
def draw_title(draw_data: DrawData, screen: Screen, tab: TabBarData, index: int) -> None:
|
||||
if tab.needs_attention and draw_data.bell_on_tab:
|
||||
fg = screen.cursor.fg
|
||||
@@ -107,12 +131,20 @@ def draw_title(draw_data: DrawData, screen: Screen, tab: TabBarData, index: int)
|
||||
if tab.is_active and draw_data.active_title_template is not None:
|
||||
template = draw_data.active_title_template
|
||||
try:
|
||||
data = {
|
||||
'index': index,
|
||||
'layout_name': tab.layout_name,
|
||||
'num_windows': tab.num_windows,
|
||||
'title': tab.title,
|
||||
}
|
||||
eval_locals = {
|
||||
'index': index,
|
||||
'layout_name': tab.layout_name,
|
||||
'num_windows': tab.num_windows,
|
||||
'title': tab.title,
|
||||
'fmt': Formatter,
|
||||
'sup': SupSub(data),
|
||||
'sub': SupSub(data, True),
|
||||
}
|
||||
title = eval(compile_template(template), {'__builtins__': {}}, eval_locals)
|
||||
except Exception as e:
|
||||
|
||||
@@ -231,6 +231,7 @@ string_capabilities = {
|
||||
'smso': r'\E[7m',
|
||||
# Enter underline mode
|
||||
'smul': r'\E[4m',
|
||||
'Smulx': r'\E[4:%p1%dm', # this is a non-standard extension that some terminals use, so match them
|
||||
# Enter strikethrough mode
|
||||
'smxx': r'\E[9m',
|
||||
# Clear all tab stops
|
||||
@@ -436,7 +437,7 @@ queryable_capabilities = cast(Dict[str, str], numeric_capabilities.copy())
|
||||
queryable_capabilities.update(string_capabilities)
|
||||
extra = (bool_capabilities | numeric_capabilities.keys() | string_capabilities.keys()) - set(termcap_aliases.values())
|
||||
no_termcap_for = frozenset(
|
||||
'Su Tc setrgbf setrgbb fullkbd kUP kDN'.split() + [
|
||||
'Su Smulx Tc setrgbf setrgbb fullkbd kUP kDN'.split() + [
|
||||
'k{}{}'.format(key, mod)
|
||||
for key in 'UP DN RIT LFT END HOM IC DC PRV NXT'.split()
|
||||
for mod in range(3, 8)])
|
||||
@@ -478,17 +479,15 @@ def get_capabilities(query_string: str, opts: 'Options') -> Generator[str, None,
|
||||
if name in ('TN', 'name'):
|
||||
yield result(encoded_query_name, names[0])
|
||||
elif name.startswith('kitty-query-'):
|
||||
from kittens.query_terminal.main import get_result
|
||||
name = name[len('kitty-query-'):]
|
||||
if name == 'version':
|
||||
from .constants import str_version
|
||||
yield result(encoded_query_name, str_version)
|
||||
elif name == 'allow_hyperlinks':
|
||||
yield result(encoded_query_name,
|
||||
'ask' if opts.allow_hyperlinks == 0b11 else ('yes' if opts.allow_hyperlinks else 'no'))
|
||||
else:
|
||||
rval = get_result(name)
|
||||
if rval is None:
|
||||
from .utils import log_error
|
||||
log_error('Unknown kitty terminfo query:', name)
|
||||
yield result(encoded_query_name)
|
||||
else:
|
||||
yield result(encoded_query_name, rval)
|
||||
else:
|
||||
try:
|
||||
val = queryable_capabilities[name]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "data-types.h"
|
||||
#include "state.h"
|
||||
#define VS15 1285
|
||||
#define VS16 1286
|
||||
|
||||
@@ -11,9 +12,19 @@ bool is_P_category(char_type);
|
||||
char_type codepoint_for_mark(combining_type m);
|
||||
combining_type mark_for_codepoint(char_type c);
|
||||
|
||||
static inline bool
|
||||
is_excluded_from_url(uint32_t ch) {
|
||||
if (OPT(url_excluded_characters)) {
|
||||
for (const char_type *p = OPT(url_excluded_characters); *p; p++) {
|
||||
if (ch == *p) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
is_url_char(uint32_t ch) {
|
||||
return ch && !is_CZ_category(ch);
|
||||
return ch && !is_CZ_category(ch) && !is_excluded_from_url(ch);
|
||||
}
|
||||
|
||||
static inline bool
|
||||
|
||||
@@ -829,9 +829,6 @@ class Window:
|
||||
def mouse_click_url(self) -> None:
|
||||
click_mouse_url(self.os_window_id, self.tab_id, self.id)
|
||||
|
||||
def mouse_discard_event(self) -> None:
|
||||
pass
|
||||
|
||||
def mouse_click_url_or_select(self) -> None:
|
||||
if not self.screen.has_selection():
|
||||
self.mouse_click_url()
|
||||
|
||||
11
setup.py
11
setup.py
@@ -374,6 +374,7 @@ def kitty_env() -> Env:
|
||||
cppflags = ans.cppflags
|
||||
cppflags.append('-DPRIMARY_VERSION={}'.format(version[0] + 4000))
|
||||
cppflags.append('-DSECONDARY_VERSION={}'.format(version[1]))
|
||||
cppflags.append('-DXT_VERSION="{}"'.format('.'.join(map(str, version))))
|
||||
at_least_version('harfbuzz', 1, 5)
|
||||
cflags.extend(pkg_config('libpng', '--cflags-only-I'))
|
||||
cflags.extend(pkg_config('lcms2', '--cflags-only-I'))
|
||||
@@ -1144,9 +1145,13 @@ def clean() -> None:
|
||||
'build', 'compile_commands.json', 'link_commands.json',
|
||||
'linux-package', 'kitty.app', 'asan-launcher',
|
||||
'kitty-profile', 'kitty/launcher')
|
||||
exclude = ('.git',)
|
||||
for root, dirs, files in os.walk('.', topdown=True):
|
||||
dirs[:] = [d for d in dirs if d not in exclude]
|
||||
|
||||
def excluded(root: str, d: str) -> bool:
|
||||
q = os.path.relpath(os.path.join(root, d), base).replace(os.sep, '/')
|
||||
return q in ('.git', 'bypy/b')
|
||||
|
||||
for root, dirs, files in os.walk(base, topdown=True):
|
||||
dirs[:] = [d for d in dirs if not excluded(root, d)]
|
||||
remove_dirs = {d for d in dirs if d == '__pycache__' or d.endswith('.dSYM')}
|
||||
for d in remove_dirs:
|
||||
shutil.rmtree(os.path.join(root, d))
|
||||
|
||||
@@ -16,6 +16,7 @@ xterm-kitty|KovIdTTY,
|
||||
it#8,
|
||||
lines#24,
|
||||
pairs#32767,
|
||||
Smulx=\E[4:%p1%dm,
|
||||
acsc=++\,\,--..00``aaffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~,
|
||||
bel=^G,
|
||||
bold=\E[1m,
|
||||
|
||||
Binary file not shown.
3
test.py
3
test.py
@@ -5,6 +5,7 @@
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
@@ -14,6 +15,8 @@ def init_env() -> None:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
warnings.simplefilter('error')
|
||||
os.environ['PYTHONWARNINGS'] = 'error'
|
||||
init_env()
|
||||
m = importlib.import_module('kitty_tests.main')
|
||||
m.run_tests() # type: ignore
|
||||
|
||||
Reference in New Issue
Block a user