Compare commits

...

79 Commits

Author SHA1 Message Date
Kovid Goyal
90164dfee7 make clean should not clean bypy build artifacts 2021-06-28 06:39:12 +05:30
Kovid Goyal
73f641cb66 version 0.21.2 2021-06-28 06:22:35 +05:30
Kovid Goyal
3d51835b9b Avoid unneccessary mallocs and copying when return code points for word 2021-06-28 05:56:49 +05:30
Kovid Goyal
f358d76409 Fix type sig 2021-06-28 05:51:49 +05:30
Kovid Goyal
ce486e9244 Move the if condition out of the inner loops 2021-06-27 21:09:06 +05:30
Kovid Goyal
691b7215a0 Unicode input kitten: Fix searching when a word has more than 1024 matches
Fixes #3773
2021-06-27 21:04:43 +05:30
Kovid Goyal
5be45d0ff2 Turn on all warnings when running test suite 2021-06-26 12:55:53 +05:30
Kovid Goyal
8a4b326127 Finish completion for ssh kitten
Fixes #3760
2021-06-26 12:28:12 +05:30
Kovid Goyal
051374cd55 Add support for comma separated completions 2021-06-26 10:52:14 +05:30
Kovid Goyal
71cd92da29 Store match group properties on the match group object 2021-06-26 09:47:21 +05:30
Kovid Goyal
c2d3a0c8b4 Completion for choice args 2021-06-26 09:18:36 +05:30
Kovid Goyal
fe8b151666 Complete option 2021-06-25 22:53:55 +05:30
Kovid Goyal
9e2d67f7a1 Show option descriptions when completing in ZSH 2021-06-25 19:13:16 +05:30
Kovid Goyal
23ea3745ca Make zsh completion headings bold 2021-06-25 18:05:18 +05:30
Kovid Goyal
d8d5a8fada More work on completion for the SSH kitten 2021-06-25 17:50:08 +05:30
Kovid Goyal
21ce0e90bf Code to get list of known host names 2021-06-25 15:04:17 +05:30
Kovid Goyal
ec420b8012 Start work on completion for ssh kitten 2021-06-25 15:04:17 +05:30
Kovid Goyal
bd67899943 Allow using super and sub scripts in tab titles
The syntax is

{sup.whatever}

works by translating enhlish letters/number to their unicode
equivalents.

Fixes #3763
2021-06-25 15:02:43 +05:30
Kovid Goyal
4b7600f3a5 Nicer error message on unknown entry point 2021-06-25 10:25:54 +05:30
Kovid Goyal
5eabd69659 ... 2021-06-25 08:03:20 +05:30
Kovid Goyal
e83be21756 Add a note about completions for the SSH kitten 2021-06-25 08:02:28 +05:30
Kovid Goyal
13c58536be Cleanup previous PR and add changelog entry 2021-06-24 21:27:15 +05:30
Kovid Goyal
512dccdbfa Merge branch 'ripgrep-context' of https://github.com/allemangD/kitty 2021-06-24 21:25:11 +05:30
David Allemang
bb3a8453e0 Make hyperlinked_grep kitten respect context
Fix regex so that context lines (from -C option) are still hyperlinked.

Also add a case so that any non-matching lines are still output raw.
2021-06-24 11:04:35 -04:00
Kovid Goyal
e337fcaadc query terminal kitten: Allow querying font face and size information
Fixes #3756
2021-06-24 13:31:50 +05:30
Kovid Goyal
890a149a5d Move generation of query responses into the query classes 2021-06-24 12:31:17 +05:30
Kovid Goyal
5b3ac259ce Fix a resource warning 2021-06-24 11:27:02 +05:30
Kovid Goyal
ce50043048 Add an issue reference to the changelog 2021-06-23 14:16:41 +05:30
Kovid Goyal
ebfbf6082f ... 2021-06-21 21:23:29 +05:30
Kovid Goyal
b44bec2207 Move processing of coca_pending_actions() into its own function
Makes process_global_state less crowded and easier to grok
2021-06-21 21:03:41 +05:30
Kovid Goyal
576ce21fc8 macOS: Fix shortcuts in the global menu bar responding slowly when cursor blink is disabled/timed out
The event loop needed to be forced to tick over immediately otherwise
the changes were not being rendered till some other event such as mouse
motion or key release was not delivered.

