Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad128a3c29 | ||
|
|
b71d2e04ca | ||
|
|
cc3c7b0286 | ||
|
|
bf6c90a69a | ||
|
|
4c9a75c1ba | ||
|
|
98f46f8bd7 | ||
|
|
dec62b1929 | ||
|
|
79f7954048 | ||
|
|
fc217dafba | ||
|
|
a6aff817cf | ||
|
|
38cb18fe92 | ||
|
|
20b6a97159 | ||
|
|
98f6e24106 | ||
|
|
4ed6364b61 | ||
|
|
cd7751681d | ||
|
|
76531d2f7a | ||
|
|
eea652f1d0 | ||
|
|
f20f4762a8 | ||
|
|
c004fd1f4d | ||
|
|
f29039db29 | ||
|
|
fbf1ec43c7 | ||
|
|
a42200a430 | ||
|
|
ad576c333c | ||
|
|
5a0a980648 | ||
|
|
b3578a4fa5 | ||
|
|
3c952b01d3 | ||
|
|
b1453a51f6 | ||
|
|
2c3a7b5f41 | ||
|
|
0df5a6c6c8 | ||
|
|
bf4dc6365a | ||
|
|
ce3322bf91 | ||
|
|
835cb31501 | ||
|
|
762dd065f3 | ||
|
|
7b91447d89 | ||
|
|
1146c4ea3f | ||
|
|
729f7b8e03 | ||
|
|
d3ec632b9f | ||
|
|
cb01b5ee44 | ||
|
|
0828096e9e | ||
|
|
ebcbed290f | ||
|
|
d6afe6f2cb | ||
|
|
4daf70f636 | ||
|
|
2f3dd72c84 | ||
|
|
dca8c65376 | ||
|
|
12d8175d9a | ||
|
|
609bcd68dd | ||
|
|
313136803c | ||
|
|
b937033411 | ||
|
|
55eeb9c11e | ||
|
|
8fbec2dfd5 | ||
|
|
c4521290b8 | ||
|
|
e12a9f3caf | ||
|
|
56f8a06362 | ||
|
|
5c4abe749b | ||
|
|
62c27b3db7 | ||
|
|
1283a2b7e5 | ||
|
|
bec620a8d3 | ||
|
|
6b0bb2e845 | ||
|
|
135818f34b | ||
|
|
2a361d1de1 | ||
|
|
761a941d43 | ||
|
|
c03630a487 | ||
|
|
0b79cbdb3b | ||
|
|
1dc6b49e02 | ||
|
|
1962f8e5a0 | ||
|
|
cabfcbcb69 | ||
|
|
48bb43a2c4 | ||
|
|
af5e3e0c18 | ||
|
|
fb8398c2eb | ||
|
|
eefd4bb3e3 | ||
|
|
581c388838 | ||
|
|
7a63ea6480 | ||
|
|
65f1329635 | ||
|
|
0df9a5d5c5 | ||
|
|
6405849a1b | ||
|
|
e9340dfaaf | ||
|
|
0509855930 | ||
|
|
4c74462763 | ||
|
|
7f69381221 | ||
|
|
75942fb47e | ||
|
|
0893d7164a | ||
|
|
27906ea853 | ||
|
|
8540ca3990 | ||
|
|
e82c5a63b5 | ||
|
|
83602d7e53 | ||
|
|
64e30e5299 | ||
|
|
83ce23ff9f | ||
|
|
c5e5ec618d | ||
|
|
bd84ec03ce | ||
|
|
d71f18566e | ||
|
|
76e75d9f89 | ||
|
|
f5695ac4f4 | ||
|
|
b748cd21cf | ||
|
|
3a8bdf3b48 | ||
|
|
196c6b07aa | ||
|
|
1978cc5b55 | ||
|
|
ea47cee23f | ||
|
|
e6844ad6f2 | ||
|
|
02d5b3eafa | ||
|
|
151dacf17f | ||
|
|
117d1b02be | ||
|
|
cb55878efd | ||
|
|
ea4751eae5 |
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
180
__main__.py
180
__main__.py
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>\
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
32
glfw/wl_client_side_decorations.c
vendored
32
glfw/wl_client_side_decorations.c
vendored
@@ -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
9
glfw/wl_init.c
vendored
@@ -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
2
glfw/wl_platform.h
vendored
@@ -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
38
kittens/diff/collect.py
Normal file → Executable 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
1
kittens/diff/main.py
Normal file → Executable 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
15
kittens/diff/options/definition.py
Normal file → Executable 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 {{{
|
||||
|
||||
7
kittens/diff/options/parse.py
generated
7
kittens/diff/options/parse.py
generated
@@ -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': [],
|
||||
}
|
||||
|
||||
|
||||
3
kittens/diff/options/types.py
generated
3
kittens/diff/options/types.py
generated
@@ -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
8
kittens/diff/options/utils.py
Normal file → Executable 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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
0
kittens/prewarm/__init__.py
Normal file
0
kittens/prewarm/__init__.py
Normal file
453
kittens/prewarm/main.py
Normal file
453
kittens/prewarm/main.py
Normal 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()
|
||||
@@ -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]:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# empty
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 */
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
185
kitty/entry_points.py
Normal 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:])
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
194
kitty/launch.py
194
kitty/launch.py
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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/*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
14
kitty/shm.py
14
kitty/shm.py
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
48
kitty_tests/prewarm.py
Normal 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'])
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
133
launcher.c
133
launcher.c
@@ -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;
|
||||
}
|
||||
|
||||
68
setup.py
68
setup.py
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)"
|
||||
@@ -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)"
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user