Compare commits

...

103 Commits

Author SHA1 Message Date
Kovid Goyal
ad128a3c29 version 0.25.2 2022-06-07 07:32:35 +05:30
Kovid Goyal
b71d2e04ca Clarify behavior of placement id when image id is zero 2022-06-07 07:22:12 +05:30
Kovid Goyal
cc3c7b0286 Fix waitpid() call on python 3.8 2022-06-06 21:22:04 +05:30
Kovid Goyal
bf6c90a69a Compare realpaths for cwd test 2022-06-06 20:46:49 +05:30
Kovid Goyal
4c9a75c1ba Remove unused code 2022-06-06 20:45:40 +05:30
Kovid Goyal
98f46f8bd7 Start work on prewarming
The prewarm process and its controller are implemented with some basic
tests.
2022-06-06 20:39:36 +05:30
Kovid Goyal
dec62b1929 Allow ignoring SHM close failures
Useful if we export the mmap beyond the lifetime of the shm object
2022-06-06 20:31:13 +05:30
Kovid Goyal
79f7954048 Cleanup previous PR 2022-06-06 17:19:52 +05:30
Kovid Goyal
fc217dafba Merge branch 'master' of https://github.com/suvayu/kitty 2022-06-06 16:55:08 +05:30
Kovid Goyal
a6aff817cf Prepare for prewarm testing 2022-06-06 15:59:34 +05:30
Suvayu Ali
38cb18fe92 diff kitten: tests for directory walking 2022-06-06 11:01:45 +02:00
Suvayu Ali
20b6a97159 Update in response to feedback
- one configuration option: ignore_paths
- use shlex to parse option to support whitespace and in-line comments
- change option type to Tuple[str, ...]
- remove ignored directories from dirnames to prevent scanning
2022-06-06 11:00:45 +02:00
Kovid Goyal
98f6e24106 Move block outside try/except as it doesnt need to be in there 2022-06-06 08:17:15 +05:30
Kovid Goyal
4ed6364b61 Fix compiler warning on 32bit builds 2022-06-06 07:19:52 +05:30
Kovid Goyal
cd7751681d hints kitten: Fix surrounding quotes/brackets and embedded carriage returns not being removed when using line number processing
Fixes #5170
2022-06-05 18:07:16 +05:30
Kovid Goyal
76531d2f7a Python wrapper function to establish controlling tty 2022-06-05 15:36:05 +05:30
Suvayu Ali
eea652f1d0 kittens/diff: move empty pattern check to dir tree walk 2022-06-05 10:11:21 +02:00
Kovid Goyal
f20f4762a8 Use builtin facility for removing cloexec 2022-06-05 12:33:34 +05:30
Kovid Goyal
c004fd1f4d Declare the set of handled signals in only one place 2022-06-05 08:01:26 +05:30
Kovid Goyal
f29039db29 Make the full signal info available in our signal handler 2022-06-05 07:22:38 +05:30
Suvayu Ali
fbf1ec43c7 diff kitten: add options to ignore paths when comparing directories
Tested locally & over SSH:

    $ kitty +kitten diff /local/path /local/path2
    $ kitty +kitten diff /local/path ssh:remote:/path
2022-06-05 00:11:30 +02:00
Kovid Goyal
a42200a430 Use the log_error function to log SIGUSR2 2022-06-04 15:38:56 +05:30
Kovid Goyal
ad576c333c Also restore signal handlers for SIGUSR1 and SIGUSR2 2022-06-04 15:15:39 +05:30
Kovid Goyal
5a0a980648 Make getting cmdline of a PID a bit more convenient 2022-06-04 15:07:08 +05:30
Kovid Goyal
b3578a4fa5 ... 2022-06-04 14:54:32 +05:30
Kovid Goyal
3c952b01d3 Fix reloading in all kitties skipping kitty processes started as kitty +open 2022-06-04 14:53:13 +05:30
Kovid Goyal
b1453a51f6 Better error message for kitty + 2022-06-04 14:47:13 +05:30
Kovid Goyal
2c3a7b5f41 Restore a couple more signal handlers that python nukes 2022-06-04 14:30:08 +05:30
Kovid Goyal
0df5a6c6c8 Utility function to test if PID is alive 2022-06-04 14:14:24 +05:30
Kovid Goyal
bf4dc6365a Add support for sigqueue() and also handle SIGUSR2
I anticipate using sigqueue() for simpler handling of SIGCHLD
notifications from the prewram process to the its parent.
2022-06-04 14:09:45 +05:30
Kovid Goyal
ce3322bf91 Remote control: Fix commands with asynchronous payloads not sending responses
Fixes #5165
2022-06-04 10:13:21 +05:30
Kovid Goyal
835cb31501 Fix incorrect dispatch of poll events when more than one peer is present 2022-06-04 09:26:57 +05:30
Kovid Goyal
762dd065f3 Nicer reverse loop 2022-06-04 09:06:35 +05:30
Kovid Goyal
7b91447d89 Use a full chekcout in CI when building docs
The :commit: docs role requires it
2022-06-04 08:00:00 +05:30
Kovid Goyal
1146c4ea3f Merge branch 'ignore-fig-osc' of https://github.com/grant0417/kitty 2022-06-04 07:44:17 +05:30
grant0417
729f7b8e03 Add custom message for Fig OSC 2022-06-03 18:57:53 -07:00
grant0417
d3ec632b9f Ignore Fig OSC codes 2022-06-03 17:07:44 -07:00
Kovid Goyal
cb01b5ee44 DRYer 2022-06-03 21:38:49 +05:30
Kovid Goyal
0828096e9e ... 2022-06-03 18:30:14 +05:30
Kovid Goyal
ebcbed290f Make loading of kitty options in kitten re-useable 2022-06-03 17:49:58 +05:30
Kovid Goyal
d6afe6f2cb Move entrypoints into their own module 2022-06-03 11:12:13 +05:30
Kovid Goyal
4daf70f636 ... 2022-06-03 11:08:41 +05:30
Kovid Goyal
2f3dd72c84 Weston: Fix client side decorations flickering on slow computers during window resize
Fixes #5162
2022-06-03 11:00:56 +05:30
Kovid Goyal
dca8c65376 Fix display of url open failures not working 2022-06-02 10:41:08 +05:30
Kovid Goyal
12d8175d9a Use inode/executable rather than application/executable 2022-06-02 10:33:56 +05:30
Kovid Goyal
609bcd68dd Fix #5161 2022-06-02 10:32:43 +05:30
Kovid Goyal
313136803c DRYer 2022-06-02 10:21:34 +05:30
Kovid Goyal
b937033411 macOS: Allow opening executables without a file extension with kitty as well
Fixes #5160
2022-06-02 09:25:30 +05:30
Kovid Goyal
55eeb9c11e Dont import kitty.complete in ssh.main 2022-06-02 08:08:33 +05:30
Kovid Goyal
8fbec2dfd5 remove unused arg 2022-06-02 08:07:53 +05:30
Kovid Goyal
c4521290b8 ... 2022-06-01 19:43:41 +05:30
Kovid Goyal
e12a9f3caf Iosevka: Fix incorrect rendering when there is a combining char that does not group with its neighbors
Fixes #5153
2022-06-01 12:00:53 +05:30
Kovid Goyal
56f8a06362 DRYer 2022-06-01 08:47:19 +05:30
Kovid Goyal
5c4abe749b Merge branch 'fontconfig-special-sources' of https://github.com/leungbk/kitty 2022-06-01 08:33:13 +05:30
Brian Leung
62c27b3db7 Add kitty/fontconfig.c to SPECIAL_SOURCES
Fixup of bec620a8d3.
2022-05-31 19:58:42 -07:00
Kovid Goyal
1283a2b7e5 ... 2022-06-01 08:21:23 +05:30
Kovid Goyal
bec620a8d3 Fix #5154 2022-06-01 08:19:11 +05:30
Kovid Goyal
6b0bb2e845 Sphinx v5 needs language to be not None 2022-06-01 06:47:06 +05:30
Kovid Goyal
135818f34b Note about __main__.py with +launch 2022-05-31 07:56:01 +05:30
Kovid Goyal
2a361d1de1 Update changelog for last PR 2022-05-31 07:46:00 +05:30
Kovid Goyal
761a941d43 Merge branch 'kovidgoyal-master' of https://github.com/jimbob3806/kitty 2022-05-31 07:41:40 +05:30
Kovid Goyal
c03630a487 ... 2022-05-31 07:26:04 +05:30
James Reid
0b79cbdb3b Add "user" tab to themes kitten
Add new tab named "user" to the built-in themes kitten - new tab
filters and displays only user-defined color themes, identified by
adding "is_user_defined" property to "Theme" class which defaults
to False, and is set True when a theme is fetched using the
"load_from_dir" method.
2022-05-30 19:25:09 +01:00
James Reid
1dc6b49e02 Remove hard coded tab string in ThemesHandler
Remove hard coded tab string and replace with self.tabs, which is
a reference to the same tab string defined in the __init__ of
ThemesHandler
2022-05-30 19:17:07 +01:00
Kovid Goyal
1962f8e5a0 Advertise edit-in-kitty a bit more 2022-05-30 15:27:23 +05:30
Kovid Goyal
cabfcbcb69 macOS: When pasting text and the clipboard has a filesystem path, paste the full path instead of the text, which is sometimes just the file name
Fixes #5142
2022-05-30 14:40:45 +05:30
Kovid Goyal
48bb43a2c4 fish: completion for edit-in-kitty clone-in-kitty
fish's wrapping facility does not preserve argv[0] so it cannot be used
unlike for zsh and bash
2022-05-30 10:50:19 +05:30
Kovid Goyal
af5e3e0c18 Add a reference to edit-in-kitty to the remote file kitten docs 2022-05-30 08:19:33 +05:30
Kovid Goyal
fb8398c2eb bash: completion for edit/clone-in-kitty 2022-05-30 08:16:51 +05:30
Kovid Goyal
eefd4bb3e3 zsh: Completion for edit-in-kitty and clone-in-kitty 2022-05-30 08:12:09 +05:30
Kovid Goyal
581c388838 ... 2022-05-29 15:02:37 +05:30
Kovid Goyal
7a63ea6480 Drop python 3.7 from CI 2022-05-29 14:54:51 +05:30
Kovid Goyal
65f1329635 Wayland: When the seat is closed remove any pending pointer animation and key repeat timers
Fixes #5145
2022-05-29 14:50:34 +05:30
Kovid Goyal
0df9a5d5c5 Fix compilation on Python 3.11
Python 3.11 deprecates the supposedly "stable API" PySys_AddXOption().
So switch to using PyConfig.xoptions instead. This means dropping
support for python 3.7 since PyConfig is not available there. So it
rolls.

Fixes #5143
2022-05-29 14:37:14 +05:30
Kovid Goyal
6405849a1b Bash integration clone test was accidentally running zsh
Fixes #5144
2022-05-29 08:46:06 +05:30
Kovid Goyal
e9340dfaaf Some more tests for last non empty command output 2022-05-28 15:22:17 +05:30
Kovid Goyal
0509855930 Allow getting the last non-empty command output easily via an action or remote control
Fixes #4973
2022-05-28 15:19:24 +05:30
Kovid Goyal
4c74462763 ssh kitten: Look in common locations for login shell if it is not in PATH 2022-05-28 11:29:09 +05:30
Kovid Goyal
7f69381221 Update changelog 2022-05-28 11:13:35 +05:30
Kovid Goyal
75942fb47e Implement edit-in-kitty for fish 2022-05-28 11:12:11 +05:30
Kovid Goyal
0893d7164a _run_kitten -> run_kitten_with_metadata 2022-05-28 11:00:25 +05:30
Kovid Goyal
27906ea853 Skip login shell detection when login shell is set to nologin 2022-05-28 08:09:30 +05:30
Kovid Goyal
8540ca3990 Workaround for nix package messing with kitty file permissions 2022-05-28 08:03:17 +05:30
Kovid Goyal
e82c5a63b5 Avoid unnecessary use of cat 2022-05-27 14:18:16 +05:30
Kovid Goyal
83602d7e53 Make chunked transmit code re-useable in fish integration script 2022-05-27 13:21:26 +05:30
Kovid Goyal
64e30e5299 Ignore permission errors when stating file to edit 2022-05-27 13:04:02 +05:30
Kovid Goyal
83ce23ff9f Use zsh's builtin stat instead of relying on the stat command 2022-05-27 12:50:52 +05:30
Kovid Goyal
c5e5ec618d Also trap SIGTERM to cleanup 2022-05-27 12:39:22 +05:30
Kovid Goyal
bd84ec03ce Implement edit-in-kitty for bash 2022-05-27 12:38:26 +05:30
Kovid Goyal
d71f18566e forgot to declare data local 2022-05-27 12:29:11 +05:30
Kovid Goyal
76e75d9f89 Ensure self.tdir exists in __del__ 2022-05-27 10:48:16 +05:30
Kovid Goyal
f5695ac4f4 Increase edit file size limit to 8MB 2022-05-27 10:45:36 +05:30
Kovid Goyal
b748cd21cf Allow opening files at a spedific line number with edit-in-kitty 2022-05-27 09:53:43 +05:30
Kovid Goyal
3a8bdf3b48 React instantly to editor window being closed 2022-05-27 09:15:16 +05:30
Kovid Goyal
196c6b07aa Merge branch 'fix-window-menu' of https://github.com/henryhchchc/kitty 2022-05-27 09:12:56 +05:30
Henry Chu
1978cc5b55 Fixed #5132 2022-05-27 11:15:16 +08:00
Kovid Goyal
ea47cee23f Document edit-in-kitty 2022-05-27 07:41:54 +05:30
Kovid Goyal
e6844ad6f2 ssh kitten: Run bash as a login shell
This fell through the cracks in the rewrite for automatic shell
integration. Fixes #5130
2022-05-26 21:24:32 +05:30
Kovid Goyal
02d5b3eafa Fix shebang 2022-05-26 21:21:36 +05:30
Kovid Goyal
151dacf17f Update changelog 2022-05-26 21:21:27 +05:30
Kovid Goyal
117d1b02be Start work on edit-in-kitty 2022-05-26 21:16:42 +05:30
Kovid Goyal
cb55878efd Fix #5129 2022-05-26 19:42:20 +05:30
Kovid Goyal
ea4751eae5 Move logic for handling chunked remote data into a separate function
Use the existing clipboard_max_size as the limit for remote data
2022-05-26 08:52:20 +05:30
69 changed files with 2054 additions and 655 deletions

View File

@@ -20,15 +20,15 @@ jobs:
cc: [gcc, clang]
include:
- python: a
pyver: 3.7
pyver: "3.8"
sanitize: 0
- python: b
pyver: 3.8
pyver: "3.9"
sanitize: 1
- python: c
pyver: 3.9
pyver: "3.10"
sanitize: 1
@@ -65,7 +65,7 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v3
with:
fetch-depth: 10
fetch-depth: 0 # needed for :commit: docs role
- name: Test for trailing whitespace
run: if grep -Inr '\s$' kitty kitty_tests kittens docs *.py *.asciidoc *.rst .gitattributes .gitignore; then echo Trailing whitespace found, aborting.; exit 1; fi

View File

@@ -1,185 +1,7 @@
#!/usr/bin/env python3
# License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
import os
import sys
from typing import List
def icat(args: List[str]) -> None:
from kittens.runner import run_kitten as rk
sys.argv = args
rk('icat')
def list_fonts(args: List[str]) -> None:
from kitty.fonts.list import main as list_main
list_main(args)
def remote_control(args: List[str]) -> None:
from kitty.remote_control import main as rc_main
rc_main(args)
def runpy(args: List[str]) -> None:
if len(args) < 2:
raise SystemExit('Usage: kitty +runpy "some python code"')
sys.argv = ['kitty'] + args[2:]
exec(args[1])
def hold(args: List[str]) -> None:
import subprocess
ret = 1
try:
ret = subprocess.Popen(args[1:]).wait()
except KeyboardInterrupt:
pass
except FileNotFoundError:
print(f'Could not find {args[1]!r} to execute', file=sys.stderr)
except Exception as e:
print(e, file=sys.stderr)
from kitty.utils import hold_till_enter
hold_till_enter()
raise SystemExit(ret)
def complete(args: List[str]) -> None:
from kitty.complete import main as complete_main
complete_main(args[1:], entry_points, namespaced_entry_points)
def open_urls(args: List[str]) -> None:
setattr(sys, 'cmdline_args_for_open', True)
sys.argv = ['kitty'] + args[1:]
from kitty.main import main as kitty_main
kitty_main()
def launch(args: List[str]) -> None:
import runpy
sys.argv = args[1:]
try:
exe = args[1]
except IndexError:
raise SystemExit(
'usage: kitty +launch script.py [arguments to be passed to script.py ...]\n\n'
'script.py will be run with full access to kitty code. If script.py is '
'prefixed with a : it will be searched for in PATH'
)
if exe.startswith(':'):
import shutil
q = shutil.which(exe[1:])
if not q:
raise SystemExit(f'{exe[1:]} not found in PATH')
exe = q
if not os.path.exists(exe):
raise SystemExit(f'{exe} does not exist')
runpy.run_path(exe, run_name='__main__')
def shebang(args: List[str]) -> None:
script_path = args[1]
cmd = args[2:]
if cmd == ['__ext__']:
cmd = [os.path.splitext(script_path)[1][1:].lower()]
try:
f = open(script_path)
except FileNotFoundError:
raise SystemExit(f'The file {script_path} does not exist')
with f:
if f.read(2) == '#!':
line = f.readline().strip()
_plat = sys.platform.lower()
is_macos: bool = 'darwin' in _plat
if is_macos:
cmd = line.split(' ')
else:
cmd = line.split(' ', maxsplit=1)
os.execvp(cmd[0], cmd + [script_path])
def run_kitten(args: List[str]) -> None:
try:
kitten = args[1]
except IndexError:
from kittens.runner import list_kittens
list_kittens()
raise SystemExit(1)
sys.argv = args[1:]
from kittens.runner import run_kitten as rk
rk(kitten)
def edit_config_file(args: List[str]) -> None:
from kitty.cli import create_default_opts
from kitty.fast_data_types import set_options
from kitty.utils import edit_config_file as f
set_options(create_default_opts())
f()
def namespaced(args: List[str]) -> None:
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 = {
# These two are here for backwards compat
'icat': icat,
'list-fonts': list_fonts,
'@': remote_control,
'+': namespaced,
}
namespaced_entry_points = {k: v for k, v in entry_points.items() if k[0] not in '+@'}
namespaced_entry_points['hold'] = hold
namespaced_entry_points['complete'] = complete
namespaced_entry_points['runpy'] = runpy
namespaced_entry_points['launch'] = launch
namespaced_entry_points['open'] = open_urls
namespaced_entry_points['kitten'] = run_kitten
namespaced_entry_points['edit-config'] = edit_config_file
namespaced_entry_points['shebang'] = shebang
def setup_openssl_environment() -> None:
# Use our bundled CA certificates instead of the system ones, since
# many systems come with no certificates in a useable form or have various
# locations for the certificates.
d = os.path.dirname
ext_dir: str = getattr(sys, 'kitty_extensions_dir')
if 'darwin' in sys.platform.lower():
cert_file = os.path.join(d(d(d(ext_dir))), 'cacert.pem')
else:
cert_file = os.path.join(d(ext_dir), 'cacert.pem')
os.environ['SSL_CERT_FILE'] = cert_file
setattr(sys, 'kitty_ssl_env_var', 'SSL_CERT_FILE')
def main() -> None:
if getattr(sys, 'frozen', False) and getattr(sys, 'kitty_extensions_dir', ''):
setup_openssl_environment()
first_arg = '' if len(sys.argv) < 2 else sys.argv[1]
func = entry_points.get(first_arg)
if func is None:
if first_arg.startswith('@'):
remote_control(['@', first_arg[1:]] + sys.argv[2:])
elif first_arg.startswith('+'):
namespaced(['+', first_arg[1:]] + sys.argv[2:])
else:
from kitty.main import main as kitty_main
kitty_main()
else:
func(sys.argv[1:])
if __name__ == '__main__':
from kitty.entry_points import main
main()

View File