Fixes #3693
2021-06-21 20:58:44 +05:30
Kovid Goyal
2ba015d0be Add an issue reference to the changelog
Fixes #3405
2021-06-21 16:46:49 +05:30
Kovid Goyal
98ae2ac96a ssh kitten: Use sh rather than /bin/sh
It's not actually required that sh be in /bin.  The hope is that fewer
users alias sh to non POSIX things than put sh in places other than /bin
2021-06-21 16:43:59 +05:30
Kovid Goyal
1c25ed7666 ssh kitten: Fix "Connection closed" message being printed by ssh when running remote commands
Don't force allocate a tty when running remote commands. This
matches ssh behavior as well.
2021-06-21 16:40:07 +05:30
Kovid Goyal
fd4c7a4ed2 ssh kitten: Support system where the login shell is a non-POSIX shell
Now the kitten passes its script to /bin/sh and detects the login shell
using various strategies based on the value of $USER on the remote
machine. The strategies are tried one after another till one succeeds:

1) getent passwd $USER
2) id -P
3) python pwd module
4) parsing /etc/passwd

The hope is that these strategies will fail on fewer systems than
relying on the login shell being POSIX compliant.
2021-06-21 16:30:49 +05:30
Kovid Goyal
32d275c138 Document how to use the new config framework with external kittens 2021-06-21 05:31:17 +05:30
Kovid Goyal
f8924286ce gen-config for third party kittens 2021-06-21 05:11:46 +05:30
Kovid Goyal
6e77345263 Ensure sys.path is preserved even if there are errors importing a custom kitten 2021-06-21 04:35:10 +05:30
Kovid Goyal
1438c64b9e Move the config generation code into kitty proper 2021-06-21 04:25:04 +05:30
Kovid Goyal
2ff1256aef mouse_discard_event -> discard_event 2021-06-21 04:01:45 +05:30
Kovid Goyal
5d169854c4 Merge branch 'typo' of https://github.com/Luflosi/kitty 2021-06-18 20:21:08 +05:30
Luflosi
e61d328cb0 Fix typo in changelog 2021-06-18 16:36:27 +02:00
Kovid Goyal
0c196888fb Clarify meaning of grabbed/ungrabbed a bit more 2021-06-18 17:56:21 +05:30
Kovid Goyal
85efed8308 Fix #3743 2021-06-18 17:54:13 +05:30
Kovid Goyal
857c88a951 Add a note to the keyboard protocol elucidating the use of separate main and alternate screen mode stacks in more detail 2021-06-18 09:58:19 +05:30
Kovid Goyal
74b0a7f6f8 Make it clearer that there are separate keyboard mode stacks for main and alternate screens 2021-06-18 09:41:35 +05:30
Kovid Goyal
5da5d00aec text typos 2021-06-18 08:20:32 +05:30
Kovid Goyal
eb59fae9b0 ... 2021-06-18 08:20:04 +05:30
Kovid Goyal
b6b38d6f44 Linux: Fix emoji/bitmapped fonts not useable in symbol_map 2021-06-18 08:01:23 +05:30
Kovid Goyal
4f4d9e0ae3 ... 2021-06-17 21:34:26 +05:30
Kovid Goyal
43c1776594 Handle systems that use less -F as pagers
Fix #3740
2021-06-17 21:33:26 +05:30
Kovid Goyal
397638998b Dont use static memory for the list of chars options
Saves a couple of KB of RAM and is more flexible in terms
of max number of allowed chars, although for large numbers one really
needs a hash for fast lookups.
2021-06-17 13:27:11 +05:30
Kovid Goyal
6ddbda00df Clean up url excluded chars PR 2021-06-17 13:11:23 +05:30
Kovid Goyal
df0bf74bfe Merge branch 'url_excluded_characters' of https://github.com/rbutoi/kitty 2021-06-17 13:01:31 +05:30
Radu Butoi
5ee0651f56 Add url_excluded_characters option to exclude characters from URLs.
This option, like select_by_word_characters, is a set of characters, but
for which to *exclude* from URL parsing. See
https://github.com/kovidgoyal/kitty/issues/3688#issuecomment-862711148.
2021-06-17 01:55:21 -04:00
Kovid Goyal
9ec1c28b35 Merge branch 'cleanup-adjust-baseline' of https://github.com/ytang/kitty 2021-06-16 21:24:59 +05:30
Yang Tang
bfdbf06731 Remove redundant code 2021-06-16 11:44:27 -04:00
Kovid Goyal
ca58b30c47 Fix compiler warning 2021-06-16 20:51:47 +05:30
Kovid Goyal
0fb2f383ac Clean up the baseline adjustment PR 2021-06-16 20:39:00 +05:30
Kovid Goyal
5d0eb1d330 Merge branch 'adjust-baseline' of https://github.com/ytang/kitty 2021-06-16 20:04:15 +05:30
Kovid Goyal
ee77144e2b Make the sample script a little nicer 2021-06-16 18:51:21 +05:30
Kovid Goyal
9d3a2cc219 Fix space based ligatures not rendering correctly
The new infinite ligature cache needed to be keyed by total number of
cells as well as total number of glyphs, otherwise a space based
ligature which is the same glyph rendered in different numbers of cells
wold render incorrectly when present in multiple forms. Fixes #3729
2021-06-16 18:41:01 +05:30
Yang Tang
e964ac86d5 Allow specifying adjust_baseline as either pixels or percentage 2021-06-16 01:11:51 -04:00
Yang Tang
bb26b3d549 Add option adjust_baseline to adjust the baseline position on macOS 2021-06-15 14:52:43 -04:00
Kovid Goyal
71c8d82c33 Merge branch 'doc-harfbuzz' of https://github.com/The-Compiler/kitty 2021-06-15 21:52:39 +05:30
Florian Bruhin
9fffe0d710 doc: Link to harfbuzz font feature format 2021-06-15 17:36:12 +02:00
Kovid Goyal
51f8b22dfa macOS: Fix middle-click to paste from clipboard not working
Fixes #3730
2021-06-15 13:45:50 +05:30
Kovid Goyal
63a36a1054 Add Smulx to terminfo
A couple of other terminals use it for styled underlines, in
contravention of the spec. But, in the interests of wider
interoperability add it to kitty as well.

Fixes #3725
2021-06-14 11:58:52 +05:30
Kovid Goyal
057084a708 Add support for XTVERSION version reporting escape code 2021-06-14 10:20:50 +05:30
Kovid Goyal
79b130ed23 version 0.21.1 2021-06-14 07:47:32 +05:30
Kovid Goyal
75e8b16ea3 Dont limit the number of possible cocoa pending actions to the number of bits in an int 2021-06-14 07:34:28 +05:30
Kovid Goyal
1b35708d89 macOS: Fix a crash when rendering ligatures larger than 128 characters
Fixes #3724
2021-06-14 07:15:52 +05:30
Kovid Goyal
ebff343a55 macOS: Add reload config to kitty global menu 2021-06-13 20:34:27 +05:30
Kovid Goyal
c7b91e5f19 Cocoa grabs control+function key presses so use dedicated cmd based shortcuts for those on macOS 2021-06-13 18:20:14 +05:30
Kovid Goyal
b9d52dfaf7 Update Changelog 2021-06-13 10:01:38 +05:30
Kovid Goyal
2c8f66586f macOS: Fix a regression in the previous release that broke rendering of strikeout
The new cell height code forgot to adjust strikeout position.
Fixes #3717
2021-06-13 09:29:30 +05:30
Kovid Goyal
219bf564f7 Fix #3719 2021-06-13 09:07:08 +05:30
Kovid Goyal
291be6f5a6 Silence incorrect compiler warning on older gcc 2021-06-12 13:15:05 +05:30
Kovid Goyal
1da2344aa3 Fix a possible crash when handling mouse clicks 2021-06-12 13:03:47 +05:30
53 changed files with 1755 additions and 700 deletions

View File

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

View File

@@ -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 = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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; }

View File

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

View File

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

View File

@@ -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
View File

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

View File

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

View File

@@ -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
View File

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

View File

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

View File

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

View File

@@ -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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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