@@ -16,6 +16,7 @@ The :doc:`ssh kitten <kittens/ssh>` is redesigned with powerful new features:
* Automatic :ref:`shell_integration` on remote machines
* Easily :ref:`clone local shell/editor config <real_world_ssh_kitten_config>` on remote machines
* Easily :ref:`edit files in your local editor <edit_file>` on remote machines
* Automatic :opt:`re-use of existing connections <kitten-ssh.share_connections>` to avoid connection setup latency
@@ -34,6 +35,44 @@ mouse anywhere in the current command to move the cursor there. See
Detailed list of changes
-------------------------------------
0.25.2 [2022-06-07]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- A new command :command:`edit-in-kitty` to :ref:`edit_file`
- Allow getting the last non-empty command output easily via an action or
remote control (:pull:`4973`)
- Fix a bug that caused :opt:`macos_colorspace` to always be ``default`` regardless of its actual value (:iss:`5129`)
- diff kitten: A new option :opt:`kitten-diff.ignore_name` to exclude files and directories from being scanned (:pull:`5171`)
- ssh kitten: Fix bash not being executed as a login shell since kitty 0.25.0 (:iss:`5130`)
- macOS: When pasting text and the clipboard has a filesystem path, paste the
full path instead of the text, which is sometimes just the file name (:pull:`5142`)
- macOS: Allow opening executables without a file extension with kitty as well
(:iss:`5160`)
- Themes kitten: Add a tab to show user defined custom color themes separately
(:pull:`5150`)
- Iosevka: Fix incorrect rendering when there is a combining char that does not
group with its neighbors (:iss:`5153`)
- Weston: Fix client side decorations flickering on slow computers during
window resize (:iss:`5162`)
- Remote control: Fix commands with large or asynchronous payloads like
:command:`kitty @ set-backround-image`, :command:`kitty @ set-window-logo`
and :command:`kitty @ select-window` not working correctly
when using a socket (:iss:`5165`)
- hints kitten: Fix surrounding quotes/brackets and embedded carriage returns
not being removed when using line number processing (:iss:`5170`)
0.25.1 [2022-05-26]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -13,7 +13,7 @@ import subprocess
import sys
import time
from functools import partial
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
from typing import Any, Callable, Dict, Iterable, List, Tuple
from docutils import nodes
from docutils.parsers.rst.roles import set_classes
@@ -85,7 +85,7 @@ master_doc = 'index'
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language: Optional[str] = None
language: str = 'en'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.

View File

@@ -378,7 +378,9 @@ id. To do so add the ``p`` key with a number between ``1`` and ``4294967295``.
When you specify a placement id, it will be added to the acknowledgement code
above. Every placement is uniquely identified by the pair of the ``image id``
and the ``placement id``. If you specify a placement id for an image that does
not have an id, it will be ignored. An example response::
not have an id (i.e. has id=0), it will be ignored. In particular this means
there can exist multiple images with ``image id=0, placement id=0``.
An example response::
<ESC>_Gi=<image id>,p=<placement id>;OK<ESC>\

View File

@@ -22,6 +22,8 @@ transferred to the remote computer. Note that this happens without needing
to install *any* special software on the server, beyond :program:`ls` that
supports hyperlinks.
.. seealso:: See the :ref:`edit-in-kitty <edit_file>` command
.. seealso:: See the :doc:`transfer` kitten
.. versionadded:: 0.19.0

View File

@@ -144,33 +144,7 @@ You can customize these actions by creating a :file:`launch-actions.conf` file
in the :ref:`kitty config directory <confloc>`, just like the
:file:`open-actions.conf` file above. For example:
.. code:: conf
# Open script files based on their shebangs
protocol file
ext sh,command,tool
action launch --hold --type=os-window kitty +shebang $FILE_PATH {SHELL}
# Open shell specific script files
protocol file
ext fish,bash,zsh
action launch --hold --type=os-window kitty +shebang $FILE_PATH __ext__
# Open directories
protocol file
mime inode/directory
action launch --type=os-window --cwd $FILE_PATH
# Open text files without fragments in the editor
protocol file
mime text/*
action launch --type=os-window $EDITOR $FILE_PATH
# Open image files with icat
protocol file
mime image/*
action launch --type=os-window kitty +kitten icat --hold $FILE_PATH
# Open ssh URLs with ssh command
protocol ssh
action launch --type=os-window ssh $URL
.. literalinclude:: ../kitty/open_actions.py
:language: conf
:start-at: # Open script files
:end-before: '''.splitlines()))

View File

@@ -33,6 +33,8 @@ Features
* :ref:`clone_shell` with all environment variables and the working directory
copied
* :ref:`Edit files in new kitty windows <edit_file>` even over SSH
* Glitch free window resizing even with complex prompts. Achieved by erasing
the prompt on resize and allowing the shell to redraw it cleanly.
@@ -263,6 +265,33 @@ state (mainly CWD and env vars) and this state is transmitted to kitty and
restored by the shell integration scripts in the cloned window.
.. _edit_file:
Edit files in new kitty windows even over SSH
------------------------------------------------
.. code-block:: sh
edit-in-kitty myfile.txt
edit-in-kitty --type tab --title "Editing My File" myfile.txt
# open file.txt at line 75 (works with vim, emacs, nano, micro)
edit-in-kitty +75 file.txt
The :command:`edit-in-kitty` command allows you to seamlessly edit files
in your default :opt:`editor` in new kitty windows. This works even over
SSH (if you use the :doc:`ssh kitten <kittens/ssh>`), allowing you
to easily edit remote files in your local editor with all its bells and
whistles.
The :command:`edit-in-kitty` command takes almost all the same arguments as the
:doc:`launch <launch>` command, so you can open a new tab instead or a new OS
window, etc. Not all arguments are supported, see the discussion in the
:ref:`clone_shell` section above.
In order to avoid remote code execution, kitty will only execute the configured
editor and pass the file path to edit to it.
.. _manual_shell_integration:
Manual shell integration

View File

@@ -2482,25 +2482,35 @@ void _glfwPlatformSetClipboardString(const char* string)
const char* _glfwPlatformGetClipboardString(void)
{
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
free(_glfw.ns.clipboardString); _glfw.ns.clipboardString = NULL;
if (![[pasteboard types] containsObject:NSPasteboardTypeString])
{
_glfwInputError(GLFW_FORMAT_UNAVAILABLE,
"Cocoa: Failed to retrieve string from pasteboard");
return NULL;
NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES};
NSArray* objs = [pasteboard readObjectsForClasses:@[[NSURL class], [NSString class]] options:options];
if (objs) {
const NSUInteger count = [objs count];
if (count) {
NSMutableString *path_list = [NSMutableString stringWithCapacity:4096]; // auto-released
NSMutableString *text_list = [NSMutableString stringWithCapacity:4096]; // auto-released
for (NSUInteger i = 0; i < count; i++) {
id obj = objs[i];
if ([obj isKindOfClass:[NSURL class]]) {
NSURL *url = (NSURL*)obj;
if (url.fileURL && url.fileSystemRepresentation) {
if ([path_list length] > 0) [path_list appendString:@("\n")];
[path_list appendString:@(url.fileSystemRepresentation)];
}
} else if ([obj isKindOfClass:[NSString class]]) {
if ([text_list length] > 0) [text_list appendString:@("\n")];
[text_list appendString:obj];
}
}
const char *text = NULL;
if (path_list.length > 0) text = [path_list UTF8String];
else if (text_list.length > 0) text = [text_list UTF8String];
if (text) _glfw.ns.clipboardString = _glfw_strdup(text);
}
}
NSString* object = [pasteboard stringForType:NSPasteboardTypeString];
if (!object)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
"Cocoa: Failed to retrieve object from pasteboard");
return NULL;
}
free(_glfw.ns.clipboardString);
_glfw.ns.clipboardString = _glfw_strdup([object UTF8String]);
if (!_glfw.ns.clipboardString) _glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to retrieve object from pasteboard");
return _glfw.ns.clipboardString;
}

View File

@@ -102,13 +102,31 @@ init_buffer_pair(_GLFWWaylandBufferPair *pair, size_t width, size_t height, unsi
return 2 * pair->size_in_bytes;
}
static bool
window_has_buffer(_GLFWwindow *window, struct wl_buffer *q) {
#define Q(which) decs.which.buffer.a == q || decs.which.buffer.b == q
return Q(left) || Q(top) || Q(right) || Q(bottom);
#undef Q
}
static void
alloc_buffer_pair(_GLFWWaylandBufferPair *pair, struct wl_shm_pool *pool, uint8_t *data, size_t *offset) {
buffer_release_event(void *data, struct wl_buffer *buffer) {
wl_buffer_destroy(buffer);
_GLFWwindow *window = _glfwWindowForId((uintptr_t)data);
if (window && window_has_buffer(window, buffer)) decs.buffer_destroyed = true;
}
static struct wl_buffer_listener handle_buffer_events = {.release = buffer_release_event};
static void
alloc_buffer_pair(uintptr_t window_id, _GLFWWaylandBufferPair *pair, struct wl_shm_pool *pool, uint8_t *data, size_t *offset) {
pair->data.a = data + *offset;
pair->a = wl_shm_pool_create_buffer(pool, *offset, pair->width, pair->height, pair->stride, WL_SHM_FORMAT_ARGB8888);
wl_buffer_add_listener(pair->a, &handle_buffer_events, (void*)window_id);
*offset += pair->size_in_bytes;
pair->data.b = data + *offset;
pair->b = wl_shm_pool_create_buffer(pool, *offset, pair->width, pair->height, pair->stride, WL_SHM_FORMAT_ARGB8888);
wl_buffer_add_listener(pair->b, &handle_buffer_events, (void*)window_id);
*offset += pair->size_in_bytes;
pair->front = pair->a; pair->back = pair->b;
pair->data.front = pair->data.a; pair->data.back = pair->data.b;
@@ -287,7 +305,7 @@ create_shm_buffers(_GLFWwindow* window) {
struct wl_shm_pool* pool = wl_shm_create_pool(_glfw.wl.shm, fd, decs.mapping.size);
close(fd);
size_t offset = 0;
#define a(which) alloc_buffer_pair(&decs.which.buffer, pool, decs.mapping.data, &offset)
#define a(which) alloc_buffer_pair(window->id, &decs.which.buffer, pool, decs.mapping.data, &offset)
a(top); a(left); a(bottom); a(right);
#undef a
wl_shm_pool_destroy(pool);
@@ -313,8 +331,6 @@ free_csd_surfaces(_GLFWwindow *window) {
static void
free_csd_buffers(_GLFWwindow *window) {
#define d(which) { \
if (decs.which.buffer.a) wl_buffer_destroy(decs.which.buffer.a); \
if (decs.which.buffer.b) wl_buffer_destroy(decs.which.buffer.b); \
memset(&decs.which.buffer, 0, sizeof(_GLFWWaylandBufferPair)); \
}
d(left); d(top); d(right); d(bottom);
@@ -352,12 +368,14 @@ ensure_csd_resources(_GLFWwindow *window) {
decs.for_window_state.scale != window->wl.scale ||
!decs.mapping.data
);
const bool needs_update = focus_changed || size_changed || !decs.left.surface;
debug("CSD: old.size: %dx%d new.size: %dx%d needs_update: %d size_changed: %d\n", decs.for_window_state.width, decs.for_window_state.height, window->wl.width, window->wl.height, needs_update, size_changed);
const bool needs_update = focus_changed || size_changed || !decs.left.surface || decs.buffer_destroyed;
debug("CSD: old.size: %dx%d new.size: %dx%d needs_update: %d size_changed: %d buffer_destroyed: %d\n",
decs.for_window_state.width, decs.for_window_state.height, window->wl.width, window->wl.height, needs_update, size_changed, decs.buffer_destroyed);
if (!needs_update) return false;
if (size_changed) {
if (size_changed || decs.buffer_destroyed) {
free_csd_buffers(window);
if (!create_shm_buffers(window)) return false;
decs.buffer_destroyed = false;
}
int32_t x, y, scale = window->wl.scale < 1 ? 1 : window->wl.scale;

9
glfw/wl_init.c vendored
View File

@@ -573,6 +573,10 @@ static void seatHandleCapabilities(void* data UNUSED,
{
wl_pointer_destroy(_glfw.wl.pointer);
_glfw.wl.pointer = NULL;
if (_glfw.wl.cursorAnimationTimer) {
removeTimer(&_glfw.wl.eventLoopData, _glfw.wl.cursorAnimationTimer);
_glfw.wl.cursorAnimationTimer = 0;
}
}
if ((caps & WL_SEAT_CAPABILITY_KEYBOARD) && !_glfw.wl.keyboard)
@@ -584,6 +588,11 @@ static void seatHandleCapabilities(void* data UNUSED,
{
wl_keyboard_destroy(_glfw.wl.keyboard);
_glfw.wl.keyboard = NULL;
_glfw.wl.keyboardFocusId = 0;
if (_glfw.wl.keyRepeatInfo.keyRepeatTimer) {
removeTimer(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer);
_glfw.wl.keyRepeatInfo.keyRepeatTimer = 0;
}
}
}

2
glfw/wl_platform.h vendored
View File

@@ -172,7 +172,7 @@ typedef struct _GLFWwindowWayland
struct zwp_idle_inhibitor_v1* idleInhibitor;
struct {
bool serverSide;
bool serverSide, buffer_destroyed;
_GLFWdecorationSideWayland focus;
_GLFWWaylandCSDEdge top, left, right, bottom;

38
kittens/diff/collect.py Normal file → Executable file
View File

@@ -4,10 +4,14 @@
import os
import re
from contextlib import suppress
from fnmatch import fnmatch
from functools import lru_cache
from hashlib import md5
from typing import (
TYPE_CHECKING, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Union
)
from kitty.guess_mime_type import guess_type
from typing import TYPE_CHECKING, Dict, List, Set, Optional, Iterator, Tuple, Union
if TYPE_CHECKING:
from .highlight import DiffHighlight # noqa
@@ -37,6 +41,8 @@ class Segment:
class Collection:
ignore_names: Tuple[str, ...] = ()
def __init__(self) -> None:
self.changes: Dict[str, str] = {}
self.renames: Dict[str, str] = {}
@@ -105,22 +111,34 @@ def resolve_remote_name(path: str, default: str) -> str:
return default
def allowed_items(items: Sequence[str], ignore_patterns: Sequence[str]) -> Iterator[str]:
for name in items:
for pat in ignore_patterns:
if fnmatch(name, pat):
break
else:
yield name
def walk(base: str, names: Set[str], pmap: Dict[str, str], ignore_names: Tuple[str, ...]) -> None:
for dirpath, dirnames, filenames in os.walk(base):
dirnames[:] = allowed_items(dirnames, ignore_names)
for filename in allowed_items(filenames, ignore_names):
path = os.path.abspath(os.path.join(dirpath, filename))
path_name_map[path] = name = os.path.relpath(path, base)
names.add(name)
pmap[name] = path
def collect_files(collection: Collection, left: str, right: str) -> None:
left_names: Set[str] = set()
right_names: Set[str] = set()
left_path_map: Dict[str, str] = {}
right_path_map: Dict[str, str] = {}
def walk(base: str, names: Set[str], pmap: Dict[str, str]) -> None:
for dirpath, dirnames, filenames in os.walk(base):
for filename in filenames:
path = os.path.abspath(os.path.join(dirpath, filename))
path_name_map[path] = name = os.path.relpath(path, base)
names.add(name)
pmap[name] = path
walk(left, left_names, left_path_map, collection.ignore_names)
walk(right, right_names, right_path_map, collection.ignore_names)
walk(left, left_names, left_path_map)
walk(right, right_names, right_path_map)
common_names = left_names & right_names
changed_names = {n for n in common_names if data_for_path(left_path_map[n]) != data_for_path(right_path_map[n])}
for n in changed_names:

1
kittens/diff/main.py Normal file → Executable file
View File

@@ -655,6 +655,7 @@ def main(args: List[str]) -> None:
opts = init_config(cli_opts)
set_diff_command(opts.diff_cmd)
lines_for_path.replace_tab_by = opts.replace_tab_by
Collection.ignore_names = tuple(opts.ignore_name)
left, right = map(get_remote_file, (left, right))
if os.path.isdir(left) != os.path.isdir(right):
raise SystemExit('The items to be diffed should both be either directories or files. Comparing a directory to a file is not valid.')

15
kittens/diff/options/definition.py Normal file → Executable file
View File

@@ -47,6 +47,21 @@ opt('replace_tab_by', '\\x20\\x20\\x20\\x20',
option_type='python_string',
long_text='The string to replace tabs with. Default is to use four spaces.'
)
opt('+ignore_name', '',
option_type='store_multiple',
add_to_default=False,
long_text='''
A glob pattern that is matched against only the filename of files and directories. Matching
files and directories are ignored when scanning the filesystem to look for files to diff.
Can be specified multiple times to use multiple patterns. For example::
ignore_name .git
ignore_name *~
ignore_name *.pyc
''',
)
egr() # }}}
# colors {{{

View File

@@ -1,7 +1,7 @@
# generated by gen-config.py DO NOT edit
import typing
from kittens.diff.options.utils import parse_map, syntax_aliases
from kittens.diff.options.utils import parse_map, store_multiple, syntax_aliases
from kitty.conf.utils import merge_dicts, positive_int, python_string, to_color, to_color_or_none
@@ -37,6 +37,10 @@ class Parser:
def hunk_margin_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['hunk_margin_bg'] = to_color(val)
def ignore_name(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
for k, v in store_multiple(val, ans["ignore_name"]):
ans["ignore_name"][k] = v
def margin_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['margin_bg'] = to_color(val)
@@ -89,6 +93,7 @@ class Parser:
def create_result_dict() -> typing.Dict[str, typing.Any]:
return {
'ignore_name': {},
'map': [],
}

View File

@@ -20,6 +20,7 @@ option_names = ( # {{{
'highlight_removed_bg',
'hunk_bg',
'hunk_margin_bg',
'ignore_name',
'map',
'margin_bg',
'margin_fg',
@@ -64,6 +65,7 @@ class Options:
syntax_aliases: typing.Dict[str, str] = {'pyj': 'py', 'pyi': 'py', 'recipe': 'py'}
title_bg: Color = Color(255, 255, 255)
title_fg: Color = Color(0, 0, 0)
ignore_name: typing.Dict[str, str] = {}
map: typing.List[typing.Tuple[kitty.types.ParsedShortcut, kitty.conf.utils.KeyAction]] = []
key_definitions: KittensKeyMap = {}
config_paths: typing.Tuple[str, ...] = ()
@@ -116,6 +118,7 @@ class Options:
defaults = Options()
defaults.ignore_name = {}
defaults.map = [
# quit
(ParsedShortcut(mods=0, key_name='q'), KeyAction('quit')), # noqa

8
kittens/diff/options/utils.py Normal file → Executable file
View File

@@ -3,7 +3,7 @@
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
from typing import Any, Dict, Iterable, Tuple, Union
from typing import Any, Container, Dict, Iterable, Tuple, Union
from kitty.conf.utils import (
KeyFuncWrapper, KittensKeyDefinition, parse_kittens_key
@@ -58,6 +58,12 @@ def syntax_aliases(raw: str) -> Dict[str, str]:
return ans
def store_multiple(val: str, current_val: Container[str]) -> Iterable[Tuple[str, str]]:
val = val.strip()
if val not in current_val:
yield val, val
def parse_map(val: str) -> Iterable[KittensKeyDefinition]:
x = parse_kittens_key(val, func_with_args.args_funcs)
if x is not None:

View File

@@ -9,8 +9,8 @@ from functools import lru_cache
from gettext import gettext as _
from itertools import repeat
from typing import (
Any, Callable, Dict, Generator, Iterable, List, Optional, Pattern,
Sequence, Set, Tuple, Type, cast
Any, Callable, Dict, Generator, Iterable, Iterator, List, Optional,
Pattern, Sequence, Set, Tuple, Type, cast
)
from kitty.cli import parse_args
@@ -44,6 +44,7 @@ DEFAULT_HINT_ALPHABET = string.digits + string.ascii_lowercase
DEFAULT_REGEX = r'(?m)^\s*(.+)\s*$'
FILE_EXTENSION = r'\.(?:[a-zA-Z0-9]{2,7}|[ahcmo])(?!\.)'
PATH_REGEX = fr'(?:\S*?/[\r\S]+)|(?:\S[\r\S]*{FILE_EXTENSION})\b'
DEFAULT_LINENUM_REGEX = fr'(?P<path>{PATH_REGEX}):(?P<line>\d+)'
class Mark:
@@ -64,6 +65,10 @@ class Mark:
self.is_hyperlink = is_hyperlink
self.group_id = group_id
def __repr__(self) -> str:
return (f'Mark(index={self.index!r}, start={self.start!r}, end={self.end!r},'
f' text={self.text!r}, groupdict={self.groupdict!r}, is_hyperlink={self.is_hyperlink!r}, group_id={self.group_id!r})')
@lru_cache(maxsize=2048)
def encode_hint(num: int, alphabet: str) -> str:
@@ -225,14 +230,14 @@ class Hints(Handler):
self.write(self.current_text)
def regex_finditer(pat: 'Pattern[str]', minimum_match_length: int, text: str) -> Generator[Tuple[int, int, Dict[str, str]], None, None]:
def regex_finditer(pat: 'Pattern[str]', minimum_match_length: int, text: str) -> Iterator[Tuple[int, int, 're.Match[str]']]:
has_named_groups = bool(pat.groupindex)
for m in pat.finditer(text):
s, e = m.span(0 if has_named_groups else pat.groups)
while e > s + 1 and text[e-1] == '\0':
e -= 1
if e - s >= minimum_match_length:
yield s, e, m.groupdict()
yield s, e, m
closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>', '*': '*', '"': '"', "'": "'", "": "", "": ""}
@@ -318,16 +323,22 @@ def ip(text: str, s: int, e: int) -> Tuple[int, int]:
return s, e
def mark(pattern: str, post_processors: Iterable[PostprocessorFunc], text: str, args: HintsCLIOptions) -> Generator[Mark, None, None]:
def mark(pattern: str, post_processors: Iterable[PostprocessorFunc], text: str, args: HintsCLIOptions) -> Iterator[Mark]:
pat = re.compile(pattern)
for idx, (s, e, groupdict) in enumerate(regex_finditer(pat, args.minimum_match_length, text)):
sanitize_pat = re.compile('[\r\n\0]')
for idx, (s, e, match_object) in enumerate(regex_finditer(pat, args.minimum_match_length, text)):
try:
for func in post_processors:
s, e = func(text, s, e)
except InvalidMatch:
continue
mark_text = re.sub('[\r\n\0]', '', text[s:e])
groupdict = match_object.groupdict()
for group_name in groupdict:
group_idx = pat.groupindex[group_name]
gs, ge = match_object.span(group_idx)
gs, ge = max(gs, s), min(ge, e)
groupdict[group_name] = sanitize_pat.sub('', text[gs:ge])
mark_text = sanitize_pat.sub('', text[s:e])
yield Mark(idx, s, e, mark_text, groupdict)
@@ -420,7 +431,7 @@ def parse_input(text: str) -> str:
def linenum_marks(text: str, args: HintsCLIOptions, Mark: Type[Mark], extra_cli_args: Sequence[str], *a: Any) -> Generator[Mark, None, None]:
regex = args.regex
if regex == DEFAULT_REGEX:
regex = fr'(?P<path>{PATH_REGEX}):(?P<line>\d+)'
regex = DEFAULT_LINENUM_REGEX
yield from mark(regex, [brackets, quotes], text, args)
@@ -699,18 +710,14 @@ def main(args: List[str]) -> Optional[Dict[str, Any]]:
def linenum_process_result(data: Dict[str, Any]) -> Tuple[str, int]:
pat = re.compile(r':(\d+)$')
for m, g in zip(data['match'], data['groupdicts']):
if m:
path, line = g['path'], g['line']
# look for trailers on path of the for :number
while True:
m = pat.search(path)
if m is None:
break
line = m.group(1)
path = path[:-len(m.group())]
lnum_pat = re.compile(r'(:\d+)$')
for match, g in zip(data['match'], data['groupdicts']):
path, line = g['path'], g['line']
if path and line:
m = lnum_pat.search(path)
if m is not None:
line = m.group(1)[1:]
path = path.rpartition(':')[0]
return os.path.expanduser(path), int(line)
return '', -1

View File

453
kittens/prewarm/main.py Normal file
View File

@@ -0,0 +1,453 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
import io
import json
import os
import select
import signal
import sys
import time
from contextlib import suppress
from dataclasses import dataclass
from importlib import import_module
from typing import (
IO, TYPE_CHECKING, Any, Dict, List, NoReturn, Optional, Union, cast
)
from kitty.child import remove_cloexec
from kitty.constants import kitty_exe
from kitty.entry_points import main as main_entry_point
from kitty.fast_data_types import establish_controlling_tty, safe_pipe
from kitty.shm import SharedMemory
if TYPE_CHECKING:
from _typeshed import ReadableBuffer, WriteableBuffer
hangup_events = select.POLLHUP
error_events = select.POLLERR | select.POLLNVAL
basic_events = hangup_events | error_events
class PrewarmProcessFailed(Exception):
pass
@dataclass
class Child:
child_id: int
child_process_pid: int
class PrewarmProcess:
def __init__(self, create_file_to_read_from_worker: bool = False) -> None:
self.from_worker_fd, self.in_worker_fd = safe_pipe()
self.children: Dict[int, Child] = {}
if create_file_to_read_from_worker:
os.set_blocking(self.from_worker_fd, True)
self.from_worker = open(self.from_worker_fd, mode='r', closefd=True)
self.from_worker_fd = -1
def take_from_worker_fd(self) -> int:
ans, self.from_worker_fd = self.from_worker_fd, -1
return ans
def __del__(self) -> None:
if self.from_worker_fd > -1:
os.close(self.from_worker_fd)
self.from_worker_fd = -1
if hasattr(self, 'from_worker'):
self.from_worker.close()
del self.from_worker
if self.worker_started:
import subprocess
self.process.stdin and self.process.stdin.close()
self.process.stdout and self.process.stdout.close()
try:
self.process.wait(timeout=1.0)
except subprocess.TimeoutExpired:
self.process.kill()
del self.process
@property
def worker_started(self) -> bool:
return self.in_worker_fd == -1
def ensure_worker(self) -> None:
if not self.worker_started:
import subprocess
self.process = subprocess.Popen(
[kitty_exe(), '+kitten', 'prewarm', str(self.in_worker_fd)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, pass_fds=(self.in_worker_fd,))
os.close(self.in_worker_fd)
self.in_worker_fd = -1
assert self.process.stdin is not None and self.process.stdout is not None
self.write_to_process_fd = self.process.stdin.fileno()
self.read_from_process_fd = self.process.stdout.fileno()
os.set_blocking(self.write_to_process_fd, False)
os.set_blocking(self.read_from_process_fd, False)
self.poll = select.poll()
self.poll.register(self.process.stdout.fileno(), select.POLLIN | basic_events)
def poll_to_send(self, yes: bool = True) -> None:
if yes:
self.poll.register(self.write_to_process_fd, select.POLLOUT | basic_events)
else:
self.poll.unregister(self.write_to_process_fd)
def reload_kitty_config(self) -> None:
if self.worker_started:
self.send_to_prewarm_process('reload_kitty_config:\n')
def __call__(
self,
tty_fd: int,
argv: List[str],
cwd: str = '',
env: Optional[Dict[str, str]] = None,
stdin_data: Optional[Union[str, bytes]] = None
) -> Child:
self.ensure_worker()
tty_name = os.ttyname(tty_fd)
if isinstance(stdin_data, str):
stdin_data = stdin_data.encode()
if env is None:
env = dict(os.environ)
cmd: Dict[str, Union[int, List[str], str, Dict[str, str]]] = {
'tty_name': tty_name, 'cwd': cwd or os.getcwd(), 'argv': argv, 'env': env,
}
total_size = 0
if stdin_data is not None:
cmd['stdin_size'] = len(stdin_data)
total_size += len(stdin_data)
data = json.dumps(cmd).encode()
total_size += len(data) + SharedMemory.num_bytes_for_size
with SharedMemory(size=total_size, unlink_on_exit=True) as shm:
shm.write_data_with_size(data)
if stdin_data:
shm.write(stdin_data)
shm.flush()
self.send_to_prewarm_process(f'fork:{shm.name}\n')
input_buf = b''
st = time.monotonic()
while time.monotonic() - st < 2:
for (fd, event) in self.poll.poll(0.2):
if event & basic_events:
raise PrewarmProcessFailed('Failed doing I/O with prewarm process')
if fd == self.read_from_process_fd and event & select.POLLIN:
d = os.read(self.read_from_process_fd, io.DEFAULT_BUFFER_SIZE)
input_buf += d
while (idx := input_buf.find(b'\n')) > -1:
line = input_buf[:idx].decode()
input_buf = input_buf[idx+1:]
if line.startswith('CHILD:'):
_, cid, pid = line.split(':')
child = self.add_child(int(cid), int(pid))
shm.unlink_on_exit = False
return child
if line.startswith('ERR:'):
raise PrewarmProcessFailed(line.split(':', 1)[-1])
raise PrewarmProcessFailed('Timed out waiting for I/O with prewarm process')
def add_child(self, child_id: int, pid: int) -> Child:
self.children[child_id] = c = Child(child_id, pid)
return c
def send_to_prewarm_process(self, output_buf: Union[str, bytes] = b'', timeout: float = 2) -> None:
if isinstance(output_buf, str):
output_buf = output_buf.encode()
st = time.monotonic()
while time.monotonic() - st < timeout and output_buf:
self.poll_to_send(bool(output_buf))
for (fd, event) in self.poll.poll(0.2):
if event & basic_events:
raise PrewarmProcessFailed('Failed doing I/O with prewarm process')
if fd == self.write_to_process_fd and event & select.POLLOUT:
n = os.write(self.write_to_process_fd, output_buf)
output_buf = output_buf[n:]
self.poll_to_send(False)
if output_buf:
raise PrewarmProcessFailed('Timed out waiting to write to prewarm process')
def mark_child_as_ready(self, child_id: int) -> bool:
c = self.children.pop(child_id, None)
if c is None:
return False
self.send_to_prewarm_process(f'ready:{child_id}\n')
return True
def reload_kitty_config() -> None:
from kittens.tui.utils import kitty_opts
kitty_opts.clear_cached()
kitty_opts()
def prewarm() -> None:
reload_kitty_config()
for kitten in ('hints', 'ssh', 'unicode_input', 'ask', 'show_error'):
import_module(f'kittens.{kitten}.main')
class MemoryViewReadWrapperBytes(io.BufferedIOBase):
def __init__(self, mw: memoryview):
self.mw = mw
self.pos = 0
def detach(self) -> io.RawIOBase:
raise io.UnsupportedOperation('detach() not supported')
def read(self, size: Optional[int] = -1) -> bytes:
if size is None or size < 0:
size = max(0, len(self.mw) - self.pos)
oldpos = self.pos
self.pos = min(len(self.mw), self.pos + size)
if self.pos <= oldpos:
return b''
return bytes(self.mw[oldpos:self.pos])
def readinto(self, b: 'WriteableBuffer') -> int:
if not isinstance(b, memoryview):
b = memoryview(b)
b = b.cast('B')
data = self.read(len(b))
n = len(data)
b[:n] = data
return n
readinto1 = readinto
def readall(self) -> bytes:
return self.read()
def write(self, b: 'ReadableBuffer') -> int:
raise io.UnsupportedOperation('readonly stream')
def readable(self) -> bool:
return True
class MemoryViewReadWrapper(io.TextIOWrapper):
def __init__(self, mw: memoryview):
super().__init__(cast(IO[bytes], MemoryViewReadWrapperBytes(mw)), encoding='utf-8', errors='replace')
def child_main(cmd: Dict[str, Any], ready_fd: int) -> NoReturn:
cwd = cmd.get('cwd')
if cwd:
try:
os.chdir(cwd)
except OSError:
with suppress(OSError):
os.chdir('/')
os.setsid()
env = cmd.get('env')
if env is not None:
os.environ.clear()
os.environ.update(env)
argv = cmd.get('argv')
if argv:
sys.argv = list(argv)
poll = select.poll()
poll.register(ready_fd, select.POLLIN | select.POLLERR | select.POLLHUP)
poll.poll()
os.close(ready_fd)
tty_name = cmd.get('tty_name')
if tty_name:
sys.__stdout__.flush()
sys.__stderr__.flush()
establish_controlling_tty(tty_name, sys.__stdin__.fileno(), sys.__stdout__.fileno(), sys.__stderr__.fileno())
main_entry_point()
raise SystemExit(0)
def fork(shm_address: str, ready_fd: int) -> int:
sz = pos = 0
with SharedMemory(name=shm_address, unlink_on_exit=True) as shm:
data = shm.read_data_with_size()
cmd = json.loads(data)
sz = cmd.get('stdin_size', 0)
if sz:
pos = shm.tell()
shm.unlink_on_exit = False
try:
child_pid = os.fork()
except OSError:
if sz:
with SharedMemory(shm_address, unlink_on_exit=True):
pass
if child_pid:
# master process
return child_pid
# child process
if shm.unlink_on_exit:
child_main(cmd, ready_fd)
else:
with SharedMemory(shm_address, unlink_on_exit=True) as shm:
stdin_data = memoryview(shm.mmap)[pos:pos + sz]
if stdin_data:
sys.stdin = MemoryViewReadWrapper(stdin_data)
try:
child_main(cmd, ready_fd)
finally:
stdin_data.release()
sys.stdin = sys.__stdin__
def main(args: List[str] = sys.argv) -> None:
read_signal_fd, write_signal_fd = safe_pipe()
notify_child_death_fd = int(sys.argv[-1])
os.set_blocking(notify_child_death_fd, False)
signal.set_wakeup_fd(write_signal_fd)
signal.signal(signal.SIGCHLD, lambda *a: None)
signal.siginterrupt(signal.SIGCHLD, False)
prewarm()
stdin_fd = sys.__stdin__.fileno()
os.set_blocking(stdin_fd, False)
stdout_fd = sys.__stdout__.fileno()
os.set_blocking(stdout_fd, False)
poll = select.poll()
poll.register(stdin_fd, select.POLLIN | basic_events)
poll.register(read_signal_fd, select.POLLIN | basic_events)
input_buf = output_buf = child_death_buf = b''
child_ready_fds: Dict[int, int] = {}
child_id_map: Dict[int, int] = {}
self_pid = os.getpid()
def check_event(event: int, err_msg: str) -> None:
if event & hangup_events:
raise SystemExit(0)
if event & error_events:
raise SystemExit(err_msg)
def handle_input(event: int) -> None:
nonlocal input_buf, output_buf
check_event(event, 'Polling of STDIN failed')
if not (event & select.POLLIN):
return
d = os.read(stdin_fd, io.DEFAULT_BUFFER_SIZE)
if not d:
raise SystemExit(0)
input_buf += d
while (idx := input_buf.find(b'\n')) > -1:
line = input_buf[:idx].decode()
input_buf = input_buf[idx+1:]
cmd, _, payload = line.partition(':')
if cmd == 'reload_kitty_config':
reload_kitty_config()
elif cmd == 'ready':
child_id = int(payload)
cfd = child_ready_fds.pop(child_id)
if cfd is not None:
os.write(cfd, b'1')
os.close(cfd)
elif cmd == 'fork':
read_fd, write_fd = safe_pipe(False)
remove_cloexec(read_fd)
try:
child_pid = fork(payload, read_fd)
except Exception as e:
es = str(e).replace('\n', ' ')
output_buf += f'ERR:{es}\n'.encode()
else:
if os.getpid() == self_pid:
child_id = len(child_id_map) + 1
child_id_map[child_id] = child_pid
child_ready_fds[child_id] = write_fd
output_buf += f'CHILD:{child_id}:{child_pid}\n'.encode()
finally:
if os.getpid() == self_pid:
os.close(read_fd)
elif cmd == 'echo':
output_buf += f'{payload}\n'.encode()
def handle_output(event: int) -> None:
nonlocal output_buf
check_event(event, 'Polling of STDOUT failed')
if not (event & select.POLLOUT):
return
if output_buf:
n = os.write(stdout_fd, output_buf)
if not n:
raise SystemExit(0)
output_buf = output_buf[n:]
if not output_buf:
poll.unregister(stdout_fd)
def handle_notify_child_death(event: int) -> None:
nonlocal child_death_buf
check_event(event, 'Polling of notify child death fd failed')
if not (event & select.POLLOUT):
return
if child_death_buf:
n = os.write(notify_child_death_fd, child_death_buf)
if not n:
raise SystemExit(0)
child_death_buf = child_death_buf[n:]
if not child_death_buf:
poll.unregister(notify_child_death_fd)
def handle_signal(event: int) -> None:
nonlocal child_death_buf
check_event(event, 'Polling of signal fd failed')
if not (event & select.POLLIN):
return
d = os.read(read_signal_fd, io.DEFAULT_BUFFER_SIZE)
if not d:
raise SystemExit(0)
signals = set(bytearray(d))
if signal.SIGCHLD in signals:
while True:
try:
pid, exit_status = os.waitpid(-1, os.WNOHANG)
except ChildProcessError:
break
# a zero return means there is at least one child process
# existing and no exit status is available
if pid == 0:
break
matched_child_id = -1
for child_id, child_pid in child_id_map.items():
if child_pid == pid:
matched_child_id = child_id
break
if matched_child_id > -1:
del child_id_map[matched_child_id]
child_ready_fds.pop(matched_child_id, None)
child_death_buf += f'{pid}\n'.encode()
try:
while True:
if output_buf:
poll.register(stdout_fd, select.POLLOUT | basic_events)
if child_death_buf:
poll.register(notify_child_death_fd, select.POLLOUT | basic_events)
for (q, event) in poll.poll():
if q == stdin_fd:
handle_input(event)
elif q == stdout_fd:
handle_output(event)
elif q == read_signal_fd:
handle_signal(event)
elif q == notify_child_death_fd:
handle_notify_child_death(event)
except (KeyboardInterrupt, EOFError, BrokenPipeError):
if os.getpid() == self_pid:
raise SystemExit(1)
raise
except Exception:
if os.getpid() == self_pid:
import traceback
traceback.print_exc()
raise
finally:
if os.getpid() == self_pid:
for fmd in child_ready_fds.values():
with suppress(OSError):
os.close(fmd)
if __name__ == '__main__':
main()

View File

@@ -8,6 +8,7 @@ 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
from .utils import ssh_options
debug
@@ -71,47 +72,6 @@ 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]:
try:
p = subprocess.run(['ssh'], stderr=subprocess.PIPE, encoding='utf-8')
raw = p.stderr or ''
except FileNotFoundError:
return {
'4': '', '6': '', 'A': '', 'a': '', 'C': '', 'f': '', 'G': '', 'g': '', 'K': '', 'k': '',
'M': '', 'N': '', 'n': '', 'q': '', 's': '', 'T': '', 't': '', 'V': '', 'v': '', 'X': '',
'x': '', 'Y': '', 'y': '', 'B': 'bind_interface', 'b': 'bind_address', 'c': 'cipher_spec',
'D': '[bind_address:]port', 'E': 'log_file', 'e': 'escape_char', 'F': 'configfile', 'I': 'pkcs11',
'i': 'identity_file', 'J': '[user@]host[:port]', 'L': 'address', 'l': 'login_name', 'm': 'mac_spec',
'O': 'ctl_cmd', 'o': 'option', 'p': 'port', 'Q': 'query_option', 'R': 'address',
'S': 'ctl_path', 'W': 'host:port', 'w': 'local_tun[:remote_tun]'
}
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]:

View File

@@ -32,21 +32,20 @@ from kitty.constants import (
cache_dir, runtime_dir, shell_integration_dir, ssh_control_master_template,
str_version, terminfo_dir
)
from kitty.options.types import Options
from kitty.shell_integration import as_str_literal
from kitty.shm import SharedMemory
from kitty.types import run_once
from kitty.utils import (
SSHConnectionData, expandvars, resolve_abs_or_config_path,
set_echo as turn_off_echo, suppress_error_logging
set_echo as turn_off_echo
)
from .completion import complete, ssh_options
from ..tui.utils import kitty_opts
from .config import init_config
from .copy import CopyInstruction
from .options.types import Options as SSHOptions
from .options.utils import DELETE_ENV_VAR
from .utils import create_shared_memory
from .utils import create_shared_memory, ssh_options
@run_once
@@ -111,18 +110,14 @@ def serialize_env(literal_env: Dict[str, str], env: Dict[str, str], base_env: Di
return '\n'.join(lines).encode('utf-8')
@run_once
def kitty_opts() -> Options:
from kitty.cli import create_default_opts
with suppress_error_logging():
return create_default_opts()
def make_tarfile(ssh_opts: SSHOptions, base_env: Dict[str, str], compression: str = 'gz', literal_env: Dict[str, str] = {}) -> bytes:
def normalize_tarinfo(tarinfo: tarfile.TarInfo) -> tarfile.TarInfo:
tarinfo.uname = tarinfo.gname = ''
tarinfo.uid = tarinfo.gid = 0
# some distro's like nix mess with installed file permissions so ensure
# files are at least readable and writable by owning user
tarinfo.mode |= stat.S_IWUSR | stat.S_IRUSR
return tarinfo
def add_data_as_file(tf: tarfile.TarFile, arcname: str, data: Union[str, bytes]) -> tarfile.TarInfo:
@@ -152,6 +147,7 @@ def make_tarfile(ssh_opts: SSHOptions, base_env: Dict[str, str], compression: st
if ssh_opts.shell_integration == 'inherited':
ksi = get_effective_ksi_env_var(kitty_opts())
else:
from kitty.options.types import Options
from kitty.options.utils import shell_integration
ksi = get_effective_ksi_env_var(Options({'shell_integration': shell_integration(ssh_opts.shell_integration)}))
@@ -732,6 +728,7 @@ def main(args: List[str]) -> NoReturn:
if __name__ == '__main__':
main(sys.argv)
elif __name__ == '__completer__':
from .completion import complete
setattr(sys, 'kitten_completer', complete)
elif __name__ == '__conf__':
from .options.definition import definition

View File

@@ -3,8 +3,52 @@
import os
import subprocess
from typing import Any, Dict, List
from kitty.types import run_once
@run_once
def ssh_options() -> Dict[str, str]:
try:
p = subprocess.run(['ssh'], stderr=subprocess.PIPE, encoding='utf-8')
raw = p.stderr or ''
except FileNotFoundError:
return {
'4': '', '6': '', 'A': '', 'a': '', 'C': '', 'f': '', 'G': '', 'g': '', 'K': '', 'k': '',
'M': '', 'N': '', 'n': '', 'q': '', 's': '', 'T': '', 't': '', 'V': '', 'v': '', 'X': '',
'x': '', 'Y': '', 'y': '', 'B': 'bind_interface', 'b': 'bind_address', 'c': 'cipher_spec',
'D': '[bind_address:]port', 'E': 'log_file', 'e': 'escape_char', 'F': 'configfile', 'I': 'pkcs11',
'i': 'identity_file', 'J': '[user@]host[:port]', 'L': 'address', 'l': 'login_name', 'm': 'mac_spec',
'O': 'ctl_cmd', 'o': 'option', 'p': 'port', 'Q': 'query_option', 'R': 'address',
'S': 'ctl_path', 'W': 'host:port', 'w': 'local_tun[:remote_tun]'
}
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
def is_kitten_cmdline(q: List[str]) -> bool:
if len(q) < 4:

View File

@@ -494,6 +494,7 @@ class Theme:
is_dark: bool = False
blurb: str = ''
num_settings: int = 0
is_user_defined: bool = False
def apply_dict(self, d: Dict[str, Any]) -> None:
self.name = str(d['name'])
@@ -501,7 +502,7 @@ class Theme:
a = d.get(x)
if isinstance(a, str):
setattr(self, x, a)
for x in ('is_dark', 'num_settings'):
for x in ('is_dark', 'num_settings', 'is_user_defined'):
a = d.get(x)
if isinstance(a, int):
setattr(self, x, a)
@@ -594,6 +595,7 @@ class Themes:
d = parse_theme(name, raw)
except (Exception, SystemExit):
continue
d['is_user_defined'] = True
t = Theme(raw.__str__)
t.apply_dict(d)
if t.name:

View File

@@ -69,6 +69,10 @@ def create_recent_filter(names: Iterable[str]) -> Callable[[Theme], bool]:
return recent_filter
def user_filter(q: Theme) -> bool:
return q.is_user_defined
def mark_shortcut(text: str, acc: str) -> str:
acc_idx = text.lower().index(acc.lower())
return text[:acc_idx] + styled(text[acc_idx], underline='straight', bold=True, fg_intense=True) + text[acc_idx+1:]
@@ -153,12 +157,13 @@ class ThemesHandler(Handler):
self.report_traceback_on_exit: Optional[str] = None
self.filter_map: Dict[str, Callable[[Theme], bool]] = {
'dark': dark_filter, 'light': light_filter, 'all': all_filter,
'recent': create_recent_filter(self.cached_values.get('recent', ()))
'recent': create_recent_filter(self.cached_values.get('recent', ())),
'user': user_filter
}
self.themes_list = ThemesList()
self.colors_set_once = False
self.line_edit = LineEdit()
self.tabs = tuple('all dark light recent'.split())
self.tabs = tuple('all dark light recent user'.split())
self.quit_on_next_key_release = -1
def update_recent(self) -> None:
@@ -389,7 +394,7 @@ class ThemesHandler(Handler):
if key_event.matches('esc') or key_event.matches_text('q'):
self.quit_on_next_key_release = 0
return
for cat in 'all dark light recent'.split():
for cat in self.tabs:
if key_event.matches_text(cat[0]) or key_event.matches(f'alt+{cat[0]}'):
if cat != self.current_category:
self.current_category = cat

View File

@@ -3,10 +3,15 @@
import sys
from contextlib import suppress
from typing import Tuple
from typing import TYPE_CHECKING, Tuple
from kitty.types import run_once
from .operations import raw_mode, set_cursor_visible
if TYPE_CHECKING:
from kitty.options.types import Options
def get_key_press(allowed: str, default: str) -> str:
response = default
@@ -50,6 +55,14 @@ def human_size(
return format_number(size / 1024**exponent, max_num_of_decimals) + sep + unit_list[exponent]
@run_once
def kitty_opts() -> 'Options':
from kitty.cli import create_default_opts
from kitty.utils import suppress_error_logging
with suppress_error_logging():
return create_default_opts()
def report_error(msg: str = '', return_code: int = 1, print_exc: bool = False) -> None:
' Report an error also sending the overlay ready message to ensure kitten is visible '
from .operations import overlay_ready

View File

@@ -1 +0,0 @@
# empty

View File

@@ -651,8 +651,9 @@ class Boss:
) -> None:
def callback_(res: Dict[str, Any], x: int, boss: Boss) -> None:
callback(res.get('response') == 'y', *args)
self._run_kitten('ask', ['--type=yesno', '--message', msg, '--default', 'y' if confirm_on_accept else 'n'],
window=window, custom_callback=callback_, default_data={'response': 'y' if confirm_on_cancel else 'n'})
self.run_kitten_with_metadata(
'ask', ['--type=yesno', '--message', msg, '--default', 'y' if confirm_on_accept else 'n'],
window=window, custom_callback=callback_, default_data={'response': 'y' if confirm_on_cancel else 'n'})
def choose(
self, msg: str, # can contain newlines and ANSI formatting
@@ -668,7 +669,7 @@ class Boss:
cmd += ['-d', default]
for c in choices:
cmd += ['-c', c]
self._run_kitten('ask', cmd, window=window, custom_callback=callback_, default_data={'response': ''})
self.run_kitten_with_metadata('ask', cmd, window=window, custom_callback=callback_, default_data={'response': ''})
def get_line(
self, msg: str, # can contain newlines and ANSI formatting
@@ -680,7 +681,7 @@ class Boss:
def callback_(res: Dict[str, Any], x: int, boss: Boss) -> None:
callback(res.get('response') or '')
cmd = ['--type', 'password' if is_password else 'line', '--message', msg, '--prompt', prompt]
self._run_kitten('ask', cmd, window=window, custom_callback=callback_, default_data={'response': ''})
self.run_kitten_with_metadata('ask', cmd, window=window, custom_callback=callback_, default_data={'response': ''})
def confirm_tab_close(self, tab: Tab) -> None:
x = get_options().confirm_os_window_close
@@ -1073,7 +1074,7 @@ class Boss:
w = self.active_window
if w is None:
return
overlay_window = self._run_kitten('resize_window', args=[
overlay_window = self.run_kitten_with_metadata('resize_window', args=[
f'--horizontal-increment={get_options().window_resize_step_cells}',
f'--vertical-increment={get_options().window_resize_step_lines}'
])
@@ -1419,10 +1420,10 @@ class Boss:
@ac('misc', 'Run the specified kitten. See :doc:`/kittens/custom` for details')
def kitten(self, kitten: str, *kargs: str) -> None:
self._run_kitten(kitten, kargs)
self.run_kitten_with_metadata(kitten, kargs)
def run_kitten(self, kitten: str, *args: str) -> None:
self._run_kitten(kitten, args)
self.run_kitten_with_metadata(kitten, args)
def on_kitten_finish(
self, target_window_id: int, end_kitten: Callable[[Dict[str, Any], int, 'Boss'], None],
@@ -1437,7 +1438,7 @@ class Boss:
@ac('misc', 'Input an arbitrary unicode character. See :doc:`/kittens/unicode_input` for details.')
def input_unicode_character(self) -> None:
self._run_kitten('unicode_input')
self.run_kitten_with_metadata('unicode_input')
@ac('tab', 'Change the title of the active tab')
def set_tab_title(self) -> None:
@@ -1446,7 +1447,7 @@ class Boss:
args = [
'--name=tab-title', '--message', _('Enter the new title for this tab below.'),
'--default', tab.name or tab.title, 'do_set_tab_title', str(tab.id)]
self._run_kitten('ask', args)
self.run_kitten_with_metadata('ask', args)
def do_set_tab_title(self, title: str, tab_id: int) -> None:
tm = self.active_tab_manager
@@ -1463,7 +1464,7 @@ class Boss:
if ec != (None, None, None):
import traceback
tb = traceback.format_exc()
self._run_kitten('show_error', args=['--title', title], input_data=json.dumps({'msg': msg, 'tb': tb}))
self.run_kitten_with_metadata('show_error', args=['--title', title], input_data=json.dumps({'msg': msg, 'tb': tb}))
@ac('mk', 'Create a new marker')
def create_marker(self) -> None:
@@ -1483,7 +1484,7 @@ class Boss:
except Exception as err:
self.show_error(_('Invalid marker specification'), str(err))
self._run_kitten('ask', [
self.run_kitten_with_metadata('ask', [
'--name=create-marker', '--message',
_('Create marker, for example:\ntext 1 ERROR\nSee {}\n').format(website_url('marks'))
],
@@ -1549,7 +1550,7 @@ class Boss:
@ac('misc', 'Click a URL using the keyboard')
def open_url_with_hints(self) -> None:
self._run_kitten('hints')
self.run_kitten_with_metadata('hints')
def drain_actions(self, actions: List[KeyAction], window_for_dispatch: Optional[Window] = None, dispatch_type: str = 'KeyPress') -> None:
@@ -2139,7 +2140,7 @@ class Boss:
def done2(target_window_id: int, self: Boss) -> None:
callback(ans)
q = self._run_kitten(
q = self.run_kitten_with_metadata(
'hints', args=(
'--ascending', '--customize-processing=::import::kitty.choose_entry',
'--window-title', title,
@@ -2285,8 +2286,8 @@ class Boss:
if failures:
from kittens.tui.operations import styled
spec = '\n '.join(styled(u, fg='red') for u in failures)
bdata = f"Unknown URL type, cannot open:\n\n {spec}".encode('utf-8')
spec = '\n '.join(styled(u, fg='yellow') for u in failures)
bdata = json.dumps({'msg': f"Unknown URL type, cannot open:\n {spec}"}).encode('utf-8')
special_window = SpecialWindow([kitty_exe(), '+kitten', 'show_error', '--title', 'Open URL Error'], bdata, 'Open URL Error')
if needs_window_replaced and tab is not None:
tab.new_special_window(special_window)

View File

@@ -23,6 +23,10 @@
#include <signal.h>
extern PyTypeObject Screen_Type;
#if defined(__APPLE__) || defined(__OpenBSD__)
#define NO_SIGQUEUE 1
#endif
#ifdef DEBUG_EVENT_LOOP
#define EVDBG(...) log_event(__VA_ARGS__)
#else
@@ -133,8 +137,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
return NULL;
}
self = (ChildMonitor *)type->tp_alloc(type, 0);
if (!init_loop_data(&self->io_loop_data)) return PyErr_SetFromErrno(PyExc_OSError);
if (!install_signal_handlers(&self->io_loop_data)) return PyErr_SetFromErrno(PyExc_OSError);
if (!init_loop_data(&self->io_loop_data, 5, SIGINT, SIGTERM, SIGCHLD, SIGUSR1, SIGUSR2)) return PyErr_SetFromErrno(PyExc_OSError);
self->talk_fd = talk_fd;
self->listen_fd = listen_fd;
if (self == NULL) return PyErr_NoMemory();
@@ -424,7 +427,7 @@ parse_input(ChildMonitor *self) {
}
if (resp) {
if (PyBytes_Check(resp)) send_response_to_peer(msg->peer_id, PyBytes_AS_STRING(resp), PyBytes_GET_SIZE(resp));
else if (resp == Py_None) send_response_to_peer(msg->peer_id, NULL, 0);
else if (resp == Py_None || resp == Py_True) send_response_to_peer(msg->peer_id, NULL, 0);
Py_CLEAR(resp);
} else send_response_to_peer(msg->peer_id, NULL, 0);
}
@@ -801,11 +804,25 @@ free_twd(ThreadWriteData *x) {
free(x);
}
static PyObject*
sig_queue(PyObject *self UNUSED, PyObject *args) {
int pid, signal, value;
if (!PyArg_ParseTuple(args, "iii", &pid, &signal, &value)) return NULL;
#ifdef NO_SIGQUEUE
if (kill(pid, signal) != 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; }
#else
union sigval v;
v.sival_int = value;
if (sigqueue(pid, signal, v) != 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; }
#endif
Py_RETURN_NONE;
}
static PyObject*
monitor_pid(PyObject *self UNUSED, PyObject *args) {
long pid;
int pid;
bool ok = true;
if (!PyArg_ParseTuple(args, "l", &pid)) return NULL;
if (!PyArg_ParseTuple(args, "i", &pid)) return NULL;
children_mutex(lock);
if (monitored_pids_count >= arraysz(monitored_pids)) {
PyErr_SetString(PyExc_RuntimeError, "Too many monitored pids");
@@ -1224,9 +1241,9 @@ read_bytes(int fd, Screen *screen) {
typedef struct { bool kill_signal, child_died, reload_config; } SignalSet;
static void
handle_signal(int signum, void *data) {
handle_signal(const siginfo_t *siginfo, void *data) {
SignalSet *ss = data;
switch(signum) {
switch(siginfo->si_signo) {
case SIGINT:
case SIGTERM:
ss->kill_signal = true;
@@ -1237,6 +1254,9 @@ handle_signal(int signum, void *data) {
case SIGUSR1:
ss->reload_config = true;
break;
case SIGUSR2:
log_error("Received SIGUSR2: %d\n", siginfo->si_value.sival_int);
break;
default:
break;
}
@@ -1416,7 +1436,7 @@ io_loop(void *data) {
typedef struct {
id_type id;
size_t num_of_unresponded_messages_sent_to_main_thread;
size_t num_of_unresponded_messages_sent_to_main_thread, fd_array_idx;
bool finished_reading;
int fd;
struct {
@@ -1570,8 +1590,7 @@ wakeup_talk_loop(bool in_signal_handler) {
static void
prune_peers(void) {
for (size_t i = 0; i < talk_data.num_peers; i++) {
size_t idx = talk_data.num_peers - 1 - i;
for (size_t idx = talk_data.num_peers; idx-- > 0;) {
Peer *p = talk_data.peers + idx;
if (p->read.finished && !p->num_of_unresponded_messages_sent_to_main_thread && !p->write.used) {
free_peer(p);
@@ -1585,7 +1604,7 @@ talk_loop(void *data) {
// The talk thread loop
ChildMonitor *self = (ChildMonitor*)data;
set_thread_name("KittyPeerMon");
if (!init_loop_data(&talk_data.loop_data)) { log_error("Failed to create wakeup fd for talk thread with error: %s", strerror(errno)); }
if (!init_loop_data(&talk_data.loop_data, 0)) { log_error("Failed to create wakeup fd for talk thread with error: %s", strerror(errno)); }
PollFD fds[PEER_LIMIT + 8] = {{0}};
size_t num_listen_fds = 0, num_peer_fds = 0;
#define add_listener(which) \
@@ -1604,13 +1623,14 @@ talk_loop(void *data) {
for (size_t i = 0; i < talk_data.num_peers; i++) {
Peer *p = talk_data.peers + i;
if (!p->read.finished || p->write.used) {
fds[num_listen_fds + num_peer_fds].fd = p->fd;
fds[num_listen_fds + num_peer_fds].revents = 0;
p->fd_array_idx = num_listen_fds + num_peer_fds++;
fds[p->fd_array_idx].fd = p->fd;
fds[p->fd_array_idx].revents = 0;
int flags = 0;
if (!p->read.finished) flags |= POLLIN;
if (p->write.used) flags |= POLLOUT;
fds[num_listen_fds + num_peer_fds++].events = flags;
}
fds[p->fd_array_idx].events = flags;
} else p->fd_array_idx = 0;
}
talk_mutex(unlock);
}
@@ -1625,13 +1645,16 @@ talk_loop(void *data) {
if (fds[num_listen_fds - 1].revents & POLLIN) {
drain_fd(fds[num_listen_fds - 1].fd); // wakeup
}
for (size_t i = num_listen_fds, k = 0; i < num_peer_fds + num_listen_fds; i++, k++) {
for (size_t k = 0; k < talk_data.num_peers; k++) {
Peer *p = talk_data.peers + k;
if (fds[i].revents & (POLLIN | POLLHUP)) read_from_peer(self, p);
if (fds[i].revents & POLLOUT) write_to_peer(p);
if (fds[i].revents & POLLNVAL) {
p->read.finished = true;
p->write.failed = true; p->write.used = 0;
if (p->fd_array_idx) {
if (fds[p->fd_array_idx].revents & (POLLIN | POLLHUP)) read_from_peer(self, p);
if (fds[p->fd_array_idx].revents & POLLOUT) write_to_peer(p);
if (fds[p->fd_array_idx].revents & POLLNVAL) {
p->read.finished = true;
p->write.failed = true; p->write.used = 0;
}
break;
}
}
} else if (ret < 0) { if (errno != EAGAIN && errno != EINTR) perror("poll() on talk fds failed"); }
@@ -1736,6 +1759,7 @@ static PyMethodDef module_methods[] = {
METHODB(monitor_pid, METH_VARARGS),
METHODB(send_data_to_peer, METH_VARARGS),
METHODB(cocoa_set_menubar_title, METH_VARARGS),
{"sigqueue", (PyCFunction)sig_queue, METH_VARARGS, ""},
{NULL} /* Sentinel */
};
@@ -1745,6 +1769,11 @@ init_child_monitor(PyObject *module) {
if (PyModule_AddObject(module, "ChildMonitor", (PyObject *)&ChildMonitor_Type) != 0) return false;
Py_INCREF(&ChildMonitor_Type);
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
#ifdef NO_SIGQUEUE
PyModule_AddIntConstant(module, "has_sigqueue", 0);
#else
PyModule_AddIntConstant(module, "has_sigqueue", 1);
#endif
return true;
}

View File

@@ -98,10 +98,17 @@ spawn(PyObject *self UNUSED, PyObject *args) {
#if PY_VERSION_HEX >= 0x03070000
PyOS_AfterFork_Child();
#endif
// See _Py_RestoreSignals in signalmodule.c for a list of signals python nukes
sigset_t signals = {0};
struct sigaction act = {.sa_handler=SIG_DFL};
#define SA(which) { if (sigaction(which, &act, NULL) != 0) exit_on_err("sigaction() in child process failed"); }
SA(SIGINT); SA(SIGTERM); SA(SIGCHLD);
SA(SIGINT); SA(SIGTERM); SA(SIGCHLD); SA(SIGPIPE); SA(SIGUSR1); SA(SIGUSR2);
#ifdef SIGXFSZ
SA(SIGXFSZ);
#endif
#ifdef SIGXFZ
SA(SIGXFZ);
#endif
#undef SA
if (sigprocmask(SIG_SETMASK, &signals, NULL) != 0) exit_on_err("sigprocmask() in child process failed");
// Use only signal-safe functions (man 7 signal-safety)
@@ -139,9 +146,6 @@ spawn(PyObject *self UNUSED, PyObject *args) {
for (int c = 3; c < 201; c++) safe_close(c, __FILE__, __LINE__);
environ = env;
// for some reason SIGPIPE is set to SIG_IGN, so reset it, needed by bash,
// which does not reset signal handlers on its own
signal(SIGPIPE, SIG_DFL);
execvp(exe, argv);
// Report the failure and exec a shell instead, so that we are not left
// with a forked but not exec'ed process
@@ -176,8 +180,27 @@ spawn(PyObject *self UNUSED, PyObject *args) {
return PyLong_FromLong(pid);
}
static PyObject*
establish_controlling_tty(PyObject *self UNUSED, PyObject *args) {
const char *ttyname;
int stdin_fd, stdout_fd, stderr_fd;
if (!PyArg_ParseTuple(args, "siii", &ttyname, &stdin_fd, &stdout_fd, &stderr_fd)) return NULL;
int tfd = safe_open(ttyname, O_RDWR, 0);
if (tfd == -1) return PyErr_SetFromErrnoWithFilename(PyExc_OSError, ttyname);
#ifdef TIOCSCTTY
// On BSD open() does not establish the controlling terminal
if (ioctl(tfd, TIOCSCTTY, 0) == -1) return PyErr_SetFromErrno(PyExc_OSError);
#endif
if (dup2(tfd, stdin_fd) == -1) return PyErr_SetFromErrno(PyExc_OSError);
if (dup2(tfd, stdout_fd) == -1) return PyErr_SetFromErrno(PyExc_OSError);
if (dup2(tfd, stderr_fd) == -1) return PyErr_SetFromErrno(PyExc_OSError);
safe_close(tfd, __FILE__, __LINE__);
Py_RETURN_NONE;
}
static PyMethodDef module_methods[] = {
METHODB(spawn, METH_VARARGS),
METHODB(establish_controlling_tty, METH_VARARGS),
{NULL, NULL, 0, NULL} /* Sentinel */
};

View File

@@ -1,7 +1,6 @@
#!/usr/bin/env python3
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import fcntl
import os
import sys
from collections import defaultdict
@@ -43,11 +42,11 @@ if is_macos:
ans[pgid].append(pid)
return ans
def cmdline_of_process(pid: int) -> List[str]:
def cmdline_of_pid(pid: int) -> List[str]:
return cmdline_(pid)
else:
def cmdline_of_process(pid: int) -> List[str]:
def cmdline_of_pid(pid: int) -> List[str]:
with open(f'/proc/{pid}/cmdline', 'rb') as f:
return list(filter(None, f.read().decode('utf-8').split('\0')))
@@ -144,7 +143,7 @@ def environ_of_process(pid: int) -> Dict[str, str]:
def remove_cloexec(fd: int) -> None:
fcntl.fcntl(fd, fcntl.F_SETFD, fcntl.fcntl(fd, fcntl.F_GETFD) & ~fcntl.FD_CLOEXEC)
os.set_inheritable(fd, True)
def remove_blocking(fd: int) -> None:
@@ -307,6 +306,15 @@ class Child:
os.close(self.terminal_ready_fd)
self.terminal_ready_fd = -1
def cmdline_of_pid(self, pid: int) -> List[str]:
try:
ans = cmdline_of_pid(pid)
except Exception:
ans = []
if not ans and pid == self.pid:
ans = list(self.argv)
return ans
@property
def foreground_processes(self) -> List[ProcessDesc]:
if self.child_fd is None:
@@ -318,7 +326,7 @@ class Child:
def process_desc(pid: int) -> ProcessDesc:
ans: ProcessDesc = {'pid': pid, 'cmdline': None, 'cwd': None}
with suppress(Exception):
ans['cmdline'] = cmdline_of_process(pid)
ans['cmdline'] = self.cmdline_of_pid(pid)
with suppress(Exception):
ans['cwd'] = cwd_of_process(pid) or None
return ans
@@ -331,7 +339,7 @@ class Child:
def cmdline(self) -> List[str]:
try:
assert self.pid is not None
return cmdline_of_process(self.pid) or list(self.argv)
return self.cmdline_of_pid(self.pid) or list(self.argv)
except Exception:
return list(self.argv)
@@ -339,7 +347,7 @@ class Child:
def foreground_cmdline(self) -> List[str]:
try:
assert self.pid_for_cwd is not None
return cmdline_of_process(self.pid_for_cwd) or self.cmdline
return self.cmdline_of_pid(self.pid_for_cwd) or self.cmdline
except Exception:
return self.cmdline

View File

@@ -613,7 +613,7 @@ cocoa_create_global_menu(void) {
[shellMenu release];
NSMenuItem* windowMenuItem =
[bar addItemWithTitle:@""
[bar addItemWithTitle:@"Window"
action:NULL
keyEquivalent:@""];
NSMenu* windowMenu = [[NSMenu alloc] initWithTitle:@"Window"];

View File

@@ -505,6 +505,16 @@ def complete_remote_command(ans: Completions, cmd_name: str, words: Sequence[str
complete_alias_map(ans, words, new_word, alias_map, complete_args=args_completer)
def complete_launch_wrapper(ans: Completions, words: Sequence[str], new_word: bool, allow_files: bool = True) -> None:
from kitty.launch import clone_safe_opts
aliases, alias_map = options_for_cmd('launch')
alias_map = {k: v for k, v in alias_map.items() if v['dest'] in clone_safe_opts()}
args_completer: Optional[CompleteArgsFunc] = None
if allow_files:
args_completer = remote_files_completer('Files', ('*',))
complete_alias_map(ans, words, new_word, alias_map, complete_args=args_completer)
def path_completion(prefix: str = '') -> Tuple[List[str], List[str]]:
prefix = prefix.replace(r'\ ', ' ')
dirs, files = [], []
@@ -664,7 +674,12 @@ def complete_kitten(ans: Completions, kitten: str, words: Sequence[str], new_wor
def find_completions(words: Sequence[str], new_word: bool, entry_points: Iterable[str], namespaced_entry_points: Iterable[str]) -> Completions:
ans = Completions()
if not words or words[0] != 'kitty':
if not words:
return ans
if words[0] in ('edit-in-kitty', 'clone-in-kitty'):
complete_launch_wrapper(ans, words[1:], new_word, allow_files=words[0] != 'clone-in-kitty')
return ans
if words[0] != 'kitty':
return ans
words = words[1:]
if not words or (len(words) == 1 and not new_word):

View File

@@ -22,7 +22,7 @@ class Version(NamedTuple):
appname: str = 'kitty'
kitty_face = '🐱'
version: Version = Version(0, 25, 1)
version: Version = Version(0, 25, 2)
str_version: str = '.'.join(map(str, version))
_plat = sys.platform.lower()
is_macos: bool = 'darwin' in _plat

View File

@@ -329,7 +329,7 @@ ensure_state(DiskCache *self) {
int ret;
if (self->fully_initialized) return true;
if (!self->loop_data_inited) {
if (!init_loop_data(&self->loop_data)) { PyErr_SetFromErrno(PyExc_OSError); return false; }
if (!init_loop_data(&self->loop_data, 0)) { PyErr_SetFromErrno(PyExc_OSError); return false; }
self->loop_data_inited = true;
}
if (!self->currently_writing.hash_key) {

185
kitty/entry_points.py Normal file
View File

@@ -0,0 +1,185 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
import os
import sys
from typing import List
def icat(args: List[str]) -> None:
from kittens.runner import run_kitten as rk
sys.argv = args
rk('icat')
def list_fonts(args: List[str]) -> None:
from kitty.fonts.list import main as list_main
list_main(args)
def remote_control(args: List[str]) -> None:
from kitty.remote_control import main as rc_main
rc_main(args)
def runpy(args: List[str]) -> None:
if len(args) < 2:
raise SystemExit('Usage: kitty +runpy "some python code"')
sys.argv = ['kitty'] + args[2:]
exec(args[1])
def hold(args: List[str]) -> None:
import subprocess
ret = 1
try:
ret = subprocess.Popen(args[1:]).wait()
except KeyboardInterrupt:
pass
except FileNotFoundError:
print(f'Could not find {args[1]!r} to execute', file=sys.stderr)
except Exception as e:
print(e, file=sys.stderr)
from kitty.utils import hold_till_enter
hold_till_enter()
raise SystemExit(ret)
def complete(args: List[str]) -> None:
from kitty.complete import main as complete_main
complete_main(args[1:], entry_points, namespaced_entry_points)
def open_urls(args: List[str]) -> None:
setattr(sys, 'cmdline_args_for_open', True)
sys.argv = ['kitty'] + args[1:]
from kitty.main import main as kitty_main
kitty_main()
def launch(args: List[str]) -> None:
import runpy
sys.argv = args[1:]
try:
exe = args[1]
except IndexError:
raise SystemExit(
'usage: kitty +launch script.py [arguments to be passed to script.py ...]\n\n'
'script.py will be run with full access to kitty code. If script.py is '
'prefixed with a : it will be searched for in PATH. If script.py is a directory '
'the __main__.py file inside it is run just as with the normal Python interpreter.'
)
if exe.startswith(':'):
import shutil
q = shutil.which(exe[1:])
if not q:
raise SystemExit(f'{exe[1:]} not found in PATH')
exe = q
if not os.path.exists(exe):
raise SystemExit(f'{exe} does not exist')
runpy.run_path(exe, run_name='__main__')
def shebang(args: List[str]) -> None:
script_path = args[1]
cmd = args[2:]
if cmd == ['__ext__']:
cmd = [os.path.splitext(script_path)[1][1:].lower()]
try:
f = open(script_path)
except FileNotFoundError:
raise SystemExit(f'The file {script_path} does not exist')
with f:
if f.read(2) == '#!':
line = f.readline().strip()
_plat = sys.platform.lower()
is_macos: bool = 'darwin' in _plat
if is_macos:
cmd = line.split(' ')
else:
cmd = line.split(' ', maxsplit=1)
os.execvp(cmd[0], cmd + [script_path])
def run_kitten(args: List[str]) -> None:
try:
kitten = args[1]
except IndexError:
from kittens.runner import list_kittens
list_kittens()
raise SystemExit(1)
sys.argv = args[1:]
from kittens.runner import run_kitten as rk
rk(kitten)
def edit_config_file(args: List[str]) -> None:
from kitty.cli import create_default_opts
from kitty.fast_data_types import set_options
from kitty.utils import edit_config_file as f
set_options(create_default_opts())
f()
def namespaced(args: List[str]) -> None:
try:
func = namespaced_entry_points[args[1]]
except IndexError:
raise SystemExit('The kitty command line is incomplete')
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 = {
# These two are here for backwards compat
'icat': icat,
'list-fonts': list_fonts,
'@': remote_control,
'+': namespaced,
}
namespaced_entry_points = {k: v for k, v in entry_points.items() if k[0] not in '+@'}
namespaced_entry_points['hold'] = hold
namespaced_entry_points['complete'] = complete
namespaced_entry_points['runpy'] = runpy
namespaced_entry_points['launch'] = launch
namespaced_entry_points['open'] = open_urls
namespaced_entry_points['kitten'] = run_kitten
namespaced_entry_points['edit-config'] = edit_config_file
namespaced_entry_points['shebang'] = shebang
def setup_openssl_environment() -> None:
# Use our bundled CA certificates instead of the system ones, since
# many systems come with no certificates in a useable form or have various
# locations for the certificates.
d = os.path.dirname
ext_dir: str = getattr(sys, 'kitty_extensions_dir')
if 'darwin' in sys.platform.lower():
cert_file = os.path.join(d(d(d(ext_dir))), 'cacert.pem')
else:
cert_file = os.path.join(d(ext_dir), 'cacert.pem')
os.environ['SSL_CERT_FILE'] = cert_file
setattr(sys, 'kitty_ssl_env_var', 'SSL_CERT_FILE')
def main() -> None:
if getattr(sys, 'frozen', False) and getattr(sys, 'kitty_extensions_dir', ''):
setup_openssl_environment()
first_arg = '' if len(sys.argv) < 2 else sys.argv[1]
func = entry_points.get(first_arg)
if func is None:
if first_arg.startswith('@'):
remote_control(['@', first_arg[1:]] + sys.argv[2:])
elif first_arg.startswith('+'):
namespaced(['+', first_arg[1:]] + sys.argv[2:])
else:
from kitty.main import main as kitty_main
kitty_main()
else:
func(sys.argv[1:])

View File

@@ -1206,7 +1206,7 @@ class ChildMonitor:
def resize_pty(self, window_id: int, rows: int, cols: int, x_pixels: int, y_pixels: int) -> None:
pass
def needs_write(self, child_id: int, data: Union[bytes, str]) -> bool:
def needs_write(self, child_id: int, data: bytes) -> bool:
pass
def set_iutf8_winid(self, win_id: int, on: bool) -> bool:
@@ -1375,3 +1375,11 @@ def shm_open(name: str, flags: int, mode: int = 0o600) -> int:
def shm_unlink(name: str) -> None:
pass
def sigqueue(pid: int, signal: int, value: int) -> None:
pass
def establish_controlling_tty(ttyname: str, stdin: int, stdout: int, stderr: int) -> None:
pass

View File

@@ -928,8 +928,14 @@ group_iosevka(Font *font, hb_font_t *hbf) {
const LigatureType current = ligature_types[G(glyph_idx)];
const LigatureType after = is_last_glyph ? LIGATURE_UNKNOWN : ligature_types[G(glyph_idx + 1)];
bool end_current_group = false;
if (current_group->num_glyphs && is_iosevka_lig_ender(before, current, after)) {
end_current_group = true;
if (current_group->num_glyphs) {
if (is_iosevka_lig_ender(before, current, after)) end_current_group = true;
else {
if (!current_group->num_cells && !current_group->has_special_glyph) {
if (is_iosevka_lig_starter(before, current, after)) current_group->has_special_glyph = true;
else end_current_group = true;
}
}
}
if (!current_group->num_glyphs++) {
if (is_iosevka_lig_starter(before, current, after)) current_group->has_special_glyph = true;
@@ -954,7 +960,7 @@ group_iosevka(Font *font, hb_font_t *hbf) {
}
current_group->num_cells += num_cells_consumed;
}
if (end_current_group) G(group_idx)++;
if (end_current_group && current_group->num_cells) G(group_idx)++;
G(glyph_idx)++;
}
}
@@ -1017,9 +1023,9 @@ group_normal(Font *font, hb_font_t *hbf) {
#if 0
char ch[8] = {0};
encode_utf8(G(current_cell_data).current_codepoint, ch);
printf("\x1b[32m→ %s\x1b[m glyph_idx: %zu glyph_id: %u (%s) group_idx: %zu cluster: %u -> %u is_special: %d\n"
printf("\x1b[32m→ %s\x1b[m glyph_idx: %zu glyph_id: %u group_idx: %zu cluster: %u -> %u is_special: %d\n"
" num_codepoints_used_by_glyph: %u current_group: (%u cells, %u glyphs) add_to_current_group: %d\n",
ch, G(glyph_idx), glyph_id, glyph_name, G(group_idx), cluster, next_cluster, is_special,
ch, G(glyph_idx), glyph_id, G(group_idx), cluster, next_cluster, is_special,
num_codepoints_used_by_glyph, current_group->num_cells, current_group->num_glyphs, add_to_current_group);
#endif

View File

@@ -2,6 +2,7 @@
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
import os
import stat
from contextlib import suppress
from typing import Optional
@@ -47,7 +48,15 @@ def initialize_mime_database() -> None:
def guess_type(path: str, allow_filesystem_access: bool = False) -> Optional[str]:
if allow_filesystem_access and is_folder(path):
is_dir = is_exe = False
if allow_filesystem_access:
with suppress(OSError):
st = os.stat(path)
is_dir = bool(stat.S_ISDIR(st.st_mode))
is_exe = bool(not is_dir and st.st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) and os.access(path, os.X_OK))
if is_dir:
return 'inode/directory'
from mimetypes import guess_type as stdlib_guess_type
initialize_mime_database()
@@ -61,6 +70,9 @@ def guess_type(path: str, allow_filesystem_access: bool = False) -> Optional[str
mt = f'text/{mt.split("/", 1)[-1]}'
if not mt and is_rc_file(path):
mt = 'text/plain'
if not mt and is_folder(path):
mt = 'inode/directory'
if not mt:
if is_dir:
mt = 'inode/directory' # type: ignore
elif is_exe:
mt = 'inode/executable'
return mt

View File

@@ -3,7 +3,12 @@
import os
from typing import Any, Dict, Iterable, List, NamedTuple, Optional, Sequence
import shutil
from contextlib import suppress
from typing import (
Any, Container, Dict, FrozenSet, Iterable, Iterator, List, NamedTuple,
Optional, Sequence, Tuple
)
from .boss import Boss
from .child import Child
@@ -11,13 +16,15 @@ from .cli import parse_args
from .cli_stub import LaunchCLIOptions
from .constants import kitty_exe, shell_path
from .fast_data_types import (
get_boss, get_options, get_os_window_title, patch_color_profiles,
set_clipboard_string
add_timer, get_boss, get_options, get_os_window_title,
patch_color_profiles, set_clipboard_string
)
from .options.utils import env as parse_env
from .tabs import Tab, TabManager
from .types import run_once
from .utils import log_error, resolve_custom_file, set_primary_selection, which
from .utils import (
get_editor, log_error, resolve_custom_file, set_primary_selection, which
)
from .window import CwdRequest, CwdRequestType, Watchers, Window
try:
@@ -491,17 +498,22 @@ def launch(
return None
def parse_opts_for_clone(args: List[str]) -> LaunchCLIOptions:
unsafe, unsafe_args = parse_launch_args(args)
default_opts, default_args = parse_launch_args()
# only copy safe options, those that dont lead to local code exec
for x in (
@run_once
def clone_safe_opts() -> FrozenSet[str]:
return frozenset((
'window_title', 'tab_title', 'type', 'keep_focus', 'cwd', 'env', 'hold',
'location', 'os_window_class', 'os_window_name', 'os_window_title',
'logo', 'logo_position', 'logo_alpha', 'color'
):
))
def parse_opts_for_clone(args: List[str]) -> Tuple[LaunchCLIOptions, List[str]]:
unsafe, unsafe_args = parse_launch_args(args)
default_opts, default_args = parse_launch_args()
# only copy safe options, those that dont lead to local code exec
for x in clone_safe_opts():
setattr(default_opts, x, getattr(unsafe, x))
return default_opts
return default_opts, unsafe_args
def parse_bash_env(text: str) -> Dict[str, str]:
@@ -549,6 +561,124 @@ def parse_null_env(text: str) -> Dict[str, str]:
return ans
def parse_message(msg: str, simple: Container[str]) -> Iterator[Tuple[str, str]]:
from base64 import standard_b64decode
for x in msg.split(','):
try:
k, v = x.split('=', 1)
except ValueError:
continue
if k not in simple:
v = standard_b64decode(v).decode('utf-8', 'replace')
yield k, v
class EditCmd:
def __init__(self, msg: str) -> None:
self.tdir = ''
self.args: List[str] = []
self.cwd = self.file_name = self.file_localpath = ''
self.file_data = b''
self.file_inode = -1, -1
self.file_size = -1
self.source_window_id = self.editor_window_id = -1
self.abort_signaled = ''
simple = 'file_inode', 'file_data', 'abort_signaled'
for k, v in parse_message(msg, simple):
if k == 'file_inode':
q = map(int, v.split(':'))
self.file_inode = next(q), next(q)
self.file_size = next(q)
elif k == 'a':
self.args.append(v)
elif k == 'file_data':
import base64
self.file_data = base64.standard_b64decode(v)
else:
setattr(self, k, v)
if self.abort_signaled:
return
self.opts, extra_args = parse_opts_for_clone(['--type=overlay'] + self.args)
self.file_spec = extra_args.pop()
self.line_number = 0
import re
pat = re.compile(r'\+(-?\d+)')
for x in extra_args:
m = pat.match(x)
if m is not None:
self.line_number = int(m.group(1))
self.file_name = os.path.basename(self.file_spec)
self.file_localpath = os.path.normpath(os.path.join(self.cwd, self.file_spec))
self.is_local_file = False
with suppress(OSError):
st = os.stat(self.file_localpath)
self.is_local_file = (st.st_dev, st.st_ino) == self.file_inode
if not self.is_local_file:
import tempfile
self.tdir = tempfile.mkdtemp()
self.file_localpath = os.path.join(self.tdir, self.file_name)
with open(self.file_localpath, 'wb') as f:
f.write(self.file_data)
self.file_obj = open(self.file_localpath, 'rb')
self.file_data = b''
self.last_mod_time = self.file_mod_time
if not self.opts.cwd:
self.opts.cwd = os.path.dirname(self.file_obj.name)
def __del__(self) -> None:
if self.tdir:
with suppress(OSError):
shutil.rmtree(self.tdir)
self.tdir = ''
def read_data(self) -> bytes:
self.file_obj.seek(0)
return self.file_obj.read()
@property
def file_mod_time(self) -> int:
return os.stat(self.file_obj.fileno()).st_mtime_ns
def schedule_check(self) -> None:
if not self.abort_signaled:
add_timer(self.check_status, 1.0, False)
def on_edit_window_close(self, window: Window) -> None:
self.check_status()
def check_status(self, timer_id: Optional[int] = None) -> None:
if self.abort_signaled:
return
boss = get_boss()
source_window = boss.window_id_map.get(self.source_window_id)
if source_window is not None and not self.is_local_file:
mtime = self.file_mod_time
if mtime != self.last_mod_time:
self.last_mod_time = mtime
data = self.read_data()
self.send_data(source_window, 'UPDATE', data)
editor_window = boss.window_id_map.get(self.editor_window_id)
if editor_window is None:
edits_in_flight.pop(self.source_window_id, None)
if source_window is not None:
self.send_data(source_window, 'DONE')
self.abort_signaled = self.abort_signaled or 'closed'
else:
self.schedule_check()
def send_data(self, window: Window, data_type: str, data: bytes = b'') -> None:
window.write_to_child(f'KITTY_DATA_START\n{data_type}\n')
if data:
import base64
mv = memoryview(base64.standard_b64encode(data))
while mv:
window.write_to_child(bytes(mv[:512]))
window.write_to_child('\n')
mv = mv[512:]
window.write_to_child('KITTY_DATA_END\n')
class CloneCmd:
def __init__(self, msg: str) -> None:
@@ -560,18 +690,17 @@ class CloneCmd:
self.pid = -1
self.history = ''
self.parse_message(msg)
self.opts = parse_opts_for_clone(self.args)
self.opts = parse_opts_for_clone(self.args)[0]
def parse_message(self, msg: str) -> None:
import base64
simple = 'pid', 'envfmt', 'shell'
for x in msg.split(','):
k, v = x.split('=', 1)
for k, v in parse_message(msg, simple):
if k in simple:
setattr(self, k, int(v) if k == 'pid' else v)
continue
v = base64.standard_b64decode(v).decode('utf-8', 'replace')
if k == 'a':
if k == 'pid':
self.pid = int(v)
else:
setattr(self, k, v)
elif k == 'a':
self.args.append(v)
elif k == 'env':
env = parse_bash_env(v) if self.envfmt == 'bash' else parse_null_env(v)
@@ -594,8 +723,30 @@ class CloneCmd:
self.history = v
edits_in_flight: Dict[int, EditCmd] = {}
def remote_edit(msg: str, window: Window) -> None:
c = EditCmd(msg)
if c.abort_signaled:
q = edits_in_flight.pop(window.id, None)
if q is not None:
q.abort_signaled = c.abort_signaled
return
cmdline = get_editor(path_to_edit=c.file_obj.name, line_number=c.line_number)
w = launch(get_boss(), c.opts, cmdline, active=window)
if w is not None:
c.source_window_id = window.id
c.editor_window_id = w.id
q = edits_in_flight.pop(window.id, None)
if q is not None:
q.abort_signaled = 'replaced'
edits_in_flight[window.id] = c
w.actions_on_close.append(c.on_edit_window_close)
c.schedule_check()
def clone_and_launch(msg: str, window: Window) -> None:
from .child import cmdline_of_process
from .shell_integration import serialize_env
c = CloneCmd(msg)
if c.cwd and not c.opts.cwd:
@@ -630,8 +781,9 @@ def clone_and_launch(msg: str, window: Window) -> None:
patch_cmdline('env', entry, cmdline)
c.opts.env = []
else:
try:
cmdline = cmdline_of_process(c.pid)
cmdline = window.child.cmdline_of_pid(c.pid)
except Exception:
cmdline = []
if not cmdline:

View File

@@ -73,6 +73,13 @@ left_shift_line(Line *line, index_type at, index_type num) {
}
}
static inline bool
line_is_empty(const Line *line) {
for (index_type i = 0; i < line->xnum; i++) {
if (line->cpu_cells[i].ch != BLANK_CHAR) return false;
}
return true;
}
typedef Line*(get_line_func)(void *, int);
void line_clear_text(Line *self, unsigned int at, unsigned int num, char_type ch);

View File

@@ -7,10 +7,40 @@
#include "loop-utils.h"
#include "safe-wrappers.h"
#include <signal.h>
#ifndef HAS_SIGNAL_FD
static int signal_write_fd = -1;
static void
handle_signal(int sig_num UNUSED, siginfo_t *si, void *ucontext UNUSED) {
int save_err = errno;
char *buf = (char*)si;
size_t sz = sizeof(siginfo_t);
while (signal_write_fd != -1 && sz) {
// as long as sz is less than PIPE_BUF write will either write all or return -1 with EAGAIN
// so we are guaranteed atomic writes
ssize_t ret = write(signal_write_fd, buf, sz);
if (ret <= 0) {
if (errno == EINTR) continue;
break;
}
sz -= ret;
buf += ret;
}
errno = save_err;
}
#endif
bool
init_loop_data(LoopData *ld) {
init_loop_data(LoopData *ld, size_t num_signals, ...) {
ld->num_handled_signals = num_signals;
va_list valist;
va_start(valist, num_signals);
for (size_t i = 0; i < ld->num_handled_signals; i++) {
ld->handled_signals[i] = va_arg(valist, int);
}
va_end(valist);
#ifdef HAS_EVENT_FD
ld->wakeup_read_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
if (ld->wakeup_read_fd < 0) return false;
@@ -19,33 +49,27 @@ init_loop_data(LoopData *ld) {
ld->wakeup_read_fd = ld->wakeup_fds[0];
#endif
ld->signal_read_fd = -1;
#ifndef HAS_SIGNAL_FD
#ifdef HAS_SIGNAL_FD
sigemptyset(&ld->signals);
if (ld->num_handled_signals) {
for (size_t i = 0; i < ld->num_handled_signals; i++) sigaddset(&ld->signals, ld->handled_signals[i]);
if (sigprocmask(SIG_BLOCK, &ld->signals, NULL) == -1) return false;
ld->signal_read_fd = signalfd(-1, &ld->signals, SFD_NONBLOCK | SFD_CLOEXEC);
if (ld->signal_read_fd == -1) return false;
}
#else
ld->signal_fds[0] = -1; ld->signal_fds[1] = -1;
if (ld->num_handled_signals) {
if (!self_pipe(ld->signal_fds, true)) return false;
signal_write_fd = ld->signal_fds[1];
ld->signal_read_fd = ld->signal_fds[0];
struct sigaction act = {.sa_sigaction=handle_signal, .sa_flags=SA_SIGINFO | SA_RESTART};
for (size_t i = 0; i < ld->num_handled_signals; i++) { if (sigaction(ld->handled_signals[i], &act, NULL) != 0) return false; }
}
#endif
return true;
}
#ifndef HAS_SIGNAL_FD
static int signal_write_fd = -1;
static void
handle_signal(int sig_num) {
int save_err = errno;
unsigned char byte = (unsigned char)sig_num;
while(signal_write_fd != -1) {
ssize_t ret = write(signal_write_fd, &byte, 1);
if (ret < 0 && errno == EINTR) continue;
break;
}
errno = save_err;
}
#endif
#define SIGNAL_SET \
sigset_t signals = {0}; \
sigemptyset(&signals); \
sigaddset(&signals, SIGINT); sigaddset(&signals, SIGTERM); sigaddset(&signals, SIGCHLD); sigaddset(&signals, SIGUSR1); \
void
free_loop_data(LoopData *ld) {
@@ -54,20 +78,16 @@ free_loop_data(LoopData *ld) {
CLOSE(wakeup_fds, 0); CLOSE(wakeup_fds, 1);
#endif
#ifndef HAS_SIGNAL_FD
signal_write_fd = -1;
CLOSE(signal_fds, 0); CLOSE(signal_fds, 1);
#endif
#undef CLOSE
if (ld->signal_read_fd > -1) {
#ifdef HAS_SIGNAL_FD
safe_close(ld->signal_read_fd, __FILE__, __LINE__);
SIGNAL_SET
sigprocmask(SIG_UNBLOCK, &signals, NULL);
#else
signal_write_fd = -1;
sigprocmask(SIG_UNBLOCK, &ld->signals, NULL);
#endif
signal(SIGINT, SIG_DFL);
signal(SIGTERM, SIG_DFL);
signal(SIGCHLD, SIG_DFL);
for (size_t i = 0; i < ld->num_handled_signals; i++) signal(ld->num_handled_signals, SIG_DFL);
}
#ifdef HAS_EVENT_FD
safe_close(ld->wakeup_read_fd, __FILE__, __LINE__);
@@ -94,30 +114,11 @@ wakeup_loop(LoopData *ld, bool in_signal_handler, const char *loop_name) {
}
bool
install_signal_handlers(LoopData *ld) {
#ifdef HAS_SIGNAL_FD
SIGNAL_SET
if (sigprocmask(SIG_BLOCK, &signals, NULL) == -1) return false;
ld->signal_read_fd = signalfd(-1, &signals, SFD_NONBLOCK | SFD_CLOEXEC);
if (ld->signal_read_fd == -1) return false;
#else
if (!self_pipe(ld->signal_fds, true)) return false;
signal_write_fd = ld->signal_fds[1];
struct sigaction act = {.sa_handler=handle_signal};
#define SA(which) { if (sigaction(which, &act, NULL) != 0) return false; if (siginterrupt(which, false) != 0) return false; }
SA(SIGINT); SA(SIGTERM); SA(SIGCHLD); SA(SIGUSR1);
#undef SA
ld->signal_read_fd = ld->signal_fds[0];
#endif
return true;
}
void
read_signals(int fd, handle_signal_func callback, void *data) {
#ifdef HAS_SIGNAL_FD
static struct signalfd_siginfo fdsi[32];
siginfo_t si;
while (true) {
ssize_t s = read(fd, &fdsi, sizeof(fdsi));
if (s < 0) {
@@ -132,18 +133,33 @@ read_signals(int fd, handle_signal_func callback, void *data) {
log_error("Incomplete signal read from signalfd");
break;
}
for (size_t i = 0; i < num_signals; i++) callback(fdsi[i].ssi_signo, data);
for (size_t i = 0; i < num_signals; i++) {
si.si_signo = fdsi[i].ssi_signo;
si.si_code = fdsi[i].ssi_code;
si.si_pid = fdsi[i].ssi_pid;
si.si_uid = fdsi[i].ssi_uid;
si.si_addr = (void*)(uintptr_t)fdsi[i].ssi_addr;
si.si_status = fdsi[i].ssi_status;
si.si_value.sival_int = fdsi[i].ssi_int;
callback(&si, data);
}
}
#else
static char buf[256];
static char buf[sizeof(siginfo_t) * 8];
static size_t buf_pos = 0;
while(true) {
ssize_t len = read(fd, buf, sizeof(buf));
ssize_t len = read(fd, buf + buf_pos, sizeof(buf) - buf_pos);
if (len < 0) {
if (errno == EINTR) continue;
if (errno != EIO && errno != EAGAIN) log_error("Call to read() from read_signals() failed with error: %s", strerror(errno));
break;
}
for (ssize_t i = 0; i < len; i++) callback(buf[i], data);
buf_pos += len;
while (buf_pos >= sizeof(siginfo_t)) {
callback((siginfo_t*)buf, data);
memmove(buf, buf + sizeof(siginfo_t), sizeof(siginfo_t));
buf_pos -= sizeof(siginfo_t);
}
if (len == 0) break;
}
#endif

View File

@@ -9,6 +9,7 @@
#include "data-types.h"
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#ifdef __has_include
#if __has_include(<sys/signalfd.h>)
@@ -33,16 +34,19 @@ typedef struct {
#endif
#ifndef HAS_SIGNAL_FD
int signal_fds[2];
#else
sigset_t signals;
#endif
int wakeup_read_fd;
int signal_read_fd;
int handled_signals[16];
size_t num_handled_signals;
} LoopData;
typedef void(*handle_signal_func)(int, void *data);
typedef void(*handle_signal_func)(const siginfo_t* siginfo, void *data);
bool init_loop_data(LoopData *ld);
bool init_loop_data(LoopData *ld, size_t num_handled_signals, ...);
void free_loop_data(LoopData *ld);
void wakeup_loop(LoopData *ld, bool in_signal_handler, const char*);
bool install_signal_handlers(LoopData *ld);
void read_signals(int fd, handle_signal_func callback, void *data);
static inline bool

View File

@@ -231,7 +231,7 @@ def ensure_macos_locale() -> None:
@contextmanager
def setup_profiling(args: CLIOptions) -> Generator[None, None, None]:
def setup_profiling() -> Generator[None, None, None]:
try:
from .fast_data_types import start_profiler, stop_profiler
do_profile = True
@@ -410,7 +410,7 @@ def _main() -> None:
global_watchers.set_extra(cli_opts.watcher)
log_error('The --watcher command line option has been deprecated in favor of using the watcher option in kitty.conf')
try:
with setup_profiling(cli_opts):
with setup_profiling():
# Avoid needing to launch threads to reap zombies
run_app(opts, cli_opts, bad_lines)
finally:

View File

@@ -234,6 +234,11 @@ protocol file
mime inode/directory
action launch --type=os-window --cwd $FILE_PATH
# Open executable file
protocol file
mime inode/executable,application/vnd.microsoft.portable-executable
action launch --hold --type=os-window $FILE_PATH
# Open text files without fragments in the editor
protocol file
mime text/*

View File

@@ -129,8 +129,8 @@ pointer_shape(PyObject *shape_name) {
static int
macos_colorspace(PyObject *csname) {
if (PyUnicode_CompareWithASCIIString(csname, "srgb")) return 1;
if (PyUnicode_CompareWithASCIIString(csname, "displayp3")) return 2;
if (PyUnicode_CompareWithASCIIString(csname, "srgb") == 0) return 1;
if (PyUnicode_CompareWithASCIIString(csname, "displayp3") == 0) return 2;
return 0;
}

View File

@@ -460,6 +460,9 @@ dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
REPORT_COMMAND(screen_pop_dynamic_colors);
screen_pop_colors(screen, 0);
break;
case 697:
REPORT_ERROR("Ignoring OSC 697, typically used by Fig for shell integration");
break;
default:
REPORT_ERROR("Unknown OSC code: %u", code);
break;
@@ -1087,6 +1090,7 @@ dispatch_dcs(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
} else IF_SIMPLE_PREFIX("ssh|", handle_remote_ssh)
} else IF_SIMPLE_PREFIX("ask|", handle_remote_askpass)
} else IF_SIMPLE_PREFIX("clone|", handle_remote_clone)
} else IF_SIMPLE_PREFIX("edit|", handle_remote_edit)
#undef IF_SIMPLE_PREFIX
} else {
REPORT_ERROR("Unrecognized DCS @ code: 0x%x", screen->parser_buf[1]);

View File

@@ -30,14 +30,15 @@ class GetText(RemoteCommand):
options_spec = MATCH_WINDOW_OPTION + '''\n
--extent
default=screen
choices=screen, all, selection, first_cmd_output_on_screen, last_cmd_output, last_visited_cmd_output
choices=screen, all, selection, first_cmd_output_on_screen, last_cmd_output, last_visited_cmd_output, last_non_empty_output
What text to get. The default of :code:`screen` means all text currently on the screen.
:code:`all` means all the screen+scrollback and :code:`selection` means the
currently selected text. :code:`first_cmd_output_on_screen` means the output of the first
command that was run in the window on screen. :code:`last_cmd_output` means
the output of the last command that was run in the window. :code:`last_visited_cmd_output` means
the first command output below the last scrolled position via scroll_to_prompt. The last three
requires :ref:`shell_integration` to be enabled.
the first command output below the last scrolled position via scroll_to_prompt.
:code:`last_non_empty_output` is the output from the last command run in the window that had
some non empty output. The last four require :ref:`shell_integration` to be enabled.
--ansi
@@ -99,6 +100,12 @@ Get text from the window this command is run in, rather than the active window.
as_ansi=bool(payload_get('ansi')),
add_wrap_markers=bool(payload_get('wrap_markers')),
)
elif payload_get('extent') == 'last_non_empty_output':
ans = window.cmd_output(
CommandOutput.last_non_empty,
as_ansi=bool(payload_get('ansi')),
add_wrap_markers=bool(payload_get('wrap_markers')),
)
elif payload_get('extent') == 'last_visited_cmd_output':
ans = window.cmd_output(
CommandOutput.last_visited,

View File

@@ -41,7 +41,7 @@ class Kitten(RemoteCommand):
retval = None
for window in self.windows_for_match_payload(boss, window, payload_get):
if window:
retval = boss._run_kitten(payload_get('kitten'), args=tuple(payload_get('args') or ()), window=window)
retval = boss.run_kitten_with_metadata(payload_get('kitten'), args=tuple(payload_get('args') or ()), window=window)
break
if isinstance(retval, (str, bool)):
return retval

View File

@@ -2859,6 +2859,30 @@ cmd_output(Screen *self, PyObject *args) {
if (self->last_visited_prompt.scrolled_by <= self->historybuf->count && self->last_visited_prompt.is_set) {
found = find_cmd_output(self, &oo, self->last_visited_prompt.y, self->last_visited_prompt.scrolled_by, 0, false);
} break;
case 3: { // last non-empty output
int y = self->cursor->y;
Line *line;
bool reached_upper_limit = false;
while (!found && !reached_upper_limit) {
line = checked_range_line(self, y);
if (!line || (line->attrs.prompt_kind == OUTPUT_START && !line->attrs.continued)) {
int start = line ? y : y + 1; reached_upper_limit = !line;
int y2 = start; unsigned int num_lines = 0;
bool found_content = false;
while ((line = checked_range_line(self, y2)) && line->attrs.prompt_kind != PROMPT_START) {
if (!found_content) found_content = !line_is_empty(line);
num_lines++; y2++;
}
if (found_content) {
found = true;
oo.reached_upper_limit = reached_upper_limit;
oo.start = start; oo.num_lines = num_lines;
break;
}
}
y--;
}
} break;
default:
PyErr_Format(PyExc_KeyError, "%u is not a valid type of command", which);
return NULL;

View File

@@ -47,11 +47,13 @@ class SharedMemory:
num_bytes_for_size = struct.calcsize(size_fmt)
def __init__(
self, name: str = '', size: int = 0, readonly: bool = False,
mode: int = stat.S_IREAD | stat.S_IWRITE,
prefix: str = 'kitty-', unlink_on_exit: bool = False
self, name: str = '', size: int = 0, readonly: bool = False,
mode: int = stat.S_IREAD | stat.S_IWRITE,
prefix: str = 'kitty-',
unlink_on_exit: bool = False, ignore_close_failure: bool = False
):
self.unlink_on_exit = unlink_on_exit
self.ignore_close_failure = ignore_close_failure
if size < 0:
raise TypeError("'size' must be a non-negative integer")
if size and name:
@@ -157,7 +159,11 @@ class SharedMemory:
"""Closes access to the shared memory from this instance but does
not destroy the shared memory block."""
if self._mmap is not None:
self._mmap.close()
try:
self._mmap.close()
except BufferError:
if not self.ignore_close_failure:
raise
self._mmap = None
if self._fd >= 0:
os.close(self._fd)

View File

@@ -620,7 +620,7 @@ def get_editor_from_env_vars(opts: Optional[Options] = None) -> List[str]:
return shlex.split(ans)
def get_editor(opts: Optional[Options] = None) -> List[str]:
def get_editor(opts: Optional[Options] = None, path_to_edit: str = '', line_number: int = 0) -> List[str]:
if opts is None:
from .fast_data_types import get_options
try:
@@ -630,9 +630,20 @@ def get_editor(opts: Optional[Options] = None) -> List[str]:
from .cli import create_default_opts
opts = create_default_opts()
if opts.editor == '.':
return get_editor_from_env_vars()
import shlex
return shlex.split(opts.editor)
ans = get_editor_from_env_vars()
else:
import shlex
ans = shlex.split(opts.editor)
if path_to_edit:
if line_number:
eq = os.path.basename(ans[-1]).lower()
if eq in ('code', 'code.exe'):
path_to_edit += f':{line_number}'
ans.append('--goto')
else:
ans.append(f'+{line_number}')
ans.append(path_to_edit)
return ans
def is_path_in_temp_dir(path: str) -> bool:
@@ -895,19 +906,24 @@ def is_kitty_gui_cmdline(*cmd: str) -> bool:
return False
if len(cmd) == 1:
return True
if '+' in cmd or '@' in cmd or cmd[1].startswith('+') or cmd[1].startswith('@'):
s = cmd[1][:1]
if s == '@':
return False
if s == '+':
if cmd[1] == '+':
return len(cmd) > 2 and cmd[2] == 'open'
return cmd[1] == '+open'
return True
def reload_conf_in_all_kitties() -> None:
import signal
from kitty.child import cmdline_of_process
from kitty.child import cmdline_of_pid
for pid in get_all_processes():
try:
cmd = cmdline_of_process(pid)
cmd = cmdline_of_pid(pid)
except Exception:
continue
if cmd and is_kitty_gui_cmdline(*cmd):
@@ -991,3 +1007,13 @@ def less_version(less_exe: str = 'less') -> int:
if m is None:
raise ValueError(f'Invalid version string for less: {o}')
return int(m.group(1))
def is_pid_alive(pid: int) -> bool:
try:
os.kill(pid, 0)
except ProcessLookupError:
return False
except Exception:
pass
return True

View File

@@ -164,7 +164,7 @@ class DynamicColor(IntEnum):
class CommandOutput(IntEnum):
last_run, first_on_screen, last_visited = 0, 1, 2
last_run, first_on_screen, last_visited, last_non_empty = 0, 1, 2, 3
DYNAMIC_COLOR_CODES = {
@@ -475,7 +475,7 @@ class Window:
self.watchers = global_watchers().copy()
self.last_focused_at = 0.
self.started_at = monotonic()
self.current_clone_data = ''
self.current_remote_data: List[str] = []
self.current_mouse_event_button = 0
self.current_clipboard_read_ask: Optional[bool] = None
self.prev_osc99_cmd = NotificationCommand()
@@ -751,6 +751,8 @@ class Window:
def write_to_child(self, data: Union[str, bytes]) -> None:
if data:
if isinstance(data, str):
data = data.encode('utf-8')
if get_boss().child_monitor.needs_write(self.id, data) is not True:
log_error(f'Failed to write to child {self.id} as it does not exist')
@@ -1029,25 +1031,36 @@ class Window:
if tab is not None:
tab.move_window_to_top_of_group(self)
def handle_remote_clone(self, msg: str) -> None:
def append_remote_data(self, msg: str) -> str:
if not msg:
if self.current_clone_data:
cdata, self.current_clone_data = self.current_clone_data, ''
ac = get_options().allow_cloning
if ac == 'ask':
get_boss().confirm(_(
'A program running in this window wants to clone it into another window.'
' Allow it do so, once?'),
partial(self.handle_remote_clone_confirmation, cdata), window=self,
)
elif ac in ('yes', 'y', 'true'):
self.handle_remote_clone_confirmation(cdata, True)
self.current_clone_data = ''
return
cdata = ''.join(self.current_remote_data)
self.current_remote_data = []
return cdata
num, rest = msg.split(':', 1)
if num == '0' or len(self.current_clone_data) > 1024 * 1024:
self.current_clone_data = ''
self.current_clone_data += rest
max_size = get_options().clipboard_max_size * 1024 * 1024
if num == '0' or sum(map(len, self.current_remote_data)) > max_size:
self.current_remote_data = []
self.current_remote_data.append(rest)
return ''
def handle_remote_edit(self, msg: str) -> None:
cdata = self.append_remote_data(msg)
if cdata:
from .launch import remote_edit
remote_edit(cdata, self)
def handle_remote_clone(self, msg: str) -> None:
cdata = self.append_remote_data(msg)
if cdata:
ac = get_options().allow_cloning
if ac == 'ask':
get_boss().confirm(_(
'A program running in this window wants to clone it into another window.'
' Allow it do so, once?'),
partial(self.handle_remote_clone_confirmation, cdata), window=self,
)
elif ac in ('yes', 'y', 'true'):
self.handle_remote_clone_confirmation(cdata, True)
def handle_remote_clone_confirmation(self, cdata: str, confirmed: bool) -> None:
if confirmed:
@@ -1447,6 +1460,14 @@ class Window:
def show_last_visited_command_output(self) -> None:
self.show_cmd_output(CommandOutput.last_visited, 'Last visited command output')
@ac('cp', '''
Show the last non-empty output from a shell command in a pager like less
Requires :ref:`shell_integration` to work
''')
def show_last_non_empty_command_output(self) -> None:
self.show_cmd_output(CommandOutput.last_non_empty, 'Last non-empty command output')
@ac('cp', 'Paste the specified text into the current window')
def paste(self, text: str) -> None:
self.paste_with_actions(text)

View File

@@ -177,7 +177,7 @@ class BaseTest(TestCase):
s = Screen(c, lines, cols, scrollback, cell_width, cell_height, 0, c)
return s
def create_pty(self, argv, cols=80, lines=100, scrollback=100, cell_width=10, cell_height=20, options=None, cwd=None, env=None):
def create_pty(self, argv=None, cols=80, lines=100, scrollback=100, cell_width=10, cell_height=20, options=None, cwd=None, env=None):
self.set_options(options)
return PTY(argv, lines, cols, scrollback, cell_width, cell_height, cwd, env)
@@ -192,12 +192,17 @@ class BaseTest(TestCase):
class PTY:
def __init__(self, argv, rows=25, columns=80, scrollback=100, cell_width=10, cell_height=20, cwd=None, env=None):
def __init__(self, argv=None, rows=25, columns=80, scrollback=100, cell_width=10, cell_height=20, cwd=None, env=None):
if isinstance(argv, str):
argv = shlex.split(argv)
pid, self.master_fd = fork()
self.is_child = pid == CHILD
self.write_buf = b''
if argv is None:
from kitty.child import openpty
self.master_fd, self.slave_fd = openpty()
self.is_child = False
else:
pid, self.master_fd = fork()
self.is_child = pid == CHILD
if self.is_child:
while read_screen_size().width != columns * cell_width:
time.sleep(0.01)
@@ -223,9 +228,11 @@ class PTY:
def __del__(self):
if not self.is_child:
fd = self.master_fd
os.close(self.master_fd)
if hasattr(self, 'slave_fd'):
os.close(self.slave_fd)
del self.slave_fd
del self.master_fd
os.close(fd)
def write_to_child(self, data):
if isinstance(data, str):
@@ -268,13 +275,14 @@ class PTY:
if not q():
raise TimeoutError(f'The condition was not met. Screen contents: \n {repr(self.screen_contents())}')
def set_window_size(self, rows=25, columns=80):
def set_window_size(self, rows=25, columns=80, send_signal=True):
if hasattr(self, 'screen'):
self.screen.resize(rows, columns)
x_pixels = columns * self.cell_width
y_pixels = rows * self.cell_height
s = struct.pack('HHHH', rows, columns, x_pixels, y_pixels)
fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, s)
if send_signal:
x_pixels = columns * self.cell_width
y_pixels = rows * self.cell_height
s = struct.pack('HHHH', rows, columns, x_pixels, y_pixels)
fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, s)
def screen_contents(self):
lines = []

View File

@@ -43,3 +43,43 @@ class TestDiff(BaseTest):
highlights = [h(0, 1, 1), h(1, 3, 2)]
self.ae(['S1SaE1ES2SbcE2Ed'], split_with_highlights('abcd', 10, highlights))
def test_walk(self):
from pathlib import Path
import tempfile
from kittens.diff.collect import walk
with tempfile.TemporaryDirectory() as tmpdir:
# /tmp/test/
# ├── a
# │ └── b
# │ └── c
# ├── d
# ├── #d#
# ├── e
# ├── e~
# └── f
# │ └── g
# └── h space
Path(tmpdir, "a/b").mkdir(parents=True)
Path(tmpdir, "a/b/c").touch()
Path(tmpdir, "b").touch()
Path(tmpdir, "d").touch()
Path(tmpdir, "#d#").touch()
Path(tmpdir, "e").touch()
Path(tmpdir, "e~").touch()
Path(tmpdir, "f").mkdir()
Path(tmpdir, "f/g").touch()
Path(tmpdir, "h space").touch()
expected_names = {"d", "e", "f/g", "h space"}
expected_pmap = {
"d": f"{tmpdir}/d",
"e": f"{tmpdir}/e",
"f/g": f"{tmpdir}/f/g",
"h space": f"{tmpdir}/h space"
}
names = set()
pmap = {}
walk(tmpdir, names, pmap, ("*~", "#*#", "b"))
self.ae(expected_names, names)
self.ae(expected_pmap, pmap)

View File

@@ -110,6 +110,7 @@ class Rendering(BaseTest):
self.ae(g('===--<>=='), [(3, 3), (2, 2), (2, 2), (2, 2)])
self.ae(g('==!=<>==<><><>'), [(4, 4), (2, 2), (2, 2), (2, 2), (2, 2), (2, 2)])
self.ae(g('-' * 18), [(18, 18)])
self.ae(g('a>\u2060<b'), [(1, 1), (1, 2), (1, 1), (1, 1)])
colon_glyph = ss('9:30', font='FiraCode-Medium.otf')[1][2]
self.assertNotEqual(colon_glyph, ss(':', font='FiraCode-Medium.otf')[0][2])
self.ae(colon_glyph, 1031)

48
kitty_tests/prewarm.py Normal file
View File

@@ -0,0 +1,48 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
import json
import os
import tempfile
from kitty.constants import kitty_exe
from . import BaseTest
class Prewarm(BaseTest):
maxDiff = None
def test_prewarming(self):
from kittens.prewarm.main import PrewarmProcess
p = PrewarmProcess(create_file_to_read_from_worker=True)
cwd = tempfile.gettempdir()
env = {'TEST_ENV_PASS': 'xyz'}
cols = 117
stdin_data = 'from_stdin'
pty = self.create_pty(cols=cols)
ttyname = os.ttyname(pty.slave_fd)
child = p(pty.slave_fd, [kitty_exe(), '+runpy', """import os, json; from kitty.utils import *; print(json.dumps({
'cterm': os.ctermid(),
'ttyname': os.ttyname(sys.stdout.fileno()),
'cols': read_screen_size().cols,
'cwd': os.getcwd(),
'env': os.environ.get('TEST_ENV_PASS'),
'pid': os.getpid(),
'stdin': sys.stdin.read(),
'done': 'hello',
}, indent=2))"""], cwd=cwd, env=env, stdin_data=stdin_data)
self.assertFalse(pty.screen_contents().strip())
p.mark_child_as_ready(child.child_id)
pty.wait_till(lambda: 'hello' in pty.screen_contents())
data = json.loads(pty.screen_contents())
self.ae(data['cols'], cols)
self.assertTrue(data['cterm'])
self.ae(data['ttyname'], ttyname)
self.ae(os.path.realpath(data['cwd']), os.path.realpath(cwd))
self.ae(data['env'], env['TEST_ENV_PASS'])
self.ae(int(p.from_worker.readline()), data['pid'])

View File

@@ -1009,9 +1009,9 @@ class TestScreen(BaseTest):
self.assertTrue(s.scroll_to_prompt())
self.ae(str(s.visual_line(0)), '$ 1')
def lco(as_ansi=False):
def lco(as_ansi=False, which=0):
a = []
if s.cmd_output(0, a.append, as_ansi):
if s.cmd_output(which, a.append, as_ansi):
pht = pagerhist(s, as_ansi=as_ansi, upto_output_start=True)
if pht:
a.insert(0, pht)
@@ -1098,3 +1098,22 @@ class TestScreen(BaseTest):
draw_prompt('p1')
draw_output(30)
self.ae(tuple(map(int, lco().split())), tuple(range(0, 30)))
# last non empty command output
s = self.create_screen()
draw_prompt('a'), draw_output(2, 'a')
draw_prompt('b'), mark_output()
self.ae(lco(), '')
self.ae(lco(which=3), '0a\n1a')
s.draw('running'), s.index(), s.carriage_return()
self.ae(lco(which=3), 'running\n')
s = self.create_screen()
draw_prompt('p1')
draw_output(30)
self.ae(tuple(map(int, lco(which=3).split())), tuple(range(0, 30)))
s = self.create_screen()
draw_prompt('p1')
draw_output(2, 'a')
draw_prompt('p1')
draw_prompt('p1')
self.ae(lco(which=3), '0a\n1a')

View File

@@ -358,7 +358,7 @@ PS1="{ps1}"
run_test('bash -l .bashrc', 'profile', rc='echo ok;read', wait_string='ok', assert_not_in=True)
run_test('bash -il -- .bashrc', 'profile', rc='echo ok;read', wait_string='ok')
with self.run_shell(rc=f'''PS1="{ps1}"\nexport ES=$'a\n `b` c\n$d' ''') as pty:
with self.run_shell(shell='bash', setup_env=partial(setup_env, set()), cmd='bash', rc=f'''PS1="{ps1}"\nexport ES=$'a\n `b` c\n$d' ''') as pty:
pty.callbacks.clear()
pty.send_cmd_to_child('clone-in-kitty')
pty.wait_till(lambda: len(pty.callbacks.clone_cmds) == 1)

View File

@@ -196,6 +196,8 @@ copy --exclude */w.* d1
self.assertTrue(methods)
import pwd
expected_login_shell = pwd.getpwuid(os.geteuid()).pw_shell
if os.path.basename(expected_login_shell) == 'nologin':
self.skipTest('Skipping login shell detection as login shell is set to nologin')
for m in methods:
for sh in self.all_possible_sh:
if 'python' in sh:

View File

@@ -44,28 +44,49 @@ safe_realpath(const char* src, char *buf, size_t buf_sz) {
}
#endif
static bool
set_xoptions(const char *exe_dir_c, const char *lc_ctype, bool from_source) {
wchar_t *exe_dir = Py_DecodeLocale(exe_dir_c, NULL);
if (exe_dir == NULL) { fprintf(stderr, "Fatal error: cannot decode exe_dir: %s\n", exe_dir_c); return false; }
wchar_t buf[PATH_MAX+1] = {0};
swprintf(buf, PATH_MAX, L"bundle_exe_dir=%ls", exe_dir);
PySys_AddXOption(buf);
PyMem_RawFree(exe_dir);
if (from_source) PySys_AddXOption(L"kitty_from_source=1");
if (lc_ctype) {
swprintf(buf, PATH_MAX, L"lc_ctype_before_python=%s", lc_ctype);
PySys_AddXOption(buf);
}
return true;
}
typedef struct {
const char *exe, *exe_dir, *lc_ctype, *lib_dir;
char **argv;
int argc;
wchar_t* xoptions[8];
int num_xoptions;
} RunData;
static bool
set_xoptions(RunData *run_data, bool from_source) {
wchar_t *exe_dir = Py_DecodeLocale(run_data->exe_dir, NULL);
if (exe_dir == NULL) { fprintf(stderr, "Fatal error: cannot decode exe_dir: %s\n", run_data->exe_dir); return false; }
size_t len = 32 + wcslen(exe_dir);
run_data->xoptions[run_data->num_xoptions] = calloc(len, sizeof(wchar_t));
if (!run_data->xoptions[run_data->num_xoptions]) { fprintf(stderr, "Out of memory allocating for bundle_exe_dir\n"); return false; }
swprintf(run_data->xoptions[run_data->num_xoptions++], len, L"bundle_exe_dir=%ls", exe_dir);
PyMem_RawFree(exe_dir);
if (from_source) {
len = 32;
run_data->xoptions[run_data->num_xoptions] = calloc(len, sizeof(wchar_t));
if (!run_data->xoptions[run_data->num_xoptions]) { fprintf(stderr, "Out of memory allocating for from_source\n"); return false; }
swprintf(run_data->xoptions[run_data->num_xoptions++], len, L"kitty_from_source=1");
}
if (run_data->lc_ctype) {
len = 32 + 4 * strlen(run_data->lc_ctype);
run_data->xoptions[run_data->num_xoptions] = calloc(len, sizeof(wchar_t));
if (!run_data->xoptions[run_data->num_xoptions]) { fprintf(stderr, "Out of memory allocating for lc_ctype\n"); return false; }
swprintf(run_data->xoptions[run_data->num_xoptions++], len, L"lc_ctype_before_python=%s", run_data->lc_ctype);
}
return true;
}
static void
free_xoptions(RunData *run_data) {
if (run_data->num_xoptions > 0) {
while (run_data->num_xoptions--) {
free(run_data->xoptions[run_data->num_xoptions]);
run_data->xoptions[run_data->num_xoptions] = 0;
}
}
}
#ifdef FOR_BUNDLE
#include <bypy-freeze.h>
@@ -133,7 +154,7 @@ canonicalize_path_wide(const char *srcpath, wchar_t *dest, size_t sz) {
}
static int
run_embedded(const RunData run_data) {
run_embedded(RunData *run_data) {
bypy_pre_initialize_interpreter(false);
char extensions_dir_full[PATH_MAX+1] = {0}, python_home_full[PATH_MAX+1] = {0};
#ifdef __APPLE__
@@ -141,20 +162,23 @@ run_embedded(const RunData run_data) {
#else
const char *python_relpath = "../" KITTY_LIB_DIR_NAME;
#endif
int num = snprintf(extensions_dir_full, PATH_MAX, "%s/%s/kitty-extensions", run_data.exe_dir, python_relpath);
if (num < 0 || num >= PATH_MAX) { fprintf(stderr, "Failed to create path to extensions_dir: %s/%s\n", run_data.exe_dir, python_relpath); return 1; }
int num = snprintf(extensions_dir_full, PATH_MAX, "%s/%s/kitty-extensions", run_data->exe_dir, python_relpath);
if (num < 0 || num >= PATH_MAX) { fprintf(stderr, "Failed to create path to extensions_dir: %s/%s\n", run_data->exe_dir, python_relpath); return 1; }
wchar_t extensions_dir[num+2];
if (!canonicalize_path_wide(extensions_dir_full, extensions_dir, num+1)) {
fprintf(stderr, "Failed to canonicalize the path: %s\n", extensions_dir_full); return 1; }
num = snprintf(python_home_full, PATH_MAX, "%s/%s/python%s", run_data.exe_dir, python_relpath, PYVER);
if (num < 0 || num >= PATH_MAX) { fprintf(stderr, "Failed to create path to python home: %s/%s\n", run_data.exe_dir, python_relpath); return 1; }
num = snprintf(python_home_full, PATH_MAX, "%s/%s/python%s", run_data->exe_dir, python_relpath, PYVER);
if (num < 0 || num >= PATH_MAX) { fprintf(stderr, "Failed to create path to python home: %s/%s\n", run_data->exe_dir, python_relpath); return 1; }
wchar_t python_home[num+2];
if (!canonicalize_path_wide(python_home_full, python_home, num+1)) {
fprintf(stderr, "Failed to canonicalize the path: %s\n", python_home_full); return 1; }
bypy_initialize_interpreter(L"kitty", python_home, L"kitty_main", extensions_dir, run_data.argc, run_data.argv);
if (!set_xoptions(run_data.exe_dir, run_data.lc_ctype, false)) return 1;
if (!set_xoptions(run_data, false)) return 1;
bypy_initialize_interpreter_with_xoptions(
L"kitty", python_home, L"kitty_main", extensions_dir, run_data->argc, run_data->argv,
run_data->num_xoptions, run_data->xoptions);
free_xoptions(run_data);
set_sys_bool("frozen", true);
set_sys_string("kitty_extensions_dir", extensions_dir);
return bypy_run_interpreter();
@@ -163,36 +187,41 @@ run_embedded(const RunData run_data) {
#else
static int
free_argv(wchar_t **argv) {
wchar_t **p = argv;
while (*p) { PyMem_RawFree(*p); p++; }
free(argv);
return 1;
}
static int
run_embedded(const RunData run_data) {
run_embedded(RunData *run_data) {
bool from_source = false;
#ifdef FROM_SOURCE
from_source = true;
#endif
if (!set_xoptions(run_data.exe_dir, run_data.lc_ctype, from_source)) return 1;
int argc = run_data.argc + 1;
wchar_t **argv = calloc(argc, sizeof(wchar_t*));
if (!argv) { fprintf(stderr, "Out of memory creating argv\n"); return 1; }
memset(argv, 0, sizeof(wchar_t*) * argc);
argv[0] = Py_DecodeLocale(run_data.exe, NULL);
if (!argv[0]) { fprintf(stderr, "Failed to decode path to exe\n"); return free_argv(argv); }
argv[1] = Py_DecodeLocale(run_data.lib_dir, NULL);
if (!argv[1]) { fprintf(stderr, "Failed to decode path to lib_dir\n"); return free_argv(argv); }
for (int i=1; i < run_data.argc; i++) {
argv[i+1] = Py_DecodeLocale(run_data.argv[i], NULL);
if (!argv[i+1]) { fprintf(stderr, "Failed to decode the command line argument: %s\n", run_data.argv[i]); return free_argv(argv); }
}
int ret = Py_Main(argc, argv);
// we cannot free argv properly as Py_Main modifies it
free(argv);
return ret;
PyStatus status;
PyPreConfig preconfig;
PyPreConfig_InitPythonConfig(&preconfig);
preconfig.utf8_mode = 1;
preconfig.coerce_c_locale = 1;
status = Py_PreInitialize(&preconfig);
if (PyStatus_Exception(status)) goto fail;
PyConfig config;
PyConfig_InitPythonConfig(&config);
config.parse_argv = 0;
config.optimization_level = 2;
status = PyConfig_SetBytesArgv(&config, run_data->argc, run_data->argv);
if (PyStatus_Exception(status)) goto fail;
status = PyConfig_SetBytesString(&config, &config.executable, run_data->exe);
if (PyStatus_Exception(status)) goto fail;
status = PyConfig_SetBytesString(&config, &config.run_filename, run_data->lib_dir);
if (PyStatus_Exception(status)) goto fail;
if (!set_xoptions(run_data, from_source)) return 1;
status = PyConfig_SetWideStringList(&config, &config.xoptions, run_data->num_xoptions, run_data->xoptions);
free_xoptions(run_data);
if (PyStatus_Exception(status)) goto fail;
status = Py_InitializeFromConfig(&config);
if (PyStatus_Exception(status)) goto fail;
PyConfig_Clear(&config);
return Py_RunMain();
fail:
PyConfig_Clear(&config);
if (PyStatus_IsExit(status)) return status.exitcode;
Py_ExitStatusException(status);
}
#endif
@@ -275,11 +304,7 @@ int main(int argc, char *argv[]) {
num = snprintf(lib, PATH_MAX, "%s/%s", exe_dir, KITTY_LIB_PATH);
if (num < 0 || num >= PATH_MAX) { fprintf(stderr, "Failed to create path to kitty lib\n"); return 1; }
#if PY_VERSION_HEX >= 0x03070000
// Always use UTF-8 mode, see https://github.com/kovidgoyal/kitty/issues/924
Py_UTF8Mode = 1;
#endif
RunData run_data = {.exe = exe, .exe_dir = exe_dir, .lib_dir = lib, .argc = argc, .argv = argv, .lc_ctype = lc_ctype};
ret = run_embedded(run_data);
ret = run_embedded(&run_data);
return ret;
}

View File

@@ -19,15 +19,15 @@ from contextlib import suppress
from functools import lru_cache, partial
from pathlib import Path
from typing import (
Callable, Dict, FrozenSet, Iterable, Iterator, List, Optional, Sequence,
Set, Tuple, Union
Callable, Dict, FrozenSet, Iterable, List, Optional, Sequence, Set, Tuple,
Union
)
from glfw import glfw
from glfw.glfw import Command, CompileKey
if sys.version_info[:2] < (3, 7):
raise SystemExit('kitty requires python >= 3.7')
if sys.version_info[:2] < (3, 8):
raise SystemExit('kitty requires python >= 3.8')
base = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, base)
del sys.path[0]
@@ -368,27 +368,18 @@ def init_env(
cflags.append('-g3')
ldflags.append('-lprofiler')
library_paths = {}
library_paths: Dict[str, List[str]] = {}
if egl_library is not None:
assert('"' not in egl_library)
library_paths['glfw/egl_context.c'] = [f'_GLFW_EGL_LIBRARY="{egl_library}"']
def add_lpath(which: str, name: str, val: Optional[str]) -> None:
if val:
if '"' in val:
raise SystemExit(f'Cannot have quotes in library paths: {val}')
library_paths.setdefault(which, []).append(f'{name}="{val}"')
desktop_libs = []
if startup_notification_library is not None:
assert('"' not in startup_notification_library)
desktop_libs = [f'_KITTY_STARTUP_NOTIFICATION_LIBRARY="{startup_notification_library}"']
if canberra_library is not None:
assert('"' not in canberra_library)
desktop_libs += [f'_KITTY_CANBERRA_LIBRARY="{canberra_library}"']
if fontconfig_library is not None:
assert('"' not in fontconfig_library)
desktop_libs += [f'_KITTY_FONTCONFIG_LIBRARY="{fontconfig_library}"']
if desktop_libs != []:
library_paths['kitty/desktop.c'] = desktop_libs
add_lpath('glfw/egl_context.c', '_GLFW_EGL_LIBRARY', egl_library)
add_lpath('kitty/desktop.c', '_KITTY_STARTUP_NOTIFICATION_LIBRARY', startup_notification_library)
add_lpath('kitty/desktop.c', '_KITTY_CANBERRA_LIBRARY', canberra_library)
add_lpath('kitty/fontconfig.c', '_KITTY_FONTCONFIG_LIBRARY', fontconfig_library)
for path in extra_include_dirs:
cflags.append(f'-I{path}')
@@ -497,19 +488,14 @@ def get_vcs_rev_defines(env: Env, src: str) -> List[str]:
return ans
def get_library_defines(env: Env, src: str) -> Optional[List[str]]:
try:
return env.library_paths[src]
except KeyError:
return None
SPECIAL_SOURCES: Dict[str, Tuple[str, Union[List[str], Callable[[Env, str], Union[Optional[List[str]], Iterator[str]]]]]] = {
'glfw/egl_context.c': ('glfw/egl_context.c', get_library_defines),
'kitty/desktop.c': ('kitty/desktop.c', get_library_defines),
'kitty/parser_dump.c': ('kitty/parser.c', ['DUMP_COMMANDS']),
'kitty/data-types.c': ('kitty/data-types.c', get_vcs_rev_defines),
}
def get_source_specific_defines(env: Env, src: str) -> Tuple[str, Optional[List[str]]]:
if src == 'kitty/parser_dump.c':
return 'kitty/parser.c', ['DUMP_COMMANDS']
if src == 'kitty/data-types.c':
return src, get_vcs_rev_defines(env, src)
with suppress(KeyError):
return src, env.library_paths[src]
return src, None
def newer(dest: str, *sources: str) -> bool:
@@ -695,13 +681,9 @@ def compile_c_extension(
for original_src, dest in zip(sources, objects):
src = original_src
cppflags = kenv.cppflags[:]
is_special = src in SPECIAL_SOURCES
if is_special:
src, defines_ = SPECIAL_SOURCES[src]
defines = defines_(kenv, src) if callable(defines_) else defines_
if defines is not None:
cppflags.extend(map(define, defines))
src, defines = get_source_specific_defines(kenv, src)
if defines is not None:
cppflags.extend(map(define, defines))
cmd = kenv.cc + ['-MMD'] + cppflags + kenv.cflags
cmd += ['-c', src] + ['-o', dest]
key = CompileKey(original_src, os.path.basename(dest))

View File

@@ -219,6 +219,8 @@ _ksi_main() {
fi
}
builtin complete -o nospace -F _ksi_completions kitty
builtin complete -o nospace -F _ksi_completions edit-in-kitty
builtin complete -o nospace -F _ksi_completions clone-in-kitty
fi
# wrap our prompt additions in markers we can use to remove them using
@@ -308,6 +310,23 @@ builtin unset -f _ksi_main
case :$SHELLOPTS: in
*:posix:*) ;;
*)
_ksi_transmit_data() {
builtin local data
data="${1//[[:space:]]}"
builtin local pos=0
builtin local chunk_num=0
while [ $pos -lt ${#data} ]; do
builtin local chunk="${data:$pos:2048}"
pos=$(($pos+2048))
builtin printf '\eP@kitty-%s|%s:%s\e\\' "${2}" "${chunk_num}" "${chunk}"
chunk_num=$(($chunk_num+1))
done
# save history so it is available in new shell
[ "$3" = "save_history" ] && builtin history -a
builtin printf '\eP@kitty-%s|\e\\' "${2}"
}
clone-in-kitty() {
builtin local data="shell=bash,pid=$$,cwd=$(builtin printf "%s" "$PWD" | builtin command base64),envfmt=bash,env=$(builtin export | builtin command base64)"
while :; do
@@ -321,18 +340,84 @@ clone-in-kitty() {
esac
shift
done
data="${data//[[:space:]]}"
builtin local pos=0
builtin local chunk_num=0
while [ $pos -lt ${#data} ]; do
builtin local chunk="${data:$pos:2048}"
pos=$(($pos+2048))
builtin printf '\eP@kitty-clone|%s:%s\e\\' "${chunk_num}" "${chunk}"
chunk_num=$(($chunk_num+1))
_ksi_transmit_data "$data" "clone" "save_history"
}
edit-in-kitty() {
builtin local data=""
builtin local ed_filename=""
builtin local usage="Usage: edit-in-kitty [OPTIONS] FILE"
data="cwd=$(builtin printf "%s" "$PWD" | builtin command base64)"
while :; do
case "$1" in
"") break;;
-h|--help)
builtin printf "%s\n\n%s\n\n%s\n" "$usage" "Edit the specified file in a kitty overlay window. Works over SSH as well." "For usage instructions see: https://sw.kovidgoyal.net/kitty/shell-integration/#edit-file"
return
;;
*) data="$data,a=$(builtin printf "%s" "$1" | builtin command base64)"; ed_filename="$1";;
esac
shift
done
# append current commands to history file
builtin history -a
builtin printf '\eP@kitty-clone|\e\\'
[ -z "$ed_filename" ] && {
builtin echo "$usage" > /dev/stderr
return 1
}
[ -r "$ed_filename" -a -w "$ed_filename" ] || {
builtin echo "$ed_filename is not readable and writable" > /dev/stderr
return 1
}
[ ! -f "$ed_filename" ] && {
builtin echo "$ed_filename is not a file" > /dev/stderr
return 1
}
builtin local stat_result=""
stat_result=$(builtin command stat -L --format '%d:%i:%s' "$ed_filename" 2> /dev/null)
[ $? != 0 ] && stat_result=$(builtin command stat -L -f '%d:%i:%z' "$ed_filename" 2> /dev/null)
[ -z "$stat_result" ] && { builtin echo "Failed to stat the file: $ed_filename" > /dev/stderr; return 1; }
data="$data,file_inode=$stat_result"
builtin local file_size=$(builtin echo "$stat_result" | builtin command cut -d: -f3)
[ "$file_size" -gt $((8 * 1024 * 1024)) ] && { builtin echo "File is too large for performant editing"; return 1; }
data="$data,file_data=$(builtin command base64 < "$ed_filename")"
_ksi_transmit_data "$data" "edit"
data=""
builtin echo "Waiting for editing to be completed..."
_ksi_wait_for_complete() {
builtin local started="n"
builtin local line=""
builtin local old_tty_settings=$(builtin command stty -g)
builtin command stty "-echo"
builtin trap -- "builtin command stty '$old_tty_settings'" RETURN
builtin trap -- "builtin command stty '$old_tty_settings'; _ksi_transmit_data 'abort_signaled=interrupt' 'edit'; builtin exit 1;" SIGINT SIGTERM
while :; do
started="n"
while IFS= read -r line; do
if [ "$started" = "y" ]; then
[ "$line" = "UPDATE" ] && break;
[ "$line" = "DONE" ] && { started="done"; break; }
builtin printf "%s\n" "$line" > /dev/stderr;
return 1;
else
[ "$line" = "KITTY_DATA_START" ] && started="y"
fi
done
[ "$started" = "n" ] && continue;
data=""
while IFS= read -r line; do
[ "$line" = "KITTY_DATA_END" ] && break;
data="$data$line"
done
[ -n "$data" -a "$started" != "done" ] && {
builtin echo "Updating $ed_filename..."
builtin printf "%s" "$data" | builtin command base64 -d > "$ed_filename"
}
[ "$started" = "done" ] && break;
done
}
$(_ksi_wait_for_complete > /dev/tty)
builtin local rc=$?
builtin unset -f _ksi_wait_for_complete
return $rc
}
;;
esac

View File

@@ -0,0 +1,8 @@
function __ksi_completions
set --local ct (commandline --current-token)
set --local tokens (commandline --tokenize --cut-at-cursor --current-process)
set tokens[1] "clone-in-kitty"
printf "%s\n" $tokens $ct | command kitty +complete fish2
end
complete -f -c clone-in-kitty -a "(__ksi_completions)"

View File

@@ -0,0 +1,8 @@
function __ksi_completions
set --local ct (commandline --current-token)
set --local tokens (commandline --tokenize --cut-at-cursor --current-process)
set tokens[1] "edit-in-kitty"
printf "%s\n" $tokens $ct | command kitty +complete fish2
end
complete -f -c edit-in-kitty -a "(__ksi_completions)"

View File

@@ -155,6 +155,19 @@ function __ksi_schedule --on-event fish_prompt -d "Setup kitty integration after
end
end
function __ksi_transmit_data -d "Transmit data to kitty using chunked DCS escapes"
set --local data_len (string length -- "$argv[1]")
set --local pos 1
set --local chunk_num 0
while test "$pos" -le $data_len
printf \eP@kitty-%s\|%s:%s\e\\ "$argv[2]" "$chunk_num" (string sub --start $pos --length 2048 -- $argv[1] | string collect)
set pos (math $pos + 2048)
set chunk_num (math $chunk_num + 1)
end
printf \eP@kitty-%s\|\e\\ "$argv[2]"
end
function clone-in-kitty -d "Clone the current fish session into a new kitty window"
set --local data
for a in $argv
@@ -175,13 +188,91 @@ function clone-in-kitty -d "Clone the current fish session into a new kitty wind
set --local b64_cwd (printf "%s" "$PWD" | base64)
set --prepend data "shell=fish" "pid=$fish_pid" "cwd=$b64_cwd" "env=$b64_envs"
set data (string join "," -- $data | tr -d "\t\n\r ")
set --local data_len (string length -- "$data")
set --local pos 1
set --local chunk_num 0
while test "$pos" -le $data_len
printf \eP@kitty-clone\|%s:%s\e\\ "$chunk_num" (string sub --start $pos --length 2048 -- $data | string collect)
set pos (math $pos + 2048)
set chunk_num (math $chunk_num + 1)
end
echo -en \eP@kitty-clone\|\e\\
__ksi_transmit_data "$data" "clone"
end
function edit-in-kitty -d "Edit the specified file in a new kitty overlay using your preferred editor, even over SSH"
set --local data
set --local ed_filename ""
set --local usage "Usage: edit-in-kitty [OPTIONS] FILE"
for a in $argv
if contains -- "$a" -h --help
echo "$usage"
echo
echo "Edit the specified file in a kitty overlay window. Works over SSH as well."
echo
echo "For usage instructions see: https://sw.kovidgoyal.net/kitty/shell-integration/#edit-file"
return
end
set --local ea (printf "%s" "$a" | base64)
set --append data "a=$ea"
set ed_filename "$a"
end
if test -z "$ed_filename"
echo "$usage" > /dev/stderr
return 1
end
if test ! \( -r "$ed_filename" -a -w "$ed_filename" \)
echo "$ed_filename is not readable and writable" > /dev/stderr
return 1
end
if test ! -f "$ed_filename"
builtin echo "$ed_filename is not a file" > /dev/stderr
return 1
end
set --local stat_result (command stat -L --format '%d:%i:%s' "$ed_filename" 2> /dev/null)
if test "$status" -ne "0"
set stat_result (command stat -L -f '%d:%i:%z' "$ed_filename" 2> /dev/null)
end
if test -z "$stat_result"
echo "Failed to stat the file: $ed_filename" > /dev/stderr
return 1
end
set --append data "file_inode=$stat_result"
set --local file_size (string match -rg '\d+:\d+:(\d+)' "$stat_result")
test "$file_size" -gt (math "8 * 1024 * 1024") && begin; echo "File is too large for performant editing"; return 1; end;
set --local file_data (command base64 < "$ed_filename")
set --append data "file_data=$file_data"
__ksi_transmit_data (string join "," -- $data | tr -d "\t\n\r ") "edit"
echo "Waiting for editing to be completed..."
set --global __ksi_waiting_for_edit "y"
set --erase data
function __ksi_react_to_interrupt -s SIGINT
if test "$__ksi_waiting_for_edit" = "y"
set --erase __ksi_waiting_for_edit
__ksi_transmit_data "abort_signaled=interrupt" "edit"
end
end
while true
set --local started "n"
while true
command stty "-echo"; set --local line (command head -n1 < /dev/tty);
test -z "$line" && break
if test "$started" = "y"
test "$line" = "UPDATE" && break;
if test "$line" = "DONE"
set started "done"; break;
end
printf "%s\n" "$line" > /dev/stderr;
return 1;
else
test "$line" = "KITTY_DATA_START" && set started "y"
end
end
test "$started" = "n" && continue;
set --local data ""
while true
command stty "-echo"; set --local line (command head -n1 < /dev/tty);
test -z "$line" && break
test "$line" = "KITTY_DATA_END" && break;
set data "$data$line"
end
if test -n "$data" -a "$started" != "done"
echo "Updating $ed_filename..."
printf "%s" "$data" | command base64 -d > "$ed_filename"
end
test "$started" = "done" && break;
end
set --erase __ksi_waiting_for_edit
functions --erase __ksi_react_to_interrupt
end

View File

@@ -1,4 +1,4 @@
#! /bin/bash
#!/bin/sh
#
# bootstrap-utils.sh
# Copyright (C) 2022 Kovid Goyal <kovid at kovidgoyal.net>
@@ -125,7 +125,7 @@ exec_bash_with_integration() {
export HISTFILE="$HOME/.bash_history"
export KITTY_BASH_UNEXPORT_HISTFILE="1"
fi
exec "$login_shell" "--posix"
exec "$login_shell" "--login" "--posix"
}
exec_with_shell_integration() {
@@ -198,6 +198,20 @@ prepare_for_exec() {
install_kitty_bootstrap
[ -n "$login_shell" ] || using_getent || using_id || using_python || using_perl || using_passwd || using_shell_env || login_shell="sh"
case "$login_shell" in
/*) ;;
*)
if ! command -v "$login_shell" > /dev/null 2> /dev/null; then
for i in /opt/homebrew/bin /opt/homebrew/sbin /usr/local/bin /usr/bin /bin /usr/sbin /sbin
do
if [ -x "$i/$login_shell" ]; then
login_shell="$i/$login_shell"
break;
fi
done
fi
;;
esac
shell_name=$(command basename $login_shell)
[ -n "$login_cwd" ] && cd "$login_cwd"
}

View File

@@ -106,6 +106,8 @@ _ksi_deferred_init() {
builtin unset "functions[_kitty]"
builtin autoload -Uz -- $comp_dir/_kitty
compdef _kitty kitty
compdef _kitty edit-in-kitty
compdef _kitty clone-in-kitty
fi
# If compdef is not set, compinit has not run yet. In this case we must
@@ -386,6 +388,21 @@ _ksi_deferred_init() {
builtin unfunction _ksi_deferred_init
}
_ksi_transmit_data() {
builtin local data="${1//[[:space:]]}"
builtin local pos=0
builtin local chunk_num=0
while [ $pos -lt ${#data} ]; do
builtin local chunk="${data:$pos:2048}"
pos=$(($pos+2048))
builtin print -nu "$_ksi_fd" -f '\eP@kitty-%s|%s:%s\e\\' "${2}" "${chunk_num}" "${chunk}"
chunk_num=$(($chunk_num+1))
done
# save history so it is available in new shell
[ "$3" = "save_history" ] && builtin fc -AI
builtin print -nu "$_ksi_fd" -f '\eP@kitty-%s|\e\\' "${2}"
}
clone-in-kitty() {
builtin local data="shell=zsh,pid=$$,cwd=$(builtin printf "%s" "$PWD" | builtin command base64)"
while :; do
@@ -408,17 +425,76 @@ clone-in-kitty() {
env="${env}$(builtin printf "%s=%s\0" "$varname" "${(P)varname}")"
done
data="$data,env=$(builtin printf "%s" "$env" | builtin command base64)"
data="${data//[[:space:]]}"
builtin local pos=0
builtin local chunk_num=0
while [ $pos -lt ${#data} ]; do
builtin local chunk="${data:$pos:2048}"
pos=$(($pos+2048))
builtin print -nu "$_ksi_fd" -f '\eP@kitty-clone|%s:%s\e\\' "${chunk_num}" "${chunk}"
chunk_num=$(($chunk_num+1))
done
# save history so it is available in new shell
builtin fc -AI
builtin print -nu "$_ksi_fd" '\eP@kitty-clone|\e\\'
_ksi_transmit_data "$data" "clone" "save_history"
}
edit-in-kitty() {
builtin local data=""
builtin local ed_filename=""
builtin local usage="Usage: edit-in-kitty [OPTIONS] FILE"
data="cwd=$(builtin printf "%s" "$PWD" | builtin command base64)"
while :; do
case "$1" in
"") break;;
-h|--help)
builtin printf "%s\n\n%s\n\n%s\n" "$usage" "Edit the specified file in a kitty overlay window. Works over SSH as well." "For usage instructions see: https://sw.kovidgoyal.net/kitty/shell-integration/#edit-file"
return
;;
*) data="$data,a=$(builtin printf "%s" "$1" | builtin command base64)"; ed_filename="$1";;
esac
shift
done
[ -z "$ed_filename" ] && {
builtin echo "$usage" > /dev/stderr
return 1
}
[ -r "$ed_filename" -a -w "$ed_filename" ] || {
builtin echo "$ed_filename is not readable and writable" > /dev/stderr
return 1
}
[ ! -f "$ed_filename" ] && {
builtin echo "$ed_filename is not a file" > /dev/stderr
return 1
}
builtin zmodload -F zsh/stat b:zstat
builtin typeset -A stat_result
builtin zstat -H stat_result "$ed_filename" || { builtin echo "Failed to stat the file: $ed_filename" > /dev/stderr; return 1; }
[ "${stat_result[size]}" -gt $((8 * 1024 * 1024)) ] && { builtin echo "File is too large for performant editing"; return 1; }
data="$data,file_inode=${stat_result[device]}:${stat_result[inode]}:${stat_result[size]}"
data="$data,file_data=$(builtin command base64 < "$ed_filename")"
_ksi_transmit_data "$data" "edit"
data=""
builtin echo "Waiting for editing to be completed..."
builtin local started="n"
builtin local line=""
builtin set -o localoptions -o localtraps
builtin local old_tty_settings=$(builtin command stty -g)
builtin command stty "-echo"
builtin trap "builtin command stty '$old_tty_settings'" EXIT
builtin trap "builtin command stty '$old_tty_settings'; _ksi_transmit_data 'abort_signaled=interrupt' 'edit'; return 1;" INT TERM
while :; do
started="n"
while IFS= read -r line; do
if [ "$started" = "y" ]; then
[ "$line" = "UPDATE" ] && break;
[ "$line" = "DONE" ] && { started="done"; break; }
builtin printf "%s\n" "$line" > /dev/stderr;
return 1;
else
[ "$line" = "KITTY_DATA_START" ] && started="y"
fi
done
[ "$started" = "n" ] && continue;
data=""
while IFS= read -r line; do
[ "$line" = "KITTY_DATA_END" ] && break;
data="$data$line"
done
[ -n "$data" -a "$started" != "done" ] && {
builtin echo "Updating $ed_filename..."
builtin printf "%s" "$data" | builtin command base64 -d > "$ed_filename"
}
[ "$started" = "done" ] && break;
done
}